为什么结构化输出如此重要
在生产环境中,LLM的输出需要被程序解析和处理。非结构化的自然语言输出虽然灵活,但带来三个严重问题:
- 解析不可靠:正则提取容易遗漏边界情况
- 集成困难:下游系统需要稳定的接口契约
- 验证缺失:无法保证输出满足业务约束
2026年,结构化输出已从"nice to have"变为"must have"。所有主流模型都提供了原生结构化输出能力。
技术方案全景
方案对比
| 方案 | 原理 | 可靠性 | 性能 | 灵活性 | 适用场景 |
|---|---|---|---|---|---|
| JSON Mode | 模型内置JSON生成 | 95% | 高 | 中 | 简单结构 |
| Function Calling | 函数签名约束 | 97% | 高 | 高 | API调用 |
| Constrained Decoding | 解码时约束 | 99% | 中 | 最高 | 严格格式 |
| Pydantic + LLM | Schema验证+重试 | 90% | 低 | 高 | 复杂校验 |
| XML标签 | 标签结构化 | 85% | 高 | 低 | 简单提取 |
JSON Mode
基本使用
import json
from openai import OpenAI
client = OpenAI()
# OpenAI JSON Mode
response = client.chat.completions.create(
model="gpt-4o-2026",
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": """你是一个信息提取助手。
请将用户输入提取为JSON格式,包含以下字段:
- name: 姓名
- age: 年龄(整数)
- skills: 技能列表(字符串数组)
- experience: 工作经验(整数,单位年)
"""
},
{
"role": "user",
"content": "张三,28岁,精通Python和JavaScript,有5年开发经验"
}
]
)
result = json.loads(response.choices[0].message.content)
print(result)
# 输出: {"name": "张三", "age": 28, "skills": ["Python", "JavaScript"], "experience": 5}
JSON Schema约束
# 2026年最新:JSON Schema强化约束
response = client.chat.completions.create(
model="gpt-4o-2026",
response_format={
"type": "json_schema",
"json_schema": {
"name": "employee_info",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "maxLength": 50},
"age": {"type": "integer", "minimum": 18, "maximum": 65},
"skills": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 20
},
"experience": {"type": "integer", "minimum": 0, "maximum": 50},
"level": {
"type": "string",
"enum": ["junior", "mid", "senior", "expert"]
}
},
"required": ["name", "age", "skills", "experience", "level"],
"additionalProperties": False
}
}
},
messages=[...]
)
各家模型JSON Mode对比
| 模型 | JSON可靠性 | Schema支持 | 性能影响 | 特殊限制 |
|---|---|---|---|---|
| GPT-4o | 95% | 完整 | <5% | 需提示JSON关键词 |
| Claude 4 | 93% | XML标签 | <3% | 推荐XML格式 |
| Gemini 2 | 94% | 部分支持 | <5% | |
| Qwen 3 | 92% | 部分 | <5% | |
| Llama 4 | 88% | 不支持 | <8% | 需Few-shot |
Function Calling
基本架构
from dataclasses import dataclass
from typing import Callable
import inspect
@dataclass
class ToolDefinition:
name: str
description: str
parameters: dict # JSON Schema
class FunctionCallingSystem:
"""
2026年Function Calling最佳实践
"""
def __init__(self, model_client):
self.model = model_client
self.tools: dict[str, ToolDefinition] = {}
self.handlers: dict[str, Callable] = {}
def register_function(self, func: Callable, description: str):
"""注册可调用函数"""
# 自动从函数签名生成Schema
sig = inspect.signature(func)
params = {}
required = []
for name, param in sig.parameters.items():
param_type = param.annotation
json_type = self._python_type_to_json(param_type)
params[name] = {
"type": json_type,
"description": self._extract_param_doc(func, name)
}
if param.default == inspect.Parameter.empty:
required.append(name)
tool = ToolDefinition(
name=func.__name__,
description=description or func.__doc__,
parameters={
"type": "object",
"properties": params,
"required": required
}
)
self.tools[func.__name__] = tool
self.handlers[func.__name__] = func
async def execute_with_functions(self, user_message: str) -> str:
"""带函数调用的对话"""
messages = [{"role": "user", "content": user_message}]
tools = [t.__dict__ for t in self.tools.values()]
while True:
response = await self.model.chat(
messages=messages,
tools=tools,
tool_choice="auto"
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
# 模型没有调用工具,返回最终回复
return message.content
# 执行函数调用
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
# 参数验证
validation = self._validate_arguments(func_name, func_args)
if not validation["valid"]:
result = f"参数错误: {validation['errors']}"
else:
# 执行函数
try:
handler = self.handlers[func_name]
result = await handler(**func_args)
except Exception as e:
result = f"执行错误: {str(e)}"
# 将结果返回给模型
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result)
})
实际应用示例
# 定义工具函数
@dataclass
class SearchResult:
title: str
url: str
snippet: str
async def search_web(query: str, max_results: int = 5) -> list[dict]:
"""搜索网络内容
Args:
query: 搜索关键词
max_results: 最大返回结果数(默认5)
"""
# 实际搜索逻辑
results = await search_engine.search(query, limit=max_results)
return [{"title": r.title, "url": r.url, "snippet": r.snippet}
for r in results]
async def get_weather(city: str, unit: str = "celsius") -> dict:
"""获取指定城市的天气信息
Args:
city: 城市名称
unit: 温度单位(celsius或fahrenheit)
"""
weather = await weather_api.get(city, unit)
return {
"city": city,
"temperature": weather.temp,
"condition": weather.condition,
"humidity": weather.humidity
}
async def send_email(to: str, subject: str, body: str) -> dict:
"""发送邮件
Args:
to: 收件人邮箱
subject: 邮件主题
body: 邮件正文
"""
await email_service.send(to, subject, body)
return {"status": "sent", "to": to}
# 注册并使用
system = FunctionCallingSystem(model_client)
system.register_function(search_web, "搜索网络获取最新信息")
system.register_function(get_weather, "查询天气信息")
system.register_function(send_email, "发送邮件")
# 执行
response = await system.execute_with_functions(
"帮我查一下北京今天的天气,然后把结果发邮件给 zhangsan@example.com"
)
Constrained Decoding
原理
Constrained Decoding(约束解码)在生成过程中实时约束token选择,确保输出符合预定义的语法规则。
class ConstrainedDecoder:
"""
约束解码器
在每个生成步骤中,只允许符合Schema的token
"""
def __init__(self, model, grammar):
self.model = model
self.grammar = grammar # 定义输出格式的语法
def generate(self, prompt: str, max_tokens: int = 500) -> str:
"""约束生成"""
generated = ""
for _ in range(max_tokens):
# 获取下一个token的概率分布
logits = self.model.get_next_token_logits(prompt + generated)
# 获取当前状态下的合法token
valid_tokens = self.grammar.get_valid_tokens(generated)
# 将非法token的概率设为负无穷
for token_id in range(len(logits)):
if token_id not in valid_tokens:
logits[token_id] = float('-inf')
# 选择概率最高的合法token
next_token = self.model.sample(logits, temperature=0.0)
if next_token == self.model.eos_token_id:
break
generated += self.model.decode(next_token)
return generated
使用Outlines库(2026年主流方案)
from outlines import models, generate
from pydantic import BaseModel, Field
from typing import Literal
# 定义输出Schema
class MovieReview(BaseModel):
title: str = Field(description="电影标题")
rating: int = Field(ge=1, le=10, description="评分1-10")
sentiment: Literal["positive", "neutral", "negative"]
summary: str = Field(min_length=50, max_length=200,
description="50-200字摘要")
key_points: list[str] = Field(min_items=2, max_items=5)
recommendation: bool
# 约束生成
model = models.transformers("meta-llama/Llama-4-70B")
generator = generate.json(model, MovieReview)
review = generator(
"请对电影《流浪地球3》进行评价。"
"考虑剧情、特效、演技等方面。"
)
# 输出保证符合MovieReview schema
print(review.title) # "流浪地球3"
print(review.rating) # 8
print(review.sentiment) # "positive"
print(review.key_points) # ["特效震撼", "剧情紧凑", ...]
语法约束示例
# 使用正则表达式约束
import outlines
model = models.transformers("Qwen/Qwen3-72B")
# 约束为电话号码格式
phone_generator = generate.regex(
model,
r"\d{3}-\d{3}-\d{4}"
)
phone = phone_generator("生成一个随机电话号码")
# 保证格式: "123-456-7890"
# 约束为选择题格式
choice_generator = generate.choice(
model,
["A", "B", "C", "D"]
)
answer = choice_generator("以下哪个是Python的数据类型?\nA. int\nB. string\nC. float\nD. 以上都是")
# 保证输出: "D"
# 约束为CSV格式
csv_generator = generate.format(
model,
"csv",
columns=["name", "age", "city"]
)
复杂场景:多步骤结构化输出
class StructuredExtractionPipeline:
"""
多步骤结构化信息提取
适用于复杂文档解析场景
"""
def __init__(self, model):
self.model = model
async def extract_from_document(self, document: str) -> dict:
"""从文档中提取结构化信息"""
# 步骤1:文档分类
doc_type = await self._classify_document(document)
# 步骤2:根据类型选择提取Schema
schema = self._get_schema_for_type(doc_type)
# 步骤3:分段提取(长文档)
if len(document) > 5000:
segments = self._split_document(document)
partial_results = []
for segment in segments:
result = await self._extract_segment(segment, schema)
partial_results.append(result)
# 合并结果
final_result = await self._merge_results(partial_results, schema)
else:
final_result = await self._extract_segment(document, schema)
# 步骤4:验证和修正
final_result = await self._validate_and_fix(final_result, schema)
return final_result
async def _extract_segment(self, segment: str, schema: dict) -> dict:
"""使用Function Calling提取"""
response = await self.model.chat(
messages=[
{
"role": "system",
"content": f"""你是信息提取专家。
请从给定文本中提取信息,严格按照以下Schema输出:
{json.dumps(schema, ensure_ascii=False, indent=2)}
注意:
1. 只输出Schema中定义的字段
2. 如果信息不存在,使用null
3. 日期格式:YYYY-MM-DD
4. 金额格式:数字,单位为元
"""
},
{"role": "user", "content": segment}
],
response_format={"type": "json_schema", "json_schema": schema}
)
return json.loads(response.choices[0].message.content)
async def _validate_and_fix(self, result: dict, schema: dict) -> dict:
"""验证并修正提取结果"""
errors = self._validate_schema(result, schema)
if errors:
# 让模型修正错误
fix_prompt = f"""以下提取结果有错误,请修正:
原始结果:{json.dumps(result, ensure_ascii=False)}
错误信息:{json.dumps(errors, ensure_ascii=False)}
Schema要求:{json.dumps(schema, ensure_ascii=False)}
请输出修正后的JSON:
"""
response = await self.model.chat(
messages=[{"role": "user", "content": fix_prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
return result
错误处理与重试策略
class RobustStructuredOutput:
"""带错误处理的结构化输出"""
MAX_RETRIES = 3
async def generate_with_retry(self, prompt: str, schema: dict) -> dict:
"""带重试的结构化生成"""
for attempt in range(self.MAX_RETRIES):
try:
# 尝试生成
response = await self._generate(prompt, schema)
# 验证
self._validate(response, schema)
return response
except JSONDecodeError as e:
# JSON解析失败
print(f"尝试 {attempt+1}: JSON解析失败 - {e}")
prompt = self._add_error_hint(prompt, str(e), schema)
except SchemaValidationError as e:
# Schema验证失败
print(f"尝试 {attempt+1}: Schema验证失败 - {e}")
prompt = self._add_correction_hint(prompt, str(e))
except Exception as e:
print(f"尝试 {attempt+1}: 未知错误 - {e}")
# 所有重试失败,使用降级方案
return await self._fallback_extraction(prompt, schema)
async def _fallback_extraction(self, prompt: str, schema: dict) -> dict:
"""降级方案:使用XML标签提取"""
xml_prompt = f"""{prompt}
请用以下XML格式输出:
<result>
"""
for field, spec in schema["properties"].items():
xml_prompt += f" <{field}></{field}>\n"
xml_prompt += "</result>"
response = await self.model.generate(xml_prompt)
# 解析XML
return self._parse_xml_response(response, schema)
性能优化建议
# 1. 缓存Schema解析
from functools import lru_cache
@lru_cache(maxsize=100)
def get_compiled_schema(schema_json: str):
"""缓存编译后的Schema"""
return jsonschema.Draft7Validator(
json.loads(schema_json)
)
# 2. 批量处理
async def batch_structured_generation(
prompts: list[str],
schema: dict,
concurrency: int = 10
):
"""并发批量结构化生成"""
semaphore = asyncio.Semaphore(concurrency)
async def process_one(prompt):
async with semaphore:
return await generate_structured(prompt, schema)
return await asyncio.gather(*[process_one(p) for p in prompts])
# 3. 流式解析
async def stream_structured_output(prompt: str, schema: dict):
"""流式输出+实时解析"""
buffer = ""
async for chunk in model.stream(prompt):
buffer += chunk
# 尝试增量解析
try:
partial = json.loads(buffer)
yield partial
except:
continue # 等待更多数据
结语
结构化输出是LLM从"对话玩具"走向"生产工具"的关键桥梁。2026年的最佳实践:
- 优先使用原生JSON Mode——简单可靠
- 需要工具调用时用Function Calling——模型原生支持
- 严格格式要求用Constrained Decoding——保证100%合规
- 复杂场景用多步骤管道——分而治之
- 始终实现错误处理——生产系统不容忍崩溃
记住:结构化输出的可靠性 = 模型能力 × Prompt设计 × 后处理验证。 三者缺一不可。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
