别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比

在开发 LLM 应用时,你一定遇到过这样的痛点:模型"口吐芬芳"给你一段自然语言,你却要写一堆正则、json.loads() 甚至 eval() 去提取信息,不仅脆弱,而且维护成本极高。

其实,LangChain 早已提供了 结构化输出(Structured Output) 的优雅方案,只需定义一个 Schema,模型就会按你的要求返回类型安全的数据。今天我们就来全面对比四种主流方式:PydanticTypedDictJSON Schema@dataclass ,并额外补充两种获取结构化结果的进阶技巧------让你不仅能拿到干净的对象,还能捕获原始消息和令牌用量。带你避开那些隐藏的"坑"!


🧱 环境准备

所有示例均基于 LangChain + OpenRouter 的 DeepSeek 模型,你也可以换成 OpenAI、Anthropic 等任何支持函数调用的模型。

python 复制代码
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os

load_dotenv(override=True)
model = init_chat_model(
    model="deepseek-v4-flash",
    model_provider="openai",
    api_key=os.getenv("OPENROUTER_API_KEY"),
    base_url=os.getenv("OPENROUTER_BASE_URL")
)

1️⃣ 方案一:Pydantic(生产环境首选)

Pydantic 是 Python 数据验证的事实标准,LangChain 对它支持最完善。你只需继承 BaseModel,加上类型注解和 Field 描述,调用 with_structured_output 即可获得类型安全的实例。

基础示例

python 复制代码
from pydantic import BaseModel, Field

class Person(BaseModel):
    name: str = Field(description="姓名")
    age: int = Field(description="年龄")
    occupation: str = Field(description="职业")

structured = model.with_structured_output(Person)
res = structured.invoke("张三是一名30岁的软件工程师")
print(res)          # name='张三' age=30 occupation='软件工程师'
print(type(res))    # <class '__main__.Person'>

✨ 高级特性

  • 可选字段 & 默认值
    使用 Optional 或设置 default,模型会尽力推理,缺失时填充默认值(但注意部分厂商可能不支持默认值)。
  • 枚举限制
    EnumLiteral 锁死可选值,比如紧急程度只能选"高/中/低"。
  • 嵌套与列表
    支持多层嵌套(建议 ≤3 层)和 list[Model],轻松提取复杂信息。
  • 字段约束
    Field(..., ge=0, le=150) 等约束会在实例化时校验 ,若 LLM 输出非法数据会抛出 ValidationError,相当于多了一道安全防线。

⚠️ 注意:约束条件只在 Pydantic 实例化时生效,部分模型可能不遵守,所以服务端返回的 JSON 仍可能"越界",务必捕获异常。

🔧 Pydantic 到底在背后做了什么?(核心工作流程)

很多同学只管调用,但不知道 LangChain 是怎么把 Pydantic 类变成 LLM 能懂的东西的。了解这个流程,能帮你更好地排查 Bug。整个过程分为 6 步

步骤 做了什么 关键动作
1️⃣ 定义模型 你写 class Person(BaseModel),加上类型和描述
2️⃣ 生成 Schema LangChain 调用 model.model_json_schema() 自动转成 JSON Schema 字典
3️⃣ 包装成工具 将这个 JSON Schema 作为 LLM 的 Tool(工具/函数) 传入请求参数中
4️⃣ LLM 推理 模型看懂 Schema,按规则生成严格符合格式的 JSON 字符串
5️⃣ Pydantic 校验 LangChain 拿到 JSON,调用 Pydantic 进行类型、约束(如 le=150)的校验
6️⃣ 实例化返回 校验通过后,JSON 被转换为** Python 对象**(Person 实例),交付给你

为什么要懂这个?

如果第 5 步校验失败(例如模型抽风输出了 age=200),程序会直接抛出 ValidationError。如果你不清楚这个流程,可能会误以为是模型没返回数据,而实际上是数据"不干净"被拦截了。


2️⃣ 方案二:TypedDict(轻量级类型提示)

如果你不想引入 Pydantic 的重量,但又希望有类型提示,TypedDict 是最佳选择。它只是类型声明,不执行运行时校验,适合快速原型。

python 复制代码
from typing_extensions import TypedDict, Annotated

class Movie(TypedDict):
    title: Annotated[str, "电影名称"]
    year: Annotated[int, "上映年份"]
    director: Annotated[str, "导演"]
    rating: Annotated[float, "评分"]

structured = model.with_structured_output(Movie)
res = structured.invoke("星际穿越")
print(res)          # 字典类型
print(type(res))    # <class 'dict'>

亮点Annotated 中可添加描述,LangChain 会将其转为 Schema 的 description。你还可以用 ... 占位符表示必填字段。

缺点:无验证,字段类型错误不会报错,只依赖 LLM 的"自觉"。


3️⃣ 方案三:JSON Schema(原始字典)

当你需要动态生成 Schema 或不想定义任何类时,直接写 JSON Schema 字典即可。LangChain 通过 method="json_schema" 支持。

python 复制代码
schema = {
    "type": "object",
    "properties": {
        "title": {"type": "string", "description": "电影名称"},
        "year": {"type": "integer", "description": "上映年份"}
    },
    "required": ["title", "year"]
}
structured = model.with_structured_output(schema, method="json_schema")
res = structured.invoke("盗梦空间")
print(res)  # {'title': '盗梦空间', 'year': 2010}

这种方法最灵活,但完全失去了类型安全和 IDE 提示,适合临时脚本或配置驱动场景。


4️⃣ 方案四:@dataclass(标准库简洁方案)

Python 内置的 @dataclass 也可以"冒充" Schema,LangChain 同样支持。配合 Pydantic 的 Field 添加描述,但无运行时验证

python 复制代码
from dataclasses import dataclass
from pydantic import Field

@dataclass
class Movie:
    title: str = Field(description="标题")
    year: int = Field(description="年份")

structured = model.with_structured_output(Movie)
res = structured.invoke("流浪地球")
print(res)  # Movie(title='流浪地球', year=2019)

优点是无额外依赖,缺点和 TypedDict 类似------不校验类型,且 Field 的支持可能不如 Pydantic 全面。


🆕 5️⃣ 进阶:如何获取结构化结果(含原始响应和令牌用量)

除了直接拿到解析后的对象,有时我们还需要原始的 AIMessage (用于调试、获取 tool_calls 或审计),或者想统计 Token 消耗。LangChain 提供了两种便捷方式。

5.1 使用 with_structured_output(..., include_raw=True)

在调用 with_structured_output 时传入 include_raw=True,返回的将是一个字典,包含三个字段:

  • raw:原始的 AIMessage 对象(包含完整的响应元数据)
  • parsed:解析后的结构化对象(如果解析成功)
  • parsing_error:解析过程中的异常(如果有)
python 复制代码
from pydantic import BaseModel, Field

class Movie(BaseModel):
    title: str = Field(description="电影标题")
    year: int = Field(description="上映年份")
    director: str = Field(description="导演")
    rating: float = Field(description="评分(10分制)")

# 开启 include_raw
structured = model.with_structured_output(Movie, include_raw=True)
resp = structured.invoke("给我介绍下电影《星际穿越》")

print(type(resp))  # <class 'dict'>
print(resp.keys()) # dict_keys(['raw', 'parsed', 'parsing_error'])

# 查看解析后的对象
print(resp['parsed'])  # Movie(title='星际穿越', year=2014, ...)

# 查看原始消息的令牌用量
print(resp['raw'].usage_metadata)  # 含 input_tokens, output_tokens 等

适用场景:你既需要结构化数据,又需要监控成本或调试原始输出。

5.2 使用 JsonOutputParser(传统管道方式)

LangChain 还提供了 JsonOutputParser,配合 ChatPromptTemplate 和管道 | 操作符使用。这种方式更加显式,适合需要自定义 Prompt 的场景。

python 复制代码
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field

class Movie(BaseModel):
    title: str = Field(description="电影标题")
    year: int = Field(description="上映年份")

parser = JsonOutputParser(pydantic_object=Movie)

prompt = ChatPromptTemplate.from_messages([
    ("system", "回答用户问题,必须始终输出一个包含 title 和 year 的 JSON 对象"),
    ("human", "问题:{question}")
])

chain = prompt | model | parser
response = chain.invoke({"question": "介绍电影《盗梦空间》"})
print(response)  # {'title': '盗梦空间', 'year': 2010}

注意JsonOutputParser 返回的是字典 ,而不是 Pydantic 实例。若想获得 Pydantic 对象,可改用 PydanticOutputParser。且此方式不能直接获取原始 AIMessage,需要额外处理。

对比

  • with_structured_output(include_raw=True) 更简洁,一步到位,推荐在大多数场景使用。
  • JsonOutputParser 更灵活,适合需要精细控制 Prompt 和解析流程的老项目。

📊 横向对比:选哪个?

方案 运行时校验 自动类型转换 IDE 友好 可获取原始消息 典型场景
Pydantic ✅ 强校验 ⭐⭐⭐⭐⭐ ✅(include_raw) 生产级 API,数据质量严苛
TypedDict ⭐⭐⭐ ✅(include_raw) 快速原型,字典操作
JSON Schema ✅(include_raw) 动态 Schema,临时调用
@dataclass ⭐⭐⭐⭐ ✅(include_raw) 轻量数据类,无校验需求
JsonOutputParser ❌(只解析) ⭐⭐⭐ ❌(需额外处理) 自定义 Prompt 管道,传统方式

我的建议 :正式项目无脑选 Pydantic 并开启 include_raw=True,既能保证数据质量,又能监控 Token 成本。若追求极致性能且对数据质量足够信任,TypedDict 也是不错的选择。


🧩 避坑指南

  1. 默认值「失灵」:不同模型供应商对默认值的支持不一致,实测 OpenRouter 表现良好,但某些闭源模型可能忽略默认值,所以最好在业务层做兜底。
  2. 嵌套不宜太深:LLM 对深层嵌套(>3 层)理解有限,容易漏字段或乱填,尽量扁平化。
  3. 描述是王道 :字段的 description 直接影响提取准确性,务必写清楚示例或范围。
  4. 校验异常处理 :使用 Pydantic 时,务必捕获 ValidationError,否则程序可能崩溃。
  5. include_raw=True 的副作用 :返回的是字典而非对象,记得取 parsed 字段才是你的结构化数据。
  6. JsonOutputParser 与 with_structured_output 的区别:前者更"底层",后者更"智能",优先推荐后者。

🚀 总结

LangChain 的 with_structured_output 将"结构化输出"这件事变得异常简单,我们只需根据场景选择合适的 Schema 定义方式。无论是追求严谨的 Pydantic,还是灵活的 JSON Schema,都能显著提升开发效率,告别繁琐的字符串解析。而 include_raw=TrueJsonOutputParser 则为我们提供了更细粒度的控制,让成本监控和调试变得轻而易举。

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多人避开那些坑! 有任何疑问,也欢迎在评论区留言交流 👇


扩展阅读LangChain 官方文档 - Structured Output

相关推荐
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
ikun_文1 小时前
Python进阶—函数编程
python·pycharm
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
Oo9201 小时前
大模型是怎么随机说话的?—— Temperature、Top-k 与 LangChain 实战
langchain
MC皮蛋侠客1 小时前
uv 系列(三):依赖、锁文件与环境同步——可重复构建的核心
python·uv
量化吞吐机1 小时前
2026年交易想法转Python,中间先补规则转译
人工智能·python
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端