结构化输出
结构化输出(Structured outputs)是遵循特定、机器可读格式(如 JSON、XML 或正则表达式定义的模式)的 LLM 响应。模型不是生成自由形式的文本,而是产生可以直接被下游系统解析和使用的数据。
下面是一个例子:
{
"name": "LLM Inference Handbook",
"author": "Modular",
"website": "handbook.modular.com",
"summary": "A practical handbook for engineers building, optimizing, scaling, and operating LLM inference systems in production."
}
为什么结构化输出很重要?
当你使用 LLM 时,输出往往是自由形式的文本。作为人类,我们可以轻松地阅读和解释这些响应。
然而,如果你正在用 LLM 构建一个更大的应用程序(例如,将模型的响应连接到另一个服务、API 或数据库),你就需要可预测的结构。否则,你的程序怎么知道要提取什么,或者哪个字段放在哪里?
这就是结构化输出的用武之地。它们为模型提供了一个清晰、机器可读的格式来遵循,使自动化和集成更可靠。
例如,假设你在构建一个分析助手,它读取支持工单并向产品团队总结洞察。你希望 LLM 返回:
- 用户提到的主要问题,
- 它们的频率,
- 以及一个整体的情感得分。
如果模型以纯文本回复,比如:
"大多数客户抱怨加载时间慢和支付错误。整体语气略微负面。"
这对人类读者没问题,但对自动化仪表盘几乎没用。你必须手动提取这些洞察,或编写复杂的解析代码。
现在把这个与下面的结构化输出进行对比:
{
"issues": [
{"topic": "Slow loading", "count": 42},
{"topic": "Payment errors", "count": 31}
],
"sentiment": "negative",
"confidence": 0.87
}
你的系统可以直接解析输出、将其存入数据库,并在仪表盘上可视化数据。无需猜测或后处理。
结构化输出如今在许多现实世界的 LLM 系统中很常见,包括:
- 信息提取: 从文档中提取实体、数字或关系,输出为 JSON 或表格。
- 数据增强: 对 CRM 或分析流水线中的记录进行分类、打标签或总结。
- 函数调用和 API 链式调用: 让 LLM 选择要调用哪个工具或端点,并以结构化方式传递参数。
- 智能体编排: 协调多步骤工作流,每一步都消费上一步的输出。
- 评估与测试: 为基准测试模型质量和准确性收集一致的响应。
- 内容审核或合规检查: 返回结构化决策,如
{ "action": "flag", "reason": "PII detected" }。
如何获得结构化输出
现在你明白了结构化输出为什么重要,下一个问题是: 你究竟如何获得它们?
当然,你可以编写自定义解析逻辑来清理并提取模型响应中所需的数据。但这种方法很快就会变得混乱。它耗时、容易出错,而且在规模化时很脆弱。每一个新的格式或规则都会给你的代码增加更多复杂性。
更好的方法是让 LLM 自己产生结构化数据,但并非所有 LLM 都开箱即用地支持结构化输出。对于这些模型,你通常需要使用某些框架,通过仔细的提示或模式定义来引导它们。当格式被清晰地书写(例如使用明确的示例或正则表达式)时,模型可以可靠地生成遵循预期结构的输出。
如今,有三种主要方式可以获得结构化输出。
无服务器模型 API 提供商
开始使用结构化输出的最简单方式是调用原生支持它们的模型 API。
OpenAI、Anthropic 和 Google 等提供商让你可以直接在 API 调用中指定模式或 JSON 结构。然后模型会自动生成遵循该模式的响应。
这个特性是从早期的JSON 模式演变而来的,后者只是要求模型以 JSON 格式响应。JSON 模式是有效的,但表现不稳定。模型常常会产生格式错误或不完整的 JSON。为了解决这个问题,OpenAI 推出了Structured Outputs,这是一个更严格的系统,强制执行模式,确保响应始终匹配定义的结构。
下面是一个使用 OpenAI 结构化输出 API 的示例:
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
# Define the expected fields with Pydantic
class SupportSummary(BaseModel):
issues: list[str]
sentiment: str
confidence: float
completion = client.chat.completions.parse(
model="gpt-5",
messages=[
{"role": "system", "content": "Summarize support ticket feedback."},
{"role": "user", "content": "The app is terrible! It crashes every time it opens."},
],
response_format=SupportSummary,
)
event = completion.choices[0].message.parsed
使用第三方模型 API 的结构化输出很简单。没有要维护的自定义解析逻辑,模型会为你处理模式校验。
然而,它也带来一些权衡:
- 供应商锁定。你被绑定到特定提供商的 API。
- 输出限制。大载荷可能被截断,导致 JSON 不完整。
- 执行不一致。并非所有提供商都能同等好地处理模式校验。
重新提示(Re-prompting)
重新提示是一种简单有效的获取结构化输出的方式,使用像 Instructor 这样的库。
它的工作原理如下:
- 你向模型发送一个描述期望格式(例如 JSON 模式)的提示。
- 库检查响应是否有效。
- 如果无效,它会自动用出错详情重新提示模型。
- 这个过程会重复,直到输出通过校验或达到重试限制。
这个循环确保你最终得到一个有效的结构化输出,而无需编写自己的重试逻辑。
它也高度灵活。你可以定义自定义正则规则,并强制日期或数字格式。它几乎适用于任何模型或 API 提供商。
代价是延迟和成本。每次重试都意味着另一次模型调用,这会增加时间和 token。如果你的模式很复杂,或者你的模型难以遵循指令,可能需要多次重试,甚至在用尽所有尝试后失败。
约束解码(Constrained decoding)
如果你在自托管开源 LLM,约束解码(也称为结构化生成)是产生结构化输出最可靠的方式之一。
这种方法不是在输出生成后验证它,而是在 token 生成期间强制结构。它确保模型只能采样符合你定义的格式或模式的 token。
下面是底层发生的事情:
当 LLM 生成文本时,它会预测每个可能的下一个 token 的概率(这些概率被称为 logits)。使用约束解码时,这些 logits 会被实时修改,以移除任何会违反你所定义结构的 token。这样模型就只能生成有效的延续内容,保证最终输出始终遵循你的模式。
这种方法很快,因为它不依赖重试或后处理。它适用于开源模型,并得到 Outlines、Microsoft Guidance 和 XGrammar 等库的支持。vLLM、SGLang 和 MAX 等推理框架也已经直接集成了这些工具。
主要优势是速度、精度和可靠性。Outlines 团队甚至展示了结构化输出可以提升 LLM 性能。