MyPrototypeWhat

ContextChef (7):Provider 适配层——让差异止于编译层

English version: ContextChef (7): The Provider Adapter Layer — Let Differences Stop at Compile Time

你花了两周在 Claude 上调好了 prompt:prefill 引导模型先思考再回答,cache breakpoint 把 input token 成本降低了 60%,工具 schema 精调到幻觉率很低。然后你需要加一个 OpenAI 的 fallback。你打开一个新文件,开始复制 prompt,手动删掉 Anthropic 专有字段。

这不是偶发的麻烦,是结构性问题。每次调优,你需要在多个文件里同步;每次供应商 API 更新,你需要在多处跟进。时间一长,三套 prompt 必然出现漂移。

设计角度:供应商差异止于编译层

适配层的设计角度是:所有供应商差异应该在 compile({ target }) 内部消化,不泄漏到业务逻辑里

这个原则的价值在于:你的 Agent 代码里没有任何 if (provider === 'anthropic') 分支。上层代码只和 ContextChef 的内部消息格式打交道,不知道也不需要知道 Anthropic 的 cache_control 字段长什么样,或者 Gemini 的 functionResponse 结构是什么。这些知识封装在适配器里,维护在一处,更新也在一处。

带来的直接好处是:切换供应商只需要改一个参数compile({ target: "anthropic" }) 改成 compile({ target: "openai" }),其余代码不动。功能上的降级(比如 prefill 在 OpenAI 上变成软约束)是真实存在的,你清楚地知道发生了什么,但你的代码不会因为这个切换而报错或崩溃。

差异在哪里,适配器如何处理

表面上三家都是”发一个消息数组,收一个响应”,但实现细节差异恰好集中在调优效果最显著的地方。

缓存是经济影响最大的差异。Manus 在 Context Engineering for AI Agents 里把 KV-cache 命中率定义为生产级 Agent 最重要的单一指标——Anthropic Claude Sonnet 的缓存价格是未缓存 token 的 1/10,input/output ratio 高达 100:1 的 Agent 里,缓存命中率提升 10% 就能降一大截成本。Anthropic 需要在消息里显式标记 cache_control: { type: 'ephemeral' };OpenAI 自动缓存前缀,无需标记;Gemini 的提示缓存是完全独立的 CachedContent API,与消息格式解耦。

ContextChef 的处理方式是:你只用 _cache_breakpoint: true 标记一次意图,适配器负责翻译——Anthropic 转成 cache_control,OpenAI 和 Gemini 忽略(它们各自有独立机制)。这是”意图标注”而非”供应商指令”。你表达的是”这里应该是缓存边界”,而不是”给这条消息加 cache_control 字段”。后者换个供应商就报错,前者不会。

Prefill 是另一个关键差异。Anthropic 原生支持在消息数组末尾追加 assistant 消息作为起始内容,可以精确控制输出格式;OpenAI 和 Gemini 不支持。适配器的处理是降级而非失败:prefill 内容被转换为 [System Note] 系统注释,模型会尽力遵守,但约束力弱于原生 prefill。你的代码不崩溃,但你知道这是个软约束,可以在应用层决定是否接受。

工具调用历史格式三家完全不同,适配器的价值在这里最直接:你的历史消息里用统一的内部格式存储工具调用,切换供应商时,适配器自动把整段历史转换成目标格式,不需要你动一行代码。

适配器可以独立使用

如果你有自己的消息组装逻辑,只需要格式转换这一层,适配器可以单独引入:

import { getAdapter } from "context-chef";
const payload = getAdapter("anthropic").compile(messages);

适配器是无状态的纯函数,输入 Message[],输出对应供应商的 payload 结构。它不依赖 ContextChef 的其他模块,可以独立集成进任何现有的 Agent 项目里。

自定义适配器:一个方法的接口

ContextChef 内置了 OpenAI、Anthropic、Gemini 三个适配器,但适配器的接口刻意设计得很窄:

export interface ITargetAdapter {
  compile(messages: Message[]): TargetPayload;
}

实现这个接口,你就可以支持任何目标格式——自托管的开源模型(如 vLLM 部署的 Qwen、Mistral)、公司内部的推理网关、或者任何 OpenAI-compatible API 之外还有特殊字段需求的供应商。

典型的使用场景是内部推理服务:公司的模型网关在标准 OpenAI 格式上额外要求几个鉴权字段,或者某个字段的格式不完全兼容。你只需要实现一个薄的 adapter,把 ContextChef 输出的内部消息格式转换成你需要的结构,其余的——历史压缩、工具裁剪、状态注入、记忆管理——全部照用,不需要重新实现。

接口只有一个方法,没有生命周期钩子,没有需要继承的基类。最小实现就是一个接收 Message[] 返回你的目标格式的纯函数,套个类包一下即可。


至此,ContextChef 系列结束。每个模块解决一个独立问题,也可以单独引入——你不需要接受整套,遇到哪个问题就引入哪个模块。