← 学习笔记

Function Calling 时,模型到底被设成了什么

既然模型吐出来的工具参数也是 JSON,那是不是也开了 JSON Mode?
不是。它走的是另一个输出通道。

读完你要拿走的三件事
  1. Function calling 不用 response_format——那管的是 content,工具调用走的是 tool_calls,两个字段
  2. 工具参数想要严格符合 Schema,也得单独打开 strict: true,不打开就只是「模型尽量照做」
  3. 一次工具调用是两个来回,中间那步执行工具的是你的代码,不是模型

一、先看完整的一次往返

从提问到拿到答案,中间到底发生了什么?

下面这四步是真实的报文结构。一步一步点,重点看每步是谁在说话。

一次工具调用的四步

第 1 步 / 4

看懂这四步,function calling 就没有秘密了。

最容易被忽略的是第 ③ 步——模型从头到尾没执行过任何东西,它只是说了句「我想调这个」。
真正去查数据库、去扣钱的,是你自己写的那几行代码。

二、两个不同的输出通道

JSON Mode 和 Function Calling 到底什么关系?

回到你的问题。模型返回的消息体里,有两个互不相干的字段:

{
  "message": {
    "role": "assistant",
    "content": null,              // ← response_format 管这里
    "tool_calls": [ ... ]       // ← function calling 管这里
  }
}
模型这一轮的输出 content 字段 模型「回答」你 response_format 管这里 JSON Mode / Structured Outputs tool_calls 字段 模型「请求」你干活 tools 定义管这里 Function Calling
JSON Mode / Structured OutputsFunction Calling
怎么开response_formattools 参数
管哪个字段contenttool_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 的权限控制。

description 比 Schema 更影响准确率

很多人花大力气雕 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——但这个订单号可能是编的,金额可能是模型看错了。

格式校验省掉的是解析工作,业务校验和权限检查一步都不能少。