ARTICLE DETAIL

建站实战干货

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

LLM输出JSON总不稳定?从Prompt到解码约束的完整容错方案

2026/9/5 12:03:57 拓冰建站 浏览量
LLM输出JSON总不稳定?从Prompt到解码约束的完整容错方案 前面有同事走过来问我“你 GET 一个 JSON 格式结果不是只要在 Prompt 里让模型仅输出 JSON 就行了吗为什么还总看到你处理解析错误”我第一反应是苦笑。这个看似简单的需求我至少在生产环境里遇见过二十多种“看上去不该发生”的失败返回了带 Markdown 围栏的 JSON、JSON 前后带了解释、字段值里混了None而不是null、key 少了尾引号、温度稍微调高结果整段格式崩掉。再加上最近工作中继续在做 LLM 与结构化数据的对接“只靠 Prompt 让 LLM 输出 JSON 不可靠”这件事非常值得聊透。这篇内容不打算只写“你要在提示词里加 JSON 三个字”而是想系统说一下模型为什么总是背弃你的要求、典型的失败模式长什么样、从提示到解码再到后处理该如何一级一级兜住最后给出一套能直接抄的容错方案。1. 先交代背景这个问题的三种典型翻车现场1.1 场景一返回了合法 Markdown却被json.loads打回第一次遇到这个问题是把 LLM 当信息抽取器用。让模型从一页客户留言里提取姓名、金额、日期要求“只输出 JSON”。模型确实输出了而且看起来非常完整格式也漂亮{ name: 张三, amount: 980.5, date: 2026-03-12 }可我们代码里用的是json.loads(response_text)直接抛Expecting value: line 1 column 1。我把返回内容一条条打出来才发现模型在外面包了一层json ... 。这就是第一类翻车模型所谓的“JSON”是展示给人看的 Markdown 代码块而不是机器可直接解析的纯文本。很多开发者在第一次联调时都会在这里卡住随后下意识地在提示词里加一句“不要输出 Markdown 代码块”但它并不能根治问题只是把错误从“外包裹”变成了别的形式。1.2 场景二解析成功字段类型却让下游崩溃第二次让我记忆深刻的坑是模型输出了一个“语法合法但语义非法”的 JSON。比如我要求order_count是整数模型给了一个字符串3我要求status只能是success或failed它输出successful我要求price是小数它却把98.5写成了98.5。这段 JSON 能顺利被解析器读出来不会报语法错误但它和约定的 JSON Schema 对不上。下游消费者的类型校验、数据库字段类型转换会直接出问题。麻烦在于这类错误发生在语法层之外只靠json.loads完全发现不了必须额外做一层 Schema 校验才能暴露。这种“静默错误”往往比直接崩溃更让人防不胜防。1.3 场景三换了环境或换了模型版本结果开始抽风第三种更诡异。同一个 Prompt同一个参数上午测试是合法 JSON下午接口返回突然多了一句“好的根据您的需求我将输出如下 JSON”。这通常不是因为模型变笨了而是大模型 API 侧本身有随机性、上下文长度分布、采样参数变化或者服务商灰度了不同版本。你没有任何代码改动结果就差了一个注释。这些经历说明一件事Prompt 是影响输出的第一手段但它的本质是在表达“期望”并不能从底层禁止非法 JSON 的生成。想在生产环境里稳定拿到机器可读数据需要把“依赖模型自觉”转变为“链路可控”。我在做这类工程时的判断标准也逐渐变成了把 JSON 可靠性当成一个完整的输入到输出的管道问题而不是一个提示词措辞问题。接下来详细拆解原因。2. 深入根因LLM 为什么会无视 JSON 格式要求2.1 我们以为是“协议”模型以为是“语气”人看到“只输出 JSON”这行字会把它理解成一条硬性规则就像看到“请锁门”一样默默遵守。但训练后的语言模型在处理自然语言时是靠语义概率去推测你希望它生成什么而不是靠规则解释器执行约束。对它来说“输出 JSON”和“输出礼貌语气”在机制层面没有本质区别都只是一个高权重的上下文信号并不是不能违背的语法边界。这也是为什么很多人反复强调“模型没有真正的指令跟随能力”。它们只是在海量文本中学到了一个模式当用户的文字包含 JSON 示例或明确格式要求时接下去的输出大概率长成 JSON。这个“大概率”就是你遇到的所有不稳定的根源。我们再怎么把 Prompt 当成代码来调它在模型眼里始终更像“语气指南”而不是“编译器语法”。2.2 token 级生成让深层语法变成概率问题要进一步理解得回到 token 生成的底层。大模型不是先画出整个 JSON 树再把它序列化出来它每一步只预测下一个 token 的概率分布然后从中采样一个 token。它没有“刚才已经写了左括号所以这里必须闭合”的全局记账本唯一的判断依据是前面的 token 和上下文提供的概率线索。比如已经生成了{它可能根据训练数据认为下一个出现name的概率最高于是就这么输出了。但只要生成途中某个 token 跳了一下后面所有概率都会跟着变化。上下文窗口越长、需要遵守的规则越多模型就越容易在某个局部位置忽略较早出现的格式指令从而产生缺失逗号、闭合符错位等语法错误。本质上格式错误不是“模型不聪明”而是它对格式的全局一致性建模力不足。2.3 温度、采样和上下文扰动带来的格式漂移很多人以为把 temperature 调到 0 问题就解决了但温度为 0 只是意味着每次都选概率最大的 token并不保证概率最大的 token 组合是合法 JSON。采样机制里的 top-p、频率惩罚等参数也会改变每一步的 token 分布。我曾经用完全相同的 Prompt 测过 50 次在 temperature0 情况下虽然大部分结果稳定但偶尔仍会出现一次“答案是对的但前面多了解释性空行”的输出。原因在于一些大型语言模型的推理过程还受系统提示长度、历史对话轮次、甚至输入文本末尾标点的影响。Prompt 变长后模型对早期约束的注意力会分散输入文本如果本身就包含大量 JSON 字符串或引号也会诱导模型在输出时模仿出残缺片段。格式漂移不是偶发而是采样系统的常态。2.4 JSON 语法细节太容易被抽样破坏最后还得承认JSON 本身对机器是宽容的文本格式但对生成模型却相当苛刻所有字符串要用双引号不能用单引号值不能是 Python 里的True、None对象最后一个成员后面不允许尾逗号控制字符必须转义所有 key 必须唯一。这些约束对写代码的人来说几乎不需要额外思考因为编译器会提示。可模型在每一步采样时并不会被一个“JSON 解析器”实时检查。它内部只是“觉得这里大概率应该有一个逗号”“那里大概率应该闭合引号”但这个概率永远不是 100%。JSON 的结构越复杂、嵌套层数越深采样到某个非法 token 的可能性就越大。说明白这一点不是要我们对 Prompt 绝望而是要认识到Prompt 可以在语义层尽量纠正模型但它无法替代语法层约束和后处理校验。3. 单靠 Prompt 会出现哪些失败模式3.1 格式化数据的五大类常见坏输出我在实践中把异常输出归纳成五类。下面这张表列出的都是真实发生过的最低级问题值得初涉这个方向的人反复对照。失败类型具体表现典型原因包裹层多余输出带json、或前后有解释性文字模型优先模仿互联网常见 Markdown 回复语法不合法缺少逗号、多尾逗号、引号不闭合、单引号代替双引号局部 token 采样错误缺少语法约束字面量写错True、False、None、NaN出现在 JSON 里模型混入了 Python 风格写法字段与值不匹配字段名拼写漂移、值类型错误、多余字段、缺必填字段未严格按 Schema 执行语义理解偏差隐藏额外内容输出一个合法 JSON 后又附带了“希望这对你有帮助”训练数据里助手回复常以礼貌收尾模型顺应概率这五类错误出现频率并不均匀。我观察到第一类和第三类在纯文本对话模型上最高发加了明确 JSON 指令后第一类会减少但第四类会更凸显。原因是模型经历了无数轮的 prompt 调优后学会了你想要“看起来像 JSON”的东西但对字段级别的 schema 理解还是不强。3.2 为什么会出现解释、Markdown 和备注有开发经验的朋友会疑惑我在 Prompt 里已经写了“什么都不要输出只要 JSON”为什么它还会在前面加“好的”这里的机制要从训练目标谈起。通用对话模型在训练阶段见过大量“用户请求 助手回复多余礼貌性内容”的语料。一旦上下文里的任务看起来像“对话里的一个请求”它就有较高概率沿着“礼貌地先答复一句再进入正题”的模式走。即使指令特别强调“禁止输出其他内容”这条约束在几千个上下文 token 里也只是普通文本权重再高也可能被用户消息本身的自然语言习惯稀释。想要在一定程度上抑制解释比较好的做法是把格式要求放到所有示例与客户内容之后的最后一句让它更贴近当前生成位置。它的权重通常比埋在一大段系统提示里更有效但不是绝对可靠。真正要拦截解释和 Markdown必须进入后处理或解码约束层面。3.3 有多少错误是“人类看不出来但 json.loads 会崩”在 Excel 里看模型输出很多错误非常隐蔽逗号是中文顿号、引号变成了中文引号、小数点前缺了 0。人类阅读时能自动脑补但json.loads会残酷拒绝。一个更误导人的现象是模型输出看起来像 JSON甚至在线 JSON 校验工具也能通过但字段名里有不可见 Unicode 字符。把输出和预期字段名肉眼比对基本发现不了只有做严格 schema 校验时才暴露。因此我强烈建议在存储或转发之前强制让程序走一遍json.loadsjsonschema.validate不要相信任何“看起来没问题”的结果。每一次格式异常都应该当成结构化输出管道的正常反馈而不是不可思议的事件。所以只靠 Prompt核心问题在于它的三个天然限制同一条口头约束不能被强制执行模型内部没有全局格式记忆输出错误无法在生成过程中被实时纠正。而要解决这三点就得分层设计。4. 提升 JSON 稳定性的三层做法4.1 第一层把约束写进提示词而不是写进“愿望清单”先说结论良好的 Prompt 设计虽不能根治问题但能把基础失败率从“经常坏”降到“偶尔坏”。这里分享几个实测有效的提示词细节。第一少用“不要输出解释”这种否定句多用“你只能输出一个 JSON 对象”这种肯定句。否定指令在模型理解中的效果比肯定指令弱而且“不要”后面往往跟着一个具体 token比如“解释”模型反而更容易在生成路径上把它激活。第二给出字段级示例和约束而不是只写“JSON”。例如告诉模型“status 字段只能取 success 或 failed不要输出其他字段”。字段级的 schema 描述比泛泛的整体要求有效得多。第三把“只输出 JSON”放到整个 Prompt 的最末端紧贴生成起点。第四在输入内容结束后再强调一次格式避免输入正文里大段引号干扰早期指令。这里给一个可以修改后直接用的 Prompt 形状你是订单解析器。根据用户输入提取字段并输出一个合法 JSON 对象。 对象只包含以下字段 - order_id: 字符串 - amount: 数字 - status: 只能是 success 或 failed 约束 1. 字段名必须完全按照上面定义拼写。 2. 不要输出额外字段。 3. 不要使用 Markdown 代码块。 4. 不要输出任何解释文字。 用户输入 {{原始文本}} 只输出 JSON 对象这类提示词已经在实践中把格式失败率降低了一个数量级。但请注意它仍然只用自然语言表达没有强制语法所以在预算有限时可以做想达到高稳定还得继续加后两层。提示词的收益也有边际递减我不建议把大量时间花在把措辞雕花上。4.2 第二层在解码层加约束让非法字符直接不被采样比提示词更强的方法是在模型解码阶段限制候选 token。任何不符合 JSON 语法的 token直接不参与概率采样这样模型在底层就无法产出不合法字符。这类方法通常被叫做结构化生成、受限生成或语法约束解码。不同框架的实现思路不完全一样。有的基于 JSON Schema 生成一个正则或形式文法然后在每个解码步检查当前候选 token 是否匹配后续合法路径有的通过上下文无关文法约束比如 GBNF 这类格式还有的直接用编程语言里的类型定义去约束输出。它们共同的特点是把“模型自觉遵守”升级成“生成器必须遵守”。这层做法最大的优势是可以从根源上让输出合法 JSON最大的限制是它不一定适用于所有商业 API。商业接口大多只给 HTTP 调用无法让你控制采样函数内部。如果条件允许比较合理的方式是在私有化部署或自建推理链路里优先启用语法约束解码在商业 API 上则退而求其次使用厂商自带的 JSON Mode 或 Function Calling第 6 部分会细说。有朋友问我用了约束解码提示词就可以随便写了吗不是。语法约束只能保证 token 序列是合法 JSON不能保证内容是用户想要的。模型仍然可能填进一个 schema 外的值或者完全错误的字段。解码约束管的是“形态”不管“语义”和“正确性”。4.3 第三层在后处理里兜底解析 JSON很多公司实际是买了商业 API 的不能改解码器这时候后处理就至关重要。核心思路是先把模型输出尽可能清洗成一个接近合法 JSON 的字符串再做解析和校验解析失败就按规则修复或重试。常见的清洗策略包括去掉 Markdown 围栏截取第一个{到最后一个}之间的内容替换True/False/None为true/false/null把中文引号替换成英文引号移除尾逗号处理数组里缺少引号的字段名等。但我要提醒清洗是无奈之举不是银弹。每增加一条修复规则就会引入新的误判风险。比如当我们把“第一个{到最后一个}”当 JSON 主体时如果输入文本中包含多层 JSON 嵌套或对话历史里也有花括号很容易截错范围。所以清洗逻辑必须配合输出内容预期做裁剪并保留最原始输出用于排障。比较好的工程实践是先记录原始输出再做清洗再解析把任何一次“清洗后仍失败”的 case 都留进样本集下次用来调 Prompt 或换方案。三层做法叠到一起组合后的稳定性要远大于任何单独一层。生产环境里我一般要求三层同时存在哪怕前两层看起来已经很可靠。5. 一个可以直接参考的工程链路5.1 链路设计从生成到落库的完整闭环完整链路简单来说有四步构造 Prompt - 调用模型 - 容错解析与 Schema 校验 - 按需重试或告警。关键点是第四步“告警”很多时候被漏掉一旦重试也失败系统就默默丢数据这是最隐蔽的生产事故。更稳的流程里还会加一层“结构修正”校验失败后把原始输出、Schema 和错误信息重新塞给模型请它再次输出一个符合要求的 JSON。这在语义抽取任务上通常很有效因为模型知道自己上次哪里不符合约定第二次纠正的准确率明显提升。不过不能无限重试建议最多两轮并设置失败计数监控。5.2 Python 版容错 Parser 和最小校验下面给一段可以放到小工具库里的容错解析代码。它按“原始输出保留 - 去 Markdown - 解析 - 修复尝试 - Schema 校验”的顺序处理基本可以作为实用模块的起点import json import re from typing import Any from jsonschema import validate, ValidationError def parse_llm_json(content: str, schema: dict | None None) - dict[str, Any]: # 保留原始输出方便排查 raw content.strip() # 去掉常见的 Markdown 围栏 text re.sub(r^(?:json)?\s*, , raw, flagsre.IGNORECASE) text re.sub(r\s*$, , text) # 取第一个 { 到最后一个 } 之间的内容 start, end text.find({), text.rfind(}) if start ! -1 and end ! -1 and end start: text text[start:end 1] # 尝试直接解析 try: data json.loads(text) except json.JSONDecodeError: # 常见错误尾逗号、Python 字面量、单引号 text re.sub(r,\s*([}\]]), r\1, text) text text.replace(True, true).replace(False, false) text text.replace(None, null) text text.replace(, ) try: data json.loads(text) except json.JSONDecodeError as exc: raise ValueError(fJSON parse failed: {exc}\nraw{raw}) from exc # 如果提供了 schema再做语义层校验 if schema is not None: try: validate(instancedata, schemaschema) except ValidationError as exc: raise ValueError(fJSON schema validation failed: {exc.message}) from exc return data核心意图是把“偶尔出现的小错误”在代码里消化掉而不是直接让调用方看到解析异常。请注意代码里的 schema 校验必填字段和类型检查它能抓住3这种类型错误。生产中建议用 Pydantic 模型来定义 schema既做数据校验也方便后续把对象转给下游。5.3 参数怎么定温度、重试次数和 schema 设计模型参数不是随手填的。以我们的经验处理格式要求严格的任务temperature 设在 0 到 0.2 之间比较合适。temperature 太高句式和内容更多样但格式崩的概率会上升设为 0并不保证 100% 合法但至少减少了随机性这一层方差。如果 API 支持 seed 参数且能提供确定性保证建议固定 seed方便复现测试。不能固定 seed 的环境则要在评测时跑多次来观察失败率不要用单次结果判断质量。重试次数我建议最多 2 次因为重试一次的成本不只是 token还包括下游等待的延迟。如果第 2 次仍失败更可能是 Prompt 或 schema 本身与任务有冲突靠重试无法解决。Schema 设计上故意“宽松”反而比“严格”更适合某些自动生成任务。比如字段名用蛇形命名减少模型对大小写的困惑布尔字段只在需要时保留避免模型输出“true/false/yes/no”的变体对字段值采用枚举约束时把允许枚举值直接放在 Prompt 和 schema 里尽量不让模型凭直觉扩展。这套链路下来实际部署后格式弹错率通常能从裸 Prompt 的 5%-10% 左右降到千分之一以下剩下的小概率错误会被重试或人工特殊处理兜完。6. 各家 JSON Mode 实测后的差异与分歧6.1 JSON Mode 不是一个统一标准现在很多大模型服务都提供了看起来很像的“JSON Mode”或“Structured Output”选项开放接口名称不同行为差异也很大。我测试下来发现有的模式确实保证了返回内容可以整体解析为合法 JSON有的模式只是保证它输出内容从统计概率上“更接近 JSON”但偶尔还是会在中间混入一段自然语言还有的必须配合某个固定字段前缀或特殊 token 才生效。所以使用前务必去查官方文档确认三个细节它是否会对内容做 schema 校验是否要求 prompt 中出现“json”字样是否会拒绝本来合法但 schema 外的字段。不确认这些就开始开发很容易在联调阶段发现“本地感觉没问题到了某个模型版本后全部报错”。另外许多用户容易误以为只要打开 JSON Mode输出就一定忠于我们传入的 JSON Schema。实际情况是部分实现的 JSON Mode 仅保证语法合法不对语义字段做任何限制。模型完全可以输出一个结构合法但缺失核心字段的 JSON。想要字段级约束请优先考虑使用带 schema 定义的 Function Calling 或专门的 Structured Output。6.2 用 Function Calling 约束参数的思路Function Calling 这类工具调用机制本质上也是让模型输出结构化参数但它和自由文本里“请输出 JSON”不是一个思路。模型会在内部生成一次调用请求参数必须匹配注册函数定义的 JSON Schema。多数主流模型服务商对工具参数生成做了一定程度的结构约束比裸 Prompt 更不容易出现语法错误。不过它也有自己的问题。函数定义里的描述会成为模型选择参数的主要来源描述不够清楚时模型可能把字符串塞进数字字段或者把必填字段留空。实践中我会把字段说明写得像对“一个爱自由发挥的实习生”交代工作一样具体。例如与其写“订单金额”不如写“订单实际支付金额单位元数字类型不要带货币符号不要带千分位逗号”。描述越接近最终存储格式模型表现越好。补充一点Function Calling 对复杂嵌套对象的表达支持程度因厂商而异。场景非常复杂时可以把每个二级子对象定义成单独的 function call或者用一层 wrapper 降低 schema 嵌套深度换取稳定性。6.3 别把 JSON Mode 默认成万无一失我在接入使用前后都会做一个“暴力测试”构建 200 条有代表性的输入每条请求重复 3 次然后统计返回结果的解析成功率、schema 校验成功率、字段值正确率。这一套跑下来才能放心上生产。尤其要测试边界输入比如输入文本里充满引号、特殊字符、超长文本、以及空内容。另一个坑是模型输出的合法 JSON 本身不一定就是正确业务答案。它可能在语法上挑不出毛病但把字段内容理解反了。JSON 稳定性只是系统工程的第一步内容准确性还需要一个单独的评测集来长期跟踪。结论是JSON Mode、Function Calling 等工具很值得用但它们是“把失败率再次降低”的优化手段不是“只要用了就交付”的终点。7. 生产中常用的一套排查清单7.1 每次格式事故按这四个步骤排查在实际运维里我们很少直接猜“为什么这轮 JSON 又崩了”而是按顺序排查。第一步看原始输出并分类它属于语法错误、字段类型错误、内容噪声、还是网络传输问题不要一开始就改 Prompt。第二步检查模型参数和环境temperature 是否被某个上游服务悄悄改了seed 是否变了模型版本有没有切换是否在请求里被插入了额外的 system prompt这些因素常常导致同样代码在不同时段表现迥异。第三步检查 Prompt 结构是否被业务文本干扰当输入文本包含大量引号或接近 JSON 的片段时建议在提示词里用更明显的分隔标记包裹原始内容比如INPUT。第四步复现并沉淀样本把失败案例加入回归集改一次 Prompt 或换一次模型版本后跑一遍全部回归观察成功率变化。坦白讲很多“原本让我抓狂的奇怪错误”走到第四步时都能发现是上下文污染或随机性造成的。把每一步形成文档团队协作时能节省大量互相扯皮的时间。7.2 问题定位速查表现象最可能原因优先处理手段输出带 Markdown 围栏模型受对话格式影响后处理剥离 提示词强调JSON 能解析但缺字段schema 描述或示例不明确给字段示例、用 Function Calling 约束字段类型不对字段描述里缺少类型与格式示例补充“字符串/数字/枚举值”的强制描述偶发夹杂解释文字采样随机性 / 对话习惯temperature 调低、格式约束放 Prompt 最后同一 Prompt 不同时间结果差异大模型版本或服务端参数变化固定 seed / 记录 model 与 temperature / 回归集清洗规则改完后出现了新错误清洗逻辑误伤内容保留清洗前后文本按样本回放比对这张表可以当作战术参考但不建议把所有问题都背下来。更值得记的是排查顺序优先区分“语法层”和“语义层”再决定是加解码约束、纠 Prompt 还是做校验。7.3 一个我常用的回归校验小技巧每次调完 Prompt 或准备更换模型前我会把之前遇到的 20 到 50 个异常案例整理成一个独立的llm_json_cases.json文件里面每条包含输入文本、期望字段、真实期望 JSON。自动化脚本把每一条输入发给模型再做解析和 schema 校验最后打印通过率。通过率掉到阈值以下时就不允许合代码。这个习惯帮我避免过很多次“看似修好了 A结果弄坏了 B”的回退问题。更关键的是回归集要持续补充生产环境里出现的每一个新失败 case。时间长了你对“哪些字段容易写错”“哪类输入最容易破坏格式”会形成非常直观的数据库。到后来哪怕新接入一个新的模型供应商也能靠这套回归集在半天内判断它是否符合上线要求。我自己现在都还保留着一个笨办法对任何“只要加一句 prompt 就能稳定输出 JSON”的说法保持怀疑拿到手先扔进回归集跑一百遍。实践下来唯一能长期依赖的是完整的格式约束、多层校验和持续回归。能够把这三件套做扎实比执着于某个灵性 Prompt 要可靠得多。