同一段带工具调用的对话,发给不同厂商长得一样吗?
本课一问:A1 第 1 课拆开过一次工具调用:模型写一条 tool_calls,harness 回填一条 role: tool。那是 OpenAI Chat Completions 的写法。同一段带工具调用的对话,发给 Anthropic、发给 OpenAI 的 Responses,长得一样吗?
这一课先不碰:回复怎么一帧一帧流回来(第 2 课);usage 怎么变成钱(第 3 课);怎么保证参数是合法 JSON(岔路 B2)。
从已有出发
A1 第 1 课的 Dig 里留过一句:「harness 对接哪一家,就说哪一家的格式——这是 A8『换模型』要处理的第一件事。」A2 的岔路 B1 又看过这一来一回在网络上的样子:每轮请求的开头原封不动是上一轮的全部内容。这一课不重讲往返本身,只问一件事:换一家,这段 JSON 要改多少?
用的是贯穿任务的一段真实对话(项目 T5 的并行调用探针,本机 Qwen/Qwen3-8B-MLX-4bit):系统提示是 miniagent 的 v2 工作规则(895 字符),任务是「Read dateparse.py and test_dateparse.py. Call read_file for BOTH files in the same reply (two tool calls at once), then stop.」;模型在同一条回复里发了 2 个 read_file,harness 把两个文件的内容回填回去(本页的工具结果由生成脚本调用项目的 Toolbox.read_file 在任务仓原文件上现跑得到)。
先自己设计:写死 Chat 格式,换 provider 只改 base URL
本课项目 miniagent 和 CoreCoder 都走 OpenAI 兼容的 Chat Completions。最省事的「换模型」:请求体一个字不改,把地址换成另一家的。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
左栏:项目录制(T5 并行探针,Qwen/Qwen3-8B-MLX-4bit · mlx_lm.server 0.31.3 · 2026-09-26),工具结果由项目 Toolbox 现跑。中、右栏:按官方文档格式在浏览器里现场转换(文档抓取 2026-09-26),不是真实请求。
失败不在某个字段名,而在结构:Anthropic 把系统提示提到顶层、把工具结果塞进下一条 user 消息;Responses 把每个调用和每个结果都拆成独立的 item,用 call_id 串起来。同一个请求体不可能同时满足三份 schema。接下来看各家怎么处理这件事。
试三件事:① 看「调用参数」一行:Chat 与 Responses 里是字符串(JSON 编码后的文本),Anthropic 里是对象——同一个 {"path": "dateparse.py"},换一家就要多一次或少一次 JSON.parse;② 切到「原始请求」:在中栏找两条 tool_result,它们在同一条 role: user 消息里;再在右栏找 function_call_output,它们是和 function_call 并列的顶层 item;③ 切到「回复与 usage」:左栏是真实回复,finish_reason 是 tool_calls;中栏换算成 Anthropic 口径后是 stop_reason: tool_use,右栏没有停止原因,只有 output 里各 item 的类型。
同一个「兼容」接口也有方言。本课项目发给本机服务的请求里有 chat_template_kwargs(Qwen 聊天模板的开关,只有这家本地服务认);本机服务把思考放在 delta.reasoning,DeepSeek 一类放在 reasoning_content,CoreCoder 两个都读(llm.py:217-218);OpenAI 自家的新模型要把 max_tokens 改名成 max_completion_tokens,CoreCoder 专门写了一个函数,在收到 400 后按报错文字改参数重发:
改进:三条路,各付各的代价
| 直连一家(+ 兜底) | 自建一层抽象 | 用第三方 SDK 抽象 | 只留一种、用满它 | |
|---|---|---|---|---|
| 代表(pin) | CoreCoder v0.7.0 | Pi 31b513e3 的 pi-ai | OpenCode v1.18.32(Vercel AI SDK) | Codex 8cabf5a |
| 怎么对付三种线格式 | 默认只说 OpenAI 兼容;不兼容的交给 litellm(CORECODER_PROVIDER=litellm) | 「协议」与「provider」拆成两维:10 种协议、40 个内置 provider,provider 只声明自己用哪种协议 | 每家一个 @ai-sdk/* 包(19 个依赖),统一成 SDK 的消息与事件 | WireApi 只剩 Responses 一个变体,本地的 Ollama、LM Studio 也按 Responses 接 |
| 代价(pin 里数得出来) | llm.py 净 259 行,但方言靠报错后再改(上面那段) | 协议实现与模型目录都要自己维护 | 逐 provider 改写消息的 transform.ts 1,909 行;给 8 个 @ai-sdk 包打了补丁;同时在试验去 SDK 化的 packages/llm(8 个协议文件) | 放弃了所有只说 Chat 的服务;配置 wire_api = "chat" 直接报错 |
| 换来什么 | 最短的路 | 能用满每家的特性,又不被 SDK 绑住 | 一次接入几十家 | 能用满一家:流里边收边执行工具、按会话设缓存键(第 2、3 课) |
三条路的理由,只有 Codex 在仓库里留了说明;CoreCoder 与 Pi 的仓库里都没有找到「为什么直连 / 为什么自建」的文字 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条源码直证其结构与计数(pin 副本现数);三家为什么这样选,只有 Codex 有维护者公告,CoreCoder 与 Pi 未找到说明——页面只陈述「做了什么、付了什么」。 出处待补 。下面分别看三处源码。
错误外推。公告 #7782(维护者 2025-12-09 发)讲的是 Codex 自己停止支持 chat/completions:理由是那套 API 生于 GPT-3.5 时代,不是为今天的 agent 场景设计的,维护它带来复杂度和回归;Chat Completions 仍在 OpenAI 平台上。第 1 课能下的结论只是「Codex 选择只押一种线格式」。
极小真数字:同一份 usage,三种写法
线格式不只在请求里不同,回复里的用量字段也不同——而且差的是含义,不只是名字。上面那条真实回复,本机服务报的是:
🔍 看同一份回复在三种格式里的样子
左栏:项目录制(T5 并行探针,Qwen/Qwen3-8B-MLX-4bit · mlx_lm.server 0.31.3 · 2026-09-26),工具结果由项目 Toolbox 现跑。中、右栏:按官方文档格式在浏览器里现场转换(文档抓取 2026-09-26),不是真实请求。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 让同一个 harness 能换到另一家模型,而不重写循环 |
| ② 为什么会不同? | 三家的请求 schema 各自定义:同一段对话按文档转换,9 个语义位置里 9 个路径不同,由转换规则直接决定 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条算法性质:按两家官方文档写的转换函数是确定的;浏览器里的 JS 与生成脚本里独立写的 python 转换,三份请求体逐字节一致(组件底部 FNV 自检)。不是真实请求:没有调用任何云端 API。 出处待补 |
| ③ 各家怎么选? | Codex 只留一种(维护者公告给了理由);Pi 自建、OpenCode 用 SDK,代价在 pin 里数得出来,理由没写 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条源码直证(pin 副本现数:transform.ts 1,909 行、8 个 @ai-sdk 补丁、10 种协议 × 40 个 provider);没有任何一家给出比较这三条路的实验。 出处待补 |
演进地图:你在这里
- 起点 写死一家的线格式CoreCoder(OpenAI 兼容 + litellm 兜底)、本课项目 miniagent(标准库 urllib)解决了最短的路:CoreCoder 的
llm.py净 259 行,换 provider 只改 base URL 与 key。卡在同一段带工具调用的对话,换成 Anthropic Messages,9 个语义位置里 9 个换了路径或类型(参数从字符串变成对象,5 条消息变 3 条);Codex 干脆不认 Chat:配置wire_api = "chat"直接报错。 - Pi 自建 pi-ai · OpenCode 用 Vercel AI SDK · Codex 只留 Responses解决了Pi 把 10 种协议与 40 个 provider 做成正交的两维;OpenCode 挂了 19 个
@ai-sdk/*依赖;Codex 的WireApi只剩 1 个变体。卡在抽象层得替每一家补差异:OpenCode 逐 provider 改写消息的transform.ts有 1,909 行,还给 8 个@ai-sdk包打了补丁;而且抽象挡不住流式与缓存的细节。 - 第二步 流式:按调用分别累积,结束才解析Anthropic 流式文档 · OpenAI function-calling 流式指南 · Codex 只收 output_item.done解决了开思考的一轮,第一帧 125.9 ms 就到;整包等回复的话,正文要等到 6,631 ms、工具调用 6,798 ms。卡在流式只让回复来得早,不让它便宜:T5 的 9 轮每轮整份重发,输入累计 15,921 token,是最后一轮 2,389 的 6.7 倍。
- 第三步 缓存感知的账单 + 稳定前缀Anthropic / OpenAI prompt caching · Codex prompt_cache_key · OpenCode applyCaching解决了按 usage 四段计价:T5 在 Claude Sonnet 5 价目下 $0.0140;CoreCoder 按全价估 $0.0371,是它的 2.64 倍。卡在缓存只省输入:开思考的 pd-v2-unlimited-s3 账单里输出占 94%;前缀里一处变、后面全失效——T10 刷新组第 7 轮 system 重建,命中从 2,072 掉到 294。(→ A9:能跑、能算账了,怎么知道它好不好?)
数字口径:pin 源码计数(Codex 8cabf5a、OpenCode v1.18.32、Pi 31b513e3、CoreCoder v0.7.0)、项目录制 T5 / T10 与逐轮 usage(本机 Qwen3-8B,token 按它的 tokenizer 数)、官方价格页快照 2026-09-26;由 a08-model-access.json 构建时现算。
一句话
先停在这里:记住三件事——系统提示放哪、调用参数是字符串还是对象、工具结果是独立消息还是塞进 user 消息。
再深一层:去岔路怎么让模型的输出一定是合法 JSON:参数是一段模型生成的文字,各家怎么保证它能被解析。
下一个缺口
回复是一点点流回来的,工具参数什么时候才能解析? → 第 2 课:流式回来的工具参数,什么时候才能解析?
这一课看的都是「整条回复」。可 agent 用的几乎都是流式接口:参数是分几段到的,三家分段的方式又各不相同。
你可能会问
- 「本课项目该用哪套 API?」 这是里程碑 M8 的决策(调研里的 U5)。本项目现在走 OpenAI 兼容,因为本机服务只有这一种;接云端时,如果只接一家,按那家的原生格式写最省事;要多家,就得像 Pi 或 OpenCode 那样付抽象层的代价。
- 「litellm 这类库会不会让这些差异都消失?」 它把各家统一成 OpenAI 的形状,差异被挪进了库里:CoreCoder 用 litellm 时要开
drop_params丢掉对方不认识的参数(llm.py:313)。本课没有测它的延迟或正确性,调研里「litellm 慢多少倍」一类说法找不到可引用的测量,已排除。 - 「Anthropic 也有 OpenAI 兼容端点吗?」 本课没有核这件事;即使有,第 2、3 课的流式事件与缓存计费也要按它自己的文档来。