大模型结构化输出与 JSON Schema 约束生成:从“自由文本“到“可靠数据“
大模型结构化输出与 JSON Schema 约束生成:从"自由文本"到"可靠数据"

一、大模型输出的"自由散漫":为什么你的 JSON 总是解析失败
大模型天生是自由文本生成器——它擅长写文章、讲故事,但不擅长输出严格格式的结构化数据。当你要求大模型输出 JSON 时,它可能给你加上 Markdown 代码块标记、在键名前后加空格、漏掉闭合括号、甚至在中途"跑题"生成一段解释文字。在 Agent 系统中,下游工具依赖 JSON 格式的输入,一次解析失败就可能导致整个工作流中断。
结构化输出技术通过约束解码(Constrained Decoding)和 JSON Schema 约束,确保大模型的每一步生成都符合预定义的格式规范,将"自由文本"转化为"可靠数据"。
二、结构化输出架构
flowchart TD
A[用户请求] --> B[JSON Schema 定义]
B --> C[约束解码引擎]
C --> C1[Token 白名单过滤]
C --> C2[状态机驱动生成]
C --> C3[格式修复兜底]
C1 --> D[结构化 JSON 输出]
C2 --> D
C3 --> D
D --> E[输出校验]
E --> E1[Schema 校验]
E --> E2[语义校验]
E1 --> F[可靠数据]
E2 --> F
2.1 JSON Schema 定义与约束
# schema_definitions.py — 常用 JSON Schema 定义
# 设计意图:为大模型结构化输出提供标准化的 Schema 定义
# API 响应 Schema
API_RESPONSE_SCHEMA = {
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["success", "error"]},
"data": {"type": "object"},
"error": {
"type": ["string", "null"],
"description": "错误信息,成功时为 null",
},
},
"required": ["status", "data"],
}
# 工具调用 Schema
TOOL_CALL_SCHEMA = {
"type": "object",
"properties": {
"tool": {"type": "string", "description": "工具名称"},
"arguments": {
"type": "object",
"description": "工具参数",
"additionalProperties": True,
},
"thought": {
"type": "string",
"description": "调用此工具的推理过程",
},
},
"required": ["tool", "arguments"],
}
# 数据提取 Schema
EXTRACT_ENTITIES_SCHEMA = {
"type": "object",
"properties": {
"entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"type": {
"type": "string",
"enum": ["person", "organization", "location", "date", "product"],
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
},
},
"required": ["name", "type", "confidence"],
},
}
},
"required": ["entities"],
}
2.2 约束解码引擎
# constrained_decoder.py — 约束解码引擎
# 设计意图:在生成过程中约束每一步的 token 选择,确保输出符合 JSON 格式
import json
import re
from typing import Generator
class JSONConstrainedDecoder:
"""基于状态机的 JSON 约束解码器
核心思想:维护 JSON 生成的当前状态(对象键、数组元素、字符串值等),
在每一步只允许生成符合当前状态的 token
"""
def __init__(self, schema: dict):
self.schema = schema
self.state_stack = ["start"] # 状态栈
self.key_stack = [] # 当前键路径
def get_allowed_tokens(self, generated_text: str) -> list[str] | None:
"""根据已生成的文本,返回下一步允许的 token
返回 None 表示不约束(自由生成)
返回空列表表示无合法 token(格式错误)
"""
# 尝试解析已生成的文本,判断当前状态
text = generated_text.strip()
# 移除 Markdown 代码块标记
text = re.sub(r'^```json\s*', '', text)
text = re.sub(r'\s*```$', '', text)
# 判断是否在 JSON 字符串内部
if self._inside_json_string(text):
return None # 字符串内部自由生成
# 判断当前 JSON 结构状态
if not text:
return ["{"] # 必须以 { 开始
# 检查未闭合的括号
open_braces = text.count("{") - text.count("}")
open_brackets = text.count("[") - text.count("]")
if open_braces > 0 or open_brackets > 0:
return None # JSON 结构未完成,允许继续生成
return [] # JSON 已完成,不允许继续生成
def _inside_json_string(self, text: str) -> bool:
"""判断当前是否在 JSON 字符串内部"""
# 简化实现:统计未闭合的引号数
in_string = False
escaped = False
for char in text:
if escaped:
escaped = False
continue
if char == "\\":
escaped = True
continue
if char == '"':
in_string = not in_string
return in_string
def post_process(self, text: str) -> str:
"""后处理:清理和修复 JSON 文本"""
# 移除 Markdown 代码块
text = re.sub(r'^```json\s*', '', text.strip())
text = re.sub(r'\s*```$', '', text)
# 移除尾部非 JSON 字符
result = ""
depth = 0
for char in text:
if char == "{":
depth += 1
elif char == "}":
depth -= 1
result += char
if depth == 0 and char == "}":
break
return result
2.3 格式修复兜底
# json_repair.py — JSON 格式修复
# 设计意图:当约束解码无法完全保证格式时,用修复逻辑兜底
import json
import re
class JSONRepair:
"""JSON 格式修复器"""
def repair(self, text: str) -> tuple[dict | None, str]:
"""尝试修复并解析 JSON 文本
返回: (解析结果, 修复说明)
"""
repairs = []
# Step 1: 清理
text = text.strip()
text = re.sub(r'^```json\s*', '', text)
text = re.sub(r'\s*```$', '', text)
text = text.strip()
# Step 2: 直接解析
try:
return json.loads(text), "无需修复"
except json.JSONDecodeError:
pass
# Step 3: 修复常见问题
# 3.1 移除尾部逗号
text = re.sub(r',\s*([}\]])', r'\1', text)
repairs.append("移除尾部逗号")
# 3.2 补全缺失的闭合括号
open_braces = text.count("{") - text.count("}")
open_brackets = text.count("[") - text.count("]")
text += "]" * open_brackets + "}" * open_braces
if open_braces > 0 or open_brackets > 0:
repairs.append(f"补全 {open_braces} 个 }} 和 {open_brackets} 个 ]")
# 3.3 修复单引号为双引号
text = text.replace("'", '"')
repairs.append("单引号替换为双引号")
# Step 4: 再次解析
try:
return json.loads(text), "修复: " + ", ".join(repairs)
except json.JSONDecodeError as e:
return None, f"修复失败: {str(e)}"
def extract_json_from_text(self, text: str) -> dict | None:
"""从混合文本中提取 JSON"""
# 尝试匹配 JSON 对象
pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
for match in re.finditer(pattern, text, re.DOTALL):
result, _ = self.repair(match.group())
if result is not None:
return result
return None
2.4 结构化输出 Pipeline
# structured_output_pipeline.py — 结构化输出完整管线
# 设计意图:整合约束解码、格式修复和 Schema 校验的完整管线
import json
import jsonschema
class StructuredOutputPipeline:
def __init__(self, schema: dict, llm_client):
self.schema = schema
self.llm_client = llm_client
self.decoder = JSONConstrainedDecoder(schema)
self.repair = JSONRepair()
async def generate(self, prompt: str, max_retries: int = 3) -> dict:
"""生成结构化输出"""
system_prompt = self._build_system_prompt()
for attempt in range(max_retries):
# 生成
raw = await self.llm_client.chat(
f"{system_prompt}\n\n{prompt}",
temperature=0.1,
)
# 后处理
cleaned = self.decoder.post_process(raw)
# 修复
result, repair_msg = self.repair.repair(cleaned)
if result is None:
# 修复失败,重试并附加错误信息
prompt += f"\n\n[上次输出格式错误,请确保输出合法 JSON]"
continue
# Schema 校验
try:
jsonschema.validate(result, self.schema)
return result
except jsonschema.ValidationError as e:
prompt += f"\n\n[Schema 校验失败: {e.message},请修正]"
# 所有重试失败,返回空结构
return self._empty_structure()
def _build_system_prompt(self) -> str:
"""构建系统提示词"""
schema_str = json.dumps(self.schema, indent=2, ensure_ascii=False)
return (
f"你必须输出符合以下 JSON Schema 的 JSON 对象,"
f"不要输出任何其他内容(不要 Markdown 代码块标记,不要解释文字):\n"
f"```json\n{schema_str}\n```"
)
def _empty_structure(self) -> dict:
"""生成符合 Schema 的空结构"""
if self.schema.get("type") == "object":
result = {}
for key, prop in self.schema.get("properties", {}).items():
if key in self.schema.get("required", []):
prop_type = prop.get("type")
if prop_type == "string":
result[key] = ""
elif prop_type == "array":
result[key] = []
elif prop_type == "object":
result[key] = {}
elif prop_type == "number":
result[key] = 0
elif prop_type == "boolean":
result[key] = False
return result
return {}
四、边界分析与架构权衡
约束解码的性能开销:基于状态机的约束解码需要在每一步生成时计算允许的 token 集合,增加约 10%-20% 的延迟。对于实时交互场景,可以考虑使用 vLLM/Outlines 等框架的 GPU 加速约束解码。
Schema 复杂度的限制:嵌套层级过深或条件逻辑过复杂的 Schema(如 oneOf、anyOf、if-then-else)可能导致约束解码器状态爆炸。建议将复杂 Schema 拆分为多个简单 Schema,分步生成。
格式修复的可靠性:修复逻辑基于启发式规则,对于严重损坏的 JSON 可能修复出"合法但语义错误"的结果。建议在修复后增加语义校验(如检查枚举值、数值范围)。
大模型的结构化能力差异:不同模型对结构化输出的支持程度不同。GPT-4 和 Claude 3.5 的 JSON 生成准确率可达 95% 以上,而开源 7B 模型可能只有 70%-80%。对于准确率要求高的场景,建议使用支持原生结构化输出的模型 API(如 OpenAI 的 Structured Outputs)。
五、总结
大模型结构化输出通过约束解码、格式修复和 Schema 校验三层保障,将自由文本转化为可靠数据。落地要点:JSON Schema 定义输出格式规范;约束解码器在生成过程中约束 token 选择;格式修复兜底处理常见格式错误;Schema 校验确保语义正确。关键权衡:约束解码保证格式但增加延迟,格式修复提高容错但可能引入语义错误,简单 Schema 可靠但表达能力有限。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)