既然模型吐出来的工具参数也是 JSON,那是不是也开了 JSON Mode?
不是。它走的是另一个输出通道。
response_format——那管的是 content,工具调用走的是 tool_calls,两个字段strict: true,不打开就只是「模型尽量照做」从提问到拿到答案,中间到底发生了什么?
下面这四步是真实的报文结构。一步一步点,重点看每步是谁在说话。
看懂这四步,function calling 就没有秘密了。
最容易被忽略的是第 ③ 步——模型从头到尾没执行过任何东西,它只是说了句「我想调这个」。
真正去查数据库、去扣钱的,是你自己写的那几行代码。
JSON Mode 和 Function Calling 到底什么关系?
回到你的问题。模型返回的消息体里,有两个互不相干的字段:
{
"message": {
"role": "assistant",
"content": null, // ← response_format 管这里
"tool_calls": [ ... ] // ← function calling 管这里
}
}
| JSON Mode / Structured Outputs | Function Calling | |
|---|---|---|
| 怎么开 | response_format | tools 参数 |
| 管哪个字段 | content | tool_calls |
| 模型在干嘛 | 回答你的问题 | 请求你执行某个动作 |
| 之后你干嘛 | 解析 JSON,用 | 执行工具,把结果发回去再问一次 |
| 几个来回 | 一个 | 至少两个 |
所以答案是:不用开 JSON Mode。
两者不是一件事的两种写法,是两个不同的输出通道。
你要模型给你数据 → 用 response_format;
你要模型让你干活 → 用 tools。
可以同时传,但同一轮里只有一个会生效——模型要么调工具(content 为 null),要么直接回答(tool_calls 为空)。response_format 只在它选择「直接回答」那一轮起作用。
工具参数也是 JSON,它的格式谁来保证?
这里是最容易踩的坑:底层机制和 Structured Outputs 完全一样,但开关是分开的。
不保证tools=[{
"type": "function",
"function": {
"name": "refund_order",
// 没写 strict —— Schema 只是「说明书」,模型尽量照做
"parameters": { "type": "object", "properties": {...} }
}
}]
引擎保证
tools=[{
"type": "function",
"function": {
"name": "refund_order",
"strict": True, // ← 关键在这一行
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"amount": {"type": "number"},
"reason": {"type": ["string", "null"]}
},
"required": ["order_id", "amount", "reason"], // 全都要写进来
"additionalProperties": False
}
}
}]
限制和 Structured Outputs 一模一样:所有字段进 required、additionalProperties: false、部分关键字不支持。
原因也一样:Schema 要被编译成文法去驱动约束解码,字段可选、可多,文法就会爆炸。
想表达「这个参数可以没有」,写成允许 null,别让它可缺失。
不写 strict: true 的后果,和 response_format 那边一模一样:
代码能跑、参数看着也对、测几次都没事——然后线上偶尔少个字段,或者类型变成了字符串 "4999" 而不是数字 4999。
约束解码能保证格式,那「该不该调」谁说了算?
这是两件完全不同的事,很多人混在一起:
| 谁保证 | 保证得了吗 | |
|---|---|---|
| 参数格式对不对 | 约束解码(引擎) | ✅ 能,物理上生不出错 |
| 该不该调这个工具 | 模型自己的判断 | ❌ 保证不了 |
| 参数内容对不对 | 模型自己的判断 | ❌ 订单号可能是编的 |
你能施加的控制只有一个粗粒度开关 tool_choice:
| tool_choice | 效果 | 什么时候用 |
|---|---|---|
"auto" | 模型自己决定(默认) | 正常对话 |
"none" | 禁止调工具,只准说话 | 这一轮只要总结 |
"required" | 必须调一个,但调哪个它定 | 你确定这轮该干活 |
| 指定函数名 | 必须调这一个 | 流程固定的那一步 |
一句话:约束解码管「形状」,管不了「决策」。
它能保证参数一定是合法的 JSON、字段一个不少,但保证不了模型在该调工具的时候调、不该调的时候不调——那是模型能力,不是引擎能力。
上手会踩到什么?
| 坑 | 说明 |
|---|---|
arguments 是字符串 |
不是对象,是 JSON 字符串,要自己 json.loads 一次。这是为了流式传输方便留下的设计。 |
忘了写 strict: true |
默认不开。参数格式变成「尽量」而不是「保证」。 |
| 并行调用的坑 | 模型默认可以一次返回多个 tool_calls。要让 strict 完全生效,官方建议关掉 parallel_tool_calls。 |
结果必须带 tool_call_id 发回 |
少了这个字段,或者调了 3 个只回 2 个结果,下一轮直接报错。 |
| 工具太多,模型选不准 | 十几个工具以上准确率明显下降。按场景分组、动态挂载,别一次全塞进去。 |
| 把模型的参数当可信输入 | 最危险的一个。参数是模型编的,尤其 user_id 这种——身份必须从会话取。展开见 MCP 的权限控制。 |
很多人花大力气雕 Schema,却给 description 写一句「查询订单」。
模型选哪个工具、什么时候选,几乎全靠 description。Schema 只在它决定调之后才起作用。
❌ 模型不知道什么时候该用 "description": "查询订单" ✅ 说清什么时候用、什么时候别用 "description": "按订单号查订单详情,含状态、金额、收货信息。 用户提到具体订单号时用这个;只是泛泛问『我的订单』 时先用 list_recent_orders。"
| 解决什么 | 层 | |
|---|---|---|
| Structured Outputs | 模型回答你时,格式可信 | 输出格式 |
| Function Calling | 模型能请求你干活 | 模型能力 |
| MCP | 工具能被复用,一次开发处处挂载 | 分发协议 |
三者是叠起来的:MCP Server 暴露的工具,最终还是通过 function calling 交给模型;而 function calling 的参数格式,靠的是和 Structured Outputs 同一套约束解码。
最后一条铁律:strict 保证的是「形状」,不是「真假」。
{"order_id": "12345", "amount": 999} 完全符合 Schema——但这个订单号可能是编的,金额可能是模型看错了。
格式校验省掉的是解析工作,业务校验和权限检查一步都不能少。