从散文到数据:报表生成的格式困境
假设你负责一个自动生成业务报表的系统:模型需要从销售数据中提取月度营收、客户流失率、区域排名,并输出成 JSON 供下游数据库写入和前端图表渲染。最初你只是把数据塞进提示词,要求“以 JSON 格式输出”,模型却经常在 JSON 外面包一层 Markdown 代码栅栏,偶尔漏掉必填字段,甚至把日期格式从 2025-11-10 换成 November 10, 2025。这些看似细小的偏差,会让下游的 json.loads 直接抛异常,或者把错误值静默写入数据库。
这类问题的根源在于:大模型本质上是按概率生成 token 的文本模型,它并不天然理解“JSON Schema”或“字段必填”这类契约。当输出被当作数据消费时,格式错误和语义错误都会破坏整条流水线。业界由此发展出三类结构化输出方案:仅提示词的 JSON 模式、约束解码(原生结构化输出)、以及验证加重试的后处理。它们解决的问题不同,失败模式也不同,工程上需要组合使用。
基线方案:仅提示词的 JSON 模式
最直接的做法是在系统提示词里写明“请以 JSON 格式返回”,并附上一个 JSON Schema 示例。OpenAI 在 2023 年 DevDay 引入的 JSON Schema 支持,以及各家云厂商提供的“JSON 模式”,都属于这一类。这类模式的保证很弱:它只保证输出是语法上合法的 JSON,不保证符合你给定的 Schema。
实际表现中,模型仍可能犯这些错:
- 把 JSON 包在 Markdown 代码栅栏里(
json ...),解析器需要先剥掉围栏。 - 生成合法 JSON,但遗漏必填字段,比如
region缺失。 - 类型不符,例如把
score输出成字符串"high"而不是数字8。 - 枚举值超出允许集合,幻觉出未定义的
status值。 - 嵌套层级漂移,比如多包一层
data或减少一层。
这些失败在演示中不常见,但一旦输入分布偏移——比如模型更新、输入变长、出现未见过的字段组合——失败率就会上升。仅靠提示词无法消除这类问题,它只适合输出形状事先未知的探索性提取,例如开放式总结、情感标签抽取。对于报表这种字段固定的场景,它不是可靠方案。
约束解码:在 token 层面强制 Schema
约束解码(constrained decoding)的思路是在模型生成每个 token 之前,根据当前已生成的前缀和 Schema,动态计算一个合法的 token 集合,把不符合模式的 token 从概率分布中遮蔽掉。这样模型在物理上无法生成违反 Schema 的 token,从而保证输出 100% 符合结构。
OpenAI 在 2024 年中期推出的“严格模式”(Strict Mode)采用的就是这种方法。其承诺是 100% 的模式合规性,而非“通常正确”。Anthropic、Google 等供应商也提供类似的原生结构化输出。这类方案的实现通常依赖一个有限状态机(FSM)或上下文无关文法(CFG),把 JSON Schema 编译成生成时的约束。
但约束解码有一个容易被忽视的代价:质量下降。模型是自左向右逐个生成 token 的,当 Schema 约束把所有高概率的 token 都排除后,模型只能从排名较低的候选中挑选。输出在结构上有效,但语义上可能显得生硬,像“拙劣的翻译”。例如,你约束分类字段只能输出两个枚举值之一,而正确答案不在其中,模型会硬选一个最接近的,而不是给出“不确定”的信号。系统不会报错,但结果是静默的错误分类。
因此,约束解码并不能消除对语义验证的需求,它只是把失败形式从“程序崩溃”变成了“静默错误”。
函数调用:把结构化输出变成工具参数
函数调用(Function Calling)是另一种结构化输出机制,它把模型输出定义为对某个工具函数的调用,函数的参数就是结构化的 JSON。以 Anthropic 的 Claude 为例,你定义一个 get_weather 工具,包含 input_schema,模型在需要时会返回一个 tool_use 块,其中 name 是工具名,input 是符合 schema 的参数对象。你的应用执行这个工具,然后把结果以 tool_result 块送回模型,模型再基于结果生成最终回答。
函数调用的关键区别在于:它不只是输出格式的约束,还定义了执行语义。模型决定“调用哪个工具、传什么参数”,应用负责执行。在报表场景中,你可以定义 generate_report 工具,参数包括 date_range、metrics、group_by 等,模型在生成报表前先调用它,把参数作为结构化输出。
与约束解码相比,函数调用通常也使用类似的约束解码技术来保证参数格式,但它额外引入了“工具选择”这一步,增加了状态机复杂度。失败模式也更丰富:模型可能选择错误的工具,或者参数虽然合法但语义上不合理(比如日期范围倒置)。
验证与重试:把模型当作不可信源
第三种方法把 LLM 输出视为不可信数据,在生成后用 Pydantic 或 Zod 等校验器验证,失败时自动重试,并把错误信息回传给模型,让它自我纠正。像 instructor 这样的 Python 库封装了这种模式:它调用 LLM,验证响应,如果验证失败,就把错误消息追加到对话中,再次调用模型。
这种方案能处理约束解码无法表达的语义约束,例如“开始日期必须在结束日期之前”“当设置了字段 A 时,字段 B 必填”“置信度分数必须解释推理过程”。这些跨字段逻辑无法用 JSON Schema 表达,但可以在 Pydantic 验证器中实现。
代价是延迟和成本:每次重试都是一次额外的 LLM 调用。工程上需要设计重试次数上限,以及耗尽后的降级策略——是停止运行、回退默认值,还是标记人工审核。
报表场景下的完整流程
把上述机制组合起来,一个典型的报表生成流水线如下:
- 用户请求生成月度销售报表。
- 系统调用模型,使用原生结构化输出(约束解码)并定义
sales_report工具,Schema 包含month、revenue、churn_rate、top_regions等字段。 - 模型返回一个
tool_use块,参数是符合 Schema 的 JSON。 - 系统用 Pydantic 校验器验证参数,检查
month格式、revenue是否为正数、top_regions是否非空。 - 如果验证失败,把错误信息作为
tool_result回传给模型,要求重新生成参数。 - 验证通过后,系统执行报表生成逻辑,写入数据库。
这个流程中,约束解码保证了结构合法性,验证器保证了语义正确性,重试处理了偶发失败。
下面用流程图展示这个流程:
flowchart TD
A[用户请求生成报表] --> B[调用模型 定义 sales_report 工具]
B --> C{模型返回 tool_use?}
C -- 否 --> D[返回错误 提示模型重新生成]
C -- 是 --> E[解析参数 JSON]
E --> F[Pydantic 校验]
F -- 失败 --> G[将错误信息作为 tool_result 回传]
G --> B
F -- 成功 --> H[执行报表生成]
H --> I[写入数据库 返回结果]
关键转折点在于:当模型没有返回 tool_use 时(比如模型错误地直接输出文本),系统需要识别并重试;当校验失败时,错误信息必须包含具体字段和原因,模型才能有效纠正。
工程选型:三种方案的对比与组合
选择哪种方案,取决于你的场景对结构保证、语义正确性、延迟和成本的要求。下表总结了主要差异:
| 方案 | 结构保证 | 语义正确性 | 延迟/成本 | 适用场景 |
|---|---|---|---|---|
| 仅提示词 JSON 模式 | 仅保证合法 JSON | 无保证 | 低 | 探索性提取、输出形状未知 |
| 约束解码(原生结构化输出) | 100% 符合 Schema | 无保证,可能静默错误 | 中 | 字段固定的数据提取、工具调用 |
| 函数调用 | 100% 符合参数 Schema | 无保证,可能错误选择工具 | 中 | Agent 工具调用、需要执行语义 |
| 验证 + 重试 | 依赖底层生成 | 可检查跨字段逻辑 | 高(重试次数) | 语义约束复杂的场景 |
实际生产系统通常组合使用:用约束解码作为第一道防线,用验证器处理语义约束,用重试处理偶发失败。例如,报表生成中,约束解码保证字段齐全,验证器检查日期范围,重试处理模型偶尔的幻觉。
对于开源模型,约束解码可以通过 vLLM、Outlines 等推理框架实现,它们同样基于 FSM 或 CFG 在采样时屏蔽非法 token。但开源方案的成熟度不一,需要自行处理 JSON Schema 编译、特殊字符转义等问题。
失败模式与监控指标
结构化输出的失败往往不是以报错形式出现,而是静默传播错误值。常见的失败模式包括:
- 格式错误:即使有约束解码,也可能出现超长输出截断,导致 JSON 不完整。
- 内容截断:当
max_tokens设置过小,模型可能在生成中途被截断,输出不完整的 JSON。 - 模式切换:模型在多次调用中改变日期格式或枚举值,导致下游解析不一致。
- 分布偏移:输入数据分布变化,模型开始输出从未见过的值。
- 工具选择错误:函数调用中,模型调用了错误的工具。
监控这些失败需要埋点。值得追踪的指标包括:每个 Schema 字段的验证失败率、重试率、Schema 覆盖率(可选字段中已填充与为空的比例)。Schema 覆盖率尤其重要:如果一个通常有值的字段开始频繁返回 null,说明上游输入变化或模型行为漂移,值得调查。
模式设计:隐藏的杠杆
即使有完美的强制手段,糟糕的 Schema 设计也会降低输出质量。几个原则:
- 先推理后输出:把自由文本推理字段放在 Schema 前面,让模型先处理证据再决定分类值。
- 让可选字段真正可选:如果输入可能缺失某信息,不要设为必填,否则模型会幻觉值。
- 保持扁平聚焦:避免 4 层以上嵌套和 50 个以上字段,拆分成多个提取调用。
- 使用描述性字段名:字段名是隐含提示,
content_moderation_category比category更明确。 - 避免不支持的 JSON Schema 特性:例如 OpenAI 严格模式要求所有属性在
required中,不支持additionalProperties: true。
结论
结构化输出是 LLM 应用与系统其余部分之间的契约。仅靠提示词不可靠,约束解码消除了结构错误但引入了语义退化,验证与重试弥补了语义检查。工程上需要组合使用,并监控漂移指标。目标不是生成“有效的 JSON”,而是生成“可信的 JSON”。