结构化输出为什么总崩:JSON 约束 / 校验 / 重试修复的工程做法(含 Python 模板)
摘要: 大模型接入业务后,结构化输出不稳定(如JSON格式错误、字段缺失等)成为常见问题,若不处理将导致解析失败、成本上升和体验下降。文章提出一套工程方案:约束→解析→校验→修复重试→降级兜底,核心包括: 明确Schema,用Pydantic/JSON Schema校验; 提取JSON片段,避免文本污染; 有限次修复重试,提示词聚焦修复; 降级策略,返回可读文本+错误码。 附Python模板,覆盖
很多团队把大模型接进业务后,最先翻车的不是“回答不够聪明”,而是结构化输出不稳定:
- 你要 JSON,模型给你“看起来像 JSON”的东西(多了注释/少了引号/多了逗号)
- 你要固定字段,模型临时加字段/漏字段/类型飘了
- 你要数组,模型给你字符串;你要数字,模型给你“约 3.5k”
这类问题如果不工程化处理,最后会变成:
上游看似输出了 JSON,下游解析报错 → 重试放大 → 成本上升 → 延迟变差 → 用户体验崩。
这篇文章给一套可复用的工程方案:约束 → 解析 → 校验 → 修复重试 → 降级兜底,并附最小 Python 模板。
0)TL;DR(先给结论)
- 不要只靠 Prompt“祈祷”:结构化输出需要“解析 + 校验 + 修复”的闭环。
- 把失败当成正常路径:准备好 3 类失败的兜底:解析失败、校验失败、语义错误。
- 工程上最稳的路线:
- 明确 Schema(字段/类型/约束/示例)
- 解析时做“JSON 提取”(别直接
json.loads(text)) - 用 Pydantic/JSON Schema 校验
- 校验失败走“修复提示词”重试(有限次数)
- 仍失败就降级输出(返回可读文本 + 错误码)
1)为什么“看起来像 JSON”≠“可解析 JSON”(失败模式清单)
常见失败你大概率都见过:
1.1 语法层失败(parse fail)
- 末尾多了逗号(trailing comma)
- 用了单引号
'、或字段名没加引号 - 混入了注释、Markdown 代码块、解释性文字
- 字符串里有未转义的换行/引号
1.2 结构层失败(validate fail)
- 缺字段、字段名拼错
- 类型不对(数字变字符串/数组变对象)
- 枚举值越界(你只允许
low/medium/high,它输出了urgent) - 约束不满足(
items不能为空,它给了空数组)
1.3 语义层失败(semantic fail)
JSON 语法正确、Schema 也过了,但内容不对:
- “金额”字段填了描述性文本
- “日期”字段不在范围内
- “操作动作”与上下文矛盾
结论:结构化输出要稳,必须把 parse / validate / semantic 三类失败都纳入链路。
2)你应该选哪种“约束方式”?(三种常见路线)
2.1 纯 Prompt 约束(最脆弱)
写法:让模型“只输出 JSON,不要解释”。
问题:在复杂任务、长上下文或温度较高时非常容易漂。
适用:PoC、内部脚本、容错高的场景。
2.2 JSON Schema / Pydantic 约束(最通用)
写法:明确字段、类型、枚举、示例;输出后做程序校验。
优点:可落地、可测试、可回归。
缺点:仍需处理“模型没按 Schema 输出”的情况(所以要有修复链路)。
适用:绝大多数生产业务。
2.3 工具调用/函数调用式约束(更强,但要配套)
写法:把输出变成“调用一个函数并填入参数”。
优点:约束更强。
缺点:对 SDK/平台能力、工具定义、权限控制要求更高;仍需要校验与兜底。
适用:工具链成熟、对稳定性要求极高的系统。
3)最小闭环:约束 → 解析 → 校验 → 修复 → 降级
我建议你把结构化输出链路固定成这样(顺序很重要):
LLM输出(text)
-> 提取JSON片段(extract)
-> 解析(parse)
-> 结构校验(validate)
-> 语义校验(optional)
-> 修复重试(repair, <=N次)
-> 降级兜底(fallback)
其中最关键的两个工程点:
- JSON 提取:别让解释性文字污染解析(尤其是模型喜欢加“好的,以下是 JSON:”)
- 有限修复重试:修复不是无限循环,必须有次数上限与错误分级
4)Python 最小模板:Pydantic 校验 + 修复重试(可复制)
4.1 定义你的输出 Schema(示例)
以“把一段用户反馈结构化”为例:
from pydantic import BaseModel, Field
from typing import Literal, List
class FeedbackItem(BaseModel):
category: Literal["bug", "feature", "question"] = Field(..., description="反馈类型")
severity: Literal["low", "medium", "high"] = Field(..., description="严重程度")
summary: str = Field(..., min_length=1, description="一句话总结")
evidence: List[str] = Field(default_factory=list, description="原文证据片段(可为空)")
4.2 JSON 提取:从文本里“捞出”第一个 JSON 对象
import json
def extract_first_json(text: str) -> str:
"""
最简单可用版:从文本中找到第一个 '{' 到最后一个 '}'。
生产环境可升级为:基于括号栈的提取,或只提取 ```json ... ```代码块。
"""
start = text.find("{")
end = text.rfind("}")
if start == -1 or end == -1 or end <= start:
raise ValueError("no_json_object_found")
return text[start : end + 1]
def parse_json_object(text: str):
raw = extract_first_json(text)
return json.loads(raw)
提醒:这是“最小可用”。如果你的模型经常输出多个 JSON、或包含嵌套大括号,建议用“括号栈”更稳。
4.3 修复重试:让模型只做“修 JSON”这件事
修复提示词的关键是:输入原始错误 + 目标 Schema + 只输出 JSON。
REPAIR_PROMPT = """你是一个严格的JSON修复器。
任务:把下面的输出修复为“可解析且符合Schema”的JSON。
要求:
1) 只输出JSON本体,不要解释,不要Markdown代码块
2) 字段必须与Schema一致:{schema}
3) 无法确定的字段用最合理的默认值(例如 evidence 为空数组)
原始输出:
{bad_output}
解析/校验错误:
{error}
"""
4.4 完整闭环:最多修复 N 次,仍失败就降级
下面用“伪调用函数”代表你的 LLM 调用(你替换成自己的 SDK 即可):
from pydantic import ValidationError
def call_llm(prompt: str) -> str:
# TODO: 替换为你的真实大模型调用(OpenAI兼容接口/自建服务都行)
#我自己用的是大模型API聚合平台147API
return ""
def run_structured(text: str, max_repair: int = 2) -> FeedbackItem:
schema = FeedbackItem.model_json_schema()
# 第一次:正常生成
prompt = f"""请把下面用户反馈结构化为JSON,字段遵循Schema:{schema}
只输出JSON,不要解释。
用户反馈:{text}
"""
out = call_llm(prompt)
last_error = None
for _ in range(max_repair + 1):
try:
obj = parse_json_object(out)
return FeedbackItem.model_validate(obj)
except (json.JSONDecodeError, ValidationError, ValueError) as e:
last_error = str(e)
if _ >= max_repair:
break
out = call_llm(
REPAIR_PROMPT.format(schema=schema, bad_output=out, error=last_error)
)
# 降级:返回一个“可用但不完美”的结构(或抛业务异常)
raise RuntimeError(f"structured_output_failed: {last_error}")
5)工程落地 Checklist(发布前自检)
5.1 Prompt 与解码参数
- 明确 Schema(字段/类型/枚举/示例)
- 输出要求写死:只输出 JSON、本体无解释
- 温度尽量低(例如
temperature=0~0.2,视模型而定) - 限制输出长度(max_tokens)避免截断
5.2 解析与校验
- 不直接
json.loads(full_text),先做 JSON 提取 - 用 Pydantic/JSON Schema 校验
- 校验失败要区分错误类型(parse/validate/semantic)
5.3 修复与降级
- 修复重试次数有限(例如 1~3 次),避免死循环
- 失败要有降级:返回可读文本/错误码/兜底模板
- 记录
repair_count与错误分布,后续才能优化
6)资源区:当你要做多模型对比或路由时,先把接入层统一
结构化输出的稳定性与成本通常需要做对比评测(不同模型、不同提示词版本)。
工程上更省事的做法是统一成 OpenAI 兼容调用方式(很多时候只改 base_url 与 api_key)。
例如某些聚合入口提供 OpenAI 兼容端点[我用的是大模型聚合平台147API](参数以其控制台/文档为准),便于你快速做 A/B 评测与路由试验。
更多推荐

所有评论(0)