章末自测

题目 1:为什么大量 if name == ... 会随着 Tool 增多越来越难维护?

假设一个 Coding Agent 从 2 个 Tool 扩展到 15 个 Tool,请从扩展性、重复修改、工具发现、权限、测试几个角度解释为什么需要 Registry,而不只是回答“代码太长”。

大量 if/elif 最大的问题不是视觉上变长,而是每新增 Tool 都必须修改中心分发函数,形成强耦合。与此同时,模型可见工具列表、真实执行能力、权限规则、Tracing 元数据可能分别维护,容易不同步。Registry 把 Tool Name、Callable 和后续 Metadata 集中管理,让新增 Tool 通过注册完成,Main Loop 不必修改;测试也可以直接验证 Registry 中有哪些 Tool。它因此成为扩展能力和保持一致性的基础。

题目 2:判断题——可以用 globals()[tool_name] 替代 Tool Registry,对吗?

请说明为什么这种方式在普通脚本里可能能跑,但在 Agent Harness 中存在明显风险。

不建议。globals() 的目标是访问当前模块全局命名空间,而不是表达 Agent 被授权使用的工具集合。里面可能存在内部函数、调试函数、敏感操作和其他 callable。如果模型能按名字动态访问 globals,就可能触达本不该暴露的能力。Tool Registry 则采用显式白名单:只有主动注册的函数才属于 Agent Tool,因此同时承担能力目录与安全边界的角色。

题目 3:代码阅读——为什么 Registry 返回 Tool 对象比只返回 Callable 更容易扩展?

假设后续要增加 description、risk_level、requires_approval、timeout,请说明如果 Registry 只保存函数会遇到什么问题。

如果 Registry 只保存 Callable,工具的描述、风险等级、审批要求等信息就需要散落在其他字典或配置中,容易和函数定义失去同步。返回 Tool 对象后,可以把 name、description、func 以及后续 Schema、Risk、Timeout 等 Metadata 放在同一实体上。这样 Provider 生成 Tool Schema、Middleware 做安全判断、Tracing 记录工具信息时,都能从同一个 Tool 对象读取,职责更集中。

题目 4:编程实现——写一个支持 register、get、list_tools、execute 的 ToolRegistry

要求完整可运行,并至少注册 read_file、list_files 两个工具;未知工具必须抛出或返回明确错误。

#下面是一种完整实现:
from typing import Any, Callable


class ToolRegistry:

    def __init__(self):
        self._tools: dict[
            str,
            Callable[..., Any]
        ] = {}

    def register(
        self,
        func: Callable[..., Any],
    ) -> None:
        name = func.__name__

        if name in self._tools:
            raise ValueError(
                f"工具已存在:{name}"
            )

        self._tools[name] = func

    def get(
        self,
        name: str,
    ) -> Callable[..., Any]:
        if name not in self._tools:
            raise KeyError(
                f"未知工具:{name}"
            )

        return self._tools[name]

    def list_tools(self) -> list[str]:
        return list(
            self._tools.keys()
        )

    def execute(
        self,
        name: str,
        arguments: dict[str, Any],
    ) -> Any:
        func = self.get(name)

        return func(
            **arguments
        )


def read_file(path: str) -> str:
    return f"读取:{path}"


def list_files() -> list[str]:
    return [
        "README.md",
        "app.py",
    ]


if __name__ == "__main__":
    registry = ToolRegistry()

    registry.register(read_file)
    registry.register(list_files)

    print(
        registry.list_tools()
    )

    print(
        registry.execute(
            "read_file",
            {
                "path": "app.py"
            },
        )
    )
#这个版本已经实现最核心 Registry 能力,后续再逐渐加入 Tool 对象、Schema、权限等 Metadata

题目 5:编程实现——实现一个 @tool 自动注册装饰器

要求函数定义以后自动进入全局 Registry,并且函数本身仍可以正常直接调用。请说明装饰器为什么需要 return func。

#可以这样实现:
from typing import Any, Callable


TOOLS: dict[
    str,
    Callable[..., Any]
] = {}


def tool(
    func: Callable[..., Any],
):
    if func.__name__ in TOOLS:
        raise ValueError(
            f"重复工具:{func.__name__}"
        )

    TOOLS[
        func.__name__
    ] = func

    return func


@tool
def read_file(path: str) -> str:
    return f"读取:{path}"


if __name__ == "__main__":
    print(
        list(TOOLS.keys())
    )

    print(
        read_file(
            "README.md"
        )
    )
#return func 很重要,因为装饰器语法本质上会把原函数名重新绑定为装饰器返回值。如果不返回原函数,read_file 可能变成 None 或其他对象,失去直接调用能力。这里我们只想增加“注册”副作用,同时保留原函数。

题目 6:综合应用——为什么模型可见 Tool 列表应该从 Registry 生成?

请结合“模型能看到 search_files,但 Registry 没有它”和“Registry 有 run_test,但模型看不到它”两个反例解释。查看参考答案

如果模型可见列表与执行 Registry 分开维护,会出现双向不一致。模型看得到 search_files 但 Registry 没有时,模型会正常产生 Tool Call,却在执行阶段 UnknownTool;Registry 有 run_test 但模型看不到时,系统明明拥有能力,模型却永远不会选择它。让 Tool Schema 从 Registry 生成,可以把“模型能看到什么”和“Harness 能执行什么”建立在同一个事实来源上,显著降低配置漂移问题。

题目 7:概念理解——Tool Registry、Tool Schema、Tool Executor 有什么区别?

请分别用一句话说明三者职责,再说明它们如何在一次 Tool Calling 中协作。

Tool Registry 负责管理“有哪些 Tool,以及真实 Callable 在哪里”;Tool Schema 负责把 Tool 的名称、描述和参数结构告诉模型;Tool Executor 负责拿到模型 Tool Call 后进行查找、校验和实际执行。一次调用中,Provider 先把 Registry 产生的 Schema 提供给模型,模型返回 Tool Call,Executor 再通过 Registry 找到真实 Tool 并执行。三者配合后,模型世界的结构化动作才能稳定映射到 Python 世界的真实函数。

进一步阅读

  • Python Decorator 相关官方文档:理解 @tool 语法背后的函数包装与名称绑定机制。
  • Python dataclasses 文档:适合定义 Tool Metadata 对象。
  • Registry Pattern 相关资料:理解注册中心如何统一管理可扩展组件。
  • Command Pattern 相关资料:Tool Call 可以理解成命令,Registry 负责找到对应 Command Handler。
  • Plugin Architecture 相关资料:后续 Skills、SubAgent、插件化能力与 Registry 思路高度相关。
  • Capability-based Security 相关资料:理解“只注册被授权能力”为什么比动态反射所有函数更安全。
  • Dependency Injection 相关资料:Registry 与依赖注入在“大量可替换实现”场景里有相似设计思想。
  • Python inspect 模块文档:下一节 Tool Schema 会用它读取函数签名和类型信息。

术语表

  • 工具注册中心(Tool Registry):Agent Harness 中统一管理可用 Tool 的组件,负责注册、查找、枚举和执行工具,也是后续 Schema、权限、Tracing 等能力的重要挂载点。
  • 工具对象(Tool):对一个可调用能力的结构化封装,除了真实 Python Callable,还可以保存 name、description、schema、risk_level 等 Metadata。
  • 注册(Register):把一个 Python 函数或 Tool 对象显式加入 Agent 可用能力集合的过程。未注册的函数默认不应该被 Agent 执行。
  • 工具查找(Tool Lookup):根据模型返回的 Tool Name,在 Registry 中查找真实 Tool 的过程。查找失败应形成明确 UnknownTool 错误。
  • 自动注册装饰器(@tool):通过 Python Decorator 在函数定义时完成 Tool 构建和 Registry 注册,让“声明这是 Agent Tool”和“真正注册”合并为一个清晰动作。
  • 工具元数据(Tool Metadata):描述工具附加属性的数据,例如 description、category、risk_level、timeout、requires_approval。它们会被 Schema、安全和 Tracing 等模块使用。
  • 能力白名单(Capability Allowlist):只允许 Agent 使用明确注册的能力,而不是让模型动态访问任意 Python 函数。Registry 天然可以承担这层边界。
  • 单一事实来源(Single Source of Truth):某类核心配置或数据只由一个地方定义。Tool Registry 应尽量成为“模型可见工具”和“真实可执行工具”的统一来源,避免两套列表漂移。

本章总结

最后再回看一眼全章主线,会更容易把这些零散概念串起来。

图片[1]-章末自测-大赛人网

你会发现,无论是自动注册、Schema 生成、权限控制,还是角色裁剪,最后其实都在围绕 Registry 这个“统一工具目录”展开。把它设计好,后面的 Tool Calling、权限控制、可见性管理都会轻松很多。

这一节我们把上一节的 Tool Calling 真正推进到了“工具系统”。

上一节模型已经能够产生:

{
  "name": "read_file",
  "arguments": {
    "path": "app.py"
  }
}

但我们没有停在一个字典。

我们进一步设计:

ToolRegistry
  ├── register
  ├── get
  ├── list_tools
  └── execute

并且把真实 Python Function 包装成:

Tool
  ├── name
  ├── description
  └── func

这样一来,Registry 不只是函数映射,而开始成为 Agent 的“能力目录”。

最重要的一点是,只有注册到 Registry 的能力,才应该被 Agent 视为 Tool。

这意味着 Registry 同时承担了扩展性和安全边界。

我们还进一步实现:

@tool
def read_file(path: str):
    ...

让函数定义与注册自动绑定。

以后新增 Tool,不再需要修改 Main Loop,也不需要在 Executor 里继续加:

elif name == ...

而是只新增:

@tool()
def search_files(...):
    ...

这正是我们想要的扩展方式,新增能力时增加 Tool,不修改 Agent 核心控制流。

如果你想验证自己真正掌握了这一节,可以做三个实验。

第一,自己实现:

register
get
list_tools
execute

第二,写一个 @tool 装饰器,让函数定义后自动注册。

第三,故意让模型请求一个不存在的 Tool,验证系统能明确返回 UnknownTool,而不是直接崩溃。

当这三个都跑通以后,我们的 Agent 已经拥有一个非常稳定的工具入口。

下一节会继续解决 Registry 里的另一个问题,模型怎么知道 read_file 需要一个 path: str 参数?

我们不能只告诉模型:

你有一个 read_file 工具。

还必须告诉它:

工具是干什么的
参数叫什么
参数是什么类型
哪些参数必填

所以第 08 节,我们会进入,Tool Schema:让模型知道工具怎么用。

到那时,Registry 中的 Python Function 会第一次自动转换成模型能够理解的结构化工具协议。

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像-大赛人网
欢迎您留下宝贵的见解!
提交
头像-大赛人网

昵称

取消
昵称

    请登录后查看评论内容