第 10 站:执行工具

模型返回了
ToolUseBlock(name="get_weather", input={"city": "北京"})。但这只是一个 JSON 对象——怎么从它变成真正执行get_weather("北京")的函数调用?
上一章:第 9 站:调用模型
路线图
上一站,模型通过 API 返回了工具调用请求。现在我们需要:
- 找到
get_weather对应的真实 Python 函数 - 传入参数执行它
- 把结果包装成
ToolResultBlock返回给 Agent
1 | 模型返回 ToolUseBlock(name="get_weather", input={"city": "北京"}) |
读完本章,你会理解:
Toolkit如何注册和管理工具函数- 从
ToolUseBlock到真实函数调用的完整路径 - 工具分组的激活/停用机制
- 中间件(Middleware)的洋葱模型
知识补全:装饰器模式
在 Python 中,装饰器(decorator)是一个函数,它”包装”另一个函数,在不修改原函数代码的情况下增加功能。1
2
3
4
5
6
7
8
9
10
11def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}")
result = func(*args, **kwargs)
print(f"完成 {func.__name__}")
return result
return wrapper
def get_weather(city):
return f"{city}: 晴"
调用 get_weather("北京") 时,实际执行的是 wrapper("北京")——它在调用真实函数前后加了日志。
Toolkit 的中间件就是这个模式的高级版本:多个装饰器层层嵌套,像洋葱一样。
Toolkit:工具箱的总管
打开 src/agentscope/tool/_toolkit.py,找到第 117 行:1
2
3# _toolkit.py:117
class Toolkit(StateModule):
"""Toolkit 是注册、管理、删除工具函数的核心模块"""
Toolkit 也继承自 StateModule——可以被序列化。
内部数据结构
1 | # _toolkit.py:157 |
tools 字典是核心——键是函数名,值是 RegisteredToolFunction 对象。
RegisteredToolFunction
打开 src/agentscope/tool/_types.py:1
2
3
4
5
6
7
8
9
10
11# _types.py:16
class RegisteredToolFunction:
name: str # 函数名
group: str | Literal["basic"] # 所属分组
source: Literal["function", "mcp_server", "function_group"] # 来源
original_func: ToolFunction # 原始 Python 函数
json_schema: dict # 自动生成的 JSON Schema
preset_kwargs: dict # 预设参数(不暴露给模型)
postprocess_func: ... | None # 后处理函数
async_execution: bool # 是否异步执行
注册一个工具函数时,Toolkit 会:
- 从函数的 docstring 和类型标注中自动提取 JSON Schema
- 把函数包装成
RegisteredToolFunction对象 - 存入
self.tools字典
注册:register_tool_function
1 | # _toolkit.py:274 |
这个方法做的事情:
- 确定函数名:如果没传
func_name,用函数的__name__ - 提取 JSON Schema:如果没传
json_schema,从函数签名和 docstring 自动生成 - 检查重名:如果有同名工具,按
namesake_strategy处理(报错/跳过/覆盖/重命名) - 创建并存储:构造
RegisteredToolFunction,存入self.tools
1 | flowchart TD |
设计一瞥:为什么不用装饰器注册工具?
很多框架(如 LangChain)用@tool装饰器注册工具函数。AgentScope 选择toolkit.register_tool_function(fn)的显式注册方式。
原因:装饰器注册是”声明式”的,函数定义时就绑定了框架。显式注册是”命令式”的,可以在运行时动态决定注册什么、什么时候注册。
代价:多写一行代码。详见卷四第 30 章。
执行:call_tool_function
1 | # _toolkit.py:853 |
@_apply_middlewares 装饰器是中间件机制的入口。我们先看核心执行逻辑,再看中间件。
执行流程
1 | # 简化的执行逻辑(_toolkit.py:853 之后) |
注意第 3 步——preset_kwargs 和 tool_call["input"] 合并。preset_kwargs 是开发者预设的参数(如 API key),不会被模型看到。
ToolResponse
1 | # _response.py:12 |
工具函数的返回值。content 是内容块列表——工具可以返回文字、图片、甚至音频。
工具分组:动态激活/停用
1 | # _toolkit.py:187 |
默认情况下,所有工具在 "basic" 分组中,始终激活。但你可以创建自定义分组:1
2toolkit.create_tool_group("advanced", description="高级工具", active=False)
toolkit.register_tool_function(complex_tool, group_name="advanced")
"advanced" 分组的工具默认不激活——模型看不到它们。Agent 可以通过 reset_equipped_tools 这个 meta 工具来激活/停用分组。
这是一种动态工具管理机制——不是所有工具都一次性给模型,而是让 Agent 自己决定需要哪些。
AgentScope 官方文档的 Building Blocks > Tool Capabilities 页面展示了工具注册、分组、中间件的使用方法。本章解释了 call_tool_function 的内部流程和 _apply_middlewares 的洋葱模型实现。
在实际项目中,自定义工具函数的典型流程包括:
- 定义带类型标注和 docstring 的 Python 函数
- 调用
toolkit.register_tool_function(fn)注册,框架自动生成 JSON Schema - 在 ReAct 循环中,模型返回
ToolUseBlock后由call_tool_function找到注册的函数并执行 - 通过中间件实现日志记录、缓存和限流等横切关注点
中间件:洋葱模型
1 | # _toolkit.py:1441 |
中间件是一个”包装”工具执行过程的函数。它的签名是:1
2
3
4
5
6
7
8
9
10
11
12
13
14async def my_middleware(
kwargs: dict, # 包含 tool_call
next_handler: Callable # 下一个中间件或真正的工具函数
) -> AsyncGenerator[ToolResponse, None]:
# 前置处理
print(f"即将调用: {kwargs['tool_call']['name']}")
# 调用下一层
async for response in await next_handler(**kwargs):
# 可以修改每个响应
yield response
# 后置处理
print("工具调用完成")
_apply_middlewares 装饰器
1 | # _toolkit.py:57 |
1 | flowchart TD |
每个中间件可以:
- 前置处理:在调用
next_handler之前做事情(日志、验证、修改参数) - 拦截响应:
async for response in可以修改或过滤每个响应块 - 后置处理:在
for循环结束后做事情(记录、缓存) - 跳过执行:不调用
next_handler,直接返回自定义响应
试一试:注册并调用一个自定义工具
这个练习不需要 API key。
目标:创建一个简单的工具函数,注册到 Toolkit,模拟调用。
步骤:
- 在
src/agentscope/tool/_toolkit.py的call_tool_function方法中(第 853 行后),加一行 print:
1 | print(f"[DEBUG] 调用工具: {tool_call['name']}, 参数: {tool_call.get('input', {})}") |
- 创建测试脚本
test_toolkit.py:
1 | import asyncio |
- 运行:
1 | python test_toolkit.py |
- 观察输出:JSON Schema 是如何从 docstring 和类型标注自动生成的。
完成后清理:1
2rm test_toolkit.py
git checkout src/agentscope/tool/
检查点
你现在理解了:
- Toolkit 是工具注册、管理、执行的核心模块
register_tool_function把 Python 函数注册为工具,自动提取 JSON Schemacall_tool_function从ToolUseBlock找到注册的函数并执行- 工具分组允许动态激活/停用工具集
- 中间件是洋葱模型,可以在工具执行前后插入自定义逻辑
自检练习:
- 如果注册了两个同名函数,默认会怎样?(提示:看
namesake_strategy参数的默认值) preset_kwargs和tool_call["input"]合并时,如果有相同的键,哪个优先?(提示:看合并顺序{**preset, **input})- 编写一个计时中间件,在工具执行前后打印耗时:
1 | import time |
调试实践:追踪工具注册和调用
这个练习不需要 API key。
技巧 1:打印注册的工具列表
在 _toolkit.py 的 register_tool_function 方法末尾(第 310 行后),加一行:1
2print(f"[DEBUG] 注册工具: {func_name} → 分组={group_name}, "
f"schema参数={list(json_schema.get('function', {}).get('parameters', {}).get('properties', {}).keys())}")
运行 test_toolkit.py,你会看到每个工具注册时自动提取的参数列表。
技巧 2:观察中间件链的执行顺序
在 _apply_middlewares 装饰器中(第 57 行),加 print 观察洋葱链:1
2
3
4
5
6
7
8
9
10
11
12
13def _apply_middlewares(func):
async def wrapper(self, tool_call):
middlewares = self._middlewares
if not middlewares:
return func(self, tool_call)
print(f"[DEBUG] 中间件链: {len(middlewares)} 层, "
f"顺序={[m.__name__ for m in middlewares]}")
chain = func
for middleware in reversed(middlewares):
chain = partial(middleware, next_handler=chain)
return chain(kwargs={"tool_call": tool_call})
注意 reversed(middlewares)——最后注册的中间件是最内层(最接近真正的函数调用)。
改完后恢复:1
git checkout src/agentscope/tool/
工具执行完毕,结果已经返回。但 ReAct Agent 不会只做一轮——它要循环,直到得出最终答案。下一站是全卷最长的一章,我们打开 ReActAgent.reply(),追踪推理-行动-总结的完整循环。
下一章:第 11 站:循环与返回