)
1. 从一次工具注册失败说起Hermes Agent 工具系统到底在解决什么如果你正在读 Hermes Agent 的源码大概率会在tools/registry.py这个文件前停下来。589 行不算长但它驱动了 70 多个工具的自注册、28 个工具集的按需加载以及运行时动态 Schema 重写。我第一次跟这条链路的时候最直观的感受是它不像传统 Agent 框架那样把所有工具塞进一个大字典或者一长串 if-else而是把「发现、过滤、重写、执行」拆成了四层每层各管一件事。这篇不打算只做源码翻译。我想把三条主线——注册表自注册、按需加载、动态 Schema 重写——拆成你能在本地跑起来、能验证、能排错的步骤。同时结合 TaoToken 的统一 Key/API 通道给出settings.json和config.toml的可复制配置骨架。你读完源码后可以直接拿这套骨架去调试自己的工具注册流程不用再从零搭环境。适合谁看已经能跑通 Hermes Agent 基础对话、想深入工具系统做二次开发或调试的人或者你正在设计自己的 Agent 工具层想参考一套经过生产验证的注册表设计。前置条件很简单本地有 Python 3.10 环境能访问 Hermes Agent 源码仓库并且有一个可用的模型 API 通道。下面所有配置和命令都围绕这个前提展开。2. TaoToken 前置统一 Key 与 API 通道的配置骨架在动源码之前先把模型通道固定下来。Hermes Agent 的工具系统本身不绑定具体模型供应商但你在调试dispatch()和动态 Schema 时需要一个稳定的 API 入口来触发真实的 tool_calls。TaoToken 在这里的角色是统一 Key 和 API 通道你只需要维护一份 Key就能在模型对话、Coding Plan、API Keys 管理之间切换不用为每个调试场景单独配一套凭证。先拿到 Key。访问控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite创建完成后在 API Keys 页面复制你的 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewriteAPI 基础地址统一用https://taotoken.net/api注意这里不加 UTM 参数保持接口地址干净。接下来把 Key 写进 Hermes Agent 的配置文件。Hermes 通常读取两个位置项目根目录的settings.json和用户级的config.toml。我建议把模型通道放在config.toml把工具系统相关的开关放在settings.json这样调试工具注册时不会误改模型配置。config.toml骨架[model] provider openai-compatible base_url https://taotoken.net/api api_key sk-你的TaoTokenKey model claude-sonnet-4-20250514 max_tokens 8192 temperature 0.2 [agent] tool_dispatch_timeout 120 enable_dynamic_schema true check_fn_ttl_seconds 30settings.json骨架{ tools: { enabled_toolsets: [terminal, browser, agent, code_execution], disabled_toolsets: [], registry_generation_cache: true, dynamic_schema_overrides: { execute_code: true, delegate_task: true } }, debug: { log_tool_registration: true, log_schema_rewrite: true } }这两个文件的作用不同config.toml决定模型请求走哪条通道settings.json决定工具系统加载哪些工具集、是否开启动态 Schema 重写。把check_fn_ttl_seconds显式写成 30是为了和源码里的_CHECK_FN_TTL_SECONDS 30.0对齐方便你在调试时观察缓存命中行为。如果你更习惯用模型对话来验证通道是否通可以先走一次模型对话页面https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite确认模型能正常返回后再进入工具系统的调试。长期做编码和 Agent 调试的话Coding Plan 会更省心https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite3. 可复制配置注册表自注册、按需加载与动态 Schema 重写的落地骨架这一节把源码里的三条主线对应到可操作的配置和代码上。你不需要改 Hermes 的核心文件只需要在自己的工具目录里新增一个工具模块然后观察它如何被 AST 扫描发现、如何被工具集过滤、如何被动态 Schema 重写。3.1 自注册发现AST 扫描的触发条件Hermes 不会显式 import 每个工具文件而是通过discover_builtin_tools()扫描tools/目录下的.py文件用_module_registers_tools()做 AST 解析检查模块顶层是否存在registry.register(...)调用。这意味着你的工具文件必须满足两个条件文件名不以__init__.py、registry.py、mcp_tool.py结尾模块顶层有registry.register()表达式。新建一个调试工具tools/debug_probe.pyfrom tools.registry import registry, tool_result, tool_error def _handle_debug_probe(args): target args.get(target, unknown) return tool_result(probeok, targettarget, sourcedebug_probe) def _check_debug_probe_available(): return True registry.register( namedebug_probe, toolsetdebug, schema{ description: 调试探针用于验证工具注册与动态 Schema 重写, parameters: { type: object, properties: { target: { type: string, description: 探针目标标识 } }, required: [target] } }, handler_handle_debug_probe, check_fn_check_debug_probe_available, requires_env[], is_asyncFalse, description调试探针工具, emoji, max_result_size_chars4096, dynamic_schema_overridesNone, )保存后Hermes 下次启动时会通过 AST 扫描发现这个文件自动 import触发顶层的registry.register()。你可以在日志里看到类似Could not import tool module之外的注册成功记录。如果没被发现先检查文件名是否被排除列表命中再检查registry.register是否在模块顶层而不是函数内部。3.2 按需加载工具集解析与禁用减法get_tool_definitions()的核心逻辑是先根据enabled_toolsets解析出工具名集合再用disabled_toolsets做减法。如果你在settings.json里只启用了[terminal, browser, agent, code_execution]那么debug工具集不会被加载debug_probe也不会出现在模型可见的工具列表里。要验证按需加载把settings.json改成{ tools: { enabled_toolsets: [terminal, browser, agent, code_execution, debug], disabled_toolsets: [browser] } }这样debug被启用browser被禁用。resolve_toolset(debug)会返回debug_probe而resolve_toolset(browser)返回的工具名会从集合中减去。你可以在model_tools.py的_compute_tool_definitions()里打断点观察tools_to_include集合的变化。注意一个特殊逻辑如果环境变量HERMES_KANBAN_TASK存在kanban工具集会被强制注入不受enabled_toolsets影响。这是为了让 kanban worker 能上报进度。调试时如果你看到kanban工具意外出现先检查这个环境变量。3.3 动态 Schema 重写通用回调与硬编码重写动态 Schema 重写分两类。一类是 Registry 支持的通用回调dynamic_schema_overrides在get_definitions()每次调用时执行返回的 dict 与静态 Schema 做浅合并。另一类是硬编码重写比如execute_code的sandbox_allowed_tools和discord的 intents 检测。先给debug_probe加一个动态回调import os def _current_debug_config(): return { description: 调试探针动态重写版, parameters: { type: object, properties: { target: { type: string, description: 探针目标标识当前模式 os.environ.get(DEBUG_PROBE_MODE, default) } }, required: [target] } } registry.register( namedebug_probe, toolsetdebug, schema{ description: 调试探针, parameters: { type: object, properties: { target: {type: string, description: 探针目标标识} }, required: [target] } }, handler_handle_debug_probe, check_fn_check_debug_probe_available, dynamic_schema_overrides_current_debug_config, )这样每次get_definitions()调用时_current_debug_config()都会执行返回的description和parameters会覆盖静态 Schema。你可以通过设置DEBUG_PROBE_MODE环境变量来观察 Schema 变化。对于execute_code的硬编码重写源码里的逻辑是if execute_code in available_tool_names: from tools.code_execution_tool import SANDBOX_ALLOWED_TOOLS, build_execute_code_schema sandbox_enabled SANDBOX_ALLOWED_TOOLS available_tool_names dynamic_schema build_execute_code_schema(sandbox_enabled, mode_get_execution_mode()) for i, td in enumerate(filtered_tools): if td.get(function, {}).get(name) execute_code: filtered_tools[i] {type: function, function: dynamic_schema} break这段代码的关键是SANDBOX_ALLOWED_TOOLS available_tool_names沙箱允许的工具集必须与实际启用的工具集取交集。如果你禁用了web_search沙箱里的execute_code也不会暴露web_search。调试时你可以故意禁用某个工具集然后检查execute_code的 Schema 里sandbox_allowed_tools是否同步减少。4. 验证请求一次工具注册与 Schema 重写的完整动作配置写完后用一次真实请求把整条链路跑通。目标是让模型调用debug_probe观察注册表是否命中、动态 Schema 是否生效、dispatch()是否返回正确 JSON。4.1 启动前检查先确认环境变量和配置文件就位export TAOTOKEN_API_KEYsk-你的TaoTokenKey export DEBUG_PROBE_MODErewrite-test export HERMES_KANBAN_TASK然后检查config.toml和settings.json是否在 Hermes 预期的路径下。通常config.toml在~/.hermes/config.tomlsettings.json在项目根目录。启动 Hermespython -m hermes_agent.cli --config ~/.hermes/config.toml --settings ./settings.json如果日志里出现Tool registration REJECTED说明有跨工具集重名冲突。检查你的debug_probe是否与已有工具同名。如果出现Could not import tool module tools.debug_probe检查文件路径和语法。4.2 触发工具调用在对话里输入请调用 debug_probe 工具target 设为 schema-rewrite-check。模型返回tool_calls后Agent Loop 会调用registry.dispatch(debug_probe, {target: schema-rewrite-check})。预期返回{probe: ok, target: schema-rewrite-check, source: debug_probe}同时在get_definitions()阶段debug_probe的 Schema 应该已经被_current_debug_config()重写description里包含当前模式rewrite-test。你可以在日志里搜索dynamic_schema_overrides或log_schema_rewrite的输出。4.3 验证缓存行为check_fn的 TTL 缓存是 30 秒。连续两次调用get_definitions()第二次应该命中缓存不会重新执行_check_debug_probe_available()。你可以把check_fn改成一个带打印的函数来观察def _check_debug_probe_available(): print([check_fn] debug_probe availability checked) return True第一次调用会打印30 秒内第二次调用不会打印。超过 30 秒后再调用会重新打印。这个行为对应源码里的_check_fn_cached()。4.4 验证注册表 generation 缓存registry._generation是单调递增的计数器。每次注册/注销/别名变更都会递增。model_tools.py的外层缓存以registry._generation和配置文件指纹为 key。你可以注册一个新工具观察_generation变化后缓存是否失效from tools.registry import registry print(before:, registry._generation) # 触发一次新注册 print(after:, registry._generation)如果_generation没变说明注册没有真正发生检查register()是否被重名保护拒绝。5. 本篇常见错排查注册失败、Schema 不生效、缓存不刷新调试工具系统时最容易卡在几个具体报错上。下面按现象、原因、解决三步走。5.1Tool registration REJECTED: xxx already registered by toolset yyy这是跨工具集重名保护。源码里的逻辑是如果已有工具的toolset与新注册的toolset不同且不是两个 MCP 工具之间的覆盖且没有显式overrideTrue就拒绝注册。解决方式有三种改工具名、改工具集、或者在register()里加overrideTrue。注意overrideTrue是主动 opt-in不要随便加否则可能覆盖内置工具。5.2 动态 Schema 重写不生效先确认dynamic_schema_overrides回调返回的是 dict而不是 None 或其他类型。源码里只处理isinstance(overrides, dict)的情况。其次确认回调没有抛异常异常会被logger.warning捕获并跳过。最后确认get_definitions()确实被调用了——如果外层缓存命中动态重写不会重新执行。你可以临时把registry_generation_cache设为false来排除缓存干扰。5.3check_fn缓存导致工具集状态不更新check_fn的 TTL 是 30 秒。如果你刚用hermes tools enable browser启用了工具集但check_fn还在缓存期内工具集可能不会立即生效。等 30 秒或者重启进程。源码注释里明确写了这个折中太短浪费探测太长影响实时性。调试时可以把check_fn_ttl_seconds临时改成 1 秒观察行为变化。5.4dispatch()返回Unknown tool: xxx说明registry.get_entry(name)没找到工具。可能原因工具没被 AST 扫描发现、注册被拒绝、工具集被禁用导致get_definitions()没把它暴露给模型。先检查discover_builtin_tools()的返回值里有没有你的模块名再检查registry._tools里有没有你的工具名。5.5 错误信息被_sanitize_tool_error()过滤后难以定位dispatch()的异常处理会调用_sanitize_tool_error()过滤掉 framing token、CDATA、反引号等结构性噪音。这可能导致原始错误信息被截断。调试时可以在dispatch()里临时打印原始异常或者把log_tool_registration和log_schema_rewrite都打开从日志里找完整堆栈。6. 语义一致 CTA把调试链路固定下来工具系统的调试不是一次性的。你每加一个工具、每改一次 Schema、每调一次工具集都需要重新验证注册、加载、重写、执行这四步。把 TaoToken 的 Key 和 API 通道固定下来能让你在模型对话、Coding Plan、API Keys 之间切换时不用重复配环境。如果你在排障或接入阶段优先用 API Keys 和接入文档https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你要验证模型对工具 Schema 的理解是否符合预期走模型对话https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite如果你长期做编码和 Agent 调试Coding Plan 更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewriteClaude Code 和 Anthropic 通道的接入说明在这里https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-codeutm_campaignrewrite https://taotoken.net/anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentanthropicutm_campaignrewrite最后留一个我调试时常用的技巧把debug_probe的dynamic_schema_overrides回调写成读取环境变量的形式这样你可以在不重启进程的情况下通过改环境变量观察 Schema 重写结果。配合check_fn的 30 秒 TTL你能在近实时的情况下验证工具集启用/禁用对 Schema 的影响。这套骨架跑通后再去看tools/registry.py的 589 行每条分支都能对应到你亲手验证过的行为。