ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Agent结构化输出工程:从JSON解析失败到稳定校验重试的完整方案

2026/10/3 15:47:00 拓冰建站 浏览量
Agent结构化输出工程:从JSON解析失败到稳定校验重试的完整方案 1. 为什么“看起来像 JSON”是 Agent 工程里最隐蔽的坑做 Agent 开发的人十有八九都经历过这个场景你精心设计了一段提示词要求模型“以 JSON 格式返回结果”模型也确实返回了花括号、方括号、双引号肉眼一看就是 JSON。你满心欢喜地把这段文本丢给json.loads()结果程序直接抛出一个JSONDecodeError报错信息还特别含糊比如Expecting property name enclosed in double quotes或者Extra data。你盯着那段文本看了半天明明长得跟 JSON 一模一样为什么就是解析不了这个问题的本质是**“看起来像 JSON”和“是合法 JSON”之间隔着一整套工程约束**。模型生成的是自由文本它只是在概率上模仿了 JSON 的视觉形态但并没有任何机制保证输出满足 JSON 的语法规范。尾随逗号、单引号、注释、未转义的换行符、中文全角标点、Markdown 代码块包裹、前后多余的客套话这些都会让下游解析器直接崩溃。我在实际项目里踩过最典型的一次坑是模型返回了这样的内容好的以下是分析结果 { name: 张三, age: 28, tags: [工程师, 摄影], } 希望对你有所帮助这段文本里json.loads()会遇到三个致命问题第一开头的“好的以下是分析结果”不是合法 JSON 的一部分第二tags数组最后一项后面多了个尾随逗号第三结尾还有一句客套话。任何一个问题单独出现都足以让解析失败而它们经常同时出现。所以这篇内容要解决的核心问题就是如何通过结构化输出工程让 Agent 的输出从“看起来像 JSON 的自由文本”变成“下游可以稳定解析的结构化数据”。这不是一个提示词技巧问题而是一套涵盖 Schema 设计、生成约束、校验重试、降级兜底的完整工程方案。适合正在做 Agent 开发、RAG 系统、自动化工作流的工程师也适合刚接触结构化输出、被解析报错折磨过的朋友。2. 结构化输出的整体设计思路与方案选型2.1 从“求模型听话”到“用机制约束”早期我做结构化输出思路非常朴素在提示词里反复强调“必须返回合法 JSON”“不要加任何解释”“不要用 Markdown 代码块”。短期看有效但只要模型换版本、温度调高、输入变复杂输出就会开始飘。原因很简单提示词是软约束它影响的是概率分布不是语法规则。真正可靠的方案是把结构化输出拆成三层防线。第一层是生成层约束尽可能让模型在解码阶段就只能产出合法结构第二层是校验层拦截对拿到的文本做严格语法校验不合格就重试第三层是修复层兜底对轻微格式问题做容错处理避免整个流程因为一个逗号挂掉。这三层缺一不可只靠任何一层都不够稳。我现在的默认策略是能用原生结构化输出能力就用原生能力用不了就用 JSON Schema 加校验重试最后再挂一个轻量修复函数。这个顺序很重要因为越靠前的方案越省事越靠后的方案越容易引入隐蔽 bug。2.2 JSON Schema 到底该设计多严很多人设计 Schema 时容易走两个极端。一个是过于宽松只写{type: object}等于没约束另一个是过于严格把每个字段都加上复杂的正则、枚举、嵌套校验结果模型经常生成失败重试成本飙升。我的经验是Schema 的严格程度要跟下游消费方的容错能力匹配。如果下游是你自己写的代码字段多一点少一点都能处理那 Schema 可以适当宽松如果下游是数据库写入、第三方接口调用那必须严格因为一个字段类型错误就可能导致整条数据报废。具体设计时有几个关键点。字段命名统一用snake_case避免模型在camelCase和snake_case之间摇摆。必填字段用required明确列出不要让模型猜。枚举值一定要写全比如状态字段就写[pending, running, done, failed]不要只写type: string否则模型可能返回“进行中”“已完成”这种中文值。数组类型要指定items否则模型可能混入null或者对象。还有一个容易被忽略的点给每个字段加description。这个描述不只是给人看的很多结构化输出实现会把它作为约束的一部分传给模型。描述写得越清楚模型理解越准确。比如age: {type: integer, description: 用户年龄单位岁范围 0-150}就比单纯写type: integer稳得多。2.3 三种主流实现路径的取舍目前做结构化输出主流有三条路径各有适用场景。第一条是原生结构化输出模式。部分模型服务提供了强制 JSON 输出的参数开启后模型在解码时会被约束只能生成合法 JSON。这条路径最省心解析成功率最高但缺点是灵活性受限有些复杂嵌套结构支持不好而且不是所有模型都提供这个能力。第二条是函数调用或工具调用模式。把想要的输出定义成一个“函数”的参数 Schema让模型去“调用”这个函数。模型返回的就不是自由文本而是结构化的参数对象。这条路径适合需要模型做决策再输出的场景比如先判断意图再填参数。缺点是 Schema 描述能力有限复杂结构表达起来比较别扭。第三条是提示词加校验重试。这条路径最通用任何模型都能用但工程量大需要自己写校验、重试、修复逻辑。我一般把它作为兜底方案前两条路径走不通时才用。实际项目里我经常混用主流程用原生结构化输出遇到不支持的模型降级到提示词加校验修复层统一处理轻微格式问题。这样既保证了稳定性又保留了灵活性。3. 核心细节解析与实操要点3.1 提示词里必须写清楚的五件事如果你走的是提示词约束这条路提示词的质量直接决定输出质量。我总结下来有五件事必须在提示词里写清楚少一件都会增加解析失败率。第一明确输出格式是 JSON且只能是 JSON。不要只说“返回 JSON”要说“你的整个回复必须是一个合法的 JSON 对象第一个字符是左花括号最后一个字符是右花括号不要有任何其他文字”。第二给出完整的 Schema 或示例。光描述字段不够最好给一个填好示例值的完整 JSON。模型对示例的模仿能力远强于对抽象描述的理解能力。第三明确禁止项。比如“不要使用 Markdown 代码块”“不要添加注释”“不要使用尾随逗号”“字符串内换行必须转义为\n”。这些禁止项要具体不要笼统地说“返回合法 JSON”。第四说明字段类型和取值范围。尤其是数字和布尔值模型很容易把28写成28把true写成true。要在提示词里强调类型。第五给出处理不了时的兜底行为。比如“如果某个字段无法确定用null填充不要省略该字段”。这样能保证输出结构完整下游不用做字段存在性判断。3.2 温度、top_p 与结构化输出的关系很多人不知道采样参数对结构化输出的稳定性影响巨大。温度越高模型越有创造力但格式漂移的概率也越大。我做结构化输出时温度一般设在 0 到 0.3 之间需要严格结构时直接设 0。top_p同理设得太高会让模型在标点、括号这些地方产生意外选择。我通常把top_p设在 0.1 到 0.5 之间。如果模型服务支持frequency_penalty和presence_penalty结构化输出场景下建议都设为 0避免模型为了“多样性”而改变格式。还有一个细节是最大输出长度。如果设得太短JSON 可能被截断导致解析失败。我一般会根据 Schema 的复杂度估算一个安全值简单对象留 500 token复杂嵌套留 2000 token 以上。截断是结构化输出里最隐蔽的失败原因之一因为截断后的文本往往前半部分完全合法只有最后缺了个括号报错信息还特别难懂。3.3 校验函数该怎么写才不留死角拿到模型输出后第一件事不是直接json.loads()而是先做一轮预处理和校验。我的校验函数一般包含这几步。先做首尾清洗。去掉首尾空白如果发现被 Markdown 代码块包裹就把json和剥掉。这一步能解决相当一部分“看起来像 JSON”的问题。然后做定位截取。从第一个{或[开始到最后一个}或]结束把中间部分截出来。这一步能去掉模型前后加的客套话。注意要配对不能简单找第一个{和最后一个}因为字符串里可能包含花括号。稳妥的做法是用括号计数法遇到字符串内的括号要跳过。接着做语法校验。用json.loads()尝试解析捕获异常。如果失败记录原始文本和异常信息进入修复或重试流程。最后做Schema 校验。语法合法不代表结构正确还要用 JSON Schema 校验器检查字段类型、必填项、枚举值。Python 里可以用jsonschema库JavaScript 里可以用ajv。这一步能拦住“语法合法但字段缺失或类型错误”的情况。3.4 重试策略不是简单重发一遍校验失败后重试很多人就是原样再发一次请求。这样做成功率提升有限因为模型面对同样的输入很可能犯同样的错误。我的重试策略是带反馈的重试。具体做法是把上一次的输出和具体的校验错误信息一起塞回提示词让模型知道错在哪。比如“你上一次的输出在age字段返回了字符串28但 Schema 要求是整数请修正后重新输出完整 JSON。”这种带错误信息的重试成功率比盲目重试高很多。重试次数我一般设 2 到 3 次。超过 3 次还失败说明要么 Schema 设计有问题要么模型能力不够继续重试只是浪费 token。这时候应该走降级路径比如返回一个默认结构或者把问题抛给人工处理。还有一个技巧是重试时降低温度。第一次用 0.3重试时降到 0进一步压缩模型的随机性。4. 实操过程与核心环节实现4.1 一个完整的结构化输出流程下面用一个具体例子串起整个流程。假设我们要做一个用户信息抽取 Agent从一段自由文本里抽取姓名、年龄、标签列表。第一步定义 Schema{ type: object, properties: { name: { type: string, description: 用户姓名 }, age: { type: integer, description: 用户年龄单位岁 }, tags: { type: array, items: {type: string}, description: 用户标签列表没有则为空数组 } }, required: [name, age, tags], additionalProperties: false }注意additionalProperties: false这个设置它能防止模型自作主张加字段。很多解析 bug 就是因为模型多返回了一个 Schema 里没定义的字段下游代码没做兼容。第二步构造提示词。我会把 Schema 和示例一起放进去你是一个信息抽取助手。请从用户输入中抽取信息并严格按照以下 JSON Schema 输出。 Schema: {上面那段 Schema} 要求 1. 你的整个回复必须是一个合法 JSON 对象不要有任何其他文字。 2. 不要使用 Markdown 代码块。 3. 字符串内不要出现未转义的换行符。 4. 如果某个字段无法确定name 用空字符串age 用 0tags 用空数组。 示例输入我叫李四今年 30 岁喜欢跑步和读书。 示例输出{name: 李四, age: 30, tags: [跑步, 读书]}第三步调用模型温度设 0拿到输出后走校验流程。第四步校验通过就返回结构化对象校验失败就带错误信息重试。4.2 括号计数截取法的实现前面提到的括号计数截取是处理“前后有客套话”的关键。直接上代码def extract_json_block(text): start -1 depth 0 in_string False escape False for i, ch in enumerate(text): if escape: escape False continue if ch \\: escape True continue if ch : in_string not in_string continue if in_string: continue if ch in {[: if depth 0: start i depth 1 elif ch in }]: depth - 1 if depth 0 and start ! -1: return text[start:i1] return None这段代码的核心是维护一个depth计数同时用in_string标记当前是否在字符串内部。字符串里的花括号不参与计数转义字符要跳过。这样就能准确找到最外层的 JSON 块即使前后有文字也不影响。我实测下来这个方法能解决大概 70% 的“前后有杂质”问题。剩下的 30% 主要是字符串内部有未转义引号这种属于模型生成错误只能靠重试解决。4.3 常见格式问题的修复函数有些格式问题很轻微不值得重试直接修复更快。我整理了一个修复函数处理这几类问题。尾随逗号用正则把,}和,]替换成}和]。注意要处理字符串内的逗号所以最好在解析失败后针对性修复而不是无脑全局替换。单引号把 JSON 里的单引号替换成双引号。这个操作有风险因为字符串内容里可能本来就有单引号。稳妥做法是只在键名位置替换或者干脆不修直接重试。中文标点把全角的“”替换成全角的替换成:全角的替换成,。这个在中文场景下很常见模型有时候会混用。未转义换行字符串内的裸换行符要替换成\n。这个用正则比较难精确处理我的做法是先用json.loads()试失败后定位到报错位置附近手动处理。修复函数的原则是能修就修修不了就重试不要为了修复引入新问题。我见过有人写了一个特别激进的修复函数把合法 JSON 也改坏了反而增加了失败率。4.4 流式输出下的结构化处理如果 Agent 是流式输出结构化处理会更麻烦因为你在生成过程中拿到的文本是不完整的。这时候不能等全部生成完再解析需要边生成边判断。我的做法是维护一个缓冲区每次收到新片段就追加进去然后尝试用括号计数法找完整 JSON 块。找到就解析找不到就继续等。同时设一个超时超过一定时间还没找到完整块就判定失败。流式场景下还有一个坑是模型可能在 JSON 前后输出多个块。比如先输出一段解释再输出 JSON再输出一段总结。这时候括号计数法要能处理多个块取第一个完整的。如果第一个块不完整继续往后找。5. 常见问题与排查技巧实录5.1 解析失败问题速查表报错信息常见原因排查方法解决手段Expecting property name enclosed in double quotes键名用了单引号或有尾随逗号打印原始文本检查键名和逗号修复函数替换或重试Extra dataJSON 后面还有多余内容检查是否有客套话或第二个 JSON 块括号计数截取Unterminated string字符串内有未转义换行或引号定位报错位置附近转义处理或重试Expecting value空值、undefined、NaN检查是否有非标准字面量替换为标准 JSON 值Invalid control character字符串内有裸控制字符检查是否有制表符、换行符转义处理Schema 校验失败字段缺失、类型错误、枚举越界打印校验错误详情带错误信息重试这张表是我从实际项目里攒出来的基本覆盖了 90% 以上的解析失败场景。遇到报错先查表能省很多时间。5.2 模型“自作聪明”加字段怎么办模型有时候会加一些 Schema 里没定义的字段比如你只要name和age它非要加个gender。这种情况用additionalProperties: false能在校验层拦住但拦住了还是要重试。我的经验是在提示词里明确写“只输出 Schema 中定义的字段不要添加任何额外字段”。同时在校验失败时把“你多返回了gender字段”这个信息带进重试提示词。双管齐下基本能解决。如果模型反复加同一个字段说明它认为这个字段很重要。这时候可以考虑把它加进 Schema给它一个明确的类型和描述反而比强行禁止更稳。5.3 中文场景下的特殊坑中文场景有几个特有的坑。第一是全角标点模型有时候会把:写成把,写成。这个在中文提示词下特别常见。我的做法是在提示词里明确写“使用英文半角标点”同时修复函数里做全角转半角。第二是中文引号“”和混用。JSON 只认所以中文引号必须替换。这个在字符串值里尤其容易出问题因为模型可能觉得中文内容配中文引号更自然。第三是编码问题。如果整个流程涉及文件读写、网络传输要确保全程 UTF-8 编码。我遇到过因为中间某个环节用了 GBK 编码导致中文变成乱码JSON 解析直接失败。这种问题排查起来很费劲因为报错信息不会告诉你编码问题只会说解析失败。5.4 高并发下的结构化输出稳定性Agent 扛并发时结构化输出的稳定性会下降。原因有几个一是并发高了之后模型服务的响应可能被截断二是重试逻辑在并发下可能放大请求量形成雪崩三是共享的校验器、修复函数如果有状态可能互相干扰。我的应对策略是重试加退避每次重试间隔指数增长避免瞬间打爆服务校验器无状态化所有函数都是纯函数不依赖外部状态设置并发上限超过上限的请求直接排队或降级不要无限重试。还有一个技巧是缓存 Schema 编译结果。JSON Schema 校验器每次编译 Schema 都有开销高并发下这个开销会累积。把编译好的校验器缓存起来能省不少 CPU。5.5 降级方案解析不了怎么办再完善的工程也有失败的时候。这时候要有降级方案不能让整个流程挂掉。我的降级策略分三级。第一级是返回默认结构所有字段用默认值填充同时标记_parse_failed: true让下游知道这条数据不可靠。第二级是返回原始文本把模型输出原样返回让下游自己决定怎么处理。第三级是抛异常适用于数据准确性要求极高的场景宁可失败也不能返回错误数据。选哪一级取决于业务场景。做数据分析时返回默认结构加标记就够了做自动交易时必须抛异常因为错误数据可能导致真实损失。6. 我踩过的坑和几条实在建议第一个坑是过度依赖提示词。我早期花了大量时间优化提示词试图让模型“永远返回合法 JSON”。后来发现这是徒劳的模型版本一换提示词效果就变了。正确的做法是把提示词当作第一道防线但一定要有校验和重试兜底。第二个坑是Schema 设计太复杂。我做过一个嵌套四层的 Schema结果模型生成成功率不到 50%。后来把结构拍平改成两层成功率直接上到 95% 以上。Schema 越简单模型越容易理解输出越稳。第三个坑是忽略截断问题。有一次线上事故就是因为最大输出长度设得太小JSON 被截断解析失败整个流程卡住。后来我把最大长度设成估算值的两倍再没出过这个问题。第四个坑是重试不带错误信息。盲目重试的成功率很低因为模型不知道错在哪。带上具体错误信息后重试成功率能提升一倍以上。几条实在建议结构化输出不是提示词问题是工程问题要按工程思路做Schema 设计要跟下游容错能力匹配不要一味求严校验、重试、修复、降级四层缺一不可中文场景要特别注意标点和编码高并发下要控制重试节奏避免雪崩。最后分享一个小技巧如果你不确定 Schema 设计得合不合理可以先拿几十条真实输入跑一遍统计解析成功率。成功率低于 90% 就说明 Schema 或提示词有问题需要调整。这个统计过程比拍脑袋改提示词靠谱得多。