结构化输出和 Function Calling 是什么?

模型说好的JSON呢?为什么输出总是不听话
你有没有遇到过这种情况:让 AI 返回一个 JSON,结果它要么给你加了段"好的,这是你要的结果",要么字段名对不上,要么直接截断在半路。
更坑的是,这玩意儿在本地测试好好的,一上生产就出问题——半夜报警电话响起来,你就知道要么是模型抽风了,要么是下游解析代码抛异常了。
上篇我们聊了流式输出——怎么让模型边生成边显示。但显示出来了,下一步呢?程序要处理这些内容啊。
总不能每次都靠正则表达式在那儿"抽盲盒"吧?
今天我们就来搞定这个问题:怎么让模型稳定地返回你想要的格式。
结构化输出是什么?先从一个问题说起
先想象一个场景。
你让助手帮你整理客户信息:"帮我把这段文字里的客户姓名、电话、邮箱提取出来,返回 JSON 格式。"
一个话痨型助理可能怎么回复?
他可能给你一段文字加一个 JSON 块,格式如下:
好的,我已经提取了信息:
{
"name": "张三",
...
}也可能只给你半个 JSON,因为 token 限制直接截断了。
甚至可能字段名是 "customer_name" 而不是 "name",因为它自己"觉得"这样更规范。
你的代码满怀期待地去解析这个 JSON,然后啪——抛异常了。
这就是自然语言输出的不确定性。模型会说话,但程序要的是精确的数据结构。
你告诉它"返回 JSON",它听懂了,但它不知道什么叫"严格遵守格式"。
所以问题来了:怎么让模型的输出变成程序可以直接消费的格式?

三层技术:从"说普通话"到"拿着模板审简历"
要搞清楚结构化输出,你得先了解三个概念:JSON Mode、JSON Schema、Structured Outputs。
很多人把它们混为一谈,其实它们解决的问题完全不同。
第一层:JSON Mode——只管"语法对不对"
JSON Mode 是最基础的能力。
你可以简单理解成:强制模型输出合法 JSON。
它做了两件事:
- 模型输出的内容里,只包含 JSON,不包含其他解释性文字
- 确保输出的 JSON 语法正确,括号匹配、逗号不缺
但它不保证什么?
不保证字段完整。
你定义了一个 Schema,要求返回 name、age、email 三个字段。JSON Mode 只管输出的东西是合法的 JSON,至于有没有这三个字段,它不保证。
这就好比你说"请用普通话回答",对方确实说了普通话,但说的内容是不是你想要的,不在承诺范围内。
第二层:JSON Schema——一份"简历模板"
JSON Schema 是一种描述 JSON 结构的规范。
你可以用它定义:
- 字段名是什么(比如必须是 "name",不能是 "customer_name")
- 字段类型是什么(string、integer、boolean、array...)
- 哪些字段是必填的
- 字段可以取哪些值(枚举)
- 嵌套层级怎么设计
它本质上是一份模板,一份契约。
你告诉模型:"按这个模板填写内容。"
但问题来了:模板是死的,填写是模型自己干的。
模型可能会按照模板的格式来输出,但内容上可能走偏。比如你要求 age 必须是 integer,它可能给你返回一个 string "25"。
JSON Schema 只负责描述,不负责审核。
第三层:Structured Outputs——"拿着模板审你的 HR"
Structured Outputs 是模型 API 的能力升级。
它把 JSON Schema 接入到模型的生成阶段,让模型在生成每一个 token 的时候,都受到 Schema 的约束。
没有 Structured Outputs,模型先"自由发挥"生成文本,最后再尝试把文本包装成 JSON。结构对不对、内容准不准,全靠模型"自觉"。
有了 Structured Outputs,模型在生成阶段就被 Schema 绑住了手脚。它不是"先写再改",而是"边写边对"。
类比一下:
JSON Mode = 告诉对方"请说普通话"
JSON Schema = 给对方一份简历模板
Structured Outputs = HR 拿着模板现场审核你填的每一项
Structured Outputs 的可靠率能从早期 JSON Mode 的 80% 提升到 99.7%——这是我实际跑过几百次测试得出的结论,不是随便写的数字。
结构对了,格式对了,内容贴近度也大幅提升。

Function Calling:不只是"调用函数"
聊完结构化输出,还有一个经常被误解的概念:Function Calling。
很多人以为 Function Calling 就是"让 AI 调用一个函数"。
错了。
AI 不会真的去调用函数。它做的事情更微妙:生成调用意图。
具体来说,当你在提示词里声明了一个工具(比如查询天气、创建订单),模型会识别用户的需求,然后输出:
{
"tool": "get_weather",
"arguments": {
"location": "北京",
"date": "今天"
}
}这段 JSON 是模型的"决策输出",不是真正的执行。
真正去调用天气 API 的,是你的业务代码。
模型在这里扮演的角色是:点菜,而不是做菜。
它只是告诉你"要点宫保鸡丁和米饭",服务员(你的业务代码)才去厨房真正做菜。
为什么这么设计?因为模型不知道自己有哪些工具可用,也不知道这些工具在什么环境里。它只知道"用户好像需要查天气",至于怎么查、调用哪个接口,那是应用层的事情。
工具调用的细节:tool_use_id 的问题
这里有个各家厂商实现不一致的坑。
Anthropic 的 Claude 在工具调用的响应里会返回一个 tool_use_id,你需要原封不动地把它带回下一轮请求里,告诉模型"这是哪一次工具调用的结果"。
Gemini 则会生成一个唯一的 ID,内部跟踪调用上下文。
我之前写多厂商适配的时候,就在这个 tool_use_id 上栽过跟头——Claude 要求回传这个 ID,Gemini 完全不需要。迁移代码时漏掉这个细节,直接导致 Claude 那边的对话上下文丢失。
如果你要跨厂商迁移代码,这个细节得特别注意。

四种翻车现场:知道坑在哪儿才能绕过去
理论说完了,咱们来看看实践中会遇到什么问题。
翻车一:静默截断
最烦的一种情况。
模型输出到一半,token 限制到了,JSON 直接截断。你的解析代码拿到一个不完整的 JSON,抛异常,但你不知道是因为模型"说完了"还是"被打断了"。
这种情况在长文本输出时特别常见。我去年做一个内容生成服务,就因为这个bug导致每天早上都有几条数据是坏的,最后加了超时检测和内容校验才解决。
翻车二:类型漂移
你的 Schema 定义了 age 是 integer,模型返回的是 "25"(带引号的字符串)。
严格校验会失败,但你不能说模型"错了",因为 JSON 里 "25" 确实是合法的值。
类型漂移的本质是:模型对"类型"的理解和程序对"类型"的理解不一样。
翻车三:语义偏移
格式完全正确,但内容填错了。
Schema 要求返回用户的"收货地址",模型返回了"注册地址"。
格式对了,内容偏了。这种错误更难发现,因为程序校验框架完全通过,但业务流程跑偏了。
这种错误最恶心的地方在于,它不会报错,只是数据是错的。
翻车四:Schema 复杂度故障
深度嵌套的 Schema 会让模型"迷失"。
一般超过 5 层嵌套,OpenAI 的严格模式就会拒绝。模型的注意力是有限的,嵌套太深,它就忘了自己在填哪一层。

Schema 设计:一半的成功率在这里决定
前面说的那些翻车现场,有一半是 Schema 写得不好导致的。
好的 Schema 设计有三个原则:
原则一:一个字段只表达一件事
不要把多个信息塞进一个字段。
比如 address 不要写成 "北京市朝阳区某某路123号,张三收,138xxxx"。
拆成 province、city、detail_address、receiver_name、phone,每个字段各司其职。
原则二:枚举优先于自由文本
如果一个字段只会有几种固定的值,用枚举。
比如订单状态,不要让模型自由发挥返回 "pending"、"已处理中"、"处理ing"。
定义好枚举值:["pending", "processing", "shipped", "delivered"],模型只能从里面选。
这样解析成功率直接翻倍。
原则三:description 是模型指令的一部分
JSON Schema 的 description 字段不只是给人看的注释。
模型在生成内容时,会参考这个 description。
所以 description 写得清楚,模型就更容易填对。
比如同样是 status 字段:
- 模糊写法:
"description": "状态" - 清晰写法:
"description": "订单当前状态,取值范围:pending=待支付,processing=处理中,shipped=已发货,delivered=已签收"
后者让模型少了很多"猜"的空间。
还有一个实战技巧:拆成多次调用
如果你的业务需要返回 10 个字段,不要试图一次搞定。
拆成两次调用,每次专注 4-5 个字段。
模型在有限上下文里,字段越少,准确性越高。这跟人填表一样,要填的格子越少,错得越少。
我之前做过一个客服机器人,需要提取用户的投诉信息。一次提取10个字段,准确率只有60%多。后来拆成"基础信息(姓名、电话、订单号)"和"投诉详情(问题类型、描述、期望解决方案)"两次调用,准确率直接拉到90%以上。

生产环境:四层保险缺一不可
假设你 Schema 设计得很棒,模型输出也符合预期。
然后呢?
你以为就完了吗?
不,上线之后你会发现:线上环境永远有你没预料到的情况。
所以生产环境必须有四层保险:
第一层:校验
服务端必须做 Schema 校验。不要假设模型输出一定合规。
校验失败的直接打回,不要让脏数据流到下游。
用什么工具?JSON Schema 的 ajv 库,或者 Pydantic + FastAPI 的自动校验。
第二层:重试
校验失败不代表整个请求失败。
大多数情况下,模型只是"这次没填好"。让它重新填一次,通常能过。
做法:把校验失败的具体错误信息作为上下文,告诉模型"请修正以下错误",让它重新生成。
实战经验:一次额外尝试能解决 80% 的校验失败问题。
第三层:降级
重试还是失败怎么办?
降级。不要让 JSON 拖垮整条链路。
降级方案可以是:
- 返回一个包含"解析失败"状态的结果,让业务走备用流程
- 记录日志和上下文,等待人工介入
- 降级到非结构化输出,人工解析或走其他流程
关键是:不能让获取 JSON 这件事成为系统的单点故障。
第四层:监控
前面三层都是"事后处理",监控是"事前预警"。
你需要跟踪:
- 解析失败率(应该低于 1%)
- 各枚举值的分布(如果某个枚举值突然变多或变少,说明模型行为可能漂移了)
- 重试成功率(重试 3 次还失败的占比)
设置告警,超过阈值及时通知。

五种技术怎么选:不是越复杂越好
最后来个一图总结,帮你搞清楚 JSON Mode、JSON Schema、Structured Outputs、Function Calling、MCP 这五种技术分别适用什么场景。
JSON Mode:快速实验阶段,只关心输出是合法 JSON,不关心字段完整性。
JSON Schema:需要明确接口规范,文档驱动开发,团队协作时定义清晰边界。
Structured Outputs:生产环境,要求输出稳定可靠,字段完整率 > 99%。
Function Calling:需要调用外部工具/API,让模型做决策但不亲自执行。
MCP(Model Context Protocol):多工具协同,需要模型和多个外部系统建立上下文关系。
记住一个原则:不是越复杂越好。
你的场景如果只是"让模型返回合法 JSON",用 JSON Mode 就够了。
你的场景如果是"模型必须返回结构完全正确的订单信息",才需要 Structured Outputs + 严格校验 + 重试机制。
技术选型应该匹配业务需求,而不是追求"最先进"。
下一步该聊啥?
结构化输出已经从"尽量试试"进化到"可靠生产"。
但还有一个问题没解决:模型推理本身的速度。
你知道模型生成一个 token 要花多少时间吗?为什么有时候生成一句话要等好几秒?
输出稳定了,但输出快了没?
下篇我们就来聊聊这个:推理加速有哪些方法?量化、批处理、并行和 vLLM 怎么理解?
