摘要

在大模型应用开发中,如何让模型输出符合预期格式的结构化数据,是一个核心技术挑战。本文将从底层原理到框架实践,系统性地分析大模型结构化输出的实现机制,并以 LangChain 框架为例,详细阐述各种结构化输出方案的技术细节与适用场景。

一、结构化输出的技术背景

1.1 问题的本质

大语言模型(LLM)本质上是一个概率性的文本生成器,其输出是自然语言文本。然而,在实际应用中,开发者往往需要模型返回特定格式的数据,例如:

  • JSON 对象用于 API 响应
  • 结构化实体用于信息抽取
  • 函数调用参数用于 Agent 工具调用

传统方案依赖于 Prompt 工程,在提示词中描述期望的输出格式,但这种方式存在固有缺陷:模型可能忽略格式要求、添加额外注释、或生成语法错误的 JSON。

1.2 解决方案的演进

结构化输出技术经历了三个主要阶段:

阶段 方案 可靠性 代表技术
第一代 Prompt 工程 低(~70%) 格式指令 + 后处理解析
第二代 输出解析器 中(~85%) LangChain OutputParser
第三代 约束解码 高(~100%) OpenAI Structured Outputs

二、底层实现原理

2.1 约束解码(Constrained Decoding)

约束解码是实现高可靠性结构化输出的核心技术。其基本原理是:在模型生成每个 token 时,根据预定义的语法规则,动态限制可选的 token 集合。

工作流程:

  1. 将 JSON Schema 预处理为上下文无关文法(Context-Free Grammar, CFG)
  2. 在每个解码步骤,根据已生成的 token 序列和 CFG 规则,计算当前位置的合法 token 集合
  3. 将不合法 token 的概率置为负无穷,确保模型只能选择合法 token

示例说明:

假设 JSON Schema 要求输出以 { 开头的对象:

生成位置 0: 合法 token = {"{"}
生成位置 1: 若已生成 "{", 合法 token = {"\"", "}"} (属性名或空对象结束)
生成位置 2: 若已生成 "{"value":", 合法 token 不包含 "{" (值不能是未闭合的对象)

2.2 上下文无关文法(CFG)

CFG 是形式语言理论中的概念,用于描述语言的语法结构。在结构化输出场景中,JSON Schema 被转换为 CFG 规则:

JSON_OBJECT  → "{" MEMBERS "}" | "{}"
MEMBERS      → PAIR | PAIR "," MEMBERS
PAIR         → STRING ":" VALUE
VALUE        → STRING | NUMBER | JSON_OBJECT | JSON_ARRAY | "true" | "false" | "null"

OpenAI 在首次处理新 Schema 时会进行 CFG 预处理,这会产生一定的延迟(通常 10 秒以内,复杂 Schema 可能需要 1 分钟)。后续请求会复用缓存的 CFG,无额外延迟。

2.3 Function Calling 机制

Function Calling(函数调用)是另一种实现结构化输出的技术路径。其原理是:

  1. 开发者定义函数签名(包含参数的 JSON Schema)
  2. 模型被训练识别何时应该调用函数,并生成符合签名的参数
  3. 模型输出包含 function_calltool_calls 字段,而非纯文本

Function Calling 的可靠性介于 Prompt 工程和约束解码之间,因为模型经过专门训练,但并非在解码层面强制约束。

三、LangChain 结构化输出实现

LangChain 提供了多层次的结构化输出方案,从底层解析器到高层抽象,满足不同场景需求。

3.1 架构概览

┌─────────────────────────────────────────────────────────┐
│                    应用层                                │
│  create_agent(response_format=...)                      │
├─────────────────────────────────────────────────────────┤
│                    策略层                                │
│  ProviderStrategy  |  ToolStrategy                      │
├─────────────────────────────────────────────────────────┤
│                    模型层                                │
│  with_structured_output()                               │
├─────────────────────────────────────────────────────────┤
│                    解析层                                │
│  PydanticOutputParser | JsonOutputParser | ...          │
├─────────────────────────────────────────────────────────┤
│                    模型提供商 API                        │
│  OpenAI | Anthropic | Google | 本地模型                  │
└─────────────────────────────────────────────────────────┘

3.2 Schema 定义方式

LangChain 支持多种 Schema 定义方式:

1. Pydantic 模型(推荐)

from pydantic import BaseModel, Field
from typing import Literal

class MovieReview(BaseModel):
    """电影评论的结构化表示"""
    title: str = Field(description="电影标题")
    rating: int = Field(description="评分,1-5分", ge=1, le=5)
    sentiment: Literal["positive", "negative", "neutral"] = Field(
        description="情感倾向"
    )
    summary: str = Field(description="评论摘要")

Pydantic 模型的优势在于:

  • 内置类型验证和约束
  • 自动生成 JSON Schema
  • 返回类型安全的 Python 对象

2. TypedDict

from typing import TypedDict

class MovieReview(TypedDict):
    title: str
    rating: int
    sentiment: str
    summary: str

TypedDict 更轻量,返回字典而非对象。

3. JSON Schema

schema = {
    "type": "object",
    "properties": {
        "title": {"type": "string"},
        "rating": {"type": "integer", "minimum": 1, "maximum": 5},
        "sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
        "summary": {"type": "string"}
    },
    "required": ["title", "rating", "sentiment", "summary"]
}

直接使用 JSON Schema 适用于动态生成 Schema 的场景。

3.3 ProviderStrategy:原生结构化输出

当模型提供商原生支持结构化输出时(如 OpenAI、Anthropic、xAI),LangChain 优先使用 ProviderStrategy:

from langchain.agents import create_agent
from langchain.agents.structured_output import ProviderStrategy

agent = create_agent(
    model="gpt-4o",
    tools=[],
    response_format=ProviderStrategy(
        schema=MovieReview,
        strict=True  # 启用严格模式,使用约束解码
    )
)

result = agent.invoke({
    "messages": [{"role": "user", "content": "评价电影《盗梦空间》"}]
})

# result["structured_response"] 是 MovieReview 实例

工作原理:

  1. LangChain 将 Pydantic 模型转换为 JSON Schema
  2. 通过 response_format 参数传递给模型 API
  3. 模型使用约束解码生成符合 Schema 的 JSON
  4. LangChain 将 JSON 反序列化为 Pydantic 对象

3.4 ToolStrategy:基于工具调用

对于不支持原生结构化输出的模型,LangChain 使用 ToolStrategy,将 Schema 包装为"工具":

from langchain.agents.structured_output import ToolStrategy

agent = create_agent(
    model="claude-3-opus",  # 假设不支持原生结构化输出
    tools=[],
    response_format=ToolStrategy(
        schema=MovieReview,
        tool_message_content="评论已结构化处理",
        handle_errors=True  # 启用错误重试
    )
)

工作原理:

  1. LangChain 将 Schema 转换为工具定义
  2. 模型通过 Function Calling 机制"调用"该工具
  3. 工具参数即为结构化输出
  4. 若验证失败,LangChain 将错误信息反馈给模型,触发重试

3.5 自动策略选择

当直接传递 Schema 类型时,LangChain 自动选择最优策略:

agent = create_agent(
    model="gpt-4o",
    tools=[],
    response_format=MovieReview  # 自动选择 ProviderStrategy
)

选择逻辑:

  • 若模型支持原生结构化输出 → ProviderStrategy
  • 否则 → ToolStrategy

3.6 底层方法:with_structured_output

with_structured_output 是 LangChain 模型类的核心方法,上述策略均基于此实现:

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o")
structured_llm = llm.with_structured_output(MovieReview)

result = structured_llm.invoke("评价电影《盗梦空间》")
# result 是 MovieReview 实例

该方法内部实现:

  1. 检测模型是否支持原生结构化输出
  2. 若支持,配置 response_format 参数
  3. 若不支持,将 Schema 转换为工具并绑定
  4. 包装输出解析逻辑

3.7 传统输出解析器

对于更细粒度的控制,LangChain 提供传统的 OutputParser:

PydanticOutputParser:

from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate

parser = PydanticOutputParser(pydantic_object=MovieReview)

prompt = PromptTemplate(
    template="分析以下电影评论并提取结构化信息。\n{format_instructions}\n评论:{review}",
    input_variables=["review"],
    partial_variables={"format_instructions": parser.get_format_instructions()}
)

chain = prompt | llm | parser
result = chain.invoke({"review": "盗梦空间是一部精彩的科幻电影..."})

工作原理:

  1. get_format_instructions() 生成格式说明文本,注入 Prompt
  2. 模型生成包含 JSON 的文本响应
  3. parser.parse() 从文本中提取 JSON 并验证

这种方式可靠性较低,因为模型可能:

  • 在 JSON 前后添加解释文字
  • 生成语法错误的 JSON
  • 忽略字段约束

四、错误处理机制

4.1 验证错误重试

ToolStrategy 支持自动重试机制:

response_format = ToolStrategy(
    schema=MovieReview,
    handle_errors=True  # 默认启用
)

当模型输出不符合 Schema 时:

  1. LangChain 捕获验证错误
  2. 将错误信息作为 ToolMessage 反馈给模型
  3. 模型根据错误信息修正输出
  4. 重复直到成功或达到最大重试次数

错误反馈示例:

================================= Tool Message =================================
Name: MovieReview

Error: Failed to parse structured output: 1 validation error for MovieReview.rating
  Input should be less than or equal to 5 [type=less_than_equal, input_value=10, input_type=int].
 Please fix your mistakes.

4.2 自定义错误处理

from langchain.agents.structured_output import StructuredOutputValidationError

def custom_handler(error: Exception) -> str:
    if isinstance(error, StructuredOutputValidationError):
        return f"格式错误,请确保:{error.validation_errors}"
    return f"未知错误:{str(error)}"

response_format = ToolStrategy(
    schema=MovieReview,
    handle_errors=custom_handler
)

五、实践建议

5.1 Schema 设计原则

  1. 字段描述清晰:使用 Field(description=...) 提供明确说明
  2. 约束合理:避免过于严格的约束导致模型难以满足
  3. 类型明确:优先使用 Literal 而非开放的 str 限定枚举值
  4. 嵌套适度:过深的嵌套结构会增加模型理解难度

5.2 策略选择建议

场景 推荐方案
OpenAI/Anthropic + 高可靠性要求 ProviderStrategy(strict=True)
通用模型 + 中等可靠性 ToolStrategy(handle_errors=True)
本地模型 + 简单 Schema with_structured_output + 重试
动态 Schema + 灵活解析 PydanticOutputParser

5.3 性能优化

  1. Schema 缓存:OpenAI 的约束解码首次请求有延迟,后续请求复用缓存
  2. 批量处理:对于大量相同 Schema 的请求,复用同一个 structured_llm 实例
  3. 降级策略:在高并发场景,可考虑降级到 ToolStrategy 减少首次延迟

六、总结

大模型结构化输出技术已从早期的 Prompt 工程演进到约束解码,可靠性显著提升。LangChain 框架通过多层抽象,为开发者提供了灵活的选择:

  • ProviderStrategy:利用模型原生能力,可靠性最高
  • ToolStrategy:基于工具调用,兼容性最广
  • OutputParser:传统方案,灵活性最强

开发者应根据模型能力、可靠性要求、性能约束等因素,选择合适的结构化输出方案。随着模型能力的持续提升,约束解码有望成为行业标准,届时结构化输出将如同类型系统一样,成为 LLM 应用开发的基础设施。


参考资料:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐