LangChain 06 - 结构化输出学习导航与实战

模块 1:你只需要记住这些


1. 什么是结构化输出

结构化输出指的是:要求模型最终返回一个符合预定义结构的数据对象(如固定字段的 JSON、Pydantic 模型、TypedDict),而不再是无格式的自然语言文本。

比如不让模型输出:

盗梦空间在2010年上映,导演是克里斯托弗·诺兰,评分9.3。

而是让它输出:

{"title": "盗梦空间", "year": 2010, "director": "克里斯托弗·诺兰", "rating": 9.3}

💡 核心目标:

把"自然语言回答"变成"程序可以稳定消费的数据"。

价值三点:

  • 更容易被代码处理:下游系统可以直接读字段,不用再从自然语言里做解析
  • 结果更稳定:减少"模型说法变了但意思差不多"导致的解析失败
  • 更适合工程化:适用于表单抽取、分类、路由、工具参数生成、工作流状态传递等场景

2. 传统方式 vs 结构化输出

传统方式(繁琐、不推荐):

  1. 提示词里苦苦要求模型"请返回 JSON,不要带任何解释"
  2. 手动 json.loads(response.content)
  3. 手动验证类型 if not isinstance(data['age'], int): raise ...
  4. 手动创建对象 person = Person(**data)

结构化输出(简洁):

structured_llm = model.with_structured_output(Person)
person = structured_llm.invoke("张三是一名 30 岁的软件工程师")

一步到位,自动解析、验证、创建对象。

为什么结构化输出这么受欢迎:

  • Prompt 变干净了:字段的 description 直接充当了 Prompt 的一部分
  • 类型安全:编辑器能自动补全,代码运行前就能做类型检查
  • 极其稳定:依托大模型厂商底层的 JSON 模式,输出错误率降到极低

3. 四种 Schema 定义方式

LangChain 1.x 支持多种 Schema 与结构化输出方式:

方式 返回类型 运行时校验 推荐度
Pydantic Schema 类实例 ✅ 字段不匹配抛异常 生产首选
TypedDict 字典 ❌ 不校验 轻量场景
JSON Schema 字典 ❌ 不校验 跨语言接口
@dataclass 字典 ❌ 不校验 简单场景

理解口诀:

Pydantic 返回对象且校验,其余三种返回字典不校验。

4. Pydantic 是重中之重

四种方式里,Pydantic 功能最丰富,是生产场景首选。它的核心要素:

  • 所有模型必须继承 BaseModel
  • 使用类型提示:strintfloatList[xxx]Optional[xxx]
  • 使用 Field() 添加字段描述,帮助 LLM 理解字段含义

高级特性速查:

  • 可选字段:Optional[int] — LLM 未填充时返回 None
  • 默认值:Field(default="默认值", description="描述")
  • 枚举类型:Enum 类或 Literal["低", "中", "高"]
  • 列表提取:List[Person]
  • 嵌套结构:模型里包含模型(建议 ≤ 3 层)
  • 限制条件:min_lengthmax_lengthge(>=)、le(<=)、gt(>)

5. with_structured_output 的工作流程

第1步:定义结构(Pydantic 模型)
    ↓
第2步:协议转换(LangChain 自动把 Pydantic 转成 JSON Schema)
    ↓
第3步:模型交互(JSON Schema 作为 Tools 传入,模型底层语法约束保证格式)
    ↓
第4步:自动解析与验证(Pydantic 解析字符串 → 字典 → 验证类型 → 返回对象)

关键理解:

你写的 Python 类 → 自动变成 JSON Schema → 约束模型输出 → 自动解析回来


模块 2:带教式理解


1. 为什么"没有描述,LLM 可能格式错误"

Field(description="...") 不只是给开发者看的注释,它会被 LangChain 转换成 JSON Schema 里的 description 字段,直接传给模型。

所以:

  • 有 description → 模型知道这个字段要填什么
  • 没 description → 模型只能靠字段名猜,容易出错

这也是为什么 Pydantic 模式比裸 JSON Schema 更好用:你只需要写 Python,description 自动就进了 Schema。

2. Optional 和 default 的区别容易混淆

很多初学者分不清这两种写法:

# 写法 1:Optional,没默认值
age: Optional[int] = Field(description="年龄")
# → LLM 没提到年龄时,返回 None

# 写法 2:有默认值
age: int = Field(default=1, description="年龄")
# → LLM 没提到年龄时,返回 1

直觉:

  • Optional = “这个字段可以不存在”
  • default = “这个字段不存在时用这个值兜底”

⚠️ 注意:不同模型提供商对 default 字段的支持是不同的,同样代码在不同平台结果可能不一样。

3. Enum vs Literal 怎么选

两种写法都能限制字段可选值:

# 方式 1:Enum 类
class Priority(str, Enum):
    LOW = "低"
    MEDIUM = "中"
    HIGH = "高"

urgency: Priority = Field(description="紧急程度")

# 方式 2:Literal(更简洁,**不想单独**定义类时用)
urgency: Literal["低", "中", "高"] = Field(description="紧急程度")

选择直觉:

  • 值少、不复用 → Literal
  • 值多、多处复用、需要 .value 访问 → Enum

4. 嵌套结构为什么要控制在 3 层以内

# 能跑,但容易出错
class Bad(BaseModel):
    user: User
        company: Company
            address: Address
                country: Country  # 4 层嵌套

LLM 能力有限,嵌套层级越深,模型越容易在某一层丢字段或填错。

💡 建议:

  • 嵌套层级 ≤ 3 层
  • 使用清晰的 description
  • 必要时拆分成多次调用

5. 为什么只有 Pydantic 会抛异常

PDF 里用了一个 fake server 做实验:故意返回字段名不匹配的数据(title1 而不是 title)。

结果:

  • Pydantic:抛 ValidationError,告诉你哪个字段 missing
  • TypedDict:直接返回 {'title1': '盗梦空间', ...},不报错
  • JSON Schema:同上,不报错
  • @dataclass:同上,不报错

因为只有 Pydantic 在运行时做了类型校验,其余三种只是"类型声明",不是"运行时强校验器"。

这个区别在生产环境很关键:

如果你需要"模型输出格式不对就立刻报错"的保障,选 Pydantic。

6. include_raw=True 什么时候有用

正常调用 .with_structured_output() 只返回解析后的对象,你拿不到原始的 AIMessage。

但有时你需要:

  • 看 token 用量(response_metadata.token_usage
  • 看 tool_calls 原始结构
  • 排查解析错误

这时传入 include_raw=True

model_with_structure = model.with_structured_output(Movie, include_raw=True)
resp = model_with_structure.invoke("...")
# resp 是一个字典,包含三个字段:
# 'raw': 原始 AIMessage
# 'parsed': 解析后的对象
# 'parsing_error': 解析错误(None 表示成功)

7. 输出解析器(JsonOutputParser)为什么不推荐

传统方式的流程是:

提示词指导(引导生成指定格式) → 模型生成文本 → 解析器转换

问题:

  • 需要在 Prompt 里手动写"必须输出 JSON"
  • 需要自己创建 JsonOutputParser(pydantic_object=Movie)
  • 需要用 chain = prompt_template | model | parser 拼链
  • 稳定性不如 with_structured_output()

所以:

**了解**即可,实际开发用 with_structured_output()。


这一章结束后,你应该能自己回答


1、结构化输出解决的核心问题是什么?

它解决的是"模型输出如何被程序稳定消费"的问题。通过预定义 Schema,让模型返回固定字段的数据对象,而不是无格式的自然语言文本,下游系统可以直接读字段,不用再从文本里做解析。

2、四种 Schema 方式中,哪种会做运行时校验?

只有 Pydantic 会做运行时校验。当模型返回的数据字段不匹配 Schema 时,Pydantic 会抛出 ValidationError。TypedDict、JSON Schema、@dataclass 都只返回未校验的字典,不会报错。

3、为什么 Field(description="...") 很重要?

因为 description 会被 LangChain 自动转换成 JSON Schema 里的字段描述,直接传给模型。没有描述,模型只能靠字段名猜,容易格式错误。有描述,模型就知道每个字段该填什么。

4、Optional 和 default 有什么区别?

Optional 表示"这个字段可以不存在",LLM 没提及时返回 None。default 表示"这个字段不存在时用这个默认值兜底"。注意不同模型提供商对 default 的支持不同,同样代码在不同平台结果可能不一样。

5、include_raw=True 有什么用?

正常调用只返回解析后的对象,拿不到原始 AIMessage。传入 include_raw=True 后返回一个字典,包含 raw(原始 AIMessage,含 token 用量等元数据)、parsed(解析后的对象)、parsing_error(解析错误信息),适合排查问题。


小测试


1. 传统方式获取结构化数据需要几步?

至少四步:① 提示词要求输出 JSON ② 手动 json.loads 解析 ③ 手动验证类型 ④ 手动创建对象。而 with_structured_output() 一步到位。

2. TypedDict 和 Pydantic 最关键的区别是什么?

TypedDict 返回字典且不做运行时校验,Pydantic 返回类实例且做运行时校验(字段不匹配会抛异常)。

3. 嵌套结构建议控制在几层以内?

💡 建议 ≤ 3 层。层级越深,模型越容易在某一层丢字段或填错。

4. 如果需要限制字段只能是几个固定值,有哪两种写法?

Enum 类或 Literal["值1", "值2"]。值少且不复用选 Literal,值多或多处复用选 Enum。

5. DeepSeek 模型支持 method="json_schema" 吗?

不支持。DeepSeek 模型服务不支持 json_schema 模式。是否可用依赖于模型供应商及 LangChain 适配器的具体实现。