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

其实,LangChain 早已提供了 结构化输出(Structured Output) 的优雅方案,只需定义一个 Schema,模型就会按你的要求返回类型安全的数据。今天我们就来全面对比四种主流方式:Pydantic、TypedDict、JSON Schema 和 @dataclass,并额外补充两种获取结构化结果的进阶技巧——让你不仅能拿到干净的对象,还能捕获原始消息和令牌用量。带你避开那些隐藏的“坑”!
所有示例均基于 LangChain + OpenRouter 的 DeepSeek 模型,你也可以换成 OpenAI、Anthropic 等任何支持函数调用的模型。
复制代码from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import osload_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")
)
Pydantic 是 Python 数据验证的事实标准,LangChain 对它支持最完善。你只需继承 BaseModel,加上类型注解和 Field 描述,调用 with_structured_output 即可获得类型安全的实例。
复制代码from pydantic import BaseModel, Fieldclass 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,模型会尽力推理,缺失时填充默认值(但注意部分厂商可能不支持默认值)。Enum 或 Literal 锁死可选值,比如紧急程度只能选“高/中/低”。list[Model],轻松提取复杂信息。Field(..., ge=0, le=150) 等约束会在实例化时校验,若 LLM 输出非法数据会抛出 ValidationError,相当于多了一道安全防线。️ 注意:约束条件只在 Pydantic 实例化时生效,部分模型可能不遵守,所以服务端返回的 JSON 仍可能“越界”,务必捕获异常。
很多同学只管调用,但不知道 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。如果你不清楚这个流程,可能会误以为是模型没返回数据,而实际上是数据“不干净”被拦截了。
如果你不想引入 Pydantic 的重量,但又希望有类型提示,TypedDict 是最佳选择。它只是类型声明,不执行运行时校验,适合快速原型。
复制代码
from typing_extensions import TypedDict, Annotatedclass 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 的“自觉”。
当你需要动态生成 Schema 或不想定义任何类时,直接写 JSON Schema 字典即可。LangChain 通过 method="json_schema" 支持。
复制代码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 提示,适合临时脚本或配置驱动场景。
Python 内置的 @dataclass 也可以“冒充” Schema,LangChain 同样支持。配合 Pydantic 的 Field 添加描述,但无运行时验证。
复制代码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 全面。
除了直接拿到解析后的对象,有时我们还需要原始的 AIMessage(用于调试、获取 tool_calls 或审计),或者想统计 Token 消耗。LangChain 提供了两种便捷方式。
with_structured_output(..., include_raw=True)在调用 with_structured_output 时传入 include_raw=True,返回的将是一个字典,包含三个字段:
raw:原始的 AIMessage 对象(包含完整的响应元数据)parsed:解析后的结构化对象(如果解析成功)parsing_error:解析过程中的异常(如果有) 复制代码from pydantic import BaseModel, Fieldclass 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 等
适用场景:你既需要结构化数据,又需要监控成本或调试原始输出。
JsonOutputParser(传统管道方式)LangChain 还提供了 JsonOutputParser,配合 ChatPromptTemplate 和管道 | 操作符使用。这种方式更加显式,适合需要自定义 Prompt 的场景。
复制代码from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Fieldclass 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 也是不错的选择。
description 直接影响提取准确性,务必写清楚示例或范围。ValidationError,否则程序可能崩溃。include_raw=True 的副作用:返回的是字典而非对象,记得取 parsed 字段才是你的结构化数据。LangChain 的 with_structured_output 将“结构化输出”这件事变得异常简单,我们只需根据场景选择合适的 Schema 定义方式。无论是追求严谨的 Pydantic,还是灵活的 JSON Schema,都能显著提升开发效率,告别繁琐的字符串解析。而 include_raw=True 和 JsonOutputParser 则为我们提供了更细粒度的控制,让成本监控和调试变得轻而易举。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多人避开那些坑! 有任何疑问,也欢迎在评论区留言交流
扩展阅读:LangChain 官方文档 - Structured Output