LangChain四种结构化输出方案:Pydantic强校验(生产首选)、TypedDict轻量无校验、JSONSchema灵活动态但无类型安全、@dataclass简洁少依赖。推荐正式项目用Pydantic并开启include_raw=True,同时获取解析结果与原始消息。
相信不少人在开发LLM应用时,都遇到过这么个糟心事儿:模型好不容易吐出一段自然语言,可你这边还得费劲巴拉地写正则、json.loads(),甚至还得用上eval()去抓取信息。这代码不仅脆弱得像纸糊的,后续维护起来更是让人头大。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
其实,LangChain早就提供了更优雅的解法——结构化输出(Structured Output)。你只需要定义一个Schema,模型就会乖乖地按你的要求,返回类型安全的数据。今天,咱们就来全方位对比一下四种主流方式:Pydantic、TypedDict、JSON Schema和@dataclass。除此之外,我还会补充两个获取结构化结果的进阶技巧,让你不仅能拿到干净的数据对象,还能顺手捕获原始消息和Token用量,顺便把那些隐藏的“坑”也一并指出来。
下面所有的示例,都基于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)) #
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)) #
亮点: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)) #
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,则提供了更细粒度的控制,让成本监控和调试变得轻而易举。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述