主题
Day 5 — 工具系统 (Tools)
读完这章你能获得什么:理解 AI Agent 的"双手"——如何定义工具、自动生成参数描述、通过注册中心管理工具,让 LLM 能精准调用。
一、前情提要
在 Day 4 中,我们让 Agent 学会了 ReAct 推理循环。但有个尴尬的问题:
Agent 有"脑子"(LLM)了,但没有"手"——它只能凭空回答问题,做不了任何事情。
用户问"现在几点了?",Agent 只能说"我不知道"或者瞎猜一个时间。因为它没有查时间的工具。
本章就要给 Agent 装上"双手"——工具系统。
二、生活类比:员工的工具箱
想象公司的工具间里有一个大工具柜(ToolRegistry),里面摆满了各种工具:
工具柜(ToolRegistry)
├── 计算器(calculator)—— 能做数学运算
├── 时钟(datetime_now)—— 能查当前时间
├── 网络浏览器(http_request)—— 能上网查资料
└── 文件读取器(read_file)—— 能读取本地文件每个工具上面都贴着一张使用说明书(JSON Schema),写清楚了:
工具名称:计算器
用途说明:做数学计算
需要的材料(参数):
- expression(必填):要计算的数学表达式,类型是字符串员工(Agent)需要用工具时,流程是这样的:
- 员工告诉老板(LLM):"我需要用计算器"
- 老板查看说明书,填好参数:"expression = (15+27)*3"
- 员工从工具柜里拿出计算器,按照参数操作
- 把结果记录下来:"126"
三、核心概念详解
3.1 Tool 基类——工具的"模具"
所有工具都继承自 Tool 基类,需要提供三个东西:
| 属性/方法 | 类比 | 说明 |
|---|---|---|
name | 工具名称标签 | LLM 通过这个名字来"点名"要用哪个工具 |
description | 用途说明 | 告诉 LLM 这个工具能干什么 |
parameters | 使用说明书 | JSON Schema 格式,描述需要哪些参数、什么类型 |
execute(**kwargs) | 实际使用工具 | 传入参数,返回结果 |
3.2 @tool 装饰器——"一行代码变工具"
手动定义 Tool 子类要写很多样板代码。@tool 装饰器让你用一个普通函数就能创建工具:
python
# 不用装饰器(繁琐)
class DatetimeNowTool(Tool):
name = "datetime_now"
description = "获取当前日期和时间"
parameters = {"type": "object", "properties": {}, "required": []}
async def execute(self, **kwargs):
return ToolResult(output=datetime.now().isoformat())
# 用装饰器(简洁)
@tool(name="datetime_now", description="获取当前日期和时间")
async def datetime_now():
return datetime.now().isoformat()魔法在哪? 装饰器会自动分析函数签名(参数名、类型标注、默认值),生成 JSON Schema。你写了 expression: str,装饰器就知道参数叫 expression、类型是 string、必填。
python
@tool(name="calculator", description="做数学计算")
async def calculator(expression: str):
"""expression: 要计算的数学表达式"""
return str(eval(expression))自动生成的 JSON Schema:
json
{
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "要计算的数学表达式"
}
},
"required": ["expression"]
}3.3 ToolResult——工具的返回值
工具执行完后,返回一个 ToolResult,包含:
| 字段 | 说明 |
|---|---|
output | 成功结果(文本) |
error | 错误信息(如果出错了) |
is_error | 是否出错 |
为什么不直接返回字符串? 因为工具可能会失败。把成功和失败分开处理,让上层代码能做不同的应对——比如工具出错了,可以告诉 LLM "这个工具执行失败了",让它换个办法。
3.4 ToolRegistry——工具柜
所有工具都注册到 ToolRegistry 里,它提供三个核心功能:
python
registry = ToolRegistry()
# 1. 注册:把工具放进柜子
registry.register(calculator_tool)
registry.register(datetime_tool)
# 2. 查找:按名字取工具
tool = registry.get("calculator")
# 3. 生成工具列表:给 LLM 看的"工具清单"
tools_for_llm = registry.get_tool_definitions()get_tool_definitions() 返回什么? 返回 OpenAI API 所需的工具定义格式:
python
[
{
"type": "function",
"function": {
"name": "calculator",
"description": "做数学计算",
"parameters": {"type": "object", "properties": {...}, "required": [...]}
}
},
# ...更多工具
]这个列表会随着 messages 一起发给 LLM,让它知道"你有这些工具可以用"。
四、工具与 Agent 的协作流程
五、动手实验指南
5.1 运行示例
bash
python day5-tools/example/main.py示例演示了:
- 用
@tool装饰器创建工具 - 注册到 ToolRegistry
- 展示自动生成的 JSON Schema
5.2 改一改,看看会怎样
实验 1:创建一个新工具 coin_flip,随机返回"正面"或"反面"。
python
@tool(name="coin_flip", description="抛硬币")
async def coin_flip():
import random
return random.choice(["正面", "反面"])实验 2:创建一个带多个参数的工具——temperature_converter(value: float, from_unit: str, to_unit: str),观察自动生成的 JSON Schema。
实验 3:故意让一个工具抛出异常,观察 ToolResult 的 error 字段是怎么填充的。
5.3 运行测试
bash
pytest day5-tools/tools/ -v六、常见问题 FAQ
Q1:JSON Schema 是什么?为什么工具需要它?
A:JSON Schema 是一种"描述 JSON 数据应该长什么样"的规范。LLM 需要知道每个工具需要什么参数(名字、类型、是否必填),才能正确地"填写工具调用请求"。类比:你告诉新员工"用计算器"但不告诉他"按哪些按钮",他也没法用。
Q2:@tool 装饰器怎么知道参数类型的?
A:Python 的 inspect 模块可以获取函数签名中的类型标注。写 expression: str 时,Python 会记录"expression 的类型是 str",装饰器读取这个信息然后映射成 JSON Schema 的 "type": "string"。
Q3:LLM 是怎么"选择"用哪个工具的?
A:LLM 收到消息和工具列表后,会根据用户的问题和工具的描述来判断。如果用户问"现在几点",LLM 看到有个工具描述是"获取当前日期和时间",就会选它。所以工具的 description 写得好不好直接影响 LLM 能不能选对工具。
下一章
工具给了 Agent "动手"的能力。但目前 Agent 的"人设"是固定的——不管用户问什么,它都用同一种方式回答。如果能根据不同任务切换不同的"模式"(翻译模式、编程模式...)就更好了。
下一章 Day 6: 技能系统 将解决这个问题。