大语言模型(Large Language Model,LLM)最擅长生成自然语言。让它解释一个概念、写一段邮件、总结会议纪要,输出给人看通常没有问题。
但工程系统要的不是“看起来合理的一段话”,而是可以被程序稳定解析的数据。
例如,用户输入一段商品描述:
特斯拉 Model Y 电动汽车售价 45990 美元,续航里程 330 英里,预计 2024 年 2 月交付。
如果模型返回:
这是一款特斯拉 Model Y,价格是 45990 美元,续航大约 330 英里,交付时间在 2024 年 2 月。
人能看懂,程序却很难可靠处理。更适合业务系统的输出应该是:
{
"company": "特斯拉",
"product": "Model Y",
"price": "45990美元",
"range": "330英里",
"delivery_date": "2024年2月"
}
这就是结构化输出(Structured Output):让模型按照预先定义的格式返回结果,例如 JSON(JavaScript Object Notation)、XML(Extensible Markup Language)、Markdown 表格、函数调用参数、表单字段,或者某种领域特定语言(Domain-Specific Language,DSL)。
结构化输出的价值不只是“格式更整齐”,而是让 LLM 能进入真实的软件链路:
flowchart LR
A[用户输入 / 文档 / 图片识别结果] --> B[LLM]
B --> C{结构是否合规}
C -- 是 --> D[业务服务]
D --> E[(数据库)]
D --> F[外部 API]
C -- 否 --> G[重试 / 修复 / 降级]
G --> B
一旦输出可解析、字段可验证、类型可检查,模型就不再只是聊天组件,而可以成为数据抽取、流程自动化、工具调用、报表生成和智能代理系统中的一个稳定环节。
1. 什么是结构化输出
结构化输出要求模型返回满足某个约束的数据,而不是任意文本。这个约束通常包括四层含义:
| 层次 | 约束内容 | 示例 |
|---|---|---|
| 格式 | 必须是 JSON、XML、CSV、Markdown 表格等 | 输出必须能被 json.loads() 解析 |
| 字段 | 必须包含指定字段 | name、age、email |
| 类型 | 每个字段必须是指定类型 | age 必须是整数,tags 必须是数组 |
| 语义 | 字段值必须符合业务含义 | 邮箱格式正确,日期不能早于当前时间 |
一个典型的 JSON Schema 可以这样定义:
{
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "文章标题"
},
"content": {
"type": "string",
"description": "正文摘要"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"metadata": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"author": {
"type": "string"
}
},
"required": ["created_at", "author"]
}
},
"required": ["title", "content", "tags", "metadata"]
}
在工程里,结构化输出通常解决四类问题:
| 场景 | 为什么需要结构化输出 |
|---|---|
| 信息抽取 | 从合同、简历、发票、客服记录中提取字段 |
| 函数调用 | 把自然语言意图转换为函数名和参数 |
| 数据入库 | 输出必须匹配数据库表结构或消息队列协议 |
| 自动化流程 | 后续系统需要根据字段值做分支判断 |
不过,结构正确不等于内容一定正确。模型可以返回合法 JSON,但字段值仍然可能编造。因此结构化输出解决的是“机器可读”和“格式可控”,事实准确性还需要检索、校验、规则和人工审核等机制配合。
2. 技术路线总览
LLM 结构化输出大致经历了从“软约束”到“硬约束”的演进。
flowchart LR
A[Prompt 引导] --> B[验证与修复]
B --> C[约束解码]
C --> D[监督式微调 SFT]
D --> E[强化学习 RL]
C --> F[API 原生结构化输出]
E --> F
不同方案的控制点不一样:
| 方法 | 控制发生在什么时候 | 可靠性 | 成本 | 适合场景 |
|---|---|---|---|---|
| Prompt 引导 | 生成前,用提示词引导 | 中 | 低 | 原型、低风险任务 |
| 验证与修复 | 生成后,解析失败再修 | 中高 | 中 | 通用生产系统、黑盒模型 |
| 约束解码 | 生成过程中屏蔽非法 token | 高 | 中高 | 严格 JSON、DSL、工具参数 |
| 监督式微调 SFT | 训练阶段让模型学习格式 | 中高 | 高 | 固定领域、稳定任务 |
| 强化学习 RL | 训练阶段用奖励优化结构和语义 | 高 | 很高 | 复杂结构、复杂推理 |
| API 原生结构化输出 | 平台封装 schema、grammar、tool call | 高 | 低到中 | 大多数业务应用 |
工程选型时可以记住一个原则:能用模型服务提供的严格结构化 API,就不要只靠提示词;能在生成阶段约束,就不要把所有压力都留给生成后的修复。
3. Prompt 引导生成:最容易上手,也最容易失控
Prompt 引导是最基础的结构化输出方法。它的核心做法是在提示词里明确告诉模型要返回什么格式。
请从输入文本中抽取商品信息,并返回 JSON。
要求:
1. 只返回 JSON,不要输出解释文字。
2. JSON 必须包含 company、product、price、range、delivery_date 五个字段。
3. 找不到的字段填 null。
输入文本:
{input}
输出格式:
{
"company": "公司名称",
"product": "产品名称",
"price": "价格",
"range": "续航里程",
"delivery_date": "交付时间"
}
更稳定的写法是加入少样本示例(Few-shot Learning),让模型看到输入和输出之间的对应关系:
任务:从商品描述中抽取结构化信息,只返回 JSON。
示例 1:
输入:
特斯拉 Model Y 电动汽车售价 45990 美元,续航里程 330 英里,预计 2024 年 2 月交付。
输出:
{
"company": "特斯拉",
"product": "Model Y",
"price": "45990美元",
"range": "330英里",
"delivery_date": "2024年2月"
}
现在处理新的输入:
{input}
Prompt 引导有三个常用技巧。
3.1 指令要具体
不要写:
帮我整理一下这个人。
更好的写法是:
请抽取用户资料,返回 JSON。
字段包括 name、age、email、interests。
age 必须是整数,interests 必须是字符串数组。
没有出现的信息填 null。
不要输出 JSON 以外的任何内容。
模型不是规则引擎,含糊的要求会扩大输出空间。字段名、类型、缺失值策略、是否允许解释文字,都应该写清楚。
3.2 降低随机性
结构化任务一般不需要太强的发散性,可以把 temperature 设置得低一些:
response = client.chat.completions.create(
model="your-model",
messages=[
{"role": "user", "content": prompt}
],
temperature=0.1,
max_tokens=800
)
低温度能减少随机性,但不能保证格式 100% 正确。模型仍然可能输出:
当然可以,下面是 JSON:
{
...
}
这种结果对人友好,对程序不友好,因为多出来的解释文字会让严格解析失败。
3.3 控制输出长度
max_tokens 太小会导致 JSON 被截断:
{
"name": "张三",
"email": "zhangsan@example.com",
"interests": [
"跑步",
截断后的结果无法解析。字段多、嵌套深、数组长时,要给足输出预算,同时限制输入长度,避免上下文挤占生成空间。
3.4 Prompt 的根本局限
LLM 按概率逐 token 生成。Prompt 只是提高“正确格式”出现的概率,不会从数学上禁止非法 token。
这意味着单靠 Prompt 会遇到几类常见问题:
| 问题 | 示例 |
|---|---|
| 多输出解释文字 | 这是你要的 JSON:{...} |
| JSON 语法错误 | 缺引号、缺逗号、括号不闭合 |
| 字段缺失 | 少了必填字段 |
| 类型错误 | age 返回 "18岁" 而不是 18 |
| 嵌套结构错位 | 数组里混入对象外字段 |
| 长输出截断 | 生成到一半停止 |
所以,Prompt 适合作为结构化输出的入口,但不适合作为生产可靠性的唯一保障。
4. 验证与修复:把不可靠输出关进工程边界
验证与修复框架的思路很直接:让模型先生成,再用代码检查;检查失败就修复、重试或降级。
flowchart TD
A[输入请求] --> B[LLM 生成]
B --> C[解析 JSON]
C -->|解析失败| F[构造错误反馈]
C -->|解析成功| D[Schema 校验]
D -->|校验通过| E[返回结构化结果]
D -->|校验失败| F
F --> G{是否超过重试次数}
G -->|否| B
G -->|是| H[返回失败 / 人工审核 / 降级策略]
这种方案的核心不是“相信模型”,而是把模型输出当作不可信输入处理。
4.1 用 Pydantic 定义结构
Pydantic 是 Python 生态里常用的数据验证库。用它可以把结构、类型和部分业务规则写成代码。
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional
class UserProfile(BaseModel):
name: str = Field(min_length=2, max_length=50)
age: int = Field(ge=0, le=150)
email: EmailStr
interests: List[str] = Field(min_length=1, max_length=10)
company: Optional[str] = None
模型输出后,程序只接受能通过校验的数据:
import json
from pydantic import ValidationError
def parse_user_profile(raw: str) -> UserProfile:
data = json.loads(raw)
return UserProfile.model_validate(data)
try:
profile = parse_user_profile(model_output)
except (json.JSONDecodeError, ValidationError) as e:
print("结构化输出不合规:", e)
4.2 Reask:带着错误重新询问
Reask 的关键在于把错误信息反馈给模型,让它只修正不合规部分。
def build_reask_prompt(original_input: str, bad_output: str, error: str) -> str:
return f"""
你上一次输出没有通过结构校验。
用户输入:
{original_input}
上一次输出:
{bad_output}
校验错误:
{error}
请重新输出一个合法 JSON。
要求:
1. 只输出 JSON。
2. 字段必须匹配 UserProfile。
3. 不要解释原因。
"""
一个完整的重试框架可以这样写:
MAX_REASKS = 2
def extract_with_reask(client, original_input: str) -> UserProfile:
prompt = build_initial_prompt(original_input)
last_output = ""
for attempt in range(MAX_REASKS + 1):
raw = call_llm(client, prompt)
last_output = raw
try:
return parse_user_profile(raw)
except Exception as e:
if attempt == MAX_REASKS:
raise
prompt = build_reask_prompt(original_input, last_output, str(e))
Guardrails、Guidance、Instructor 等框架本质上都在封装类似能力:声明结构、调用模型、验证输出、失败后修复或重试。
4.3 自动修复适合处理什么
自动修复适合处理格式层面的错误,例如:
| 错误类型 | 是否适合自动修复 |
|---|---|
| JSON 外包了一层解释文字 | 适合 |
| 字符串里多了单位,需要转数字 | 适合,但要谨慎 |
| 缺少可推导字段 | 适合 |
| 关键字段无法从输入得出 | 不适合直接编造 |
| 模型生成事实错误 | 需要外部校验 |
| 工具调用涉及资金、权限、删除操作 | 必须加业务审批或权限检查 |
验证与修复能显著提高可用率,但代价也很明确:多次调用会增加延迟和费用;重试次数过多还可能让系统在异常输入上卡住。因此生产系统要设置最大重试次数,并设计失败路径。
5. 约束解码:在生成时禁止非法 token
Prompt 和 Reask 都是在“影响模型”或“修复结果”。约束解码(Constrained Decoding)更进一步:它在模型生成每个 token 时,根据语法规则屏蔽掉不合法的候选 token。
生成普通文本时,模型会给出下一个 token 的概率分布:
P(next_token | prefix)
约束解码会增加一个规则检查器:
flowchart LR
A[当前输出前缀] --> B[模型预测下一个 token 分布]
A --> C[语法状态机 / JSON Schema / CFG]
C --> D[合法 token 集合]
B --> E[屏蔽非法 token]
D --> E
E --> F[从合法 token 中选择]
F --> G[更新输出前缀和语法状态]
G --> A
假设要求输出 JSON,当当前前缀是:
{
"name"
下一个合法 token 大概率只能是 :,而不是任意文字。约束解码会把不符合 JSON 语法的 token 概率置零,只允许模型在合法集合里选择。
5.1 有限状态机和上下文无关文法
常见约束可以由两类机制表达:
| 机制 | 英文缩写 | 适合表达 |
|---|---|---|
| 有限状态机 | FSM(Finite State Machine) | 简单格式、正则、固定模板 |
| 上下文无关文法 | CFG(Context-Free Grammar) | 嵌套结构、JSON、SQL 子集、DSL |
JSON 这种有嵌套括号的格式,用 CFG 更自然。简化后的 JSON 对象可以被看成:
object -> "{" members "}"
members -> pair | pair "," members
pair -> string ":" value
value -> string | number | object | array | true | false | null
约束解码引擎会把 schema 或 grammar 转换成可增量检查的状态结构,随着 token 逐个生成,不断更新可选 token 集。
5.2 约束解码能保证什么,不能保证什么
约束解码最强的是格式保证:
| 能保证 | 不能完全保证 |
|---|---|
| JSON 语法正确 | 字段值真实 |
| 括号、引号、逗号位置正确 | 抽取内容一定来自输入 |
| 必填字段存在 | 日期、金额等业务规则一定合理 |
| 类型大体正确 | 复杂跨字段一致性 |
| 输出符合 DSL 语法 | SQL 执行结果符合业务意图 |
例如,约束解码可以保证:
{
"age": 18
}
不会变成非法 JSON,但它无法仅靠语法判断 age 是否从用户输入中真实出现。语义校验仍然需要检索、规则、业务数据库或人工审核。
5.3 黑盒模型的限制
传统约束解码需要访问模型的 logits,也就是每个候选 token 的概率。很多商业模型只暴露文本生成 API,不暴露 logits,这会让外部开发者无法直接在 token 级别施加约束。
一种折中做法是“草图引导约束解码”(Sketch-Guided Constrained Decoding,SketchGCD)。它把黑盒模型的自由输出当作草图,再用本地小模型或约束解码器进行修正。
flowchart LR
A[用户输入] --> B[黑盒 LLM 自由生成]
B --> C[草图 Sketch]
C --> D[本地辅助模型]
D --> E[约束解码器]
E --> F[符合 Schema 的最终输出]
这种方案不需要访问黑盒模型内部概率,但它也不是免费的:需要部署辅助模型,并处理草图与目标结构之间的对齐问题。
5.4 严格格式可能伤害推理质量
格式限制越强,模型可选择的表达空间越小。在一些推理任务中,模型如果一开始就被要求输出复杂 JSON,可能会把注意力放在“括号和字段”上,而不是先完成推理。
一种常见缓解方式是 NL-to-Format:先让模型用自然语言完成思考或草稿,再转换成目标结构。
sequenceDiagram
participant U as 用户
participant M as LLM
participant V as 校验器
U->>M: 提交问题
M->>M: 先生成自然语言推理草稿
M->>M: 将草稿转换为目标 JSON
M->>V: 输出 JSON
V-->>M: 不合规时返回错误
M-->>U: 返回校验通过的结构化结果
在工程实现中,可以把自然语言推理保留在内部,不返回给用户;最终只暴露结构化结果。这样既给模型足够推理空间,又让下游系统拿到稳定格式。
6. 监督式微调:让模型把格式习惯学进参数
监督式微调(Supervised Fine-Tuning,SFT)是在已有模型基础上,用带标签的输入输出样本继续训练。它不是在提示词里“临时提醒模型”,而是通过训练改变模型参数,让模型更习惯生成某类结构。
一个结构化抽取训练样本通常长这样:
{
"messages": [
{
"role": "user",
"content": "从文本中抽取商品信息:特斯拉 Model Y 电动汽车售价 45990 美元,续航里程 330 英里。"
},
{
"role": "assistant",
"content": "{\"company\":\"特斯拉\",\"product\":\"Model Y\",\"price\":\"45990美元\",\"range\":\"330英里\",\"delivery_date\":null}"
}
]
}
SFT 的目标是让模型在类似输入下更高概率直接输出正确结构。
6.1 LoRA 降低微调成本
直接全量微调大模型成本很高。LoRA(Low-Rank Adaptation of Large Language Models,低秩适配)通过冻结原模型大部分参数,只训练少量低秩矩阵来降低成本。
flowchart LR
A[预训练模型权重冻结] --> B[插入 LoRA 适配层]
C[结构化输出训练数据] --> D[训练 LoRA 参数]
D --> E[合并或挂载适配器]
E --> F[领域结构化输出模型]
LoRA 适合任务稳定、格式固定、样本足够的场景,例如:
| 场景 | SFT 是否适合 |
|---|---|
| 固定发票字段抽取 | 适合 |
| 某类客服工单分类 | 适合 |
| 固定 JSON 模板生成 | 适合 |
| 每天变化的临时 schema | 不太适合 |
| 强依赖复杂推理的结构生成 | 需要结合其他方法 |
6.2 数据集比训练方法更重要
SFT 的效果高度依赖数据质量。结构化输出数据集至少要覆盖这些情况:
| 数据要求 | 说明 |
|---|---|
| 正常样本 | 输入信息完整,输出字段齐全 |
| 缺失字段样本 | 训练模型学会用 null,而不是编造 |
| 边界样本 | 长文本、多实体、重复字段、歧义表达 |
| 反例样本 | 输入不包含目标信息时应拒绝或返回空结构 |
| 格式一致性 | 字段名、日期格式、单位格式必须统一 |
| 业务规则 | 金额、时间、枚举值、数组长度等规则要明确 |
如果训练数据里同一个字段有时叫 created_at,有时叫 createTime,模型会把这种混乱也学进去。SFT 不会自动纠正数据集里的规范问题。
6.3 SFT 高原:数据越多不一定越好
在复杂任务中,增加样本量可能很快遇到收益变小的阶段,这常被称为 SFT 高原(SFT Plateau)。
原因在于 SFT 主要学习“输入到输出”的模式映射。对简单抽取任务,这很有效;但对需要规划、推理、组合生成的任务,仅靠更多静态样本可能不够。
例如,从图表生成代码、从复杂需求生成嵌套 DSL、从多约束任务生成可执行计划,都不只是套格式。模型需要理解约束之间的关系,甚至需要试错和反馈。SFT 对这种动态反馈不敏感,因此常需要强化学习或外部验证器配合。
还有一个关键点:SFT 提高的是模型生成合法结构的概率,不是硬性保证。即使模型经过微调,生产系统仍然应该保留 schema 校验或约束解码。
7. 强化学习:用奖励信号优化复杂结构生成
强化学习(Reinforcement Learning,RL)适合处理 SFT 难以突破的复杂任务。它不是只告诉模型“标准答案长什么样”,而是让模型生成多个候选结果,再根据奖励函数判断哪个更好。
结构化输出中的奖励可以设计得很细:
| 奖励项 | 含义 |
|---|---|
| JSON 可解析 | 输出是否是合法 JSON |
| Schema 合规 | 是否包含必填字段、类型是否正确 |
| 字段级准确率 | 每个字段值是否正确 |
| 结构复杂度匹配 | 嵌套层级、数组长度是否符合要求 |
| 语义一致性 | 输出是否忠实于输入 |
| 工具执行成功 | 生成的参数能否让外部工具正常执行 |
一个简单奖励函数可以写成:
def reward(output, schema, expected):
score = 0.0
if is_valid_json(output):
score += 0.2
if matches_schema(output, schema):
score += 0.3
score += 0.3 * field_f1(output, expected)
score += 0.2 * semantic_similarity(output, expected)
return score
真实训练中会更复杂,还要处理奖励作弊、训练稳定性和模型退化问题。
7.1 Schema 强化学习
Schema 强化学习(Schema Reinforcement Learning,SRL)把 schema 校验器放进训练循环,让模型在训练阶段持续收到结构反馈。
flowchart TD
A[输入 + Schema] --> B[策略模型生成多个候选输出]
B --> C[Schema 校验器]
B --> D[语义评估器]
C --> E[结构奖励]
D --> F[内容奖励]
E --> G[总奖励]
F --> G
G --> H[PPO 等算法更新模型]
H --> B
常见训练过程包括三个阶段:
| 阶段 | 作用 |
|---|---|
| 采样 | 模型根据当前策略生成多个结构化候选 |
| 奖励 | 校验器和评估器给每个候选打分 |
| 更新 | 用 PPO(Proximal Policy Optimization,近端策略优化)等算法调整模型策略 |
RL 的优势是反馈更细。即使一个输出没有完全正确,只要比另一个候选更接近 schema 或语义目标,就可以得到更高奖励。模型能通过这种差异学习到复杂结构生成策略。
7.2 结构化思维 ToS
结构化思维(Thoughts of Structure,ToS)借鉴了思维链(Chain-of-Thought,CoT)的思想,但关注点不是普通推理步骤,而是输出结构本身。
模型在生成最终 JSON 前,先内部规划结构:
需要输出一个订单对象。
订单包含 order_id、customer、items、total_amount。
items 是数组,每个元素包含 sku、quantity、price。
如果没有优惠信息,coupon 字段为 null。
再生成最终结果:
{
"order_id": "A1024",
"customer": {
"name": "张三",
"phone": "138****8888"
},
"items": [
{
"sku": "SKU-001",
"quantity": 2,
"price": 99.0
}
],
"total_amount": 198.0,
"coupon": null
}
ToS 的意义在于让模型先理解结构约束,再填充字段。复杂嵌套、条件字段、多实体关系抽取等任务会更需要这种规划能力。
8. API 原生结构化输出:把复杂能力封装成接口
很多主流模型服务已经把结构化输出做成 API 能力。开发者不再只靠 Prompt,可以直接传入 JSON Schema、函数定义或 grammar。
8.1 JSON Mode、Structured Outputs 和 Function Calling 的区别
| 能力 | 约束强度 | 能保证什么 | 不能保证什么 |
|---|---|---|---|
| JSON Mode | 中 | 输出是 JSON 格式 | 不保证符合指定 schema |
| Structured Outputs | 高 | 输出匹配 JSON Schema | 不保证字段值真实 |
| Function Calling | 高 | 输出函数名和参数对象 | 不保证工具调用一定安全 |
| Grammar / CFG | 很高 | 输出符合指定语法或 DSL | 不保证语义正确 |
JSON Mode 解决的是“别输出普通文本”,Structured Outputs 解决的是“必须按我的结构输出”,Function Calling 解决的是“把自然语言意图转换成可执行工具参数”。
8.2 用 JSON Schema 约束输出
不同平台的 SDK 参数名可能不同,但核心思想一致:把 schema 作为请求的一部分传给模型。
from openai import OpenAI
client = OpenAI()
schema = {
"type": "object",
"properties": {
"company": {"type": "string"},
"product": {"type": "string"},
"price": {"type": ["string", "null"]},
"range": {"type": ["string", "null"]},
"delivery_date": {"type": ["string", "null"]}
},
"required": ["company", "product", "price", "range", "delivery_date"],
"additionalProperties": False
}
response = client.responses.create(
model="gpt-4.1",
input="特斯拉 Model Y 电动汽车售价 45990 美元,续航里程 330 英里。",
text={
"format": {
"type": "json_schema",
"name": "product_info",
"schema": schema,
"strict": True
}
}
)
print(response.output_text)
这种方式的优势是开发体验简单:schema 是代码和模型之间的契约,服务端负责尽量保证返回结果符合契约。
8.3 函数调用:把意图变成参数
函数调用适合让模型选择工具并生成参数。例如用户说:
帮我查一下旧金山明天的天气。
模型不应该直接编造天气,而应该返回类似结构:
{
"name": "get_weather",
"arguments": {
"location": "San Francisco",
"date": "tomorrow"
}
}
业务系统拿到参数后,再调用真实天气 API。
sequenceDiagram
participant U as 用户
participant M as LLM
participant S as 业务服务
participant W as 天气 API
U->>M: 帮我查旧金山明天天气
M-->>S: get_weather(location, date)
S->>W: 查询天气
W-->>S: 返回真实天气数据
S->>M: 工具结果
M-->>U: 用自然语言解释结果
函数调用的重点是:模型只负责理解意图和组织参数,真实操作由外部系统完成。涉及支付、删除、发券、修改权限等高风险动作时,仍然要加权限校验、二次确认和审计日志。
8.4 CFG / Lark Grammar:约束 DSL 和工具参数
当输出不是普通 JSON,而是 SQL 子集、数学表达式、规则引擎 DSL 时,JSON Schema 不一定够用。上下文无关文法(Context-Free Grammar,CFG)可以直接限制语法。
例如,只允许模型生成加法和乘法表达式:
from openai import OpenAI
client = OpenAI()
grammar = r"""
start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT
"""
response = client.responses.create(
model="gpt-5",
input="Use the math_exp tool to add four plus four.",
tools=[
{
"type": "custom",
"name": "math_exp",
"description": "Creates valid mathematical expressions",
"format": {
"type": "grammar",
"syntax": "lark",
"definition": grammar
}
}
]
)
print(response.output)
这个 grammar 会限制模型只能生成合法数学表达式,而不是随便输出解释文字。类似方式也可以用于 SQL 片段、搜索过滤表达式、工作流 DSL、配置规则等场景。
9. 评估结构化输出:先看能不能解析,再看对不对
普通自然语言生成常用 BLEU、ROUGE 等指标,但它们不适合直接评估结构化输出。结构化输出有一个硬门槛:格式不合规时,内容再像也没法进入业务系统。
合理的评估应该分两层。
flowchart TD
A[模型输出] --> B{格式有效性}
B -- 不通过 --> X[失败]
B -- 通过 --> C{Schema 合规}
C -- 不通过 --> X
C -- 通过 --> D[语义准确性评估]
D --> E[字段级指标 / 业务指标 / LLM-as-a-Judge]
9.1 第一层:结构合规性
结构合规性是硬指标,可以用脚本自动判断:
| 指标 | 检查内容 |
|---|---|
| 格式有效性 | 是否是合法 JSON、XML 或 DSL |
| 字段完整性 | 必填字段是否存在 |
| 类型正确性 | 字符串、数字、数组、对象是否匹配 |
| 架构一致性 | 是否完全符合 JSON Schema |
| 额外字段控制 | 是否出现 schema 不允许的字段 |
Python 中可以用 jsonschema 做校验:
import json
from jsonschema import validate, ValidationError
def validate_output(raw: str, schema: dict) -> bool:
try:
data = json.loads(raw)
validate(instance=data, schema=schema)
return True
except (json.JSONDecodeError, ValidationError):
return False
9.2 第二层:语义准确性
结构通过后,再评估内容是否正确:
| 指标 | 适合场景 |
|---|---|
| Exact Match | 字段值必须完全一致,如订单号 |
| Field-level F1 | 实体抽取、标签抽取 |
| 数值误差 | 金额、坐标、评分 |
| 规则校验 | 日期范围、枚举值、跨字段一致性 |
| LLM-as-a-Judge | 摘要质量、复杂推理、开放字段 |
LLM-as-a-Judge 指用另一个强模型当评审器,判断输出是否忠实、完整、相关。它适合语义判断,但不应该替代格式校验。格式是否合法,必须用确定性代码检查。
一个结构化输出评测集通常要记录:
{
"input": "用户输入或文档内容",
"schema": "目标结构",
"expected": "标准结构化答案",
"model_output": "模型输出",
"valid_json": true,
"schema_pass": true,
"field_f1": 0.92,
"semantic_score": 0.88
}
StructEval、JSON mode eval 等评测思路都强调:结构合理性和语义准确性要分开看,否则很容易把“格式漂亮但内容错误”的结果误判为好结果。
10. 生产环境中的推荐架构
一个可靠的结构化输出服务不应该只包含一次模型调用,而应该包含 schema 管理、生成、校验、修复、监控和降级。
flowchart TD
A[业务方请求] --> B[Schema 注册中心]
A --> C[Prompt / Tool 构造器]
B --> C
C --> D[LLM 调用层]
D --> E[结构校验器]
E -->|通过| F[语义校验 / 业务规则]
F -->|通过| G[返回结果]
E -->|失败| H[Reask 修复]
F -->|失败| H
H --> I{重试次数}
I -->|未超限| D
I -->|超限| J[降级 / 人工审核 / 返回错误]
D --> K[日志与指标]
E --> K
F --> K
关键设计点包括:
| 设计点 | 建议 |
|---|---|
| Schema 版本管理 | 给每个 schema 加版本号,避免字段变更影响旧调用方 |
| 严格解析 | 不要用正则随意截取 JSON,优先使用可靠解析器 |
| 最大重试次数 | 通常 1 到 3 次,避免异常输入导致成本失控 |
| 降级路径 | 失败后进入人工审核、规则抽取或返回明确错误 |
| 日志记录 | 保存输入摘要、schema 版本、失败原因、重试次数 |
| 安全边界 | 工具调用参数必须经过权限和业务校验 |
| 流式输出 | 流式 JSON 在完成前通常不可解析,需要缓冲或增量解析器 |
| 监控指标 | schema 通过率、重试率、平均延迟、字段准确率 |
11. 技术选型建议
不同团队、不同任务,对可靠性和成本的要求不同。可以按下面的方式选择方案。
| 需求 | 推荐方案 |
|---|---|
| 快速验证一个想法 | Prompt + 低 temperature |
| 普通生产抽取任务 | Structured Outputs API + 本地 schema 校验 |
| 模型服务不支持严格 schema | Prompt + Pydantic/jsonschema + Reask |
| 输出必须 100% 语法合法 | 约束解码或平台原生 strict schema |
| 输出是 SQL、规则 DSL、数学表达式 | CFG / Lark Grammar |
| 固定领域、大量重复任务 | SFT 或 LoRA |
| 复杂结构、复杂推理、SFT 收益停滞 | SRL / RL + 验证器奖励 |
| 高风险工具调用 | Function Calling + 权限校验 + 人工确认 |
实践中经常组合使用:
flowchart LR
A[Prompt 清晰描述任务] --> B[API strict schema / 约束解码]
B --> C[本地 schema 校验]
C --> D[业务规则校验]
D --> E[必要时 Reask]
E --> F[日志监控与评估集回归]
强约束负责格式,本地校验负责工程边界,业务规则负责语义安全,评估集负责持续发现退化。
12. 常见坑
12.1 把 JSON 合法当成答案正确
合法 JSON 只是最低要求。模型可能返回:
{
"company": "苹果",
"product": "Model Y",
"price": "45990美元"
}
格式没问题,但 company 错了。实体抽取、发票解析、医疗报告等场景必须做字段级准确率评估。
12.2 Schema 设计过度复杂
Schema 越复杂,生成难度越高,延迟和失败率也可能上升。能拆分的任务不要强塞进一个巨大 JSON。复杂流程可以拆成多轮:
flowchart LR
A[抽取基础实体] --> B[校验实体]
B --> C[抽取关系]
C --> D[生成最终结构]
12.3 缺失字段策略不明确
必须提前规定:
| 情况 | 推荐处理 |
|---|---|
| 输入没有出现字段 | 返回 null |
| 字段可由其他字段计算 | 明确是否允许计算 |
| 字段不确定 | 返回 null 或增加 confidence |
| 多个候选值 | 返回数组或规定选择规则 |
否则模型很容易为了填满字段而编造。
12.4 忽视 token 截断
结构化输出一旦被截断,整个结果通常不可用。长数组、长文本字段、多层嵌套都要提前估算输出长度。
12.5 直接执行模型生成的工具参数
函数调用不是安全机制。模型可能误解用户意图,也可能受到提示注入影响。凡是涉及权限、资金、隐私、写操作的工具调用,都必须经过业务系统二次校验。
13. 发展方向
结构化输出正在从“让模型像 JSON”变成“让模型成为可靠的数据接口”。几个方向会越来越重要:
| 方向 | 变化 |
|---|---|
| 多模态结构化生成 | 从图片、音频、视频中抽取结构化字段 |
| 自适应解码 | 根据任务难度动态选择 Prompt、grammar、schema 或自由生成 |
| SFT 与 RL 结合 | SFT 学基础格式,RL 优化复杂结构和语义反馈 |
| 更强的 grammar 支持 | 直接生成 SQL 子集、配置语言、工作流 DSL |
| 端到端评估平台 | 结构通过率、字段准确率、业务成功率统一监控 |
结构化输出的核心目标始终没有变:让 LLM 输出的数据能被程序稳定消费。Prompt 可以让模型“更愿意”遵守格式,验证修复可以兜底,约束解码可以禁止非法语法,SFT 和 RL 可以让模型学会更复杂的结构规律,API 原生能力则把这些复杂机制封装成更容易使用的工程接口。
在真实系统里,最可靠的方案通常不是单点技术,而是组合:用 schema 明确契约,用约束或 API 控制生成,用校验器守住边界,用评估集持续检查质量。这样,LLM 才能从一个会说话的模型,变成能稳定接入业务流程的数据生成组件。