Chapter 02
协议机制
上一章把工具调用拆成「模型只请求、harness 执行」的 5 个角色。这一章把那段「结构化请求」放大到字段级:它在 Anthropic 和 OpenAI 里到底长什么样,以及模型究竟怎么「生成」出它。
本章你将建立的 schema
- ①
tool定义 /tool_use/tool_result三者的精确字段,与作为停机信号的stop_reason。 - ② Anthropic 与 OpenAI 的术语和数据形状差异:
input对象 vsarguments字符串、user 角色 vs tool 角色。 - ③
tool_choice、并行调用、流式三者的语义边界。 - ④ 深一层:模型靠后训练 + 特殊 token +(可选)约束解码产出调用,JSON 合法性不是天生的。
2.1tool 定义:name / description / input_schema
一个工具定义就是 name + description + 一份 JSON Schema 参数表,提前声明给模型。
工具调用的起点不是代码,而是一段声明。在发起请求时,caller 把可用工具列表放进 tools 字段,每个工具至少包含三件事:name(模型用来指名调用)、description(模型用来判断「这工具是干什么的、什么时候该用」)、参数表(模型用来知道每个参数叫什么、是什么类型、哪些必填)。
参数表的格式是 JSON Schema(一套用 JSON 描述「另一段 JSON 该长什么样」的标准)。Anthropic 把它放在 input_schema 字段里,OpenAI 放在 function.parameters 里——同一份东西,两个字段名。
关键在于这段定义的去向:它会被序列化成文本、拼进模型的 context(通常落在系统提示区)。模型据这段文字决定选哪个工具、每个参数填什么值。所以一个工具定义的本质,是写给模型读的 prompt——description 写得含糊,模型就选错、填错(04 章专门展开「为什么 description 是最高杠杆」)。
{
"name": "get_weather",
"description": "查询指定城市的当前天气。当用户询问实时天气时调用。",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名,如 San Francisco"
}
},
"required": ["location"]
}
}
这是 Anthropic 风格的写法。input_schema 内部就是标准 JSON Schema:顶层是一个 object,properties 列出每个参数,required 标出必填项。模型读到它,就知道「调 get_weather 时必须给一个叫 location 的字符串」。
2.2一次往返的报文:tool_use + stop_reason + tool_result
模型回一个 tool_use 块并停机,caller 执行后用一条 user 消息把 tool_result 送回去。
续用 01 章的天气例子,看 Anthropic Messages API 里一次完整往返的报文形状。整个循环是四拍:
- caller 发请求(带上
tools定义); - 模型回一条
assistant消息,stop_reason为"tool_use",且 content 里含一个tool_use块; - caller 在自己的程序里执行那个工具;
- caller 发一条
role:"user"消息,content 里装一个tool_result块——模型据此继续。
// ② 模型这一轮的回复(assistant)
{
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "toolu_01A",
"name": "get_weather",
"input": { "location": "San Francisco" }
}
]
}
// ③ 你的程序执行后,把结果作为一条 user 消息发回
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A",
"content": "15°C, 多云",
"is_error": false
}
]
}
stop_reason:"tool_use" 后模型就停了;真正调 API(左侧小框)发生在你的程序里,模型在等第 ③ 条。tool_use 与 tool_result 必须严格 1:1 配对,并通过 tool_use_id 关联。若上下文裁剪 / 压缩时丢了 tool_use 却留下 tool_result(或两者顺序错乱),API 会直接报 400。03 章会讲循环里怎么维护这份配对不被破坏。
2.3Anthropic ↔ OpenAI 术语对照
同一套机制,两家的字段名和数据形状不同。读别人的代码、跨厂商迁移时,这张表是高频参照。
| 概念 | Anthropic | OpenAI (Chat Completions) |
|---|---|---|
| 模型的调用请求 | tool_use 块 | tool_calls[] |
| 返回结果 | tool_result 块(role: user) | 一条 role:"tool" 消息 |
| 关联 id | tool_use_id | tool_call_id |
| 参数 payload | input(已是对象) | function.arguments(JSON 字符串,要自己 parse) |
| 停止信号 | stop_reason:"tool_use" | finish_reason:"tool_calls" |
| schema 字段 | input_schema | function.parameters |
| 强制调用某工具 | tool_choice:{"type":"any"} | tool_choice:"required" |
OpenAI 的 arguments 是 JSON 字符串,拿到手要先 JSON.parse 才能当对象用;Anthropic 的 input 直接就是对象,可直接取字段。但两家都不是模型在「执行」——它们都只是把模型生成的请求换了个数据形状交还给你,剩下的活仍归你的程序。
2.4tool_choice:让不让、让不让得了哪个
tool_choice 控制四种姿态:自由决定、必须调某个、钉死某个、完全禁止。
tool_choice 决定模型在这一轮对工具的姿态。四种语义(括号内为两家的命名):
- auto(默认)——模型自行决定调不调、调哪个。
- any(Anthropic)/ required(OpenAI)——必须调某个工具,但不指定是哪个,由模型从工具列表里挑。
- 指定某工具——钉死调用哪个:Anthropic 用
{"type":"tool","name":"x"},OpenAI 用{"type":"function","function":{"name":"x"}}。 - none——禁止调用任何工具,模型只能直接回文本。
设了 tool_choice:"any"(OpenAI 的 required),模型一定会调你心里想的那个工具吗?
展开答案
不一定。any / required 只强制「调某个工具」,并不指定是哪个——模型仍会从工具列表里自己挑。要钉死具体工具,得用指定形式 {"type":"tool","name":...}(OpenAI 对应 {"type":"function","function":{"name":...}})。把「强制调用」和「强制调哪个」分开记。
2.5并行工具调用
一个回合可以请求多件事;两家的结果回填形状不同,但都靠 id 对应。
一个 assistant turn 可以一次带多个 tool_use 块(OpenAI 对应 tool_calls 数组里多个元素)。回填方式两家不同:
- Anthropic:把多个
tool_result放进同一条 user 消息的 content 数组里。 - OpenAI:每个
tool_call_id对应一条独立的role:"tool"消息。
不论哪种形状,结果与请求的对应关系都靠 id(tool_use_id / tool_call_id)维系。机制上,并行只是「一个回合里请求多件事」,执行仍由 harness 负责——彼此独立的调用可以并发跑,从而省墙钟时间(03 章会讲并行带来的隐患,例如互相依赖或写同一资源)。
2.6流式:参数是一点点拼出来的
流式下工具参数逐 chunk 到达,中途是残缺 JSON,block 结束才完整。
开启流式后,工具参数不是一次性给全,而是逐 chunk 到达:Anthropic 通过 input_json_delta 事件吐出 partial_json 片段;OpenAI 通过分块的 delta.tool_calls[].function.arguments 累加。在到达过程中,手里的内容是不完整、甚至不合法的 JSON——只有当这个工具调用 block 结束时,所有片段拼起来才构成一个完整可解析的对象。
流式输出到一半,拿到的工具参数 JSON 能直接解析吗?
展开答案
不能。中途的 partial_json 是残缺片段(比如只到 {"location": "San Fr),强行 parse 会失败。必须等这个 block 结束、片段拼全后再解析。设计启示:别在流式中途就拿参数去执行工具,否则要么解析报错、要么拿到半截参数误触发。
2.7深一层:模型到底怎么「生成」一个调用
工具调用就是普通生成的 token;JSON 合法性靠事后重试或约束解码,不是天生的。
前六节都在讲「报文长什么样」。这一节下沉一层,讲模型究竟怎么把那个 tool_use 块生成出来——这是读官方文档读不到、却决定了你怎么排错的那层。
一个工具调用就是普通生成的 token
模型并没有一个「调用工具」的特殊能力开关。harness 把每个工具的 {name, description, input_schema} 序列化进 context;模型经过后训练(在成千上万条工具调用轨迹上微调),学会在该调用时输出一段被 tokenizer 认得的、包着特殊 token 的结构化片段;harness 再按模式匹配这些特殊 token,从中解析出 name 与那段 JSON 参数,组装成 API 里你看到的 tool_use 块。整段过程,模型做的事和「写一句话」没有本质区别——都是在预测下一个 token。
JSON 合法性不是天生的
既然只是 token 预测,纯生成完全会吐出非法 JSON(少个引号、括号不配对、字段名拼错)。合法性靠工程手段补,主流是两条路:
- ① 事后解析重试——拿到输出就尝试 parse,失败了就把错误信息塞回去、重新提示模型再生成一次。简单、不改采样过程,但有失败重试的额外开销与延迟。
- ② 约束解码(
constrained decoding)——把JSON Schema提前编译成一套语法 / 状态机,在模型和采样器之间插一个 logit 处理器:每一步只允许「符合语法的下一个 token」,把所有不符合的 token 的 logit 强行设成 −∞。于是采样器只会采到 schema 合法的 token。模型权重一字不改,被改的只是采样时每一步的候选集合。
关键代价与边界
约束解码看着像银弹,但它有明确的边界:只保证语法,不保证语义。schema 说 location 是字符串,约束解码能保证模型确实吐出一个字符串、整体 JSON 也合法;但模型把用户问的 "San Francisco" 填成 "New York",它一点办法没有——参数形状对、值却错(valid but wrong)是这条路绕不开的盲区。代价上还有两笔:每生成一个 token 都要算一次 mask 的运行时开销,以及把 schema 编译成语法 / 状态机的一次性延迟。
「模型选了正确的工具」和「模型填对了参数」是两件事——约束解码能帮第一件的形状(保证输出是合法调用),帮不了第二件的对错(参数值是否真的正确,仍取决于模型的理解与你的 description)。
§本章 self-check
合上页面,先用自己的话答一遍,再展开对照。答得出形状、对得上字段,才算这章的 schema 进了脑子。
- 在 Anthropic 协议里,
tool_result是放在哪个 role 的消息里发回去的?这和 OpenAI 有什么不同? - OpenAI 的
tool_calls[].function.arguments拿到手能不能直接当对象访问?为什么? - (设计层)约束解码能保证模型「填对参数」吗?它到底保证了什么、保证不了什么?
展开参考答案
- 放在
role:"user"的消息里,tool_result作为该 user 消息 content 数组里的一个块,靠tool_use_id关联回请求。OpenAI 不同:它用一条独立的role:"tool"消息装结果,靠tool_call_id关联——Anthropic 没有专门的 tool 角色。 - 不能直接当对象。OpenAI 的
arguments是一个 JSON 字符串,必须先JSON.parse(或等价反序列化)才能访问字段。对比之下 Anthropic 的input已经是对象,可直接取值。 - 不能保证填对参数。约束解码只保证语法 / 形状合法:输出一定是符合 schema 的合法 JSON(类型对、结构对、必填项在)。它保证不了语义正确:参数值是否真的对(valid but wrong 仍会发生),取决于模型的理解和工具 description 的质量。
把同一个工具写成两家的形状
拿 §2.1 的 get_weather,分别写出 Anthropic 和 OpenAI 的工具定义,再各写一次往返的报文骨架(请求 → 模型的调用请求 → 结果回填)。对照表 2.1,列出你改动的每一处字段名和数据形状。
提示(卡住再展开)
重点盯 4 处:定义里 input_schema vs function.parameters;请求 tool_use vs tool_calls;参数 对象 vs 字符串;回填 role:user vs role:tool。