11 - Structured Output (结构化输出)
核心概念
Structured Output = 将 LLM 的自然语言输出转为结构化 Python 对象
在 LangChain 1.0 中,使用 with_structured_output() 方法结合 Pydantic 模型,可以确保 LLM 返回符合预定义模式的数据。
基本用法
定义 Pydantic 模型
from pydantic import BaseModel, Field
class Person(BaseModel):
"""人物信息"""
name: str = Field(description="姓名")
age: int = Field(description="年龄")
occupation: str = Field(description="职业")使用 with_structured_output()
from langchain.chat_models import init_chat_model
model = init_chat_model("groq:llama-3.3-70b-versatile")
# 创建结构化输出的 LLM
structured_llm = model.with_structured_output(Person)
# 调用
result = structured_llm.invoke("张三是一名 30 岁的软件工程师")
# result 是 Person 实例
print(result.name) # "张三"
print(result.age) # 30
print(result.occupation) # "软件工程师"核心组件
1. Pydantic BaseModel
所有结构化输出的数据模型都必须继承 BaseModel:
from pydantic import BaseModel
class MyModel(BaseModel):
field1: str
field2: int2. Field 描述
使用 Field() 添加字段描述,帮助 LLM 理解:
from pydantic import Field
class Book(BaseModel):
title: str = Field(description="书名")
author: str = Field(description="作者")
year: int = Field(description="出版年份")重要:description 会传递给 LLM,帮助它正确填充字段。
3. 类型注解
Pydantic 支持丰富的类型:
from typing import Optional, List
class Product(BaseModel):
name: str # 必填字符串
price: float # 必填浮点数
description: Optional[str] # 可选字符串
tags: List[str] # 字符串列表高级特性
可选字段
class User(BaseModel):
username: str
email: Optional[str] = None # 可以为 None
age: Optional[int] = None默认值
class Config(BaseModel):
timeout: int = 30 # 默认 30
retry: bool = True # 默认 True
max_attempts: int = Field(3, description="最大重试次数")枚举类型
from enum import Enum
class Priority(str, Enum):
LOW = "低"
MEDIUM = "中"
HIGH = "高"
class Task(BaseModel):
title: str
priority: Priority # 只能是 LOW/MEDIUM/HIGH列表提取
class Person(BaseModel):
name: str
age: int
class PeopleList(BaseModel):
people: List[Person] # 多个 Person 对象
structured_llm = model.with_structured_output(PeopleList)
result = structured_llm.invoke("张三 30岁,李四 25岁")
# result.people = [Person(name="张三", age=30), Person(name="李四", age=25)]嵌套模型
class Address(BaseModel):
city: str
district: str
class Company(BaseModel):
name: str
address: Address # 嵌套模型
structured_llm = model.with_structured_output(Company)
result = structured_llm.invoke("阿里巴巴在杭州滨江区")
# result.address.city = "杭州"
# result.address.district = "滨江区"工作原理
传统方式 vs 结构化输出
传统方式(繁琐):
# 1. 提示词要求 JSON
prompt = "以JSON格式返回:{name, age, occupation}"
response = model.invoke(prompt)
# 2. 手动解析
import json
data = json.loads(response.content)
# 3. 手动验证类型
if not isinstance(data['age'], int):
raise ValueError("age must be int")
# 4. 手动创建对象
person = Person(**data)结构化输出(简洁):
# 一步到位
structured_llm = model.with_structured_output(Person)
person = structured_llm.invoke("张三是一名 30 岁的软件工程师")
# ✅ 自动解析、验证、创建对象幕后流程
1. Pydantic 模型 → JSON Schema
Person → {
"type": "object",
"properties": {
"name": {"type": "string", "description": "姓名"},
"age": {"type": "integer", "description": "年龄"}
}
}
2. JSON Schema → LLM (函数调用)
LLM 被强制返回符合 schema 的 JSON
3. JSON → Pydantic 对象
自动验证类型并创建 Person 实例实际应用
1. 客户信息提取
class CustomerInfo(BaseModel):
name: str = Field(description="客户姓名")
phone: str = Field(description="电话号码")
email: Optional[str] = Field(None, description="邮箱")
issue: str = Field(description="问题描述")
structured_llm = model.with_structured_output(CustomerInfo)
conversation = """
客户: 我是李明,电话 138-1234-5678,订单没发货
"""
info = structured_llm.invoke(f"提取客户信息:{conversation}")
# info.name = "李明"
# info.phone = "138-1234-5678"
# info.issue = "订单没发货"应用:
- 自动填充 CRM 系统
- 工单自动分类
- 客服辅助
2. 产品评论分析
class Review(BaseModel):
product: str
rating: int = Field(description="评分 1-5")
pros: List[str] = Field(description="优点列表")
cons: List[str] = Field(description="缺点列表")
structured_llm = model.with_structured_output(Review)
review = structured_llm.invoke("""
iPhone 15 很棒!摄像头强大,手感好。但是价格贵,没有充电器。4分。
""")
# review.product = "iPhone 15"
# review.rating = 4
# review.pros = ["摄像头强大", "手感好"]
# review.cons = ["价格贵", "没有充电器"]应用:
- 批量处理用户评论
- 自动生成分析报告
- 发现产品改进点
3. 文档信息提取
class Invoice(BaseModel):
invoice_number: str
date: str
total_amount: float
items: List[str]
structured_llm = model.with_structured_output(Invoice)
invoice_text = """
发票号: INV-2024-001
日期: 2024-01-15
总金额: 1299.00
商品: MacBook Pro, AppleCare+
"""
invoice = structured_llm.invoke(f"提取发票信息:{invoice_text}")
# invoice.invoice_number = "INV-2024-001"
# invoice.total_amount = 1299.00应用:
- 自动化财务处理
- OCR 后结构化
- 数据录入
常见问题
1. LLM 未填充某些字段怎么办?
使用 Optional 和默认值:
class Data(BaseModel):
required_field: str # 必填
optional_field: Optional[str] = None # 可选
with_default: int = 100 # 有默认值2. 如何限制字段的可选值?
使用枚举:
from enum import Enum
class Status(str, Enum):
ACTIVE = "激活"
INACTIVE = "未激活"
class User(BaseModel):
status: Status # 只能是 ACTIVE 或 INACTIVE3. 复杂嵌套结构会出错吗?
LLM 能力有限,建议:
- 嵌套层级 ≤ 3 层
- 使用清晰的
description - 必要时拆分成多个调用
4. 如何验证提取的准确性?
见下一章 12_validation_retry - 验证和重试机制。
5. 所有模型都支持吗?
大部分现代模型支持(通过函数调用):
- ✅ OpenAI (gpt-4, gpt-3.5-turbo)
- ✅ Anthropic (claude-3)
- ✅ Groq (llama-3)
- ❌ 某些旧模型不支持
如果不支持,LangChain 会回退到提示词 + JSON 解析。
最佳实践
# 1. 使用清晰的字段描述
class Good(BaseModel):
created_at: str = Field(description="创建时间,格式 YYYY-MM-DD")
class Bad(BaseModel):
created_at: str # 没有描述,LLM 可能格式错误
# 2. 合理使用 Optional
class Good(BaseModel):
email: Optional[str] = None # 邮箱可能没有
class Bad(BaseModel):
email: str # 强制必填,可能导致提取失败
# 3. 使用枚举限制值
class Good(BaseModel):
status: Status # 枚举
class Bad(BaseModel):
status: str # 可能返回任意字符串
# 4. 列表设置合理的描述
class Good(BaseModel):
tags: List[str] = Field(description="产品标签,如 '电子产品', '手机'")
class Bad(BaseModel):
tags: List[str] # LLM 不知道该提取什么
# 5. 嵌套模型保持简单
class Good(BaseModel):
user: User # 1 层嵌套
settings: dict # 复杂数据用 dict
class Bad(BaseModel):
user: User
company: Company
address: Address
country: Country # 4 层嵌套,容易出错核心要点
- with_structured_output(Model) - 将 LLM 输出转为 Pydantic 对象
- Pydantic BaseModel - 定义数据模式
- Field(description=...) - 帮助 LLM 理解字段含义
- Optional[T] - 可选字段
- List[T] - 列表类型
- Enum - 限制可选值
- 嵌套模型 - 处理复杂结构(≤3 层)
- 自动验证 - Pydantic 自动检查类型
下一步
12_validation_retry - 学习如何验证提取结果并处理错误重试