同一 schema,三套私语:Qwen、Mistral、DeepSeek 的工具调用序列化
工具调用(tool calling)在 API 层是 OpenAI tools 的统一形状,落到开源模型的输入序列里却是三套互不相通的私语。本篇对照 Qwen、Mistral、DeepSeek 的序列化原文,讲清 chat template 这层翻译发生在哪里、换模型时的迁移成本花在哪。
同一份 get_weather 的 JSON Schema,Qwen 权重上跑得好好的,换成 Mistral 突然就不会调工具了。问题多半出在模型各自的序列化方言上,业务代码几乎不用动。
同一 schema,三套私语。本篇对照 Qwen、Mistral、DeepSeek 的序列化原文,把这层翻译拆开看。
行业的公约数:OpenAI tools API 的形状
先看大家都同意的部分。行业事实标准是 OpenAI tools API 的形状(slides L2 p.20):同一个 JSON Schema 放进请求的 tools 字段,模型吐出结构化调用,执行结果按 call ID 配对回填。
OpenAI Function Calling 指南 定下的这套契约,几乎被所有推理框架与 harness 原样采纳。
请求侧,你声明工具的参数 schema:
{
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } }
}
}
}]
}
模型侧返回一条带 ID 的结构化调用。你执行完工具,把结果按同一个 ID 回填进对话:
[
{ "role": "assistant",
"tool_calls": [{ "id": "call_01",
"function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" } }] },
{ "role": "tool", "tool_call_id": "call_01", "content": "(工具执行结果)" }
]
对 harness 来说,世界到此为止:schema 进、调用出、ID 配对。
但这份统一只停留在 API 层。往下再挖一层,每家开源模型都在说自己的方言(slides L2 p.20-23)。
三套私语:同一调用的三种展开
把上面那条 get_weather 调用分别交给三家的模板,得到的文本完全不同(slides L2 p.20-23)。Qwen3.8 走 XML 风格标签:
<tool_call>
<function=get_weather>
<parameter=city>北京</parameter>
</function>
</tool_call>
Mistral Small 4 用控制串 [TOOL_CALLS] 领起一段 JSON,以控制 token </s> 收尾,call ID 直接写在 JSON 里:
[TOOL_CALLS] [{"name": "get_weather", "arguments": {"city": "北京"}, "id": "call_01"}]</s>
DeepSeek V3.2 则把调用包进 <|DSML|function_calls> 专用块,块内负载同样是 JSON:
<|DSML|function_calls>
{"name": "get_weather", "arguments": {"city": "北京"}}
| 模型 | 调用语法 | 私语出处 |
|---|---|---|
| Qwen3.8 | <tool_call><function=…><parameter=…> 标签 | tokenizer_config.json(模板内嵌) |
| Mistral Small 4 | [TOOL_CALLS] [{…}]</s>(控制 token + call ID) | Mistral cookbook 的 tokenization 指南 |
| DeepSeek V3.2 | <|DSML|function_calls> 块 | encoding_dsv32.py |
这三套语法都不是秘密,出处就标在上表右列。想确认某个模型到底说什么方言,第一步永远是翻它的 tokenizer 配置。别猜。
模板层:统一与私语的和解点
两层的矛盾在一处和解:聊天模板(chat template)。按 Transformers Chat Templates 文档 的机制,服务端用 Jinja 模板把 messages 与 tools 渲染成模型的单条输入序列。
你交给 API 的结构化请求,进模型前先被模板压扁成一段文本。模型吐出的私语,也由模板 / 推理层反向解析回结构化调用。
你在 Qwen2.5 输入序列里看到的 <|im_start|>,就是模板渲染的产物。不是谁手敲的字符串。
一份 JSON Schema 进 tools,一条带 call_01 的结构化调用出,工具结果按 ID 回填。全程只跟结构化对象打交道。
一整段被模板压扁的文本:控制 token、标签、JSON 负载混在自然语言里。模型只在这门方言上训练过。
私语为何存在,又为何与你无关
训练语料的格式就是推理的格式。模型在什么格式上学会调工具,推理时就必须在什么格式上看到工具,换了格式等于让它讲一门没学过的语言。
控制 token 也需要特殊 token id:[TOOL_CALLS]、</s>、<|DSML| 这类控制串在词表里有专属 id,独立于普通文本切分,不会被用户的自然语言误触发。解析器也无需猜测「这是不是一次调用」。
这套复杂性对应用层是透明的:harness 只见 API 形状,序列化发生在模板 / 推理层。换模型时的迁移成本不在业务代码,而在两处配套:
- 模板:tools 怎么渲染进序列,私语怎么解析回来
- 采样参数(sampling parameters)
业务代码里那份 JSON Schema,原封不动。
私语只约定调用长什么格式,不保证模型吐出来的一定合法:参数漏字段、括号不闭合都可能发生。要硬保证输出合法,得靠文法约束解码(constrained decoding),那是下一节的内容。
与本讲的连接
本篇下钻的是讲次页「工具调用」板块的序列化层。伴读笔记里 ReAct 的推理与行动交替定义了调用的行为节奏,本篇补的是每次 Action 在开源模型词表里被写成什么字符。
资料卡里的 Transformers Chat Templates 文档 对应模板源码,OpenAI Function Calling 指南 对应 API 契约,工具调用与 MCP 的长文则把这套序列化接到更广的工具生态。下一节的约束解码接手保证那一半,和本篇的格式合成闭环。
换模型前,把三套方言再对一遍:
- Qwen:XML 风格标签
- Mistral:控制串领起一段 JSON
- DeepSeek:专用块包住 JSON
本节论文与规范
本节资料
- Transformers Chat Templates 文档 规范进阶官方引用
讲清 Jinja 模板如何把 messages 与 tools 序列化成模型实际输入:工具定义塞进 system、调用与结果按角色回填。这是理解 Qwen、Mistral、DeepSeek 三家工具调用「私语」的底层说明书,本讲三家 token 化对照的公共底座。
- OpenAI Function Calling 指南 规范入门官方引用
tools=[{type:function,…}] 进请求、模型返回 tool_calls、以 call_id 配对回填结果的形状,已成行业事实标准。指南给出从定义工具 schema 到拿到结构化输出的完整链路,是动手第一站,本讲调用机制板块多次对照此文档。
中文系列长文:从 Function Calling 演进史讲到解码层实现技巧(特殊 token、语法掩码),再到 MCP 生态全貌。中文技术社区里少见的深度与准确度兼备的工具调用综述,适合作为英文材料的中转站——先读它建立框架,再啃英文原文。
AI 生成 · 仅供参考,与课程方无关。选中正文里的任何一段也能直接问我(解释一下 / 举个例子)。
本详解以 CMU 11-768 AI Agents (Fall 2026) 课程大纲为参考框架独立撰写,AI 辅助起草、人工抽审。 涉及的官方课件、论文与课程材料版权归原所有方,讲义为站内镜像、其余外链原文出处; 观点与文字(除标注引用外)为作者原创。