Tool 注册、校验与执行
对应源码
| 主题 | 源码 |
|---|---|
| 单个 Tool 契约 | nanobot/agent/tools/base.py::Tool |
| 插件发现和实例化 | nanobot/agent/tools/loader.py::ToolLoader |
| 注册、schema 和参数校验 | nanobot/agent/tools/registry.py::ToolRegistry |
| Tool Call 归一化 | nanobot/providers/base.py::ToolCallRequest、parse_tool_arguments |
| 执行批次和结果回填 | nanobot/agent/runner.py::AgentRunner._execute_tools、AgentRunner._run_tool、AgentRunner._run_core |
学习目标
读完后,你应当能够:
- 区分 Tool、ToolRegistry 和 ToolLoader;
- 说明工具如何被发现和注册;
- 说明模型参数在执行前经过哪些处理;
- 解释并发安全工具和顺序工具的区别;
- 找到工具错误如何回到模型。
前置知识
- 理解 JSON Schema;
- 理解 Tool Call ID;
- 已阅读 AgentRunner 主循环。
1. Tool 定义单个能力契约
基类:
nanobot/agent/tools/base.py::Tool
每个 Tool 主要提供:
namedescriptionparametersexecute(**kwargs)enabled(config)create(config, context)concurrency_saferead_onlyexclusive- scope 等属性
其中:
- name 必须与模型 Tool Call 精确匹配;
- description 和 parameters 会影响模型是否以及如何调用;
- execute 才执行真实副作用;
- concurrency、安全和 scope 属性影响运行策略。
2. ToolLoader 负责发现和实例化
ToolLoader 使用两条发现路径:
flowchart LR PKG["nanobot.agent.tools 包"] --> P["pkgutil.iter_modules"] P --> CLASS["发现 Tool 子类"] EP["entry point: nanobot.tools"] --> EXT["外部插件类"] CLASS --> LOAD["读取配置、enabled、scope"] EXT --> LOAD LOAD --> REG["ToolRegistry.register"]
内置 Tool:
- 通过 pkgutil 扫描模块;
- 跳过内部模块;
- 找到 Tool 子类;
- 根据配置、scope 和 context 创建实例。
外部 Tool:
- 通过 Python entry point group
nanobot.tools。
名称冲突时,内置工具优先,避免插件静默覆盖核心工具。
3. ToolRegistry 保存可用实例和 schema
ToolRegistry 的核心职责:
- register/unregister/get;
- 生成稳定排序的 tool definitions;
- 精确解析工具名;
- 参数形状归一化;
- 类型转换和 schema 校验;
- 执行工具。
Tool definitions 会缓存,直到 register/unregister 使缓存失效。内置工具和以 mcp_ 开头的工具分别排序,有利于提示缓存稳定。
4. 工具名执行时必须精确匹配
Registry 可以为错误消息生成“你是不是想调用……”的建议,但不会使用模糊名称直接执行。
原因:
- 模糊执行可能调用错误能力;
- 安全敏感工具不能依赖相似度;
- schema 和权限都绑定精确工具。
5. 参数处理顺序
ToolRegistry.prepare_call():
flowchart TD A["name + raw params"] --> B{"精确找到 Tool?"} B -->|否| X["返回 not found 错误和建议"] B -->|是| C["coerce JSON 字符串"] C --> D["必要时解包 arguments"] D --> E{"params 是对象?"} E -->|否| Y["返回参数形状错误"] E -->|是| F["tool.cast_params"] F --> G["tool.validate_params"] G -->|有错误| Z["返回 schema 错误"] G -->|通过| H["交给 execute"]
关键边界:
- 空参数变成空对象;
- 合法 JSON 字符串可解析;
- 数组、标量或破损 JSON 不应被猜成可执行参数;
- replay 历史可以有兼容修复,但新 Tool Call 的执行路径不能靠猜。
源码中的成功路径很短:
tool = self._tools.get(name)
if not tool:
suggestion = self._suggest_name(str(name))
# 返回 not-found 错误;suggestion 只进入错误提示
...
params = self._coerce_params(tool, params)
if not isinstance(params, dict):
# 返回“参数必须是 JSON object”错误
...
cast_params = tool.cast_params(params)
errors = tool.validate_params(cast_params)
if errors:
# 返回 schema 校验错误
...
return tool, cast_params, None节选来自 ToolRegistry.prepare_call(),省略号只替代错误文案。顺序不能随意交换:必须先精确定位 Tool,才能依据它的 schema 解包、转换和校验参数;只有最后的 None error 才表示允许执行。
6. Runner 负责工具执行策略
虽然 ToolRegistry.execute() 可以执行单个工具,AgentRunner 的 _execute_tools() 还负责:
- 将
concurrency_safe工具分组; - 安全工具可并发 gather;
- 非并发安全工具顺序执行;
- 记录 tool event;
- 分类异常;
- 处理 workspace 和 SSRF 违规;
- 根据
fail_on_tool_error判断是否 fatal。
ToolRegistry:找到、校验、调用一个工具
AgentRunner:组织一批 Tool Calls 的执行策略和错误语义7. 错误通常作为 Tool Result 回到模型
Tool 名不存在、参数错误或普通执行异常,通常会转换成以 Error 开头的结果,并附提示让模型分析后换一种方式。
这允许模型:
- 修正参数;
- 选择另一个工具;
- 停止尝试并向用户解释。
安全违规会被进一步分类:
- workspace violation;
- SSRF;
- 外部目标重复尝试;
- fatal tool error。
不能在 Tool 中捕获所有异常后只返回空字符串,否则 Runner 和模型都失去诊断信息。
8. Tool Result 进入下一轮
Runner 为每个调用创建:
{
"role": "tool",
"tool_call_id": "...",
"name": "...",
"content": "..."
}在写入前还会:
- 将复杂结果转换为文本;
- 限制长度;
- 必要时把大结果持久化到 workspace;
- 保留可回放边界。
9. Tool、Skill、MCP 的最小区别
| 类型 | 进入模型的形式 | 执行位置 |
|---|---|---|
| Tool | JSON schema + 运行时执行 | nanobot 进程 |
| Skill | Markdown 指导或按需知识 | 上下文,不直接执行 |
| MCP Tool | 转换为 nanobot Tool schema | 外部 MCP server |
后续扩展章节会详细比较。当前只需记住:
“教模型怎么做”优先考虑 Skill;“让模型执行动作”才是 Tool 或 MCP。
观察任务
在一次文件读取 Tool Call 中观察:
ToolCallRequest.name
ToolCallRequest.arguments
ToolRegistry.get_definitions()
ToolRegistry.prepare_call()
tool.concurrency_safe
tool result
下一轮 messages 中的 role=tool故意让模型调用不存在的路径,观察错误是否作为 tool result 返回,而不是直接让整个进程崩溃。
源码与测试
nanobot/agent/tools/base.pynanobot/agent/tools/loader.pynanobot/agent/tools/registry.pynanobot/agent/runner.py::_execute_toolsnanobot/agent/runner.py::_run_tooltests/agent/test_tool_loader_entrypoints.pytests/agent/test_tool_loader_scopes.pytests/agent/test_runner_tool_execution.pytests/agent/test_runner_safety.py
自检
- ToolLoader 和 ToolRegistry 为什么不能合并理解?
- 为什么名称建议只用于报错,不用于真实执行?
- replay 参数修复为什么不能直接复用在新调用执行上?
concurrency_safe应由谁声明、由谁执行?- 普通工具错误为什么可以回到模型?
下一步
阅读 流式输出、错误与停止。