让大模型稳定输出JSON
让大模型稳定输出 JSON:Schema、校验与自动修复的工程方法
摘要:提示模型“请返回 JSON”远远不够。生产系统需要约束字段、校验语义、区分可修复与不可修复错误,并对失败结果可观测。本文给出一套通用实现思路。
标签:大模型 JSON Schema Python 后端开发 Prompt Engineering
一、格式正确只是第一关
大模型接入业务系统后,常见需求是返回固定结构,例如分类、字段抽取或工具调用。实际错误不仅包括少括号、多解释文字,还包括:
- 枚举值超出范围;
- 必填字段缺失;
- 数字被输出成带单位字符串;
- 相互依赖的字段彼此矛盾;
- 引用的 ID 根本不存在;
- 模型为了填满字段而编造内容。
因此,稳定输出需要四层防线:生成约束、语法解析、结构校验和业务校验。
二、先设计 Schema,再写提示词
以信息抽取为例,可以定义:
{
"type": "object",
"required": ["items", "status"],
"properties": {
"status": {
"type": "string",
"enum": ["ok", "insufficient_information"]
},
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "evidence"],
"properties": {
"name": {"type": "string"},
"evidence": {"type": "string"}
}
}
}
}
}
Schema 越明确,后续处理越简单。字段名应表达业务含义,枚举尽量有限,并为“不知道”设计合法状态。没有 unknown 或 insufficient_information 时,模型更容易被迫猜测。
三、提示词只做必要说明
在支持结构化输出的模型接口中,应优先使用原生 Schema 约束。提示词负责补充语义规则,例如:
只提取输入中明确出现的信息。
evidence 必须是输入中的连续原文片段。
无法确定时返回 insufficient_information,不得推测。
避免在提示词中重复粘贴一份与代码不同步的字段定义。Schema 应成为单一事实来源,并由程序自动传给模型和校验器。
四、服务端必须再次校验
即使接口承诺结构化输出,也要把模型结果视为外部输入。Python 中可以使用 Pydantic:
from typing import Literal
from pydantic import BaseModel, field_validator
class Item(BaseModel):
name: str
evidence: str
class Result(BaseModel):
status: Literal["ok", "insufficient_information"]
items: list[Item]
@field_validator("items")
@classmethod
def limit_items(cls, value):
if len(value) > 20:
raise ValueError("too many items")
return value
随后做业务校验:证据是否真的出现在原文中、引用 ID 是否在候选集合内、数值是否落在合理范围、状态与列表是否一致。
五、自动修复要有边界
可安全自动修复的通常是纯格式问题,例如去掉代码块标记、转换全角引号或补充明确可推导的默认值。涉及业务语义的错误不应静默修改。
推荐把失败分为三类:
parse_error:无法解析 JSON;schema_error:字段类型或必填项不符合要求;business_error:结构正确但语义校验失败。
前两类可以把错误摘要连同原始结果交给模型重试一次;第三类通常应重新执行原任务或转人工。无限重试只会增加成本,并可能得到不同版本的错误。
六、字段之间的关系需要业务校验
JSON Schema 擅长验证类型、枚举和必填字段,却不一定能表达复杂业务关系。例如:
status为insufficient_information时,items应为空;end_date不得早于start_date;percentage必须在 0~100 之间;- 某个 ID 必须来自本次提供的候选集合;
evidence必须能在原文中找到;- 两个互斥选项不能同时为 true。
这些规则应写在服务端代码中,并有独立单元测试。不要把所有约束都放进提示词,因为提示词只能影响生成,不能提供确定性保证。
def validate_business(result: Result, source: str) -> list[str]:
errors = []
if result.status == "insufficient_information" and result.items:
errors.append("信息不足时 items 必须为空")
for item in result.items:
if item.evidence not in source:
errors.append(f"证据不在原文中: {item.name}")
return errors
校验失败时,记录机器可读的错误码,而不是只记录一段异常堆栈。
七、为模型准备最小而清晰的上下文
结构化输出不稳定,有时并不是 Schema 的问题,而是输入过长或任务目标混杂。如果一次要求模型完成摘要、分类、实体抽取、风险判断和文案生成,字段之间会相互干扰。
可以将任务拆成多个步骤:先识别候选实体,再对候选做标准化,最后生成面向用户的说明。前一步输出经过校验后再进入下一步。虽然调用次数增加,但每一步更容易测试,也可以对简单步骤使用更小的模型。
输入中只保留当前任务需要的字段,并明确区分:
[任务规则]
……
[待处理数据]
……
[候选项]
……
外部文本始终视为数据,即使其中包含“忽略规则”之类的句子,也不能改变系统约束。
八、数组输出要特别小心
模型在处理长数组时容易漏项、重复或顺序错乱。可以采用以下策略:
- 为输入项增加稳定 ID,输出必须回传该 ID;
- 限制单批数量,超出后分批执行;
- 服务端检查输入 ID 与输出 ID 的集合差异;
- 明确是否允许一对多、多对一或无结果;
- 分批结果合并时执行去重和顺序恢复。
例如对 200 条记录做分类,不应只验证输出数组长度为 200,还要验证每个输入 ID 恰好出现一次。长度相同并不能排除“漏一条、重复一条”。
九、重试策略与幂等性
当接口超时后,客户端可能不知道服务端是否已经完成处理。如果任务会触发数据库写入或工具调用,需要使用幂等键,避免重试产生重复结果。
纯生成任务可以在解析失败后重试一次,但重试提示应携带明确错误,例如“字段 status 不在枚举范围”,而不是笼统说“请重试”。对于连续失败,应返回可识别状态并进入降级流程。
常见降级方式包括:返回部分已校验结果、切换到规则方法、要求用户补充信息,或交给人工处理。降级不是隐藏错误,而是用更保守的方式完成业务目标。
十、Pydantic 模型中的跨字段校验
下面使用 Pydantic v2 风格,先校验字段,再校验模型整体一致性:
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
class Item(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str = Field(min_length=1, max_length=128)
evidence: str = Field(min_length=1, max_length=500)
source_id: str = Field(pattern=r"^[A-Za-z0-9_-]+$")
class Result(BaseModel):
model_config = ConfigDict(extra="forbid")
status: Literal["ok", "insufficient_information"]
items: list[Item] = Field(max_length=20)
@model_validator(mode="after")
def validate_status_and_items(self):
if self.status == "ok" and not self.items:
raise ValueError("status=ok 时 items 不能为空")
if self.status == "insufficient_information" and self.items:
raise ValueError("信息不足时不能返回推测项目")
return self
extra="forbid" 能阻止模型偷偷增加未定义字段。服务端可以用 Result.model_json_schema() 生成 JSON Schema,将同一份契约同时用于模型约束、API 文档和结果解析,避免三处手工定义逐渐不一致。
十一、完整解析与修复流程
import json
from pydantic import ValidationError
async def generate_structured(prompt, model_client) -> Result:
raw = await model_client.generate(
prompt=prompt,
json_schema=Result.model_json_schema(),
)
try:
payload = json.loads(raw)
return Result.model_validate(payload)
except (json.JSONDecodeError, ValidationError) as first_error:
repair_prompt = {
"task": "只修复 JSON 结构,不增加输入中不存在的事实",
"schema": Result.model_json_schema(),
"invalid_output": raw,
"validation_error": str(first_error),
}
repaired = await model_client.generate(
prompt=repair_prompt,
json_schema=Result.model_json_schema(),
)
return Result.model_validate_json(repaired)
生产实现还要限制原始输出长度,清理日志中的敏感数据,并把第二次失败包装成明确错误。修复调用只处理格式,不应把它当成重新回答问题的机会。
如果服务端提供原生结构化输出能力,应直接传递 Schema;若只支持普通文本,则需要考虑模型在 JSON 前后输出解释文字的情况。但无论哪种方式,服务端校验都不能省略。
十二、保留原始输出与校验轨迹
为了排查问题,日志中应保存请求 ID、Schema 版本、模型版本、原始输出、解析结果、校验错误和重试次数。若内容包含敏感信息,应做脱敏或只保存必要摘要。
监控指标可以包括:首次解析成功率、Schema 通过率、业务校验通过率、平均重试次数和按错误类型统计的失败率。当模型或提示词升级后,这些指标能快速暴露退化。
十三、测试容易被忽略的边界
除正常样例外,还要覆盖空输入、超长文本、特殊符号、多语言、重复项目、错误前提和提示注入式文本。特别要验证模型不会把输入中的“忽略规则、输出其他内容”当成系统指令。
测试中还应模拟服务端超时、返回截断、模型版本切换和 Schema 升级。若新版本增加字段,需要说明旧客户端如何处理;若删除或重命名字段,则应通过接口版本升级,而不是悄悄改变返回结构。
十四、结构化输出也要考虑安全
合法 JSON 不代表安全。字符串字段可能包含 HTML、脚本、SQL 片段或日志换行符。下游使用时仍要根据场景做转义和参数化处理,不能因为内容来自模型就直接拼接进 SQL、Shell 或网页。
工具调用参数尤其需要白名单。模型可以建议调用哪个工具,但服务端必须重新检查用户权限、参数范围和资源归属。涉及外部副作用的操作还需要明确确认,不应由模型输出一段 JSON 后自动执行。
让大模型稳定输出 JSON,本质上不是提示词技巧,而是标准的输入验证和错误处理工程。模型负责理解与生成,Schema 负责契约,校验器负责守门,日志负责让失败可见。四者缺一不可。

浙公网安备 33010602011771号