📖 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(怎么选)

dataclassPydantic 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 只需遍历 工具注册表 就知道"我有哪些工具可用"

🛠 第二步:动手实操

  1. 跑教程:python tutorial_01_decorators.py,8 步从"一等公民"到"类装饰器"。
  2. 打开模板: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())
  3. 运行自测:python day02_practice_template.py,4 个测试全部 ✅(测试 2 应显示拦截了类型错误)。
  4. 验收:python day02_practice_validator.py(需要 pip install pydantic)。验收会真的调用你注册的计算器、传错误类型试探你的验证逻辑、检查 OpenAI 格式。

⚠️ 避坑

  • ❌ Pydantic v1 和 v2 API 不兼容(.schema() 已改名 model_json_schema()),确认装的是 v2
  • ❌ 忘写 @functools.wraps(func) —— 验收脚本会专门检查这一点
  • ✅ 类型注解本身不做运行时检查,Pydantic 模型才做;两者配合使用

🔗 延伸资源