怎么让模型的输出一定是合法 JSON?
本课一问:工具参数、结构化的最终答案,都是模型生成的一段文字。怎么保证它一定能被 JSON.parse、一定符合 schema?
这一课先不碰:参数分段流回来时什么时候解析(第 2 课);约束解码在语法层面怎么实现(A2 第 3 课 讲过 Codex 的 Lark 语法)。
从已有出发
A1 第 1 课留过一句话:「协议并没有让输出变得一定合法……两种方式的详细对照在 A8。」这里兑现。A1 第 3 课的「服务端丢弃调用」、A2 第 3 课的约束解码,是同一个问题的两头。
先自己设计:交给服务端解析,或者自己约定格式
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
两组真实运行并排:左边关闭思考的原生工具调用,第 7 轮起连着三次「调用被丢弃」;右边提示词约定 JSON 的运行,解析都成功,但第 3 轮写满上限被截断。
数据:本课项目 miniagent 的真实运行(src/data/project/agent-traces.json,2026-09-26 导出;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数取自服务端 usage;思考截取约 600 字、正文与工具输出约 900 字(保留头尾)。耗时受本机同时运行的其他任务影响,只能粗看;token 与轮数才可比。
两组运行差的不只是调用方式:右边开着思考,左边关着。这不是受控对照,只能说明「两种方式都会出问题,出的问题不一样」,不能比较谁更稳。
原生方式的问题是错误被吞掉:harness 看不到原文,只能笼统地提醒「重发」,真实运行里提醒两次一次都没救回来。自己解析的好处是能拿到原文、把具体错误告诉模型;坏处是格式是临时发明的,模型没专门训练过。要从源头保证合法,只能让服务端在采样时就按 schema 约束——下面分开看。
finish_reason: tool_calls 加空列表;这 7 次分布在 5 次运行里(T5 的流式运行又丢了 3 次)。提示词约定 JSON(自己解析):2 次运行、13 个调用,解析失败 0 次——可有 6 轮回复写满 4,096 token 被截断。两种方式都没有「保证合法」。 同一段参数,五种实现
试三件事:① 默认预设是「正则里的反斜杠没转义」:这是项目 README 记下的服务端丢弃原因(模型在改 _DAY_FIRST 的正则时写出 \d,JSON 里这不是合法转义),五行里哪几行是「静默出错」?② 换到「缺 required 的 old_string」:JSON 合法、schema 不合法——本机服务照单全收,本课项目在执行前拦下;③ 换到「多一个 schema 里没有的字段」:这份 schema 没写 additionalProperties: false,校验不拦;strict 模式要求必须写它。
CoreCoder 的那一行来自这段源码——第 2 课看过它按 index 分槽拼参数,问题在最后几行:
OpenCode 把这件事交给 AI SDK 的修复钩子:工具名大小写不对就改名;修不了的,改写成一个叫 invalid 的工具,把原工具名和错误信息写进参数,模型下一轮看得到:
改进:约束采样的两个开关
Anthropic 的结构化输出文档把它分成两个互补的机制 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条调研档案 R5A #7 满票(3-0);2026-09-26 在官方文档复核:两个机制的分工、约束采样、例外情况均为官方原文。 出处Anthropic Structured outputs(2026-09-26 抓取) ↗需联网 :
JSON 输出:output_config.format | 严格工具:工具定义上的 strict: true | |
|---|---|---|
| 约束什么 | 模型这一次回复的正文:必须是符合给定 schema 的 JSON | 模型调用工具时:工具名必须在清单里,参数必须符合该工具的 schema |
| 用在 agent 的哪里 | 最后交结果、子 agent 交回结构化摘要、评测时让判官输出分数 | 每一轮的工具调用——本课一直在处理的那段参数文字 |
| 怎么做到 | 按 schema 编译出语法,采样时逐 token 约束(约束解码,A2 第 3 课) | 同上 |
| 文档写明的例外 | 拒答(stop_reason: refusal)、写到 max_tokens 被截断时可能不合 schema;字符串 enum 的大小写不保证 | 同左 |
| 代价 | 首次用某个 schema 要编译语法(缓存 24 小时);会多注入一段系统提示,改动它会让 prompt 缓存失效 | schema 只支持 JSON Schema 的一个子集;对象必须写 additionalProperties: false |
两个开关管的不是一件事:一个管模型说什么(最终回复的形状),一个管模型怎么调用(工具名与参数)。只开 JSON 输出,工具调用的参数照样可能坏;只开严格工具,最后交回来的总结照样是自由文本。而且两者都有文档写明的例外——截断、拒答——所以 harness 端的「解析失败就回报错误」仍然不能省。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 模型生成的参数或答案能被程序可靠地解析、符合约定的结构 |
| ② 为什么可能有效? | 采样时按 schema 编译的语法约束,每一步只允许合法的下一个 token,生成出来的文字必然合法(截断、拒答除外) 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条算法性质:约束解码只从语法允许的 token 里采样,完整生成的文本一定属于该语法;例外(max_tokens 截断、拒答)是文档写明的边界。 出处待补 |
| ③ 实验上呢? | 本课项目没有接任何带约束采样的服务;真实运行只说明了两种不约束的方式各有各的失败(服务端丢弃 7 次 / 截断 6 轮) 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:本机 Qwen3-8B,两组方式的运行配置不同(一个开思考一个关),次数少,不能比较优劣。 出处待补 |
演进地图:你在这里
这一课挂在演进图「加一层抽象」那一步旁边:它处理的是各家线格式里同一个位置——调用参数那段文字。
- 起点 写死一家的线格式CoreCoder(OpenAI 兼容 + litellm 兜底)、本课项目 miniagent(标准库 urllib)解决了最短的路:CoreCoder 的
llm.py净 259 行,换 provider 只改 base URL 与 key。卡在同一段带工具调用的对话,换成 Anthropic Messages,9 个语义位置里 9 个换了路径或类型(参数从字符串变成对象,5 条消息变 3 条);Codex 干脆不认 Chat:配置wire_api = "chat"直接报错。 - Pi 自建 pi-ai · OpenCode 用 Vercel AI SDK · Codex 只留 Responses解决了Pi 把 10 种协议与 40 个 provider 做成正交的两维;OpenCode 挂了 19 个
@ai-sdk/*依赖;Codex 的WireApi只剩 1 个变体。卡在抽象层得替每一家补差异:OpenCode 逐 provider 改写消息的transform.ts有 1,909 行,还给 8 个@ai-sdk包打了补丁;而且抽象挡不住流式与缓存的细节。 - 第二步 流式:按调用分别累积,结束才解析Anthropic 流式文档 · OpenAI function-calling 流式指南 · Codex 只收 output_item.done解决了开思考的一轮,第一帧 125.9 ms 就到;整包等回复的话,正文要等到 6,631 ms、工具调用 6,798 ms。卡在流式只让回复来得早,不让它便宜:T5 的 9 轮每轮整份重发,输入累计 15,921 token,是最后一轮 2,389 的 6.7 倍。
- 第三步 缓存感知的账单 + 稳定前缀Anthropic / OpenAI prompt caching · Codex prompt_cache_key · OpenCode applyCaching解决了按 usage 四段计价:T5 在 Claude Sonnet 5 价目下 $0.0140;CoreCoder 按全价估 $0.0371,是它的 2.64 倍。卡在缓存只省输入:开思考的 pd-v2-unlimited-s3 账单里输出占 94%;前缀里一处变、后面全失效——T10 刷新组第 7 轮 system 重建,命中从 2,072 掉到 294。(→ A9:能跑、能算账了,怎么知道它好不好?)
数字口径:pin 源码计数(Codex 8cabf5a、OpenCode v1.18.32、Pi 31b513e3、CoreCoder v0.7.0)、项目录制 T5 / T10 与逐轮 usage(本机 Qwen3-8B,token 按它的 tokenizer 数)、官方价格页快照 2026-09-26;由 a08-model-access.json 构建时现算。
一句话
先停在这里:回主线第 2 课,看参数分段流回来时的解析时机。
再深一层:把 miniagent 的工具 schema 都补上 additionalProperties: false,再跑一遍 tests.test_tools:哪些现有用例依赖了「多传字段也不报错」?
下一个缺口
回主线:回复是一点点流回来的,工具参数什么时候才能解析? → 第 2 课
你可能会问
- 「本机的 mlx_lm.server 支持约束采样吗?」 本课没有核。项目 README 记录的现象是:它用普通解析器切出
<tool_call>里的 JSON,失败就丢弃。 - 「Pi 呢?」 勘误表记录:Pi 0.86.0 起内置工具默认「严格优先」的 JSON Schema 约束采样(pin 31b513e3 之后的版本),约束解码不再是 Codex 独有;本课 pin 版本上没有展开。
- 「提醒模型重发,为什么救不回来?」 原生方式下 harness 看不到坏掉的原文,只能笼统地说「你的 JSON 坏了」;模型重写同一段正则,又写出同样的
\d。本课项目的提醒文本在loop.py的DROPPED_NUDGE。