芥末
发布于 2025-10-15 / 0 阅读
0
0

大语言模型结构化输出:从 Prompt 到约束解码和 Schema 强化学习

大语言模型(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() 解析
字段必须包含指定字段nameageemail
类型每个字段必须是指定类型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 校验
模型服务不支持严格 schemaPrompt + 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 才能从一个会说话的模型,变成能稳定接入业务流程的数据生成组件。


评论