1. 模块概述
工具系统(nanobot/agent/tools/)是 Agent 能力的核心。通过 Tool 抽象基类和 ToolRegistry 注册表,实现工具的自动发现、参数验证、并发执行和安全管理。
📍 核心源码:
nanobot/nanobot/agent/tools/base.py#L1-L297、nanobot/nanobot/agent/tools/registry.py#L1-L183
1.1 内置工具一览
| 工具 | 模块 | 类型 | 说明 |
|---|---|---|---|
read_file |
filesystem.py |
只读 | 读取文件内容 |
write_file |
filesystem.py |
读写 | 写入文件 |
edit_file |
filesystem.py |
读写 | 精确字符串替换编辑 |
apply_patch |
apply_patch.py |
读写 | 应用 unified diff patch |
exec |
shell.py |
读写 | 执行 Shell 命令 |
web_search |
search.py |
只读 | Web 搜索 |
web_fetch |
web.py |
只读 | 获取 URL 内容 |
list_dir |
filesystem.py |
只读 | 列出目录内容 |
grep |
filesystem.py |
只读 | 内容搜索 |
find_files |
filesystem.py |
只读 | 文件名搜索 |
spawn |
spawn.py |
读写 | 生成子 Agent |
message |
message.py |
读写 | 向用户发送消息 |
generate_image |
image_generation.py |
读写 | AI 图片生成 |
cron |
cron.py |
读写 | 定时任务管理 |
my |
self.py |
读写 | 运行时状态读写 |
run_cli_app |
cli_apps.py |
读写 | 运行 CLI 应用 |
exec_session |
exec_session.py |
读写 | 持久 Shell 会话 |
notebook_edit |
context.py |
读写 | Jupyter Notebook 编辑 |
complete_goal |
long_task.py |
读写 | 完成持续目标 |
task |
long_task.py |
读写 | 创建任务 |
1.2 在系统中的位置
graph TB
subgraph "工具定义层"
Tool[Tool ABC]
FS[FileSystem Tools]
SH[Shell Tools]
WEB[Web Tools]
SP[Spawn/Subagent]
CR[Cron Tools]
end
subgraph "管理层"
TR[ToolRegistry]
TL[ToolLoader]
TC[ToolContext]
end
subgraph "执行层"
AR[AgentRunner]
SC[Schema Validation]
end
Tool --> FS
Tool --> SH
Tool --> WEB
Tool --> SP
Tool --> CR
TL -->|load| TR
TR -->|get_definitions| AR
AR -->|execute| TR
TR -->|prepare_call| SC
2. 命名体系与易混淆函数对比
2.1 命名规律拆解
| 前缀 | 操作对象 | 操作 | 含义 |
|---|---|---|---|
_cast_ |
params |
— | 对参数进行类型转换 |
_cast_ |
value |
— | 根据 schema 转换单个值 |
_cast_ |
object |
— | 递归转换嵌套对象 |
_coerce_ |
params |
— | 强制类型转换入口 |
_coerce_ |
argument_value |
— | JSON 字符串→对象转换 |
_unwrap_ |
arguments_ |
payload |
解包 {"arguments": {...}} 包装 |
_resolve_ |
type |
— | 解析 JSON Schema 类型 |
validate_ |
params |
— | 验证参数是否符合 schema |
validate_ |
json_schema_value |
— | 递归验证 JSON Schema 值 |
prepare_ |
call |
— | 解析+转换+验证一次工具调用 |
_suggest_ |
name |
— | 模糊匹配工具名(帮助 LLM 纠错) |
2.2 易混淆函数对比表
| 对比维度 | cast_params |
validate_params |
|---|---|---|
| 执行阶段 | 验证前 | 转换后 |
| 修改输入 | ✅ 可能改变参数值(如 “123”→123) | ❌ 只读检查 |
| 返回值 | 转换后的参数字典 | 错误消息列表(空=通过) |
| 容错性 | 转换失败保留原值 | 验证失败返回错误列表 |
| 一句话区分 | cast_params 尽力把参数变成正确类型 |
validate_params 严格检查参数是否合法 |
| 对比维度 | execute (ToolRegistry) |
execute (Tool) |
|---|---|---|
| 调用者 | AgentRunner | ToolRegistry.execute |
| 参数处理 | 自动 prepare_call + hint | 直接执行 |
| 错误处理 | 捕获所有异常,附加 hint | 异常传播给调用者 |
| 一句话区分 | ToolRegistry 的 execute 是带完整包装的安全入口 | Tool 的 execute 是纯执行,异常由上层处理 |
3. API Signatures
graph TB
subgraph "Tool 抽象接口"
name["name [property]"]
desc["description [property]"]
params["parameters [property]"]
execute["execute(**kwargs) [abstract]"]
to_schema["to_schema()"]
cast_params["cast_params(params)"]
validate_params["validate_params(params)"]
end
subgraph "ToolRegistry"
register["register(tool)"]
get["get(name) -> Tool"]
get_definitions["get_definitions() -> list"]
prepare_call["prepare_call(name, params)"]
execute["execute(name, params)"]
end
subgraph "Schema 工具"
validate_json_schema_value["validate_json_schema_value()"]
fragment["fragment(value)"]
tool_parameters["@tool_parameters decorator"]
end
Tool 基类
1 | class Tool(ABC): |
ToolRegistry
1 | class ToolRegistry: |
4. 数据结构深度解析
4.1 Tool
4.a 结构体存在的理由
Tool 抽象基类定义了 Agent 能力的统一契约。每个工具必须声明它的名称、描述和参数 schema——这三者直接构成了 LLM function call 的接口定义。如果没有统一的 Tool 基类,新增工具需要修改 AgentRunner 的执行逻辑,而不是简单地注册即可。
4.b 字段三层分析表
| 字段 | 设计动机 | 反事实 | 替代方案 |
|---|---|---|---|
read_only |
标记工具是否无副作用,影响并发策略 | 所有工具串行执行,严重影响响应速度 | 可以用独立的 side_effects 列表,但 boolean 更简洁 |
concurrency_safe |
决定是否可与其他工具并发 | 需要 AgentRunner 硬编码哪些工具可并发 | 可以让 LLM 决定,但 LLM 不了解工具实现细节 |
exclusive |
标记必须单独执行的工具(如 spawn) | 独占工具与其他工具并发执行可能产生竞态 | 可以用全局锁,但粒度太粗 |
config_key |
插件元数据:对应配置键名 | 工具配置需要硬编码映射 | 可以用注册时的额外参数,但元数据在类上更清晰 |
_plugin_discoverable |
控制是否被 pkgutil 自动发现 | 内部工具被意外注册到生产环境 | 可以用白名单,但默认可见+选择性隐藏更安全 |
4.2 ToolRegistry
4.a 结构体存在的理由
ToolRegistry 是工具的管理中心。它不仅是工具字典的包装,还提供了定义缓存(
_cached_definitions)以稳定 LLM 提示缓存命中率,以及参数预处理流水线(prepare_call)以在工具执行前进行类型转换和验证。
4.b 字段三层分析表
| 字段 | 设计动机 | 反事实 | 替代方案 |
|---|---|---|---|
_cached_definitions |
缓存排序后的工具定义,稳定 prompt cache 命中 | 每次 LLM 调用都重新生成定义列表,cache 命中率降低 | 可以用 LRU cache,但显式失效更可控 |
_lookup_key |
规范化工具名用于模糊匹配建议 | LLM 拼错工具名时直接报 “not found”,用户体验差 | 可以用 Levenshtein 距离,但字母数字规范化更快 |
5. 函数逐行精讲
5.1 ToolRegistry.prepare_call() — 工具调用预处理
1 | def prepare_call(self, name, params) -> tuple[Tool | None, Any, str | None]: |
5.2 Tool.cast_params() — 类型转换
1 | def _cast_value(self, val, schema): |
6. 关键算法剖析
6.1 工具发现与加载
ToolLoader(nanobot/agent/tools/loader.py)通过 pkgutil.walk_packages 扫描 nanobot.agent.tools 包下的所有模块,自动发现 Tool 子类:
1 | nanobot.agent.tools.* |
6.2 工具分区并发执行
AgentRunner._partition_tool_batches 根据 concurrency_safe 和 exclusive 属性自动分区:
concurrency_safe=True的工具放在同一组,通过asyncio.gather并发执行exclusive=True或concurrency_safe=False的工具单独执行- 组内并发,组间串行
7. 设计决策分析
7.1 为什么 cast_params 和 validate_params 是两个独立步骤?
决策:类型转换和验证分离,prepare_call 先 cast 后 validate。
权衡:
- ✅ LLM 传入的参数经常类型不匹配(如字符串 “123” 而非整数 123),先转换再验证提高容错性
- ✅ 转换逻辑可复用(子类覆盖
_cast_value) - ❌ 转换和验证的边界有时模糊(如 enum 值的大小写规范化属于哪种?)
7.2 为什么工具定义需要缓存?
决策:get_definitions() 缓存结果直到工具注册/注销。
权衡:
- ✅ LLM 提供商(尤其是 Anthropic)使用 prompt caching,稳定的工具定义顺序提高 cache 命中率
- ✅ builtins 和 MCP 工具分别排序,使 builtins 的 cache 不受 MCP 工具变化影响
- ❌ 如果工具定义依赖动态状态,缓存会导致过期
8. 学习检查点
📝 本章小结
- Tool 基类定义了
name/description/parameters/execute四个核心契约 - ToolRegistry 提供定义缓存、参数预处理和模糊匹配建议
- 参数处理流水线:coerce → cast → validate → execute
- 工具发现通过 pkgutil 自动扫描,无需手动注册
- 并发执行根据
concurrency_safe属性自动分区
🤔 思考题
为什么
_coerce_params中要特殊处理{"arguments": {...}}的包装?这种包装是从哪来的?参考答案
某些 LLM(尤其是较旧模型或在特定 prompt 格式下)会将工具参数包装在
{"arguments": {...}}中(nanobot/nanobot/agent/tools/registry.py#L148-L155)。这是因为这些模型被训练为将arguments视为顶层键。_unwrap_arguments_payload检测这种情况:如果参数只有arguments键,且工具 schema 的 properties 中不包含arguments字段,则解包内层参数。但如果工具确实有一个叫arguments的参数,则保留包装。_suggest_name使用字母数字规范化而不是编辑距离,这样做有什么局限?参考答案
字母数字规范化(
nanobot/nanobot/agent/tools/registry.py#L35-L37)只能纠正大小写、下划线和连字符的变体(如readfile→read_file),无法处理拼写错误(如read_fiel→read_file不会匹配)。这是因为建议必须是确定性的——编辑距离可能返回错误的”最近”匹配,导致 LLM 以错误的参数调用错误的工具。保守的匹配策略避免了这种风险。