📖 Day 2 学习页:类型系统与装饰器进阶
Day 2:类型系统与装饰器进阶
🎯 今日目标:能用 Pydantic v2 定义"Agent 工具的输入/输出 Schema",并写一个带参数的装饰器把任意函数自动注册为 Agent 工具——这是后面所有天的工具系统底座。
⏱ 今日安排(约 2 小时)
| 时间 | 内容 | 方式 |
|---|---|---|
| 30min | 理论学习:Pydantic 数据验证 + 装饰器三层嵌套 | 阅读 |
| 30min | 跑教程:装饰器 8 步教程 | 运行 + 修改 |
| 60min | 动手实操:完成练习模板 13 个 TODO | 编码 |
📖 第一步:理论学习
1. 为什么 Agent 需要数据验证
LLM 返回的内容不可信:你要它给数字,它可能给字符串;你要它给城市名,它可能幻觉出不存在的东西。 Pydantic 的作用:定义好"数据长什么样",不符合就当场报错,而不是让错误数据流进你的工具。
from pydantic import BaseModel, Field, ValidationError
class 计算器输入(BaseModel):
表达式: str = Field(..., description="四则运算表达式")
# ✅ 合法输入:通过
参数 = 计算器输入(表达式="3 + 5 * 2")
# ❌ 非法输入:当场拦截(表达式应为 str,传了 int)
try:
计算器输入(表达式=12345)
except ValidationError as e:
print("拦截成功:", e.errors()[0]["msg"])
# 生成 JSON Schema(给 LLM 看的参数说明就靠它)
print(计算器输入.model_json_schema())
注意:Pydantic 只做类型校验,不做安全过滤。"import os; os.system(...)" 是合法的 str,类型校验拦不住它—— 拦截危险内容要在工具内部自己做(白名单、沙箱)。两层防护缺一不可。
2. dataclass vs Pydantic(怎么选)
| dataclass | Pydantic v2 | |
|---|---|---|
| 用途 | 纯内存数据结构 | 需要校验/序列化的边界数据(API 输入输出、LLM 返回) |
| 校验 | 无 | 自动类型转换 + 校验 + 报错 |
| 性能 | 快 | v2 用 Rust 核心,也很快 |
| Agent 中 | 内部状态(如 AgentStep) | 工具 Schema、消息模型、配置 |
3. 装饰器:三步从入门到工具注册
本质:装饰器 = 接受一个函数,返回一个新函数(给手机套壳,壳能在原功能外加点行为)。
import functools
def 计时装饰器(原函数):
@functools.wraps(原函数) # ✅ 必须加!保留原函数的 __name__/__doc__
def 包装函数(*args, **kwargs):
开始 = time.time()
结果 = 原函数(*args, **kwargs)
print(f"⏱ {原函数.__name__} 耗时 {time.time()-开始:.4f}s")
return 结果
return 包装函数
@计时装饰器
def 慢速加法(a, b):
time.sleep(1)
return a + b
慢速加法(3, 5) # 自动打印耗时
致命陷阱:不加 @functools.wraps,被装饰函数的 __name__ 会变成"包装",
__doc__ 变 None——Agent 靠函数名和 docstring 选工具,丢了就全乱。
带参数的装饰器 = 三层嵌套(多包一层"参数层"):
def 重试(最大次数=3): # 第 1 层:收参数
def 装饰器(原函数): # 第 2 层:收函数
@functools.wraps(原函数)
def 包装(*args, **kwargs): # 第 3 层:真正执行
for i in range(最大次数):
try:
return 原函数(*args, **kwargs)
except Exception as e:
if i == 最大次数 - 1:
raise
time.sleep(0.5)
return 包装
return 装饰器
@重试(最大次数=3)
def 不稳定的网络请求(): ...
Agent 实战:工具注册装饰器(今天练习的核心模式,LangChain 的 @tool 就是它):
工具注册表 = {}
def 工具(名称, 描述):
def 装饰器(func):
@functools.wraps(func)
def 包装(*args, **kwargs):
return func(*args, **kwargs)
工具注册表[名称] = {"函数": 包装, "描述": 描述}
return 包装
return 装饰器
@工具("计算器", "执行四则运算")
def 计算(表达式): return eval(表达式)
# 之后 Agent 只需遍历 工具注册表 就知道"我有哪些工具可用"
🛠 第二步:动手实操
- 跑教程:
python tutorial_01_decorators.py,8 步从"一等公民"到"类装饰器"。 - 打开模板:day02_practice_template.py,13 个 TODO 分四组:
- TODO 1.2-1.3:为天气查询、文件搜索定义输入/输出 Schema(照抄计算器的写法)
- TODO 2.1-2.5:实现装饰器三层嵌套(输入验证 → 调用原函数 → 输出验证 → 注册进注册表)
- TODO 3.2-3.3:用 @工具 注册至少 3 个工具
- TODO 4.1-4.2:
调用工具(查注册表并调用)和列出所有工具(转成 OpenAI Function Calling 格式,用model_json_schema())
- 运行自测:
python day02_practice_template.py,4 个测试全部 ✅(测试 2 应显示拦截了类型错误)。 - 验收:
python day02_practice_validator.py(需要pip install pydantic)。验收会真的调用你注册的计算器、传错误类型试探你的验证逻辑、检查 OpenAI 格式。
⚠️ 避坑
- ❌ Pydantic v1 和 v2 API 不兼容(
.schema()已改名model_json_schema()),确认装的是 v2 - ❌ 忘写
@functools.wraps(func)—— 验收脚本会专门检查这一点 - ✅ 类型注解本身不做运行时检查,Pydantic 模型才做;两者配合使用
🔗 延伸资源
- Pydantic V2 官方文档
- mypy 官方文档(静态类型检查)