Function Calling的真实过程
你在 Claude 或 ChatGPT 里输入「北京今天天气怎么样?」,模型会回一段 JSON:
JSON
{
"type": "tool_use",
"name": "get_weather",
"input": { "city": "北京" }
}
然后你的代码拿到这个 JSON,去调天气 API,把结果塞回给模型,模型再用自然语言回复用户。
看起来很自然,像模型真的「调用了一个函数」。
但如果你停下来想一秒:模型是怎么知道要输出这段 JSON 的?它怎么知道有个工具叫 get_weather?它怎么知道参数应该是 city 而不是 location?
先说结论:模型不会调用任何函数。
Function Calling 这个名字是有误导性的。实际发生的事情是:
你在 API 请求里塞了一份「工具菜单」——一组 JSON Schema,描述了每个工具叫什么、干什么、参数是什么格式
模型看到了这份菜单,结合用户的问题,决定要不要使用某个工具
如果要用,模型生成一段符合 Schema 的 JSON——这就是所谓的「函数调用」
你的代码解析这段 JSON,你来执行真正的函数
执行结果塞回对话,模型看到结果后继续回复

整个过程中,模型做的事只有一件:输出一段符合格式的 JSON。执行是你做的。
这个区分非常重要。如果你把 Function Calling 理解成「模型能调用函数」,你会低估参数校验的重要性——觉得模型既然「调用了」,参数总不会错吧?错。模型只是生成了一段 JSON,它可以把任何东西填进去。
约束解码:如何保证输出的JSON合法?
主要靠两件事:训练 + 约束解码。
训练好理解——模型在训练阶段看过大量的「输入 Schema + 输出符合 Schema 的 JSON」的样本,学会了怎么根据 Schema 生成对应的 JSON。
但光靠训练不够。OpenAI 公布过一个数据:单靠训练,JSON Schema 符合率只有 93%。93% 在生产环境远远不够——100 次调用里有 7 次格式错误,Agent 跑 10 轮就大概率崩一次。
所以还需要约束解码(Constrained Decoding)。
回顾之前说的自回归生成:模型每次只生成一个 Token,从词表里选概率最高的。约束解码做的事就是:在选 Token 之前,把不合法的选项排除掉。
具体来说:
把 JSON Schema 编译成一套语法规则(上下文无关文法)
每生成一个 Token,检查:接下来哪些 Token 在语法上合法
不合法的 Token 概率设为 0
剩下的合法 Token 重新归一化,正常采样
训练 + 约束解码 = 100% 格式合法。
但要注意:约束解码只保证格式,不保证语义。
✅ 能保证:city 字段一定是 string 类型
❌ 不能保证:city 的值是真实存在的城市(模型可以填「哥谭市」)
❌ 不能保证:模型选对了工具(应该用 get_weather 却用了 search)

所以你仍然需要参数校验、执行前检查、以及清晰的错误反馈
工具越多越不准
Function Calling 在少量工具的时候表现很好。但当工具数量上去之后,准确率会明显下降。

为什么会这样?里面有三个退化机制在起作用:
第一:注意力稀释。 工具越多,每个工具的描述在上下文里占的比例越小。模型的注意力被分散了,就像你同时看 50 个菜单页,反而不知道点什么。
第二:语义碰撞。 当工具多了,难免有功能相似的。search_files 和 find_files 有什么区别?read_file 和 get_file_content 呢?当工程量起来之后,这种重复工具的工具很容易出现,模型容易搞混。
第三:预算挤压。 每个工具的 JSON Schema 都要塞进上下文。46 个 GitHub MCP 工具就吃掉 42,000 Token——还没开始对话,上下文已经被占了一大块。留给用户消息和对话历史的空间就少了。
模型出现幻觉
工具选对了,参数格式也对了。但还有一个坑:参数的值可能是假的。
这就是所谓的「工具幻觉」。模型会:
伪造文件路径:你让它读
/src/utils.ts,它可能输出/src/helpers/utils.ts——这个路径不存在,是模型根据常见项目结构「猜」的编造 ID:让它删除某条记录,它可能编一个看起来像 UUID 但完全不存在的 ID
猜测 URL:让它访问某个 API,它可能拼一个看起来合理但并不存在的端点
这些幻觉的麻烦在于:约束解码拦不住它们。 约束解码只管类型——路径是 string 就放行,ID 是 string 就放行,URL 是 string 就放行。至于这个 string 的值是真是假,完全不在它的检查范围内。所以 JSON Schema 验证一路绿灯,但真正执行的时候必然会失败。
要应对这个问题,你需要在 Schema 设计和执行链路上都做防御。
第一:用 enum 约束可选值
如果参数的合法值是有限的,用 enum 而不是裸的 string。
JSON
{
"action": {
"type": "string",
"enum": ["read", "write", "delete"]
}
}
模型在约束解码下只能从这三个值里选,没法编造第四个。
第二:执行前校验
在真正调用函数之前,做一次业务级校验。比如文件路径:先检查文件是否存在;如果不存在,返回一个带有建议的错误信息。
第三:清晰的错误反馈
好的错误信息:
"文件 /src/helpers/utils.ts 不存在。当前目录下有 /src/utils.ts 和 /src/lib/helpers.ts,你要找的是哪个?"
坏的错误信息:
"ENOENT: no such file or directory"
前者给了模型足够的信息来纠正自己。后者让模型一脸懵——可能会换个路径再试,也可能换个完全不相关的策略。
Structured Output
你可能意识到了:Function Calling 本质上就是让模型输出一段符合特定格式的 JSON。
那如果我不需要调用函数,只是想让模型按固定格式输出呢?比如让模型打分:
JSON
{
"score": 8,
"issues": ["变量命名不规范", "缺少错误处理"],
"pass": true
}
这就是 Structured Output——跟 Function Calling 用的是同一套技术(约束解码),只是场景不同。
场景一:上下文摘要
Agent 对话跑了几十轮,上下文快满了,需要压缩。你不能让模型自由发挥写摘要——万一漏掉了关键信息呢?
用 Structured Output 强制输出格式:
JSON
{
"summary": "用户要求重构 auth 模块,已完成 login/logout,待处理 token refresh",
"key_decisions": ["使用 JWT 替代 session", "refresh token 存 httpOnly cookie"],
"pending_tasks": ["实现 token refresh 端点", "添加 CSRF 防护"],
"important_context": ["项目用 Next.js 14", "数据库是 PostgreSQL"]
}
强制模型按这个结构输出,每个字段都不能省。这样压缩后的摘要是结构化的,后续 Agent 可以精确地读取 pending_tasks 来继续工作,而不是从一段自然语言里「猜」还有什么没做完。
场景二:生成式 UI
这是 Structured Output 最酷的一个应用。让模型基于 JSON Schema 来描述 UI 组件:
JSON
{
"type": "card",
"title": "天气预报",
"children": [
{ "type": "text", "content": "北京 · 晴 · 25°C", "style": "heading" },
{ "type": "chart", "data": [22, 25, 28, 26, 24], "labels": ["周一", "周二", "周三", "周四", "周五"] },
{ "type": "button", "label": "查看详情", "action": "navigate:/weather/beijing" }
]
}
前端拿到这个 JSON,直接渲染成真实的 UI 组件。模型不用写 HTML/CSS,只需要按 Schema 描述「我想要什么」,渲染层负责「怎么画」。Vercel AI SDK 的 Generative UI 就是这个思路,基于 JSON 来渲染组件 UI。
约束解码在这里的价值特别大——保证输出的 JSON 一定能被前端解析,不会出现格式错误导致页面白屏。
场景三:信息提取
从非结构化文本里提取结构化数据:
JSON
{
"name": "张三",
"company": "某科技公司",
"role": "CTO",
"contact": "zhangsan@example.com"
}
这个场景也特别常见,一个 LLM Call 就能搞定,通过 Structure Output 拿到 JSON 结构化数据。
什么时候不该用 Structured Output
有个容易踩的坑:在模型需要自由推理的阶段强制 JSON 格式,反而会降低推理质量。
约束解码在每一步都在限制 Token 的选择范围。当模型需要深度思考、权衡多个方案、做复杂推理的时候,这些限制可能影响它「想清楚」的能力。
什么意思呢?想象你在写一篇文章,但每个段落都必须严格按固定模板来。你可能为了凑格式而牺牲了表达的准确性。
所以一般的做法是:
推理阶段(Agent 在思考下一步该做什么):不用 Structured Output,让模型自由生成文本
动作阶段(Agent 决定调用哪个工具、传什么参数):用 tool_use,约束输出格式
输出阶段(需要固定格式的结果):用 Structured Output
Claude 的 tool_use 设计天然就把这两个阶段分开了:模型可以在同一次回复里先输出一段自由文本(思考过程),再输出 tool_use 块(结构化的工具调用)。文本不受约束,JSON 严格约束——各取所需。
