你是不是也遇到过这种情况:明明在 Prompt 里写了“只输出 JSON”,大模型还是会时不时返回一段 Markdown 代码块、多一个逗号、漏一个字段,甚至把布尔值true输出成字符串"true"。线上系统一旦接了解析逻辑,轻则异常重试,重则直接把脏数据写进数据库。
我在多个线上大模型项目中踩过同样的坑。早期习惯性认为“只要把 Prompt 写得更严格,问题就能解决”,但实际投入产出比很低。因为大模型生成 JSON 是概率行为,不是编译行为,再详细的 Prompt 也只能提高格式正确的概率,无法做到 100% 保证。后来我把思路从“单一依赖 Prompt”转向“Prompt + 校验 + 后处理 + 重试”的整套防御体系,输出稳定率才真正稳定在可用水平。
本文会完整拆解这套方案的设计思路和代码实现,覆盖大多数大模型项目的通用场景。适合正在做 LLM 应用、Agent 工具调用、结构化信息抽取,或者被模型输出格式折腾过的开发者阅读。读完你可以直接把这些方法沉淀到自己的项目里,作为一套可复用的 JSON 稳定输出管道。
1. 问题背景:大模型输出 JSON 为什么会“飘”
1.1 JSON 输出在 LLM 项目里的典型场景
JSON 是大模型应用中最常用的结构化数据交换格式。比如:
- 信息抽取:从一段文本中提取人名、时间、金额、地点等结构化字段。
- 工具调用:模型判断需要调用哪个函数,并以 JSON 返回函数参数。
- Agent 决策:让模型返回“下一步动作”和“动作参数”。
- 对话状态管理:抽取用户意图和槽位信息,供后端业务逻辑继续处理。
这些场景有一个共同点:大模型输出不能直接给用户看,必须由程序解析成对象,再驱动后续业务。一旦 JSON 格式不合法,整条链路就会断掉。
1.2 输出不稳定的常见表现
在线上项目中,模型输出的问题往往不是每次都错,而是“时好时坏”。我总结下来主要有这几种表现:
| 表现 | 示例 |
|---|---|
| 带 Markdown 代码块包裹 | json\n{...}\n |
| 字段漏掉或字段名变化 | 把user_name输出成username |
| 布尔值变字符串 | 把true输出成"true" |
| JSON 被截断 | 对话长度达到上限,输出停在中间 |
| 多余逗号或注释 | {"name": "张三",}或// 这是注释 |
| 输出纯文本解释 | “好的,以下是你要的 JSON:{...}” |
这些问题不是偶然发生,而是会随机出现。对线上系统来说,哪怕概率只有 2%~5%,在日均调用量较大的项目里也是不可忽略的故障源。
1.3 根因分析:为什么光调 Prompt 不够
大模型本质上是一个“按概率预测下一个 token”的模型。它并不是先构造一棵完整的 JSON 语法树,再序列化输出,而是逐个 token 生成。因此 JSON 格式的“正确性”来自于训练数据中的统计规律,而不是语法级的强制保证。
温度、采样参数、模型版本、token 截断、输入文本的复杂性都会影响输出稳定性。Prompt 能显著提升格式正确率,比如通过 few-shot 示例和明确的格式约束,但无法做到确定性。这就好比我们让一个人工助手“严格按照表格填写”,他大多数时候会遵守,但偶尔也会漏填或填错。
所以,正确的工程思路应该是:用 Prompt 尽量提高正确率,用校验发现问题,用后处理修复可修复的错误,用重试和降级兜底不可修复的情况。四者缺一不可。
2. 稳定方案总览:四层防御体系
2.1 四层防御体系的职责划分
我用“四层防御”来组织整套方案,每一层都有明确边界,又互相衔接:
- 第一层:Prompt 端结构化约束。目标是把格式正确率从“可能不稳定”提升到“大多数情况正确”。
- 第二层:输出校验。用确定性代码检查模型输出,判断 JSON 是否合法、字段是否齐全、类型是否正确。
- 第三层:后处理与格式规整。对可以修复的错误做自动修复,比如去掉 Markdown 包裹、补全截断 JSON。
- 第四层:重试与降级。对修复不了的问题,重新组织 Prompt 发起重试;重试仍然失败时,走降级策略。
这四层的关系可以理解为:Prompt 负责“少出错”,校验负责“发现错”,后处理负责“修小错”,重试负责“重来一次”。
2.2 每层需要注意的边界
每一层都应该只做自己职责范围内的事,不要越界。例如:
- 不要试图在 Prompt 里解决所有问题,否则 Prompt 会越来越复杂,维护难度直线上升。
- 不要用大模型去校验大模型的输出,这既增加成本,也不一定能保证正确。
- 不要在后处理阶段对错误数据做“强行猜值”,修复不了就交给重试。
边界清晰之后,方案的可维护性会好很多。后续模型升级或者切换供应商时,只需要调整 Prompt 层,其他三层基本不用动。
2.3 方案选择原则
这套方案不绑定具体的模型供应商。无论你使用的是 OpenAI、Claude、文心一言还是开源模型,都可以套用。区别只在于:
- 如果模型服务商提供了 JSON Mode、结构化输出或工具调用能力,优先使用。
- 如果没有这些能力,就加强 Prompt 约束和后处理修复。
- 如果模型可以本地部署,可以在解码参数上做更多控制,例如降低温度、固定随机种子。
整体原则是:优先利用平台能力,然后用自己的工程代码兜底。
3. 环境准备与版本说明
3.1 本文示例使用的技术栈
下面的示例以 Python 为主,因为 Python 在处理文本清洗、JSON 解析和重试逻辑方面非常方便。具体依赖如下:
- Python 3.10+
- OpenAI SDK(或其他兼容 OpenAI 接口的 SDK)
- pydantic 2.x,用于结构校验
- json_repair,用于 JSON 容错修复(可选)
- tenacity,用于重试策略(也可手写重试)
需要说明的是:不同版本的 SDK 和第三方库在接口上存在差异,本文不会完全依赖某个固定版本,而是重点演示实现思路。你实际安装时,建议按项目要求固定版本。
安装命令可以参考:
pip install openai pydantic json_repair tenacity如果你的网络环境不能直接安装某些包,也可以选择手写 JSON 修复函数,后面我会给出替代实现。
3.2 示例项目结构
为了方便阅读,我先给出一个简单的项目结构:
llm_json_demo/ ├── config.py # 模型配置 ├── prompt_templates.py # Prompt 模板 ├── json_utils.py # JSON 清洗与后处理 ├── validators.py # 校验逻辑 ├── llm_client.py # 模型调用封装 ├── pipeline.py # 完整输出管道 └── main.py # 运行入口这个结构不复杂,但体现了分层思想。后面我会按模块讲解,并在第 8 章把完整流程串起来。
4. 第一层防御:Prompt 端结构化约束
4.1 设计原则:越具体越好
Prompt 是提高 JSON 输出正确率最前端的手段。但很多人在写 Prompt 时,只会写一句“请输出 JSON”。这对模型来说约束太弱,正确的做法是:
- 定义清楚任务角色。
- 明确输出格式和字段列表。
- 给出字段类型和取值范围。
- 提供结构化示例。
- 明确禁止多余内容。
下面是一个信息抽取场景的 Prompt 模板示例:
""" 你是一个信息抽取助手。请从用户输入的文本中抽取指定字段,并严格按照 JSON 格式输出。 要求: 1. 只输出 JSON 对象,不要输出任何解释、前缀或 Markdown 代码块。 2. JSON 对象必须包含以下字段: - name: string,人名 - age: int,年龄 - city: string,城市 - tags: array[string],标签 3. 如果字段不存在,统一使用空字符串 "" 或空数组 []。 4. 不要使用单引号,不要添加注释。 示例: 输入:张三今年25岁,住在北京,喜欢编程和篮球。 输出:{"name": "张三", "age": 25, "city": "北京", "tags": ["编程", "篮球"]} 用户输入: {user_input} """这一段 Prompt 其实做了三件事:明确任务边界、约束字段结构、提供示例。对大多数模型来说,这已经能显著降低格式错误率。
4.2 使用 few-shot 示例强化格式
在大模型没有专门的 JSON Mode 时,few-shot 示例几乎是约束输出格式最有效的方式。模型会倾向于模仿示例的结构。
示例不宜太多,2~3 个即可,太多会占用上下文,也会分散模型注意力。示例要覆盖:
- 某个字段为空的输出。
- 数组字段只有一项的输出。
- 数字和字符串的区别。
例如,针对“字段不存在”的情况补一个示例:
输入:李四没有提供年龄信息,所在城市未知。 输出:{"name": "李四", "age": 0, "city": "", "tags": []}这个示例的价值在于:它告诉模型,即使信息缺失,也要保证 JSON 结构完整,而不是省略字段。
4.3 调低温度和其他解码参数
如果模型接口允许调整温度,结构化输出场景建议把温度调低,比如 0 到 0.3。温度越低,模型输出越稳定,但创造性也会降低。对 JSON 抽取类任务来说,稳定性优先于创造性。
还有一个容易被忽略的参数是max_tokens。如果输出过长被截断,后面的 JSON 就会不完整。对于结构较复杂的 JSON,建议在估算 token 后留出 20% 到 30% 余量。当然,也不能无限调大,否则会拉高成本和延迟。
4.4 优先使用模型的 JSON Mode
如果你使用的模型服务商支持 JSON 输出模式,优先使用。例如 OpenAI 的response_format={"type": "json_object"},或者通过工具调用(function calling)返回结构化参数。
这类平台能力本质上是一种约束解码方式,比单纯靠 Prompt 更可靠。使用示例:
response = client.chat.completions.create( model="gpt-4o-mini", response_format={"type": "json_object"}, messages=[ {"role": "system", "content": "你是一个 JSON 输出助手。"}, {"role": "user", "content": "抽取文本中的字段,输出 JSON。"} ], temperature=0.1, )注意:不同供应商的 JSON Mode 参数名不完全相同,请以官方文档为准。另外,即使使用了 JSON Mode,代码层面仍然需要做校验,因为它只能保证“看起来是 JSON”,不能保证字段一定符合你的业务要求。
5. 第二层防御:JSON 输出校验
5.1 第一步:基础 JSON 解析校验
模型输出拿到手之后,第一件事就是尝试用json.loads解析。这是最基础的校验,能拦住大部分格式错误。
import json def parse_json(text: str): try: return json.loads(text) except json.JSONDecodeError as e: raise ValueError(f"JSON 解析失败: {e}")这里要注意:直接解析失败时,不要立刻重试,因为有些错误可以通过后处理修复。修复逻辑我放在第 6 章,校验层只负责“判断当前文本是否为合法 JSON”。
5.2 第二步:字段与类型校验
JSON 能解析成功,不代表字段结构正确。例如,业务上要求age是 int,模型可能输出"25"。因此还需要做字段存在性检查和类型检查。
def validate_fields(data: dict, schema: dict): errors = [] for field, expected_type in schema.items(): if field not in data: errors.append(f"缺少字段: {field}") continue value = data[field] if expected_type == "int" and not isinstance(value, int): errors.append(f"字段 {field} 类型错误: 期望 int, 实际 {type(value).__name__}") elif expected_type == "string" and not isinstance(value, str): errors.append(f"字段 {field} 类型错误: 期望 string, 实际 {type(value).__name__}") elif expected_type == "array" and not isinstance(value, list): errors.append(f"字段 {field} 类型错误: 期望 array, 实际 {type(value).__name__}") return errors这种手写校验适合字段少、结构固定的场景。字段一多,我更推荐使用 Pydantic。
5.3 使用 Pydantic 做结构化校验
Pydantic 是 Python 生态中非常好用的数据校验库,它可以把 JSON 数据映射到模型类上,并对字段类型、默认值、嵌套结构统一校验。
from pydantic import BaseModel, Field class PersonInfo(BaseModel): name: str age: int = Field(0, ge=0) city: str = "" tags: list[str] = [] def validate_with_pydantic(data: dict): try: return PersonInfo.model_validate(data) except Exception as e: raise ValueError(f"结构校验失败: {e}")用 Pydantic 的好处是:
- 字段类型自动转换,比如字符串数字可以转成 int。
- 可以设置默认值,缺失字段不会直接报错。
- 支持复杂嵌套结构和枚举校验。
- 代码可读性比手写校验高很多。
如果你的项目使用 Java,类似的能力可以用 Jackson 的ObjectMapper配合 DTO 类实现。思路是一样的,核心是“用强类型结构去约束模型输出”。
5.4 业务规则校验
类型校验之外,还需要做业务规则校验。比如:
age不能为负数。- 城市字段必须是枚举列表内的值。
tags数组不能为空。
这类校验没有通用代码,因为规则依赖具体业务。但设计上建议把业务校验单独拆成一个函数。
def validate_business(person: PersonInfo): errors = [] if person.age < 0 or person.age > 150: errors.append("年龄超出合理范围") if person.city not in ["北京", "上海", "广州", "深圳"]: errors.append("城市不在支持列表中") return errors业务校验失败意味着模型输出“格式对但内容不对”,这种错误后处理无法修复,最合适的做法是进入重试流程,或者使用约束更强的 Prompt 再次请求。
6. 第三层防御:后处理与格式规整
6.1 清理 Markdown 代码块包裹
模型经常把 JSON 包在 Markdown 的代码块里。虽然这方便人在终端阅读,但对程序解析来说是多余内容。
清理方式比较简单:
import re def strip_markdown_code_block(text: str) -> str: pattern = r"^```(?:json)?\s*([\s\S]*?)\s*```$" match = re.match(pattern, text.strip()) if match: return match.group(1).strip() return text.strip()如果输出里还夹杂了“好的,以下是你要的 JSON:”这类前缀,可以使用更粗暴的提取策略:找到第一个{和最后一个},截取中间内容。
def extract_json_object(text: str) -> str: start = text.find("{") end = text.rfind("}") if start == -1 or end == -1 or start >= end: raise ValueError("未找到 JSON 对象") return text[start:end + 1]同理,对于 JSON 数组输出,可以查找第一个[和最后一个]。这个策略在大多数场景下都足够可靠。
6.2 修复截断的 JSON
当输出因为max_tokens不够而被截断时,常见特征是字符串只有一半,或者整个 JSON 尾部缺少}或]。
修复方式要分情况。最简单的策略是“逐步补全”:
def repair_truncated_json(text: str) -> str: repaired = text.strip() open_braces = repaired.count("{") - repaired.count("}") open_brackets = repaired.count("[") - repaired.count("]") if open_braces > 0: repaired += "}" * open_braces if open_brackets > 0: repaired += "]" * open_brackets return repaired这种修复只适用于“恰好被截断”的场景,如果字符串值本身被截断到一半,比如{"name": "张,修复后依然是非法 JSON。对于这种情况,更稳妥的做法是放弃修复,直接重试。所以在修复之前,建议先判断一下是否值得修复,避免把脏数据修成错误数据。
6.3 使用专用容错库
Python 社区有一些专门解决“解析不严格 JSON”的库,比如json_repair和demjson3。以json_repair为例:
import json_repair def repair_json_text(text: str) -> dict: repaired_data = json_repair.loads(text) if repaired_data is None: raise ValueError("JSON 修复失败") return repaired_data这类库能够处理多余逗号、单引号、缺少引号、注释等问题。但要注意:
- 第三方容错库不一定覆盖所有非法格式。
- 修复后的结果需要再次进行字段校验和业务校验。
- 在生产环境使用第三方库前,要评估其维护状态和安全性。
如果你不想引入第三方库,也可以只做 6.1 和 6.2 中的正则清理和括号补全,覆盖最常见的两类问题。
6.4 其他语言的 JSON 后处理思路
如果你使用的是 Java,思路类似。可以用 Jackson 的JsonParser配合容错配置,或者先通过正则提取 JSON 片段再做解析。不过 Java 侧我更建议“不要试图做太复杂的修复”,因为 Java 类型系统严格,修复带来的误判风险更高。
另一个常见的坑是字段名映射问题。比如 Java Bean 中定义了userName,模型返回的是user_name,反序列化后字段为null。这类问题可以在 DTO 字段上加@JsonProperty("user_name")注解解决。字段命名不一致的问题,本质上也可以通过 Prompt 端声明“必须使用 snake_case 输出”来规避。
7. 第四层防御:重试机制与降级方案
7.1 什么情况需要重试
不是所有错误都需要重试。我把错误分成三类:
- 网络类错误:超时、连接中断、限流。需要重试,且重试要带退避策略。
- 格式类错误:JSON 无法解析、字段缺失。可以先修复,修复不了再重试。
- 业务类错误:格式正确,但内容不符合业务规则。可以重试,但大概率还是失败,需要换 Prompt 或换采样参数。
如果把所有错误都无脑重试,会浪费成本、增加延迟,甚至放大限流问题。
7.2 带指数退避的重试实现
使用tenacity可以方便地实现带指数退避的重试策略:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type class LLMJSONError(Exception): pass @retry( retry=retry_if_exception_type(LLMJSONError), stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), reraise=True, ) def call_with_retry(call_func): return call_func()如果你不想引入额外依赖,手写一个重试循环也可以:
import time def call_with_retry_manual(call_func, max_retries=3, base_delay=1): last_error = None for attempt in range(max_retries): try: return call_func() except Exception as e: last_error = e delay = base_delay * (2 ** attempt) print(f"第 {attempt + 1} 次调用失败: {e}, {delay}s 后重试") time.sleep(delay) raise last_error在使用重试时,需要注意请求是否幂等。如果模型调用本身只是“文本生成”,大部分情况下幂等。但如果你的接口会写数据库、发消息、扣费,重试前一定要保证接口幂等,否则可能造成重复扣费或重复写入。
7.3 针对不同失败类型的 Prompt 调整
重试不是简单地把同一句话再问一遍。对格式类错误,可以在重试时强化格式约束,比如把上一轮的失败原因追加到 Prompt 里:
def build_retry_prompt(original_prompt: str, error_message: str) -> str: return f""" {original_prompt} 注意:你上一次的输出未能通过程序解析,错误信息如下: {error_message} 请严格按照 JSON 格式重新输出,不要包含任何额外内容。 """这里的关键是:把“错误反馈”作为新 Prompt 的一部分。模型看到上一次的错误信息后,往往能纠正自己的格式。这种思路类似于“自我纠正”,但它是由确定性的程序错误信息驱动的,效率比让模型凭空检查更高。
对于业务类错误,比如枚举值不合法,可以在重试 Prompt 中明确给出合法值列表。对于网络类错误,不需要修改 Prompt,只要退避重试即可。
7.4 降级方案与人工兜底
重试次数超过上限后,系统不能一直阻塞下去。降级方案的设计依赖具体业务,常见做法有:
- 返回默认 JSON,比如
{"success": false, "reason": "model_output_invalid"}。 - 使用缓存中最接近的成功结果作为兜底。
- 把失败记录写入队列,进入人工审核或异步补偿流程。
- 对于非核心链路,直接跳过本次调用。
这里可以借鉴消息队列的处理思路:把超时重试的失败任务投递到死信队列或错误表,由定时任务或人工介入处理。在项目中使用类似 RabbitMQ 这类消息中间件时,可以把“模型调用失败”当成一条普通消息来处理,设置手动确认、重试次数和死信队列,从而避免在同步调用链路里卡死。
8. 完整实战:搭建稳定的 JSON 输出管道
8.1 需求描述
假设我们有一个简单需求:用户输入一段商品描述,模型输出结构化 JSON,字段包括商品名称、价格、品类、库存状态和标签。我们需要把这些字段稳定解析出来。
8.2 定义 Prompt 模板
# 文件路径:prompt_templates.py EXTRACT_PROMPT = """ 你是一个商品信息抽取助手。请从用户输入的文本中抽取商品信息,并严格按照 JSON 格式输出。 字段要求: - name: string,商品名称 - price: float,价格数值,不要带货币符号 - category: string,商品品类 - in_stock: boolean,库存状态 - tags: array[string],商品标签 约束: 1. 只输出 JSON 对象,不要输出 Markdown 代码块或解释。 2. 如果信息缺失,name 和 category 使用空字符串,in_stock 使用 false,tags 使用空数组。 3. 价格必须是数字类型,例如 99.9,不要输出 "99.9元"。 示例: 输入:小米手机,价格2999元,属于数码产品,有货,支持5G,黑色。 输出:{"name": "小米手机", "price": 2999.0, "category": "数码产品", "in_stock": true, "tags": ["5G", "黑色"]} 用户输入: {user_input} """8.3 封装模型调用
# 文件路径:llm_client.py from openai import OpenAI client = OpenAI() def call_model(prompt: str, temperature: float = 0.1) -> str: response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=temperature, ) return response.choices[0].message.content这里我使用了 OpenAI SDK 的通用写法。如果你使用的是其他厂商的 SDK,把client的初始化和model参数替换成对应实现即可。
8.4 定义校验模型
# 文件路径:validators.py from pydantic import BaseModel, Field class ProductInfo(BaseModel): name: str price: float = Field(ge=0) category: str in_stock: bool tags: list[str] def validate_product(data: dict) -> ProductInfo: return ProductInfo.model_validate(data)8.5 实现完整管道
# 文件路径:pipeline.py import json from json_utils import extract_json_object, repair_truncated_json from validators import validate_product from llm_client import call_model from prompt_templates import EXTRACT_PROMPT def generate_product_json(user_input: str, max_retries: int = 3) -> dict: prompt = EXTRACT_PROMPT.format(user_input=user_input) last_error = None for attempt in range(max_retries): try: raw_output = call_model(prompt) # 第一步:清理 Markdown 包裹和多余文字 candidate = extract_json_object(raw_output) # 第二步:尝试直接解析,失败则尝试修复 try: data = json.loads(candidate) except json.JSONDecodeError: repaired = repair_truncated_json(candidate) data = json.loads(repaired) # 第三步:结构校验和业务校验 product = validate_product(data) return product.model_dump() except Exception as e: last_error = e print(f"第 {attempt + 1} 次尝试失败: {e}") # 第四步:重试时追加错误信息 prompt = f"{prompt}\n\n注意:上一次输出解析失败,错误信息:{e}\n请重新输出合法 JSON。" raise RuntimeError(f"模型输出多次解析失败: {last_error}")8.6 运行与验证
# 文件路径:main.py from pipeline import generate_product_json if __name__ == "__main__": test_input = "华为Mate60 Pro,价格6999元,属于数码产品,目前缺货,支持卫星通话,昆仑玻璃。" result = generate_product_json(test_input) print(result)预期输出类似:
{ "name": "华为Mate60 Pro", "price": 6999.0, "category": "数码产品", "in_stock": false, "tags": ["卫星通话", "昆仑玻璃"] }这个管道虽然简单,但已经包含了四层防御的核心逻辑。你可以在validate_product中扩展更多的业务规则,也可以在重试前加入更复杂的 Prompt 修正策略。
9. 常见问题与排查清单
9.1 高频问题排查表
我把线上项目中最常见的问题整理了一下,方便你对号入座。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 输出被 Markdown 代码块包裹 | 模型默认按 Markdown 格式化 | 正则剔除代码块,或 Prompt 中明确禁止 |
| JSON 被截断 | max_tokens 不够 | 调大 max_tokens;或在后处理中补全括号 |
| 字段名和定义不一致 | Prompt 没有给出明确字段列表 | 使用 few-shot 示例,或通过 Pydantic 映射别名 |
| 布尔值变成字符串 | 模型对类型理解不足 | 在 Prompt 中强调类型;校验时做类型转换 |
| 解析成功但字段缺失 | 源文本没有对应信息 | 在 Prompt 中说明缺省值规则,校验时设置默认值 |
| 调用返回限流错误 | 请求频率超过服务商限制 | 指数退避重试,降低并发,增加本地缓存 |
| 重试多次仍然失败 | Prompt 本身有歧义,或模型能力不足 | 重写 Prompt,简化任务,考虑切换更强模型 |
| 返回内容为空或拒绝回答 | 输入触发了内容安全策略 | 检查触发原因,调整输入措辞,或检查账号权限 |
其中“输入触发了内容安全策略”这类情况要特别注意。有些模型服务商在检测到敏感内容时,会返回错误或空内容。此时重试往往无法解决问题,需要从输入文本和业务合规角度去排查,而不是盲目加重试次数。
9.2 排查流程建议
遇到模型输出不稳定时,建议按下面的顺序排查:
- 查看原始输出日志,确认是格式问题、内容问题,还是网络问题。
- 如果格式问题占比较高,优先检查 Prompt 是否给出了明确的字段类型和示例。
- 如果模型服务商支持 JSON Mode,优先开启。
- 校验一下当前任务的
max_tokens是否充足。 - 检查业务校验规则是否过于严格,导致大量输出被判失败。
- 最后再考虑调整模型版本或切换服务商。
排查时一定要保留“原始输出”。很多团队只记录了解析后的对象,丢失了原始输出,导致问题无法复现。建议在日志中同时记录模型原始输出、修复过程、校验结果和重试次数,这样可以快速定位问题发生在哪一层。
10. 最佳实践与工程建议
10.1 Prompt 侧的最佳实践
Prompt 需要像代码一样做版本管理。不要直接在线上改 Prompt,建议把 Prompt 模板化,并加上版本号。
PROMPT_VERSION = "2025.06.01"当模型输出质量出现波动时,可以通过版本号快速回滚到历史 Prompt。另外,不要在一个 Prompt 里塞太多任务,任务越单一,格式越稳定。如果业务逻辑复杂,可以拆成多个步骤,每个步骤只输出一个 JSON。
10.2 校验与后处理侧的最佳实践
校验逻辑必须使用确定性代码,不依赖大模型。判断“是否为合法 JSON”这种事,用json.loads就足够了。
后处理修复要“保守”。只修复你明确能判断的错误,比如 Markdown 包裹、括号截断。对于不确定的修复结果,宁可返回错误进入重试,也不要输出一个看起来正确但事实错误的数据。
10.3 重试与运维侧的最佳实践
重试要有限度。无限重试会放大故障,建议最多重试 2 到 3 次。重试间隔使用指数退避,并加入随机抖动,避免多个请求在同一时间点集中重试。
线上环境建议给模型调用接口增加超时设置。默认超时时间不宜过长,否则一个慢请求可能拖垮整个线程池。超时时间建议根据模型延迟分布来设置,一般设置在 10 到 30 秒之间比较常见,具体按你的业务和模型服务商调整。
对于调用量较大的项目,建议增加缓存层。同样的输入如果之前已经成功抽取过,直接返回缓存结果,既能降低成本,也能提高稳定性。
10.4 安全与合规提醒
在处理模型输出时,要注意提示注入风险。尤其是当用户输入被直接拼进 Prompt 时,用户可能试图通过输入内容覆盖你的格式约束。面对这种情况,最好的做法是:
- 把系统 Prompt 和用户输入明确分隔。
- 对输出内容做敏感信息过滤。
- 不要直接信任模型返回的数据并写入数据库,必须经过校验层。
- 模型输出中如果包含 HTML、SQL、代码片段,按业务需要进行转义或过滤。
涉及线上数据和数据库写入时,务必在测试环境验证整个流程,保留原始输出日志,并确保任何删除、更新操作都有备份和回滚方案。这套稳定方案的目标是提高系统的可用性,但绝不能以牺牲数据安全为代价。
如果你正在被大模型输出格式问题困扰,建议先把手上的 Prompt 优化一版,再接入校验和后处理,最后补上重试和日志监控。四层都到位之后,线上输出的稳定性会有非常明显的提升。值得收藏备用,下次遇到别的大模型项目也能直接复用这套思路。