Agent Harness 源码拆解(二):LLM 抽象层——把四十家模型方言收敛成一条事件流
接一家模型 API 很容易,接到第十家,你才会开始疼。疼的不是请求怎么写——curl 一下的事——而是”方言”:同样一句”模型要调工具”,Anthropic 用 tool_use 块,OpenAI 用 function_call 塞进一个叫 tool_calls 的数组,Google 又是另一套叫法。你把每家的适配代码写上三遍,就得到一个没人敢动的 adapter 目录。
这一章看六家怎么处理”多 provider 收敛”这个问题的。答案比想象中分得开:有人把方言压进一张数据表,有人把问题拆成四个正交维度,有人干脆绕开方言、先解决缓存。
pi:四十二家 provider,进出只有一个协议
pi 面对的是最狠的版本:packages/ai 里塞着 42 个 provider、10 种 wire 方言,而进出只有一个协议。上一篇已经埋了伏笔——那个”永不 throw 的函数类型”。但真正难的不是签名本身,而是签名背后那些 provider 私有的编码怎么被体面地处理掉。
举个最刁钻的例子:thinking 签名。模型的推理过程往往带签名(Anthropic 的 thinkingSignature、OpenAI 的加密 reasoning、Google 的 thoughtSignature),这些签名是 provider 私有的、跨家互不相认。pi 的处理原则一句话:签名按不透明数据透传,跨层不理解内容。它不知道也不想知道签名里是什么,原样搬过去。
这条原则在”中途换模型”时变成一张降级表——长会话里切便宜模型是刚需,而各家的签名和 tool-call ID 格式互不兼容。pi 把它做成 transformMessages():同 provider 原生回放(签名保留);跨模型时 thinking 降级成纯文本、redacted thinking 直接丢弃、thoughtSignature 剥离;OpenAI 那串 450 字符的 tool-call ID 要裁成 Anthropic 的字母数字下划线规则。原则就三句:签名不透明透传、同族原生回放、异族有损降级但保持结构合法。
另一个值得记的决定:方言差异全沉在 compat 数据里,而不是子类层级里。某个 provider 的 max_tokens 字段名不同,加一条数据就行,不新建抽象类。OpenAICompletionsCompat 一张表挂 27 个字段,11 种 thinking 方言的命名差异都在里面。
代价也很实在:那张 compat 表和 3726 行的目录生成脚本(generate-models.ts)本身就是长期负债。pi 的模型目录不运行时拉取、而是离线生成,为的是字面量类型和”数据变更走 git diff 评审”——但这意味着加一个新模型要走一遍生成管线,不是改一行配置。
opencode:把问题拆成四个正交维度
opencode 的解法更”学院派”:它把”接一家 provider”拆成四元组——Protocol / Endpoint / Auth / Framing。
- Protocol 回答”这个 API 长什么样”——请求体怎么变、流式帧怎么解读;
- Endpoint 回答”部署在哪”——URL、header 约定;
- Auth 回答”用什么凭据”;
- Framing 回答”流怎么分帧”——SSE 还是 AWS 的二进制 event-stream。
它的架构文档里有一句关键注释:A Protocol is not a deployment。Protocol 不知道 URL、不知道 header、不知道 auth,那些是 Route.make() 的部署关切。正是这个分离,让 DeepSeek、Together、Cerebras 全部复用同一个 OpenAIChat.protocol,不用每家 fork 300 行。
于是 openai-compatible 成了正儿八经的一等公民——任意 OpenAI 兼容端点都能零分叉接入。国内模型生态里大量端点都标榜”OpenAI 兼容”,这个分解在国内场景尤其划算。
代价:Protocol<Body, Frame, Event, State> 这种泛型签名,对”只接两三家”的场景是明确的过度设计。四个维度每个都要想清楚,对新手是门槛——而且它整个建立在 Effect 上,心智成本上一篇已经提过。
ZCode:先别管方言,先想清楚什么会变
ZCode 换了个角度:与其纠结方言,不如先回答”提示词里什么稳定、什么会变”。
它给每段提示词两个坐标——落在 system 还是 meta_user、是 stable 还是 dynamic。组装器据此产出最多三条 system 消息,然后把一切会中途变的东西(当前模式、日期、输出样式、todo、新到的会话指导)统统赶出 system,改成按轮生成的 <system-reminder> 附件。
换来的是三样东西:同一会话里 provider 请求的头部字节不变;每条 system 消息各带一个 cacheControl 断点;缓存命中率成为一个有落点的事件字段,而不是玄学。
代价:多条 system 消息意味着要写兼容层。部分旧式 OpenAI 兼容协议只接受一个开头 system,ZCode 在序列化边界按原顺序 join("") 合并,还不补任何分隔符——因为动态块自己带着左边界 \n\n,补了字节就对不上了。这条”所有 provider 字节一致”的纪律,是它最容易被抄漏的地方。
grok-build:提示词是数据,不是代码
grok-build 的答案最反常识:它把系统提示词当成数据,编译进二进制,还做了 XOR 混淆。
注释里自嘲式地承认了真相:”This is obfuscation, not security; the seeds live in-repo.“——种子就在仓库里,混淆只是让 strings 输出里别直接躺着明文。
但两个防御细节是真有用的:解密后的明文用 Zeroizing<String> 包着,drop 时主动擦除,不让提示词留在堆上被 swap 或 heap dump 捡走;密文过期则解密报错——模板改了忘记重新加密,构建直接失败,不会带着旧密文上线。
代价:模板化到 minijinja 之后,”读这个 harness 的提示词”变成了一件要先解密、或找明文副本才能干的事。XOR 混淆还徒增了”看似安全”的错觉,得靠注释里那句话时刻提醒自己。
Maka 和 CodeWhale 的一笔
Maka 的 LLM 层走 AI SDK 后端,它的架构文档里诚实标注了 AiSdkBackend 过大是已知债——承认成本、配合追踪 issue,这本身也算一种写法。CodeWhale 的贡献在另一处:它的提示词分层规则只在一处声明,其他每一层只描述自己做什么、不描述自己排第几——优先级规则一旦在多个地方重复声明,必然漂移。这条会留到 Memory 与扩展那章展开。
如果你要自己写这层
把六家的答案折算成场景:
- 只接一两家:别建抽象层,直接写 adapter。抽象层的成本在”只有两个实现”时是纯开销,等第三家出现再说。
- 接多家、且长会话要中途换模型:学 pi 的事件协议 + 降级回放表。签名透传、异族降级这两条,是换模型不炸的前提。
- 国内生态、大量 OpenAI 兼容端点:学 opencode 的四元组,把
openai-compatible当一等公民。 - 长会话、心疼 token:学 ZCode 的 stable/dynamic 切分,稳定前缀逐字节冻结,会变的东西赶出 system。
四个最容易踩的翻车点:
一是为每家写一个 adapter 类,30 家就 30 个类,方言差异散成 30 处 if——pi 的答案是”方言差异是数据不是类型”,opencode 的答案是”四个维度正交,别把部署塞进协议”。
二是把 thinking 签名当普通文本处理。一压缩、一转发就丢了签名,换模型时回放直接崩溃。签名只能不透明透传。
三是抽象层抛异常。上游 provider 炸了,异常一路穿透到 agent 循环,每一层都要 try/catch——回到上一篇”永不 throw”那一条:失败编码成流内事件。
四是缓存前缀每轮重算。系统提示、工具表这些稳定内容每轮重渲染,等于把最长的稳定前缀每轮重新计价。这个留到上下文工程那章展开,但根子在 LLM 层。
下一篇讲 Agent Loop:主循环到底长什么样、turn 怎么算一次、用户中途插话怎么打断——以及 CodeWhale 为什么宁可删掉占位引擎也要保证”全仓库只有一个循环”。
本篇术语
- 方言(dialect):同一语义在不同 provider 里的不同表达(工具调用的三种叫法、字段名的差异、流式分帧方式)
- 事件协议(event protocol):把模型的流式输出统一成一串结构化事件(
text_delta、toolcall_delta、done、error),而不是各家的原始分帧 - SSE:Server-Sent Events,流式接口最常用的分帧格式;AWS 的 event-stream 是它的二进制近亲
- prompt cache:provider 对”请求前缀没变”的部分直接复用上次算力,前缀变一个字节缓存就失效
- 签名(signature):provider 给推理过程/工具调用加的不透明凭证,用于回放校验,跨家互不相认
- adapter:把一种 provider 方言翻译成内部统一表示的适配代码
- 一等公民:某种能力(如
openai-compatible协议)被设计为一等抽象,而不是临时兼容层
参考资料
参照系仓库(结论基于 2026-09 下旬至 10 月上旬各仓库 HEAD):
- earendil-works/pi ——
packages/ai的 1940 行 README 即设计文档,42 provider / 10 方言收敛 - anomalyco/opencode ——
packages/llm四元组与route/protocol.ts注释 - xai-org/grok-build ——
xai-grok-agent/templates/加密模板与 minijinja - zai-org/ZCode ——
P/core/src/context/builder.ts六段序与三条 system 消息 - apache/maka
- Hmbown/CodeWhale
Pi 相关源码分析:
- 梁典典,《Pi Agent 为什么设计得好》
- iceyao,《Pi Agent Harness 源码深度技术解析》
Harness 设计方法论文献:
Provider 官方文档:
- OpenAI,《Prompt Caching》——精确前缀匹配、static-first 排序、最高 90% 缓存折扣,佐证 ZCode/CodeWhale 的缓存前缀冻结设计