让大模型返回JSON给程序处理,开发全过上线却偶发解析失败,捞出原始返回一看JSON五花八门包代码块加客气话:大模型结构化输出避坑复盘

我们做了个功能让大模型把一段非结构化的文本提取成结构化的 JSON 再交给后端程序去处理,开发时测了十几条效果完美 JSON 解析得妥妥的就高高兴兴上线了。可上线没多久后端的告警就开始零星地响——JSONDecodeError 解析失败。我很纳闷:同样的代码同样的调用为什么有的能解析有的就报错?我把那些解析失败的大模型实际返回的原始内容捞出来一看真是哭笑不得,大模型返回的 JSON 五花八门:有的在 JSON 外面裹了一层 Markdown 代码块标记,有的在最前面加了句客气话好的这是您要的JSON,有的 JSON 末尾多了个尾随逗号,还有的某个字段我要的是数字它给了我个字符串。这一堆乱象让我有了一个清醒到有些扎心的认知:大模型本质上是一个生成文本的模型而不是一个返回数据的 API,当你请求它返回一段 JSON 时它并不像传统 API 那样保证返回的一定是严格合法的 JSON,它只是在尽量配合你的要求生成一段它觉得像 JSON 的文本,而这段文本完全可能夹带它的口头禅包着代码块或在格式细节上出错。这篇文章从这次大模型返回的 JSON 解析失败的事故出发,讲透大模型结构化输出避坑:认清大模型是生成器不是可靠API、用 JSON 模式和结构化输出从源头强约束、消费端做容错解析加 schema 校验加带反馈重试、提示词强约束与 temperature 调低、把输出不确定性纳入工程考量,以及一个根本认知——AI 工程是在不确定性上构建可靠,要从确定性思维升级到与不确定性共处。

我们做了个功能,让大模型把一段非结构化的文本,提取成结构化的 JSON,再交给后端程序去处理。开发时测了十几条,效果完美,JSON 解析得妥妥的,就高高兴兴上线了。可上线没多久,后端的告警就开始零星地响——JSONDecodeError,解析失败。我很纳闷:同样的代码、同样的调用,为什么有的能解析、有的就报错?我把那些解析失败的、大模型实际返回的原始内容捞出来一看,真是哭笑不得,大模型返回的"JSON"五花八门:有的在 JSON 外面裹了一层 Markdown 的代码块标记;有的在最前面加了句客气话"好的,这是您要的 JSON:";有的 JSON 末尾多了个尾随逗号;还有的某个字段,我要的是数字,它给了我个字符串……

这一堆乱象,让我对"让大模型返回结构化数据"这件事,有了一个清醒到有些扎心的认知:大模型,本质上是一个"生成文本"的模型,而不是一个"返回数据"的 API。当你"请求"它返回一段 JSON 时,它并不像传统 API 那样"保证"返回的一定是严格合法的 JSON;它只是在"尽量配合"你的要求,生成一段它觉得"像 JSON"的文本——而这段文本,完全可能夹带它的口头禅、包着代码块、或在某个格式细节上出错。我犯的错误,是把大模型当成了一个"会严格遵守 JSON 契约的可靠接口",而它其实是个"努力配合、但不打包票的生成器"。这篇文章,就从这次"大模型返回的 JSON 解析失败"的事故讲起,聊聊让大模型稳定地输出结构化数据,这个 AI 应用里极其高频、却坑很多的工程问题。

故障现场:一个不那么"听话"的 JSON 生成器

先把大模型返回的那些"花式不合法 JSON"摆出来,你一看就知道有多防不胜防:

# 我期望大模型返回的(纯净、合法的 JSON):
{"name": "张三", "age": 30, "tags": ["vip"]}

# 而它实际可能返回的各种"变体", 每一种都会让 JSON.parse 报错:

# 变体1: 外面裹了 markdown 代码块标记(三反引号 json ... 三反引号)
(代码块标记)json
{"name": "张三", "age": 30, "tags": ["vip"]}
(代码块标记结束)

# 变体2: 前面加了客气话/解释
好的, 根据您提供的信息, 这是提取出的 JSON:
{"name": "张三", "age": 30, "tags": ["vip"]}

# 变体3: 尾随逗号(JSON 规范不允许)
{"name": "张三", "age": 30, "tags": ["vip"],}

# 变体4: 字段类型不对(我要 number, 它给了 string)
{"name": "张三", "age": "30 岁", "tags": ["vip"]}

# 变体5: 字段缺失 / 多了意料外的字段
{"name": "张三"}    # age 和 tags 没了

看明白这个坑有多防不胜防了吗?这些"变体"的根源,都在于一件事:大模型生成的是"文本",而我们却拿一个"严格的 JSON 解析器"去处理它。 JSON 解析器是死板的、不容半点差错的——多一个字符(代码块标记、客气话)、少一个括号、多一个逗号,它都会无情地抛 JSONDecodeError;而大模型生成的文本,恰恰是"灵活的、有创造性的、不那么精确的"——它会"想当然"地加上它觉得有用的客气话、会习惯性地用它见过无数次的 Markdown 代码块来包裹 JSON。一个"严格死板"的消费者(解析器),撞上一个"灵活随性"的生产者(大模型),解析失败几乎是必然的。

而它之所以"偶发"——开发时十几条都好好的、上线才零星报错——是因为大模型的输出带有一定的随机性:同样的提示词,它大部分时候可能返回纯净的 JSON,但偶尔(尤其在输入内容比较特殊时)就会"发挥"一下,加点客气话、或包个代码块。开发时样本少,没撞上它"发挥"的那些情况;上线后请求量大、输入五花八门,它"发挥"的概率累积起来,解析失败就时不时地冒出来了。这种"开发顺利、上线偶发"的特征,和大模型输出的随机性,是这类问题的典型指纹。

第一件事:认清大模型是"生成器",不是"可靠 API"

要解决这个问题,必须先扭转一个根本的认知误区:不要把大模型,当成一个"输入确定、输出也确定且格式严格保证"的传统 API;它是一个"概率性的文本生成器"——你给它的提示词,是在"引导"它生成你想要的东西,而不是在"命令"它必须精确地、100% 合规地返回某种格式。它会尽力配合,但"尽力"不等于"保证"。

传统 API vs 大模型, 在"输出契约"上的本质区别:

  传统 API: 你调它, 它按代码逻辑, 返回严格符合约定格式的数据
            → 输出是确定的、有保证的, 你可以放心地直接解析

  大模型:   你给提示词"请返回JSON", 它生成一段"它认为像JSON"的文本
            → 输出是概率性的、尽力而为的, 可能夹带杂质、可能格式微错
            → 你不能假设它一定合法, 必须"防着它出错"

  一句话: 对传统API的输出可以"信任", 对大模型的输出必须"校验"。

这个认知转变是后面一切解法的基础:既然大模型的输出是"尽力而为、不打包票"的,那么消费它输出的代码,就必须建立在"它随时可能给我不合规的东西"这个悲观假设之上,主动地去引导、约束、校验、容错——而不能像对待一个可靠 API 那样,天真地拿到就直接解析、用了就完事。我那次的错误,本质就是用"信任传统 API"的心态,去对待了一个"需要被校验的生成器"。把大模型的输出,当成"来自一个不完全可靠来源的、需要校验的外部输入"来对待——这个心态,是做好一切大模型应用的前提(这也和我之前聊的"边界数据要校验"一脉相承)。

第二件事:正解之一——用 JSON 模式 / 结构化输出强约束

解决思路分两层:第一层,从"源头"上尽量让大模型输出纯净合法的 JSON。现在主流的大模型 API,大多提供了专门的能力来做这件事——JSON 模式(JSON mode)、结构化输出(Structured Output)、或函数调用(Function Calling)。它们能从模型层面"强制"输出是合法的 JSON、甚至严格符合你给的 schema,远比单纯在提示词里"求"它靠谱。

# 正解1: 开启 JSON 模式 / 结构化输出 (以 OpenAI 兼容接口为例)
resp = client.chat.completions.create(
    model="...",
    messages=[...],
    # 关键: 告诉 API "我要的就是 JSON", 模型层面保证输出是合法 JSON
    response_format={"type": "json_object"},
)
# 更强的: 结构化输出, 直接给一个 schema, 强制输出严格符合它
resp = client.chat.completions.create(
    model="...",
    messages=[...],
    response_format={
        "type": "json_schema",
        "json_schema": {"schema": MY_SCHEMA, "strict": True},  # 严格按schema
    },
)
# 这样拿到的输出, 是合法JSON、且字段都符合schema, 解析失败的概率大大降低

这是从源头治理的最优解:与其在提示词里苦口婆心地"恳求"大模型"请只返回 JSON、别加别的",不如直接用 API 提供的 JSON 模式/结构化输出能力,从模型生成的层面就"强制"它的输出是合法 JSON、甚至严格符合你的 schema。这就像把"靠对方自觉遵守约定"升级成了"用机制强制保证约定"——后者当然可靠得多。所以,凡是需要大模型返回结构化数据的场景,第一选择永远是看你用的模型/API 支不支持 JSON 模式或结构化输出,支持就一定用上——它能从根上消灭绝大多数"格式不合法"的问题。而 Function Calling(让模型按你定义的函数签名/参数 schema 来"调用函数")本质上也是结构化输出的一种,在做 Agent、做工具调用时,它同样能保证参数是结构化、合规的。我把"治理大模型结构化输出"的思路画成图:

这张图的主线是"源头强约束 + 消费端兜底"两道防线:源头上,优先用 JSON 模式/结构化输出从模型层面强制格式;但即便如此,消费端依然要做容错解析和 schema 校验(因为没有任何东西能 100% 保证)。两道防线叠加,才能让"大模型返回结构化数据"这件事真正稳下来。

第三件事:正解之二——消费端容错解析 + 校验 + 重试

第二层,是在"消费端"做好防御。因为即便开了 JSON 模式,或者你的模型不支持 JSON 模式只能靠提示词,你都不能 100% 信任拿到的就是完美的 JSON——所以消费端必须做三件事:容错解析(尽量从脏文本里把 JSON 抠出来)、schema 校验(确认结构和字段类型都对)、失败重试(给它再来一次的机会)。

import json, re
from jsonschema import validate, ValidationError

def parse_llm_json(raw: str, schema: dict, retry_fn=None, max_retry=2):
    for attempt in range(max_retry + 1):
        try:
            # 1. 容错: 先尝试从可能的脏文本里"抠"出 JSON 主体
            cleaned = extract_json(raw)
            obj = json.loads(cleaned)        # 2. 解析
            validate(obj, schema)            # 3. schema 校验结构和类型
            return obj                       # 全通过, 安全返回
        except (json.JSONDecodeError, ValidationError) as e:
            if attempt < max_retry and retry_fn:
                # 4. 失败重试: 把错误信息反馈给模型, 让它改正后重出
                raw = retry_fn(f"上次输出有误({e}), 请只返回合法JSON")
            else:
                raise

def extract_json(text: str) -> str:
    # 容错: 去掉markdown代码块标记、抠出第一个 { 到最后一个 } 之间的内容
    text = re.sub(r"```(?:json)?|```", "", text).strip()
    start, end = text.find("{"), text.rfind("}")
    return text[start:end + 1] if start != -1 else text

这套消费端的"三件套"层层设防:容错解析(extract_json)负责对付那些"夹带杂质"的输出——把 Markdown 代码块标记去掉、把开头的客气话剥离、只抠出从第一个 { 到最后一个 } 的 JSON 主体,这一步就能救回大量"内容是对的、只是外面包了点杂质"的情况;schema 校验(validate)负责确认"抠出来的 JSON,结构和字段类型都符合预期"(字段全不全、类型对不对),把那些"格式合法但内容不符预期"的也拦住;失败重试(retry_fn)则是最后的补救——如果解析或校验失败了,把具体的错误信息反馈给大模型,让它"看到自己错在哪、改正后重新输出一次",往往第二次就对了。"容错解析 + schema 校验 + 带反馈的重试"这套组合,是消费大模型结构化输出的标准防御姿势——它承认"模型可能出错",并为每一种出错都准备好了应对。源头强约束(JSON 模式)加上消费端这套兜底,双管齐下,才能让大模型的结构化输出真正稳如磐石。

第四件事:提示词的技巧,以及 temperature 的影响

如果你的模型不支持 JSON 模式,或者你想进一步提高纯净度,提示词本身也有不少能提升输出稳定性的技巧;另外,有一个常被忽略的参数 temperature,也直接影响输出的"稳定/发散"程度。

# 提示词技巧 + temperature 调节
prompt = """
请从下面的文本中提取信息, 严格按照以下 JSON 格式返回:
{"name": "姓名(字符串)", "age": 年龄(整数), "tags": ["标签(字符串数组)"]}

要求(重要):
1. 只返回 JSON 本身, 不要任何解释、不要客气话、不要 markdown 代码块。
2. 直接以 { 开头, 以 } 结尾。
3. 字段类型严格按上面标注: age 必须是整数。
4. 找不到的字段, 用 null。

待提取的文本:
{input}
"""
resp = client.chat.completions.create(
    model="...",
    messages=[{"role": "user", "content": prompt}],
    temperature=0,   # 关键! 提取/结构化这类任务, temperature 设0(或很低)
                     # 让输出更确定、更稳定, 减少它"自由发挥"
)

这里两个关键点:第一,提示词要明确、强硬地约束格式——给出确切的 JSON 模板、明确要求"只返回 JSON、别加任何客气话和代码块、直接以大括号开头结尾、字段类型严格如标注、缺失用 null"。把要求写得越死、越没有歧义,模型"自由发挥"的空间就越小。第二,也是常被忽略的——把 temperature 调低(设为 0 或接近 0)。 temperature 控制模型输出的"随机性/创造性":值越高,输出越发散、越有创意(适合写文案、头脑风暴);值越低,输出越确定、越稳定(适合提取、分类、结构化这类"要的就是确定结果"的任务)。对"结构化输出"这种你期望它老老实实、稳定输出的任务,一定要把 temperature 设低——这能显著减少它"今天这么返回、明天那么返回"的随机抖动,让格式更稳定、结果更可复现。我把治理结构化输出的各种手段汇总成一张表:

手段 作用 层次
JSON模式/结构化输出 模型层面强制合法JSON+schema 源头(最优)
提示词强约束 明确要求只返回JSON、给模板 源头
temperature 调低 减少随机性, 输出更确定稳定 源头
容错解析 从脏文本抠出JSON主体 消费端
schema 校验 确认结构和字段类型符合预期 消费端
失败重试(带反馈) 把错误反馈给模型让它改正重出 消费端

第五件事:大模型输出的"不确定性",要纳入工程考量

这次事故,本质上暴露的是大模型一个绕不开的特性——输出的不确定性。它和传统程序"同样输入必得同样输出"的确定性截然不同,这给工程带来一系列新挑战,不只 JSON 格式这一桩。我把大模型输出不确定性带来的常见工程问题和对策整理成一张表:

不确定性表现 带来的问题 应对
格式不稳定(本次) 解析失败 JSON模式+容错+校验+重试
同输入不同输出 结果不可复现, 难测试 temperature调0, 记录种子
内容幻觉/编造 给出看似合理实则错的内容 提示约束+给依据+人工校验
偶尔不遵守指令 没按要求做 提示强化+校验输出+重试
输出长度/内容波动 下游处理逻辑要兼容 消费端做好兼容和兜底

这张表传达的核心,是一个做 AI 应用必须接受的现实:大模型是"概率性"的,它的输出天生带有不确定性;而我们写的传统程序逻辑,是"确定性"的、容不得意外的。把一个"不确定的生产者"接到一个"要求确定的消费者"上,中间的这道鸿沟,需要工程去弥合。弥合的办法,就是这一整套——用 JSON 模式、低 temperature、强提示词从源头收窄它的不确定性,再用容错解析、schema 校验、重试、兜底在消费端兜住残留的不确定性。做大模型应用,很大一部分工程量,恰恰就花在"驯服这种不确定性"上——你要时刻假设它的输出可能不符合预期,并为这种"不符合预期"做好万全的准备。谁把这种不确定性当成头等大事去认真对待、去层层设防,谁做出来的 AI 应用就稳;谁天真地把大模型当成一个确定可靠的 API、拿到输出就直接用,谁就会像我那次一样,在生产里被它的"自由发挥"打个措手不及。

一张"让大模型稳定输出结构化数据"的决策图

把这次踩坑沉淀成一张图。每当你要让大模型返回 JSON 等结构化数据给程序用时,照着它走一遍:

这张图把"源头收窄不确定性 + 消费端兜住不确定性"的完整链路串了起来:源头(JSON模式/强提示词/低temperature)尽量让它输出对,消费端(容错解析/校验/重试/兜底)假设它可能输出错。两头都做到,大模型的结构化输出就能从"上线偶发崩溃"变成"稳如磐石"。这张图,基本就是做这类功能的标准作业流程。

我立下的几条结构化输出规矩

这次"大模型 JSON 解析失败"的事故后,团队做 AI 功能的规范里加了这么几条:

  1. 优先用 JSON 模式/结构化输出:需要结构化数据时,首选模型/API 的 JSON mode、structured output 或 function calling,从源头强制格式。
  2. 提示词明确强约束:给确切的 JSON 模板,明确"只返回 JSON、不要客气话和代码块、字段类型严格、缺失用 null"。
  3. 结构化任务 temperature 调低:提取、分类、结构化这类要确定结果的任务,temperature 设 0 或接近 0。
  4. 消费端必做容错解析:别直接 JSON.parse 原始输出,先剥离代码块/客气话、抠出 JSON 主体再解析。
  5. 必做 schema 校验:解析后用 schema 校验结构和字段类型,别假设字段都在、类型都对。
  6. 失败带反馈重试:解析/校验失败时,把错误反馈给模型让它改正重出,并限制重试次数。
  7. 永远准备兜底:重试若仍失败,要有兜底逻辑(默认值/降级/明确报错),别让一次解析失败搞崩整个流程。

这几条里,我最想强调的是贯穿始终的那个心态——"假设它会出错,并为出错做好准备"。第一到三条是"努力让它别出错",第四到七条是"它万一出错了我也接得住"。两者缺一不可,但后者尤其重要、也尤其容易被忽视:因为大模型是概率性的,你永远无法 100% 保证它的输出完美,所以"它出错时我怎么办"这个问题,必须在写代码时就回答好,而不能寄希望于"它应该不会出错吧"。我那次的惨痛,正是因为只做了前半截(在提示词里要求了 JSON)、却完全没做后半截(拿到就直接解析、没有任何容错和兜底)——于是模型一"发挥",我的程序就当场崩了。做 AI 应用,一半是"引导模型把事做对"的乐观努力,另一半是"模型没做对时我也稳得住"的悲观防御;两手都要硬,缺了防御那一手,你的 AI 功能就是个随时会被模型的随机性绊倒的玻璃人。

写在最后:与"不确定性"共处,是 AI 工程的新课题

这次"大模型 JSON 解析失败"的经历,让我触摸到了 AI 工程与传统软件工程之间一个根本的、范式级的差异。传统软件工程,建立在"确定性"这块坚实的基石之上:同样的输入,必然得到同样的输出;一个函数的行为,是可预测、可复现、有保证的——我们整个工程方法论(测试、调试、契约),几乎都构建在这份确定性之上。而大模型,引入了一种我们过去很少需要直面的东西——本质性的"不确定性":同样的输入,它可能给你不同的输出;它的行为是概率性的、"尽力而为"的,而非确定的、有保证的。我那次的措手不及,根源就在于:我仍然带着"确定性"时代的思维惯性(以为请求 JSON 就一定得到合法 JSON),去对待一个"不确定性"的新事物。

想通这一点,我意识到:做好 AI 应用,需要我们完成一次思维的升级——从"假设一切都是确定的、可保证的",升级到"接受并妥善地与不确定性共处"。这不是说要放弃对可靠性的追求,恰恰相反,是要用一套新的工程手段,去"驯服"这份不确定性,在一个不确定的地基上,盖出可靠的房子。这套手段,就是这篇文章讲的这些:用强约束去收窄它、用校验去检查它、用重试去补救它、用兜底去接住它。传统工程是"在确定性上构建可靠",而 AI 工程是"在不确定性上构建可靠"——后者更难,但也正是 AI 时代工程师价值的新所在。

所以,如果你也正在从传统开发转向做 AI 应用,我想把这次踩坑最想说的话送给你:请主动地、彻底地放下"确定性"的思维惯性,去正视并拥抱大模型的"不确定性"。别再期待它像个传统 API 那样给你确定可靠的输出;而要把"它的输出是概率性的、可能出错的"当成第一前提,然后用强约束、校验、重试、兜底这一整套工程手段,去把这份不确定性,妥善地管理起来、兜底起来。谁能更早地完成这次从"确定性思维"到"与不确定性共处"的转变,谁就能更稳地驾驭大模型,做出真正可靠的 AI 产品。那一段段"花式不合法"的 JSON,最终教给我的,正是这份与不确定性共处的智慧——它让我明白,在 AI 的世界里,可靠不再是"理所当然的前提",而是"需要你用工程去主动争取的结果"。愿你也能在这片充满不确定性的新大陆上,稳稳地盖起你那座可靠的房子。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

服务突然大面积报错、写文件上传写库全线失败,登上机器 df 一看磁盘竟然满了:日志从上线起从没切割清理、悄悄撑爆整块磁盘的全线崩溃避坑复盘

2026-6-1 14:06:09

技术教程

用户刚保存提示成功一刷新却还是旧内容、下单成功转头查这单却偶发报订单不存在:读写分离架构下主从复制延迟读到旧数据的踩坑复盘

2026-6-1 14:18:21

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索