Skip to content

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)需要用工具时,流程是这样的:

  1. 员工告诉老板(LLM):"我需要用计算器"
  2. 老板查看说明书,填好参数:"expression = (15+27)*3"
  3. 员工从工具柜里拿出计算器,按照参数操作
  4. 把结果记录下来:"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: 技能系统 将解决这个问题。