Hermes agent的tools是怎么落地应用的系列

结论先说:Hermes 的 tools 架构是"注册表 + toolset 暴露 + agent 执行器"的三层结构,不是简单把 tools/ 目录里所有函数都塞给模型。

核心链路

  1. 工具发现
    tools/registry.py 会扫描 tools/*.py,只导入存在顶层 registry.register(...) 的模块。导入本身触发注册。

  2. 工具注册表
    tools/registry.py 的 ToolRegistry 保存:
    nametoolset、OpenAI tool schema、handler、check_fn、env 需求、是否 async、emoji、结果大小限制等。

  3. schema 生成
    model_tools.py 的 get_tool_definitions() 根据启用/禁用的 toolsets 生成最终给模型看的 tools。

    真正计算在 model_tools.py。

  4. toolset 决定"模型看不看得到"
    toolsets.py_HERMES_CORE_TOOLS 是默认核心工具集合。
    toolsets.pyhermes-cli 等平台 toolset 再组合这些工具。

    关键点:工具文件自注册只表示 registry 知道它;必须在 toolset 中解析出来,模型才看得到。

  5. Agent 初始化加载 tools
    agent/agent_init.py 调 get_tool_definitions(),然后把工具名放进 agent.valid_tool_names

  6. 模型发 tool call 后执行

    并发/顺序执行入口在 agent/tool_executor.py 和 agent/tool_executor.py。

    单个工具调用会走 agent/agent_runtime_helpers.py。

    普通 registry 工具最终走 model_tools.py 的 handle_function_call() 和 tools/registry.py 的 dispatch()

当前实际注册内容

我本地导入后看到 71 个工具,按 toolset 大致是:

  • web: web_search, web_extract
  • terminal: terminal, process
  • file: read_file, write_file, patch, search_files
  • browser: navigate/click/type/scroll/snapshot/console/vision/images 等
  • browser-cdp: browser_cdp, browser_dialog
  • vision: vision_analyze
  • image_gen: image_generate
  • video: video_analyze
  • video_gen: video_generate
  • tts: text_to_speech
  • code_execution: execute_code
  • delegation: delegate_task
  • todo, memory, session_search, clarify
  • cronjob
  • messaging: send_message
  • homeassistant
  • discord, discord_admin
  • feishu_doc, feishu_drive
  • yuanbao
  • kanban
  • computer_use
  • moa: mixture_of_agents
  • skills: skills_list, skill_view, skill_manage
  • x_search
  • 插件带来的 spotify_*

注意:这个数量包含 repo 内插件发现后注册的工具,不等于纯 tools/ 目录文件数。

代表性写法

文件工具是最标准的例子:tools/file_tools.py。每个工具通常有:

python 复制代码
registry.register(
    name="read_file",
    toolset="file",
    schema=READ_FILE_SCHEMA,
    handler=_handle_read_file,
    check_fn=_check_file_reqs,
    emoji="...",
    max_result_size_chars=100_000,
)

Web 工具展示了 async handler:tools/web_tools.py。

Terminal 工具展示了复杂 schema、后台进程、超时、PTY、通知等:tools/terminal_tool.py。

handler 必须返回 JSON 字符串。辅助函数在 tools/registry.py 和 tools/registry.py,也就是 tool_error() / tool_result()

插件与 MCP

自定义或本地工具,按项目规则不应该优先改核心 tools/,而应该走插件。插件通过 hermes_cli/plugins.py 的 ctx.register_tool() 注册,底层仍然进入同一个 registry。

MCP 工具也是动态注册进 registry:tools/mcp_tool.py。MCP 会用 mcp-<server> 作为 toolset,并注册别名:tools/mcp_tool.py。

一个重要变化:MCP discovery 不再是 model_tools.py 的导入副作用,注释写在 model_tools.py。各入口要自己显式发现,避免 gateway 事件循环被慢 MCP 卡住。

工具启用方式

平台级启用配置在 hermes_cli/tools_config.py。

CLI 在 cli.py 读取当前平台启用 toolsets。

Gateway 在 gateway/run.py 按平台加载。

TUI 在 tui_gateway/server.py 也有自己的加载函数。

实际用户侧就是 hermes tools / setup 保存到 platform_toolsets,再由 _get_platform_tools() 转成 enabled toolsets。

几个关键坑

  • todomemorysession_searchdelegate_task 虽然注册在 registry,但属于 agent-level tools,会被 agent loop 特殊处理;直接走普通 handle_function_call() 会被拦:model_tools.py。
  • check_fn 有 30 秒 TTL 缓存:tools/registry.py。改 env/config 后不是每次立即生效。
  • discover_builtin_tools() 只识别模块顶层 registry.register(...)。如果把注册藏在函数里,自动发现不会导入它。
  • 注册同名工具默认会被拒绝,插件要有意覆盖必须 override=True:tools/registry.py。
  • 大输出不是简单截断。执行器会把超大结果持久化到 sandbox 文件,再给模型预览:tools/tool_result_storage.py;整轮预算控制在 tools/tool_result_storage.py。
  • TOOL_TO_TOOLSET_MAP 在 model_tools.py 是 discovery 后构建的兼容常量;动态 MCP/插件刷新后,优先信 registry 实时查询,不要过度依赖旧常量。

我对架构的评价

整体设计是对的:registry 解耦工具实现,toolset 控制暴露面,agent executor 负责权限、并发、结果预算、插件 hook。这比"每个入口手写工具列表"稳很多。

但也有明显技术债:browser_tool.pyterminal_tool.pycode_execution_tool.py 都偏巨型模块;toolset 静态配置也比较长,平台差异容易漂移。后续如果要继续扩展,最好坚持插件路线,并把核心大工具按 backend/schema/handler/guardrail 拆小,而不是继续往单文件里堆。

换一种方式讲:Hermes 的 tools 系统,本质上不是"工具目录",而是一个 给 LLM 建立可控行动空间的运行时协议层

也就是说,它解决的不是"Python 怎么调用函数",而是这几个更深的问题:

  1. 这个 agent 此刻允许做什么?
  2. 模型看到的动作空间如何被压缩、过滤、动态修正?
  3. 模型发来的 tool call 如何变成可信、可审计、可中断、可并发、可限流的真实操作?
  4. 外部插件、MCP、平台专属能力如何接进来,但不污染核心?
  5. 工具结果如何返回给模型而不把上下文撑爆?

所以它的内核可以抽象成这条链:

text 复制代码
工具实现
  ↓ 注册
Registry:系统知道有哪些能力
  ↓ 选择
Toolset:本轮/本平台允许暴露哪些能力
  ↓ Schema
model_tools:把能力翻译成模型 API 能理解的 JSON Schema
  ↓ 模型决策
LLM 输出 tool_call
  ↓ 执行器
Agent runtime / tool_executor:校验、审批、并发、拦截、调用
  ↓ 返回
结果预算、持久化、插件 hook、写回 conversation

真正的核心不是 tools/,而是 "能力从存在到可见,再到可执行"的分层控制

第一层:工具存在,不等于模型能用

比如 tools/file_tools.py 注册了 read_file/write_file/patch/search_files。这一步只是告诉 registry:

系统里有这么几个函数,它们叫什么、schema 是什么、handler 是什么、属于哪个 toolset。

但是模型并不会自动看到它们。

模型是否看到,要看 toolsets.py 里的工具集合,以及平台配置最终启用了哪些 toolsets。

这就是 Hermes 的第一个重要设计:能力注册和能力暴露分离

为什么要分离?

因为 Hermes 不是只有 CLI。它还有 gateway、TUI、ACP、cron、API server、Discord、Feishu、Home Assistant 等入口。不同入口的风险边界不同:

  • CLI 可以用 terminal
  • ACP 要更偏代码编辑
  • API server 不适合 interactive clarify
  • Discord 可以额外暴露 Discord 工具
  • 某些工具只有 env/key 存在时才应该出现

所以 registry 回答的是:

系统会什么?

toolset 回答的是:

这个场景允许模型做什么?

这两个问题不能混在一起。

第二层:schema 不是文档,是模型的操作界面

model_tools.py 的 get_tool_definitions() 很关键。它不是简单返回 registry 里的 schema,而是在做"模型可见世界"的最终裁剪。

比如:

  • enabled toolsets 解析成具体工具名
  • disabled toolsets 再做减法
  • check_fn 失败的工具被隐藏
  • execute_code 的 schema 会根据当前实际可用工具重建
  • browser schema 会在 web 工具不可用时移除误导描述
  • Discord schema 会根据 bot 权限动态变化
  • 最后还要做 schema sanitizer,兼容不同模型后端

这说明一件事:tool schema 是行为约束,不只是说明书。

对 LLM 来说,它看到的 schema 就是它能采取行动的边界。如果 schema 里说"你可以调用 web_search",但实际 web_search 不可用,模型就会被误导。因此 Hermes 做了很多动态修正,目的就是让模型的"认知地图"和真实运行时尽量一致。

这是深层内核之一:
工具系统不是给人看的 API,而是给模型看的可行动作空间。

第三层:handler 不是直接执行,而是进入管控管线

模型发出 tool call 后,不是直接 handler(args)

先进入 agent/tool_executor.py 或顺序路径,再到 agent/agent_runtime_helpers.py。

这里开始有真正的 agent runtime 语义:

  • 解析模型参数
  • 修复错误工具名
  • 检查 interruption
  • 触发插件 pre_tool_call
  • 做 guardrail
  • 文件修改前 checkpoint
  • destructive terminal command 前 checkpoint
  • 多个 tool call 可以并发执行
  • 工具执行中可以回调 UI 显示状态
  • 执行结果会被预算控制
  • 最后 append 成 tool message 回 conversation

也就是说,Hermes 的 tool call 不是普通函数调用,而是一个 事务化的 agent action

这个 action 需要考虑:

  • 能不能执行
  • 怎么展示给用户
  • 怎么被中断
  • 怎么被审计
  • 失败怎么反馈
  • 输出太大怎么办
  • 多工具并发时顺序怎么保持
  • 插件能不能阻止或改写结果

这就是为什么代码看起来厚。它厚不是因为"调用函数复杂",而是因为它在把 LLM 的不稳定输出包进一个相对可靠的执行环境。

第四层:有些工具是假 registry 工具,真 agent 工具

这个点很容易误解。

model_tools.py 里有:

python 复制代码
_AGENT_LOOP_TOOLS = {"todo", "memory", "session_search", "delegate_task"}

这些工具虽然也注册 schema,让模型能看见,但执行时不能普通 dispatch。因为它们需要 agent 内部状态:

  • todo 需要当前 agent 的 todo store
  • memory 需要 memory store / memory manager
  • session_search 需要当前 session DB
  • delegate_task 需要当前 agent 的 delegation runtime

所以它们是"对模型暴露为工具",但真实执行属于 agent loop。

这揭示了另一个内核:
工具系统不是纯函数系统,它承载 agent 状态。

普通工具像 web_search,输入 query 输出 JSON。

agent-level 工具像 delegate_task,会影响运行时结构,甚至生成子 agent。

所以 Hermes 的 tools 里混合了两类能力:

text 复制代码
纯外部能力:web/file/browser/terminal/tts
运行时能力:memory/todo/delegation/session_search

前者是"做事"。

后者是"改变 agent 自己如何做事"。

第五层:结果不是直接塞回上下文

LLM 工具系统最容易炸的地方,是工具返回太多内容。

Hermes 在 tools/tool_result_storage.py 做了一个很重要的设计:大结果持久化。

逻辑是:

text 复制代码
工具返回内容
  ↓
如果小,直接进上下文
  ↓
如果大,写入 sandbox 文件
  ↓
上下文里只放 preview + 文件路径
  ↓
模型需要细节时再 read_file 分页读取

这其实是给 LLM 做"外部记忆分页"。否则一次 search_filesterminalweb_extract 就可能把上下文塞爆。

所以 tool result storage 不是优化项,而是 Hermes 能持续长任务的基础设施。

第六层:插件/MCP 是同一套能力模型的外延

插件的 ctx.register_tool() 最终还是进 registry:hermes_cli/plugins.py。

MCP 工具也是转成 registry entry:tools/mcp_tool.py。

这说明 Hermes 没有给插件/MCP 另起一套调用机制,而是把它们全部归一到:

text 复制代码
name + toolset + schema + handler + check_fn

这很好,因为 agent runtime 不需要关心工具来自哪里。

坏处是:registry 成了一个非常关键的全局能力总线。它必须处理:

  • 名字冲突
  • toolset alias
  • MCP 动态刷新
  • 插件覆盖内置工具
  • check_fn 缓存
  • schema 兼容性

所以 tools/registry.py 的 ToolRegistry 不是普通 map,而是 Hermes 的"能力注册中心"。

如果用一句话概括

Hermes tools 的深层内核是:

把 LLM 的文本决策,约束成一个动态、可配置、可审计、可中断、可扩展的行动系统。

不是"模型调用 Python 函数"。

而是"模型在一个被 Hermes 精心裁剪过的行动空间里选择动作,然后 Hermes runtime 负责把动作安全地落地"。

你读代码时可以这样分层看

不要从 browser_tool.py 这种大文件开始读,会被淹没。建议按这个顺序:

  1. 看注册协议:
    tools/registry.py

  2. 看暴露策略:
    toolsets.py

  3. 看 schema 生成:
    model_tools.py

  4. 看 tool call 执行:
    agent/agent_runtime_helpers.py

  5. 看并发和结果处理:
    agent/tool_executor.py

  6. 最后才看具体工具,比如:
    tools/file_tools.py
    tools/terminal_tool.py

你可以把它想成一个操作系统的小内核:

text 复制代码
registry      = 设备表
toolsets      = 权限/能力配置
schema        = 系统调用 ABI
tool call     = 用户态请求
executor      = 内核调度器
handler       = 驱动实现
result store  = I/O 缓冲和分页
plugins/MCP   = 外接设备

这个类比比"工具函数列表"更接近 Hermes tools 的真实结构。

相关推荐
装不满的克莱因瓶1 分钟前
了解 LangChain 中的 LLM 与 ChatModel 的差异
人工智能·python·ai·langchain·llm·agent·chatmodel
dingzd954 分钟前
跨境社媒运营越到后面 越比拼账号的表达稳定性
大数据·人工智能·矩阵·内容营销
云烟成雨TD6 分钟前
Spring AI 1.x 系列【54】Retry 机制分析
java·人工智能·spring
没事别瞎琢磨8 分钟前
八、环境隔离——构建安全的子进程环境
人工智能·node.js
手写码匠9 分钟前
从零实现 Prompt 工程引擎:结构化提示、自动优化与多轮自省体系
人工智能·深度学习·算法·aigc
甲维斯11 分钟前
Claude Fable5首测,GPT5.5和国产模型弱爆了!
人工智能
颜酱18 分钟前
LangChain 工具调用:从原理、入门到落地
langchain·llm
swipe19 分钟前
做多轮对话 Agent,为什么我建议把短期记忆放到 Redis
后端·面试·llm
2301_8185277819 分钟前
瑜伽服面料科技——AI加速创新材料研发
人工智能
键盘侠伍十七21 分钟前
Gandalf Lakera AI Prompt Injection 靶场深度教程:从 Level 1 到 Level 8 全面攻防解析
人工智能·prompt·ai安全