Agent 的 JSON Mode 我弃了——三个项目全翻车,改用 function calling 做结构化输出后稳了

📅 发布时间:2026/7/13 20:55:47
Agent 的 JSON Mode 我弃了——三个项目全翻车,改用 function calling 做结构化输出后稳了 我在做的 App 叫雷达鸭后端是 uniCloud 撑的。最近在重构一个数据汇总 Agent 的时候又把 JSON Mode 这个坑踩了一遍。这算是第三次了我觉得有必要写篇文章把这事说清楚。先扔结论如果你的 Agent 既要做结构化输出又要调用外部工具别用 JSON Mode。不是它不能用是它跟工具调用一起用的时候事情会变得很诡异。项目一客服 Agent去年我们做了一个客服自动回复 Agent。需求很简单——用户问一个问题Agent 分析意图然后返回一个标准格式的 JSON里面包含回复内容、推荐商品标签、是否转人工三个字段。我们一开始用了 OpenAI 的response_format: { type: json_object }。前两周跑得好好的直到有一天运营同学反馈说后台收到了一堆解析失败的错误。我查日志发现 LLM 返回的 JSON 里偶尔会出现undefined作为字段值或者干脆把 markdown 代码块也包进去了。最离谱的一次模型返回的 JSON 后面还跟着一大段解释性文字说为什么这么回复。json_object模式按说应该只返回 JSON可模型就是会在某些不确定的情况下夹带私货。我加了后处理——先正则提取 JSON 片段再解析。这能兜住 90% 的情况但剩下的 10% 会在凌晨三点把你叫起来。项目二数据汇总 Agent第二个项目就是开头提到的那个数据汇总 Agent。这个 Agent 需要先从数据库拉数据工具调用然后生成一份分析报告结构化输出。我们一开始的设计是第一步用 function calling 查数据库第二步把结果拼进 prompt再用 JSON Mode 要求模型输出报告。问题出在第二步。当第一步返回的数据量比较大时模型在 JSON Mode 下的输出质量明显下降。字段会漏嵌套数组的格式会歪甚至直接返回不完整的 JSON。我怀疑是 JSON Mode 的约束和上下文长度产生了某种冲突——模型既要处理大量输入数据又要严格遵守输出格式两者打架。我试过缩短输入数据但那样报告的质量又不够。也试过把报告拆成多段生成但上下文衔接又是一堆问题。那段时间我们每天都在修解析器而不是在做业务。项目三复杂配置生成第三次是一个页面配置生成模块。这个配置是嵌套三层的数据结构包含条件判断、数组、映射关系。JSON Mode 直接不支持这种复杂度的约束。OpenAI 的文档写得明白json_object只能保证输出是合法 JSON不保证结构一定符合你的 schema。我们试过用 JSON Schema 约束response_format: { type: json_schema, ... }但发现了三个问题。一是兼容性问题。不是所有模型和版本都支持json_schema。我们切换模型的时候这部分代码要重写。二是调试困难。当输出不符合 schema 时你根本不知道模型是在哪个字段上偏离了。错误信息就是一句JSON parse error你得把原始输出打印出来肉眼对比。三是它跟工具调用不能混用。一个请求里你不能既让模型调用工具又要求它用 JSON Mode 返回最终结果。至少 OpenAI 的 API 是这样设计的。这意味着如果你的 Agent 需要先用工具查数据再生成报告你得拆成两次请求延迟和成本都翻倍。转向伪工具方案第三次翻车之后我们团队开会有人提了一个想法既然 function calling 的输出格式那么稳定为什么不把结构化输出也伪装成一个工具调用具体做法就是定义一个伪工具比如叫format_response它的参数 schema 就是你想要的输出结构。然后你把这个工具放进 tools 列表里在 system prompt 里告诉模型完成任务后调用 format_response 提交你的最终结果。我一开始是怀疑的。这听起来像是一种 hack不太正经。但试了三天之后我把之前写的 JSON 解析器全删了。原因是 function calling 的格式稳定性是由模型训练保证的。tool_calls 的arguments字段虽然是个字符串但它的结构一致性比 JSON Mode 高得多。而且function calling 的 schema 是显式声明的模型偏离 schema 的概率明显低于 JSON Mode 下偏离隐式约束的概率。真正有用的是伪工具可以跟真正的工具调用混在一起。模型可以先调用query_database查数据再调用format_response提交报告全部在一个请求流里完成。不需要拆两次请求延迟从 4-6 秒降到 2-3 秒。代码示例我们现在的 Agent 核心结构大概长这样。伪工具的定义// tools/responseFormatter.tsexportconstformatResponseTool{type:functionasconst,function:{name:format_response,description:将最终结果按指定格式提交。分析完成后必须调用此工具。,parameters:{type:object,properties:{reply:{type:string,description:给用户的回复内容},tags:{type:array,items:{type:string},description:关联标签},transfer:{type:boolean,description:是否转人工},confidence:{type:number,description:置信度 0-1}},required:[reply,transfer]}}};真正的工具混在一起// tools/index.tsexportconstallTools[queryDatabaseTool,// 真正的数据查询sendNotificationTool,// 真正的消息推送formatResponseTool,// 伪工具结构化输出];Agent 执行层的关键逻辑// agent/executor.tsinterfaceAgentResult{done:boolean;result?:Recordstring,unknown;}asyncfunctionrunAgent(userInput:string):PromiseAgentResult{constmessages[{role:systemasconst,content:systemPrompt},{role:userasconst,content:userInput}];// 循环执行直到模型调用 format_responsewhile(true){constresponseawaitllm.chat.completions.create({model:gpt-4o,messages,tools:allTools,tool_choice:auto});constmsgresponse.choices[0].message;messages.push(msg);if(msg.tool_calls){for(constcallofmsg.tool_calls){if(call.function.nameformat_response){// 命中伪工具解析并返回constargsJSON.parse(call.function.arguments);return{done:true,result:args};}// 真正的工具执行constresultawaitexecuteRealTool(call.function.name,call.function.arguments);messages.push({role:toolasconst,tool_call_id:call.id,content:JSON.stringify(result)});}}}}System prompt 里的关键一句constsystemPrompt你是客服助手。分析用户需求后先调用相关工具获取数据完成后必须调用 format_response 提交你的回复。不要直接输出 JSON。;两周后的对比改用伪工具方案跑了两周对比之前的 JSON Mode有几个明显的变化。解析失败率从每周十几次降到零。不是因为我们的解析器变强了是因为模型输出格式本身就稳了。function calling 的arguments字段在训练时被强化过很少出现多余的 markdown 包裹或者字段类型错乱。延迟降了差不多一半。以前查数据和出报告要拆两次请求现在一次请求里模型自己决定先查后出。省了往返时间也省了一次 API 调用费用。调试变简单了。如果输出不符合预期我直接看 tool_calls 里 format_response 的 arguments 就行。不需要在一堆文本里抠 JSON 片段。当然这个方案也不是银弹。如果模型没调用 format_response 就回复了纯文本你得有兜底的逻辑。我们在代码里加了一个检测如果一轮循环结束没有 tool_calls就把模型的回复内容当成reply字段confidence 强制设为 0.3然后返回。这个兜底两周触发过一次原因是 prompt 里忘了强调必须调用。还有一个细节伪工具的 description 要写得很明确。我们之前写得太短模型偶尔会在没完成分析的时候就调用 format_response。后来把 description 改成了分析完成后必须调用此工具提交最终结果这个问题就消失了。说明描述里的时态词和强制词对模型行为确实有影响。说实话我到现在也没想明白为什么 JSON Mode 在简单场景下表现不错一到复杂场景就频繁翻车。可能是训练数据里的 function calling 样本质量更高也可能是 JSON Mode 的约束在推理阶段被当成了建议而不是规则。不管原因是什么我们的经验是伪工具方案在实战中更可控。如果你的 Agent 也在用 JSON Mode 做结构化输出而且偶尔会被格式解析问题折磨我建议你试试伪工具方案。至少对我们来说删掉那堆 JSON 正则解析器的那一天感觉比发布新功能还爽。你遇到过类似的问题吗或者你用过其他更稳的方案关于我我是老三10 年以上软件开发经验软件设计师人工智能应用工程师。目前专注鸿蒙应用开发ArkTS和 Web 前端也在探索 AI 自动化方向。不定期在 CSDN 分享鸿蒙和 AI 相关的技术文章。本文遵循 MIT 协议转载请注明出处。