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 主要提供:

  • name
  • description
  • parameters
  • execute(**kwargs)
  • enabled(config)
  • create(config, context)
  • concurrency_safe
  • read_only
  • exclusive
  • 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 的最小区别

类型进入模型的形式执行位置
ToolJSON schema + 运行时执行nanobot 进程
SkillMarkdown 指导或按需知识上下文,不直接执行
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.py
  • nanobot/agent/tools/loader.py
  • nanobot/agent/tools/registry.py
  • nanobot/agent/runner.py::_execute_tools
  • nanobot/agent/runner.py::_run_tool
  • tests/agent/test_tool_loader_entrypoints.py
  • tests/agent/test_tool_loader_scopes.py
  • tests/agent/test_runner_tool_execution.py
  • tests/agent/test_runner_safety.py

自检

  1. ToolLoader 和 ToolRegistry 为什么不能合并理解?
  2. 为什么名称建议只用于报错,不用于真实执行?
  3. replay 参数修复为什么不能直接复用在新调用执行上?
  4. concurrency_safe 应由谁声明、由谁执行?
  5. 普通工具错误为什么可以回到模型?

下一步

阅读 流式输出、错误与停止。