拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Hermes Agent 工具系统源码全解析:注册表自注册、按需加载与动态 Schema 重写(TaoToken 配置骨架版)

Hermes Agent 工具系统源码全解析:注册表自注册、按需加载与动态 Schema 重写(TaoToken 配置骨架版)

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_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite

创建完成后,在 API Keys 页面复制你的 Key:

https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite

API 基础地址统一用:

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 = 30

settings.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_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite

确认模型能正常返回后,再进入工具系统的调试。长期做编码和 Agent 调试的话,Coding Plan 会更省心:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite

3. 可复制配置:注册表自注册、按需加载与动态 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.py:

from tools.registry import registry, tool_result, tool_error def _handle_debug_probe(args): target = args.get("target", "unknown") return tool_result(probe="ok", target=target, source="debug_probe") def _check_debug_probe_available(): return True registry.register( name="debug_probe", toolset="debug", 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_async=False, description="调试探针工具", emoji="", max_result_size_chars=4096, dynamic_schema_overrides=None, )

保存后,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( name="debug_probe", toolset="debug", 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_KEY="sk-你的TaoTokenKey" export DEBUG_PROBE_MODE="rewrite-test" export HERMES_KANBAN_TASK=""

然后检查config.toml和settings.json是否在 Hermes 预期的路径下。通常config.toml在~/.hermes/config.toml,settings.json在项目根目录。启动 Hermes:

python -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 工具之间的覆盖,且没有显式override=True,就拒绝注册。解决方式有三种:改工具名、改工具集、或者在register()里加override=True。注意override=True是主动 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_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

如果你要验证模型对工具 Schema 的理解是否符合预期,走模型对话:

https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite

如果你长期做编码和 Agent 调试,Coding Plan 更适合:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite

Claude Code 和 Anthropic 通道的接入说明在这里:

https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claude-code&utm_campaign=rewrite https://taotoken.net/anthropic?utm_source=taotoken_aicg_blog_end&utm_content=anthropic&utm_campaign=rewrite

最后留一个我调试时常用的技巧:把debug_probe的dynamic_schema_overrides回调写成读取环境变量的形式,这样你可以在不重启进程的情况下,通过改环境变量观察 Schema 重写结果。配合check_fn的 30 秒 TTL,你能在近实时的情况下验证工具集启用/禁用对 Schema 的影响。这套骨架跑通后,再去看tools/registry.py的 589 行,每条分支都能对应到你亲手验证过的行为。

返回列表