🎯 课程主题
以 ToolStrategy 为主线,详解通过 schema 参数使用 Pydantic 类型实现 Agent 结构化输出,并结合真实工具调用演示完整工作流。
📝 核心知识点
1. ToolStrategy 的三大参数
- 概念说明:
ToolStrategy有三个核心参数控制结构化输出行为。 - 关键细节:
| 参数 | 作用 | 必填 |
|---|---|---|
schema | 定义结构化的输出格式(Pydantic / TypedDict / JSON Schema / @dataclass / Union) | 是 |
tool_message_content | 自定义伪工具返回消息的 content 内容 | 否 |
handle_errors | 结构化输出失败时的重试策略 | 否 |
2. Pydantic 类型的使用
- 概念说明:通过继承
BaseModel+Field定义结构化 schema,传入ToolStrategy(schema=...)即可。 - 关键细节:
- 支持
Field(description=...)为字段添加语义描述,帮助模型理解字段含义。 - 支持
Literal定义枚举约束、default/...(required)控制字段必填。 - 输出结果为 Pydantic 对象,可直接通过属性访问(如
result.name)。 - 底层通过伪工具调用机制实现:Agent 将 schema 包装为"工具",模型"调用"该工具完成结构化。
- 支持
3. 结构化输出 + 真实工具的组合使用
- 概念说明:Agent 需要先调用真实工具获取数据,再将数据按 schema 格式化输出。
- 关键细节:
system_prompt中必须明确指令顺序:先搜索/查询工具 → 再生成结构化报告。- 如果不明确顺序,Agent 可能跳过工具调用,直接输出不完整的结果。
- 需要加入"找不到数据时返回空对象"的兜底指令,避免模型反复重试工具调用。
🏗️ 架构与工作流
Agent.invoke("分析张三客户")
│
├─→ [真实工具1] search_customer_db("张三")
│ └─ ToolMessage: {name: "张三", level: "VIP", ...}
│
├─→ [真实工具2] send_email("张三") ← 仅VIP客户触发
│ └─ ToolMessage: "邮件已发送"
│
└─→ [伪工具] CustomerAnalysis (ToolStrategy)
└─ structured_response: {name, level, activity, ...}
消息链路:
HumanMessage → AIMessage(tool_calls: search_db)
→ ToolMessage(查询结果)
→ AIMessage(tool_calls: send_email) ← VIP才触发
→ ToolMessage(发送结果)
→ AIMessage(tool_calls: CustomerAnalysis) ← 伪工具
→ ToolMessage(结构化结果)
→ structured_response
💻 代码实战
from pydantic import BaseModel, Field
from typing import Literal, Optional
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
# ========== 示例1:简单信息提取 ==========
class ContactInfo(BaseModel):
"""用户联系方式"""
name: str = Field(description="用户姓名")
email: str = Field(description="邮箱地址")
phone: str = Field(description="电话号码")
agent = create_agent(
model=model,
response_format=ToolStrategy(schema=ContactInfo)
)
response = agent.invoke({
"messages": [{"role": "user", "content": "小明的邮箱是 shangguigu@163.com,电话是 13012341234,请提取信息。"}]
})
print(response["structured_response"])
# ContactInfo(name='小明', email='shangguigu@163.com', phone='13012341234')
# ========== 示例2:组合真实工具 + 结构化输出 ==========
class CustomerAnalysis(BaseModel):
"""客户分析报告"""
name: str = Field(description="客户姓名")
level: Literal["普通", "VIP", "SVIP"] = Field(description="客户等级")
activity: Optional[str] = Field(default="未知", description="最近活跃度")
spend: Optional[str] = Field(default="未知", description="消费水平")
email_sent: bool = Field(description="是否已发送感谢邮件")
# --- 定义两个真实工具 ---
def search_customer(name: str) -> dict:
"""从数据库查询客户信息"""
db = {
"张三": {"level": "VIP", "activity": "高", "spend": "50000"},
"李四": {"level": "普通", "activity": "低", "spend": "500"},
}
return db.get(name, {})
def send_thanks_email(name: str) -> str:
"""发送感谢邮件(仅VIP)"""
return f"已向 {name} 发送感谢邮件"
# --- 创建Agent ---
agent = create_agent(
model=model,
tools=[search_customer, send_thanks_email],
system_prompt=(
"1. 先使用 search_customer 工具查询指定客户的信息。\n"
"2. 如果客户是 VIP,调用 send_thanks_email 发送感谢邮件。\n"
"3. 基于搜索和邮件发送结果,生成结构化的客户分析报告。\n"
"4. 如果找不到客户信息或查询与客户记录无关,返回空对象,不发送邮件。"
),
response_format=ToolStrategy(schema=CustomerAnalysis)
)
response = agent.invoke({
"messages": [{"role": "user", "content": "请分析张三客户"}]
})
print(response["structured_response"])
# CustomerAnalysis(name='张三', level='VIP', activity='高', spend='50000', email_sent=True)
# 测试非VIP客户
response = agent.invoke({
"messages": [{"role": "user", "content": "请分析李四客户"}]
})
# CustomerAnalysis(name='李四', level='普通', ..., email_sent=False)
# 注意:不会调用 send_thanks_email
⚠️ 常见问题与避坑指南
- system_prompt 的指令顺序至关重要:必须先引导 Agent 调用真实工具获取数据,再执行结构化输出;顺序颠倒会导致工具被跳过。
- 添加兜底逻辑:当查询不到数据时,应告知 Agent 返回空对象并停止,避免反复重试工具调用造成死循环。
- 不同模型提供商对 Pydantic 字段的支持度不同:如
default默认值、复杂校验等,OpenRouter 平台支持度优于部分国产平台(如 CloseAI),需根据实际测试选择。 - ToolStrategy 的
schema参数可省略 key 名称:直接传值ToolStrategy(ContactInfo)等价于ToolStrategy(schema=ContactInfo)。
💡 个人总结与延伸
- Pydantic 是 LangChain 结构化输出中最推荐的 schema 定义方式,类型安全、IDE 友好、校验能力强。
- 结构化输出 + 工具调用的组合是 Agent 开发的核心模式:先用工具采集数据,再按 schema 规整输出,适用于报表生成、客户分析等场景。
- 生产环境中建议使用
Literal而非自定义 Enum 来定义枚举约束,LangChain 对Literal的支持更加稳定。