一个所有人都踩过的坑
你写了个 prompt 让 LLM 输出 JSON。Examples 给了三个,instruction 写得清清楚楚:"Output ONLY valid JSON, no markdown, no commentary"。
99 次它老实照做。第 100 次它给你来一句:"Here's the JSON you requested:" 后面跟一段 markdown 代码块。
或者更恶心的------JSON 倒是出了,但少了个逗号、多了个尾随逗号、字符串里有未转义的引号,
json.loads()直接炸。你加 retry,加 regex 兜底,加更狠的 prompt。问题永远不能彻底根除。
因为你解决问题的层级错了。
Tool Use 系统有两层,分开看才能想清楚
scss
[模型] → 生成 tool_use 参数(概率性)
↓
[API 层 schema 验证] → 校验通过(deterministic)
↓
[你的代码] → 拿到符合 schema 的 JSON
这两层做的事完全不一样。把它们分清楚,是理解所有 LLM 结构化输出问题的钥匙。
Deterministic 的部分:API 给你的硬保证
当你用 tool_use 而不是 prompt-based JSON 时,下面这些东西API 在底层强制保证:
- 返回的类型结构 ------一定是
tool_useblock,一定包含你定义的字段 - 字段类型 ------你定义
age: integer,绝不会收到字符串 - enum 约束 ------你定义
status: enum["pending", "approved"],绝不会收到第三个值 - required 字段一定存在(这点要小心,下面会讲为什么)
- JSON 语法合法------不会出现缺括号、引号、逗号这种 prompt-based JSON 最常翻车的事故
API 在模型生成 tool_use 时强制 输出匹配 schema 的结构。如果模型试图输出不合规的内容,API 会限制 token 选择空间或重试(实现细节因模型而异),最终保证你拿到的 tool_use 一定符合 schema。这是 constrained sampling 或类似机制在底层做的事。
这是结构层的 deterministic 保证。你拿到的东西在格式上一定能 parse,一定符合契约。
Probabilistic 的部分:LLM 永远的命门
但有些东西,schema 锁不住------它们活在语义这一层,不是结构层:
字段值的语义对错。 age: 25 是合法 integer,但申请人真实年龄可能是 35。schema 不知道也管不了。
字段值的存在性。 如果字段是 required 但源文档里没这个信息,模型会编造一个值来满足 required 约束------这是大坑。schema 不会让模型说"我不知道",它只会让模型说"我必须给你一个 integer,那就给个看起来合理的"。这就是 fabrication 风险的根源。
跨字段一致性。 start_date: "2025-01-01" 和 end_date: "2024-12-01" 各自合法,但语义上矛盾------结束日期早于开始日期。schema 拦不住。
复杂条件依赖。 比如"如果 employment_status=student,则 employer 字段应为空"------这种 schema 表达不了。
用一个例子把边界画出来
json
tool_schema = {
"name": "extract_application",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 18},
"status": {"type": "string",
"enum": ["full_time", "part_time", "contract"]}
},
"required": ["name", "age", "status"]
}
}
Deterministic 给你的保证:
✅ 一定是 JSON object ✅ name 一定是 string ✅ age 一定是 ≥ 18 的 integer ✅ status 一定是三个 enum 值之一 ✅ 三个字段一定都存在
Deterministic 给不了的保证:
❌ name 是不是真的从文档里读出来的(可能编造一个 "John Smith") ❌ age: 25 是不是申请人真实年龄 ❌ 文档里写 "intern" 时模型该选哪个 enum 值(可能瞎选 part_time) ❌ 文档没写 age 时------模型会编一个来满足 required 约束,绝不会拒绝输出
值得反复盯着看这两组对比。 上半部分是 API 帮你扛的,下半部分是你自己要扛的。
为什么 prompt-based JSON 是双重赌博
把 tool_use 跟其他常见做法摆在一起对比,你会看到一个很扎心的事实:
| 做法 | JSON 合法性 | Schema 合规 | 字段值正确 |
|---|---|---|---|
| regex + retry | 概率性 | 概率性 | 概率性 |
| prompt + examples | 概率性 | 概率性 | 概率性 |
| 两步法(先文本后格式化) | 第一步概率性,第二步概率性 | 概率性 | 错误在两步间放大 |
| tool_use | Deterministic | Deterministic | 概率性(无解,所有方案都一样) |
tool_use 把能 deterministic 的部分 全锁死了。剩下的概率性部分(值的正确性)是 LLM 本质问题,任何方案都解决不了 ------但 tool_use 至少保证了一件事:当值正确时,结构一定正确。
其他三个方案有一个额外的失败模式: "值对了但格式炸了" 。模型明明把名字抽对了、年龄填对了,结果 JSON 多了个逗号,整条链路废掉。这种失败本来可以避免,prompt-based JSON 自找的。
Prompt-based JSON 等于在 LLM 本来就够难的概率性问题上,又叠加了一层完全可以消除的结构性失败模式。
"Most Reliable" 不等于 "Guaranteed Correct"
这里有个常被混淆的细节。tool_use 是最可靠 的抽取方案,但不是绝对可靠。这两个词不一样:
- Most reliable = 在所有方案中失败模式最少
- Guaranteed correct = 永远不出错(任何 LLM 系统都不可能做到)
tool_use 消除了结构性 失败(malformed JSON、错字段名、错类型、漏字段),保留了语义性失败(值不对)。
把这件事讲清楚很重要------很多人误以为切到 tool_use 就万事大吉了,实际上你只是把战场清理出来,让你只需要处理真正难的那部分问题。
剩下那部分语义错误怎么办
结构层锁死之后,剩下的语义错误要用别的招数处理:
Optional / nullable 字段,消除 fabrication 压力。 如果 age 在源文档里可能没有,就不要把它设成 required。让模型可以返回 null,比让它编一个数字强一百倍。这是治 fabrication 的根本药。
Retry with error feedback,处理可恢复的语义错误。 抽出来的数字明显不对(比如 net 比 gross 大)?把错误信号回灌给模型让它重抽。这套路适合那些有明确校验规则的字段。
Human review routing,处理不可恢复的语义错误。 模型在不熟悉的语言、不熟悉的格式上信心很低时,与其硬抽不如直接路由到人工。Schema 上挂一个 confidence 字段,低于阈值就走人工。
Calculated vs stated 双字段,让一致性约束变成信号而非编造压力。 比如一个发票同时有"金额"和"单价 × 数量"两个来源,让模型分别抽出来,下游对账。这比让模型自己保证一致性可靠得多------你把矛盾点显式化了。
这套思路放大了看,是 agent 系统的基础姿势
结构 vs 语义的分工不只是 JSON 抽取的事,它是 agentic 系统设计的基础。
比如一个多 agent 系统里,SQLAgent 的输出能被下游 NLSummarizer 可靠消费,靠的是 tool_use 锁定的结构契约 ------SQL 字符串确实是 string,确实满足 required 字段。但 SQLAgent 是否真的写对了 SQL(join 对不对、where 条件对不对),那是 evaluation agent 或 critic agent 的活,不是 schema 能解决的。
每一层都只解决它该解决的问题:
- Schema 解决结构
- Constrained decoding 解决格式
- Retry / critic 解决可恢复语义错误
- Human review 解决不可恢复语义错误
把这几层混在一起想,就永远在打地鼠。分开,每一层用合适的工具,整个系统就清爽了。
一句话总结
tool_use 给的是结构层面的 deterministic 保证------schema 合规、类型正确、必填存在、JSON 合法。
值的语义正确性是 LLM 概率本质决定的,永远是概率性的。
但 tool_use 之所以是"most reliable",因为它把能锁死的全锁死了 ,把无法锁死的部分暴露在更高层级去处理------而 prompt-based JSON 在 LLM 已经够难的概率性问题上,又叠加了一层完全可以消除的结构性失败模式。
这就是为什么遇到结构化输出问题,答案几乎永远是 tool_use 而不是更好的 prompt。Prompt 调得再精巧,也是在概率性的赌场里下注;tool_use 是直接把规则刻进游戏引擎里。