生成结构、产品解析与模型协议不一致:追问三次发布才稳定¶
这个图片解读产品的最终输出是结构化 JSON:要么是答案,要么是一条向用户追问的问题。模型按生成结构输出,产品按自己的类型解析,内核负责提交。2026 年 9 月 12 日到 15 日,追问这个动作先后在三处各失败一次,前后发了三版才稳定,按时间线记。
第一层:生成结构允许 null,产品类型不接受¶
9 月 12 日只读审查近期生产失败时发现。当天一起真实失败里,一侧候选超时,另一侧模型返回的补问字段带非法 null,双边都没成。顺着这条失败核对代码,看到同一批字段在三处定义不一致:
两端各自成立,合起来是断的:模型按生成结构输出 null 完全合规,解析端按产品类型拒绝,模型会被自己的解析器拒绝。
确认分两步。第一步本地复现:用真实的内核参数校验和真实的产品解析函数跑同一个值,null 格式校验端接受、解析端拒绝,规范值(空数组、0)两端都接受,复现脚本和输出都存了档,排除了“模型乱输出”。第二步排除版本差异:服务器上的生成结构文件与本地 SHA256 一致,线上跑的就是这版代码。
修复统一两端的有效取值,保留选项和 ID 的校验。没有采用“加一句提示词让模型别返回 null”的做法:生成结构允许 null,模型返回 null 是按规则办事,提示词改不了合同。旧空值格式现在作为可纠正的参数错误返回给模型,让它在同一轮内修正,不再整轮失败。真实内核终止工具测试验证了这个行为:旧空值格式返回可纠正工具错误,合法补问仍匹配服务端已签发的请求。第一次直接推生产被分支保护规则拒绝,改走 PR 合并后正常发布。
同一类缺口后来又出过一次:9 月 15 日排查发现产品发给模型的扁平生成结构允许答案为 null,解析端要求非空正文;一起真实失败里模型返回了只有类型字段、没有内容的追问——解析器把 null 清掉后只剩类型字段,产品校验失败。此后把这类检查改成直接比对生成结构和产品解析两端的定义,只修已知位置挡不住下一处。
第二层:模型没调提交工具,文本被直接交给 JSON 解析¶
9 月 15 日首次发布。线上首轮解读 57.06 秒完成,追问 4.12 秒就失败。内核的工具模式仍允许模型直接返回普通文本:追问时模型没有调用提交工具、直接输出了文本,产品把这段文本交给 JSON 解析,失败。首轮走的是已验证的路径,覆盖不到追问这种失败模式,这次失败是真实发布才暴露的,失败样本存档。
第一个补丁想强制模型必须调用提交工具。拿真实接口验证:该模型的思考模式直接以 400 拒绝这种参数组合,这个方案没有部署。退回的方案是 kernel 0.6.5 保持接口支持的 auto 模式,在原有 max_turns 循环内反馈“缺少提交”,让模型在同一轮内补调提交工具;草稿不作为完成答案持久化。不为此新增内核重试框架。
本地验证:产品 88 项测试、干净环境安装后再跑 88 项通过;内核 315 项执行(3 项因缺数据库 DSN 跳过)、WebUI 和打包冒烟通过;真实本地首轮 32 秒、无资源字段追问 4 秒完成;总预算压到 1 秒时 runtime 真实收到取消、终态 incomplete。通过后第二次发布。
第三层:被拒草稿回传,破坏模型的续接协议¶
第二次发布后,线上首轮 34.59 秒正常,追问在连续纠正几轮后触发上游协议错误(400,指向 reasoning_content 字段)。排查先固定变量:失败样本原样保留,再核对模型实际收到的续接内容,确认被拒草稿被原样回传,失败与纠正上下文相关。这个模型是思考模式,多轮续接对 reasoning_content 有自己的约定;被拒草稿混进续接后约定被破坏,上游拒绝请求。
最终补丁 kernel 0.6.6 去掉被拒草稿的回传,只保留系统纠正信息,调用次数和时间预算不变。验证:产品 88 项、干净安装 88 项通过;内核 315 项执行(3 项跳过),装好 wheel 后再跑 149 项内核和服务端测试通过。真实本地首轮 32 秒,四次追问分别 4、24、4、12 秒全部完成;其中一次经过 11 次模型调用后成功,用量持续累计,整轮重试为 0;1 秒预算取消也通过。第三次发布后,四项公网实测全部完成:新任务约 35 秒,无资源字段的追问约 12 秒,两个历史失败会话各约 4 秒重放成功。
三层失败的共同点:本地类型一致、首轮请求成功,都不代表链路一致。生成结构与产品解析要直接比对两端定义;提交行为要用真实接口验证,“强制调用工具”这种组合只有上游能告诉你支不支持;纠正反馈要按目标模型的上下文协议构造,不能把内部状态原样塞回去。