有一种工具链故障很烦:你问模型“能不能用 terminal / write_file / memory”,它不是调用失败,而是压根不知道这些工具存在。
NousResearch/hermes-agent issue #22573 记录的就是这种情况。用户在 Hermes v0.13.0 里发现:
Telegram 和 CLI session 里,native tools 全部消失。
只剩 MCP tools。
更诡异的是,单独跑 Python 测试时,工具注册链路一切正常;真正进入 Gateway / CLI live runtime 后,工具列表就被削成了 MCP-only。
这篇文章想拆的不是“怎么重启服务”,而是一个更隐蔽的配置陷阱:**一个 hermes-* 平台工具集被写进 agent.disabled_toolsets 后,为什么会把所有 native tools 一锅端。**
现象:模型不是不会用 terminal,而是根本看不到 terminal
issue 里的现场非常具体。
用户安装 Hermes v0.13.0,并配置了 MCP servers:
obsidian + gdrive + notion + tavily
这些 MCP server 合起来提供 42 个工具。
理论上,session 里还应该有 Hermes native tools,例如:
terminal
write_file
read_file
memory
send_message
web_search
cronjob
预期工具数量大致是:
28 native tools + 42 MCP tools = 70 tools 左右
但 live session 文件里看到的是:
Tool count: 42
Has terminal: False
Platform: telegram
也就是说,模型调用层只收到了 MCP 工具定义;Hermes 自己的 native tools 没有进入 session。
这类问题很容易被误判成模型问题。实际上模型能不能调用工具,第一步取决于 runtime 传给模型的 tool schemas。如果 terminal 根本没在 schemas 里,换模型也不会 magically fix。
最容易误导人的线索:standalone 测试全是好的
这个 issue 有意思的地方在于:单独测每一段,结果都正常。
例如:
from tools.registry import registry, discover_builtin_tools
discover_builtin_tools()
print('terminal' in registry.get_all_tool_names())
结果显示 native tools 注册正常。
再测平台工具集解析:
from hermes_cli.tools_config import _get_platform_tools
result = _get_platform_tools(config, 'telegram')
也能解析出 terminal、file、memory、web、messaging 等工具集。
再测:
from model_tools import get_tool_definitions
result = get_tool_definitions(['hermes-telegram'], [], quiet_mode=True)
同样能返回 native tools。
这就是最难排的点:
孤立链路正确,live runtime 错误。
当你只看 registry、只看 config、只看 get_tool_definitions() 的干净调用,很容易得出“代码没问题,是环境坏了”的结论。
但 live runtime 里的输入参数,才是真正决定 session 工具列表的东西。
关键线索:agent.disabled_toolsets 里混进了 hermes-yuanbao
后续评论里,用户找到了真正触发点。
原始配置里有这样一段:
agent:
disabled_toolsets:
- discord
- discord_admin
- homeassistant
- moa
- rl
- hermes-yuanbao
- spotify
- browser-cdp
问题就藏在这一行:
- hermes-yuanbao
它看起来像“禁用 Yuanbao 平台相关工具”,但在 Hermes 的工具系统里,hermes-* 不是普通工具分类,而是平台 composite toolset。
这些平台 composite toolset 会展开到一组核心 native tools。
换句话说:
hermes-yuanbao
不是一个小工具。
它解析出来可能是一整套 _HERMES_CORE_TOOLS。
于是,_compute_tool_definitions() 处理 disabled toolsets 时发生了连锁反应。
一行配置如何把所有 native tools 删掉
可以把过程简化成这样:
1. enabled_toolsets 里包含 telegram 需要的 native tools
2. disabled_toolsets 里出现 hermes-yuanbao
3. resolve_toolset("hermes-yuanbao") 展开为 _HERMES_CORE_TOOLS
4. tools_to_include.difference_update(resolved)
5. 所有 native tools 被移除
6. MCP tools 不在 _HERMES_CORE_TOOLS 里,所以留下
这就解释了为什么最终 live session 里只剩 42 个 MCP tools。
不是 MCP 特别坚强,而是它们的命名和来源不在这次 difference_update 的打击范围内。
用 issue 里的模拟结果表达,就是:
disabled_with_bug = [..., 'hermes-yuanbao', ...]
_compute_tool_definitions(...)
-> count=42, has_terminal=False
移除 hermes-yuanbao 后:
disabled_fixed = [...]
_compute_tool_definitions(...)
-> has_terminal=True
这就把“只有 MCP tools load”的谜底串起来了。
为什么换模型没有用?
issue 里用户测试过多个模型或 provider,包括 Claude、Kimi、Gemini 等,现象不变。
这是符合预期的。
因为问题发生在模型调用前:
Hermes runtime 组装 tool schemas
↓
native tools 已经被 disabled_toolsets 过滤掉
↓
provider 收到的工具列表只有 MCP tools
↓
模型自然无法调用 terminal/write_file/memory
如果工具 schema 没传进去,模型不可能调用一个它看不见的函数。
所以这类问题排查顺序应该是:
session JSON / tool schemas
→ enabled_toolsets / disabled_toolsets
→ toolset resolve 结果
→ provider 请求参数
→ 模型行为
不要一开始就纠结模型“会不会用工具”。
两个真正该修的点
从 issue 评论看,这个问题不是单点误操作,而是两个设计边界叠在一起。
第一,hermes tools TUI 可能把平台 composite toolset 写进:
agent.disabled_toolsets
但这个字段更适合放普通工具分类,而不是 hermes-* 这种平台级 composite。
第二,_compute_tool_definitions() 在处理 disabled set 时,没有防御 hermes-* 名称。
一旦平台 composite 被当成 disabled toolset 解析,就会展开成核心工具全集,然后把 native tools 全部扣掉。
一个防御性修法是:
if toolset_name.startswith("hermes-"):
logger.warning(
"Skipping platform toolset '%s' in disabled_toolsets",
toolset_name,
)
continue
更完整的修法则应该让配置写入层区分:
平台开关
工具分类开关
具体工具开关
不要把平台 composite 和工具分类混在同一个 disabled list 里。
排查时可以直接看这三类证据
遇到 “Hermes terminal missing / only MCP tools load / native tools absent” 时,不要只问模型。
先看 session 里实际工具数量:
import json, glob
latest = sorted(glob.glob('~/.hermes/sessions/session_*.json'))[-1]
data = json.load(open(latest))
tools = data.get('tools', [])
print('Tool count:', len(tools))
print('Has terminal:', any(t['function']['name'] == 'terminal' for t in tools))
print('Has write_file:', any(t['function']['name'] == 'write_file' for t in tools))
再看配置里有没有危险项:
agent:
disabled_toolsets:
- hermes-xxx
只要 disabled_toolsets 里出现 hermes-*,就要高度怀疑它会展开成平台核心工具集。
最后确认 live runtime 读的是哪份配置、哪份代码:
from hermes_cli.config import get_config_path, load_config
import run_agent, model_tools, sys
print(get_config_path())
print(load_config().get('agent', {}).get('disabled_toolsets'))
print(run_agent.__file__)
print(model_tools.__file__)
print(sys.executable)
这一步能排除“我改了 A 配置,但 gateway 读的是 B 配置”的常见坑。
和 DeepAI API 中转站一起部署时,应该怎么分层看?
很多 Hermes 用户会把工具系统、Gateway、模型 Provider、API 中转站放在同一条链路里:
Telegram / CLI
→ Hermes Gateway / runtime
→ tool schemas
→ model provider
→ OpenAI-compatible API
如果你使用 DeepAI API 中转站,建议在模型调用层重点观察:
- Base URL 是否指向
https://api.deepai.wang/v1; - API Key 是否正确;
- model id 是否存在;
- provider 返回的错误码;
- latency、usage、限流和账单统计。
但像 terminal、write_file、memory 这类工具是否进入 session,优先看 Hermes runtime 生成的 tool schemas。
一个实用判断是:
工具 schema 里没有 terminal → 先查 Hermes toolsets/config
工具 schema 里有 terminal,但模型拒绝/不会调用 → 再查模型能力和 prompt
provider 请求失败 → 再查 API Base URL、Key、模型名、网络和额度
这样分层,问题会清楚很多。
小结:disabled list 不是垃圾桶
#22573 的教训很直接:
不要把平台 composite toolset 当成普通工具分类塞进 disabled_toolsets。
hermes-yuanbao 这种名字看起来只是一个平台,但它解析后可能覆盖一整套 _HERMES_CORE_TOOLS。当 _compute_tool_definitions() 对它做差集时,native tools 会被成批移除。
最终表现就是:
MCP tools 还在,terminal / write_file / memory 全没了。
这类问题的排查关键,不是换模型,也不是盲目重装,而是看 live session 最终拿到了哪些 tool schemas,再倒推 enabled/disabled toolsets 的解析过程。
工具链越复杂,配置字段的语义边界越重要。一个看似合理的 disabled 项,可能就是整条工具链断掉的地方。