工具搜索
当一个会话挂载了许多 MCP 服务器或非核心插件工具时,它们的 JSON schema 在每个回合都会消耗相当一部分上下文窗口——即便其中只有少数与用户实际问的问题相关。
工具搜索(Tool Search) 是 Hermes 为该问题提供的可选渐进式披露层。激活后,MCP 和插件工具在模型可见的 tools 数组中被三个桥接工具替代,模型按需加载每个具体工具的 schema。
Hermes 默认直接加载其工作集核心工具(terminal、read_file、write_file、patch、search_files、todo、memory、browser_*、web_search、web_extract、clarify、execute_code、delegate_task 以及 _HERMES_CORE_TOOLS 的其余部分)。冷启动、事件触发的内置工具在被列入 tools.tool_search.defer 时可被延迟;随附的精选列表覆盖 computer_use、session_search 和部分桌面助手等工具。MCP 和非核心插件工具自动符合条件。显式 defer 列表会替换精选列表,而 defer: [] 使每个工具都保持热切。
工作原理
当工具搜索为某回合激活时,模型看到三个新工具替代被延迟的工具:
tool_search(queries, limit?) 搜索延迟工具目录(一个或多个查询)
tool_describe(names) 加载一个或多个工具的完整 schema
tool_call(calls) 调用延迟工具;`calls` 是 {name, arguments} 数组
calls 每次调用占一项;单个本地调用是长度为 1 的数组。只有 connectors__ 名称可批量合并;混合批次和多本地批次会被拒绝。
典型交互如下:
Model: tool_search(["create a github issue", "send a slack message"])
→ { results: [ { query: "create a github issue",
matches: ["mcp_github_create_issue", ...] },
{ query: "send a slack message",
matches: ["mcp_slack_post_message", ...] } ],
tools: { mcp_github_create_issue: { description: "...",
required: ["title"], ... },
mcp_slack_post_message: { ... } } }
Model: tool_describe(["mcp_github_create_issue", "mcp_slack_post_message"])
→ { tools: { mcp_github_create_issue: { parameters: { ... } },
mcp_slack_post_message: { parameters: { ... } } } }
Model: tool_call({ calls: [{ name: "mcp_github_create_issue",
arguments: { title: "...", body: "..." } }] })
→ { ok: true, issue_number: 42 }
tool_search 调用中的每个查询都独立针对同一目录搜索(limit 按查询生效);按查询的分组只携带工具名,而共享的 tools 映射一次性保存每个命中工具的描述和必需参数名。查询会做词干提取,因此 "issues" 能找到 create_issue。返回无命中的查询分组会附带一个 available_sources 摘要,列出已连接服务器,使词面未命中不会被误判为能力缺失。tool_describe 在一次调用中解析所有请求的名称;未知名称报告在 not_found 中,而不使批次其余部分失败。
当模型调用 tool_call 时,Hermes 解开桥接,按模型直接调用该底层工具的方式派发它。工具调用前钩子、护栏、审批提示和工具调用后钩子全部针对真实工具名运行——而非针对 tool_call。CLI 和网关中的活动流同样解开,让你看到底层工具,而非桥接。
何时激活?
工具搜索采用分层披露:只要存在任何延迟工具(MCP/插件或显式命名的内置工具),桥接就激活;随目录规模缩放的是目录有多少保持可见,而非 schema 是否延迟。
| 层 | 条件 | 模型看到什么 |
|---|---|---|
| 0 | 无延迟工具 | 所有工具热切,无桥接。直通。 |
| 1 | 延迟目录的列示能放进预算 | 桥接 + 每份延迟工具的 skill 风格清单(名称 + 简短描述,超预算时降级为仅名称)。降级按服务器进行:当一个超大服务器(Cloudflare)与小服务器(Linear)并列时,小服务器保留逐工具列示,只有超大服务器坍缩为一行摘要。 |
| 2 | 逐工具列示即使仅名称也超出所有服务器预算(例如 Cloudflare 平铺 API 表面:约 3,300 个工具,名称约 32K token) | 裸桥接 + 每服务器一行摘要(服务器名 + 工具数),使模型知道哪些域可达;单个工具只能通过 tool_search 发现。 |
列示预算为 min(threshold_pct% 上下文, listing_max_tokens)。每次构建 tools 数组时都会重新判定,因此会话中途增删 MCP 服务器会在下一次组装时把会话在层之间移动。
配置
tools:
tool_search:
enabled: auto # auto(默认)、on 或 off
threshold_pct: 5 # 列示预算占上下文的百分比
search_default_limit: 5
max_search_limit: 25
listing: auto # 内嵌分组的 名称+描述 目录清单
listing_max_tokens: 4000
defer: # 替换精选默认;[] 使每个工具都热切
- computer_use
- session_search
- image_generate
- todo_list
- process_manage
- cronjob_manage
默认 defer 列表还包含 hermes_cli/config_defaults.py 中列出的精选桌面 GUI 助手。它是随附精选集合的唯一事实来源;运行时回退使用同一值。
| 键 | 默认 | 含义 |
|---|---|---|
enabled | auto | auto/on 在存在至少一个延迟工具时激活;off 完全禁用(一切保持热切)。auto 目前是 on 的别名——它预留给未来一种模式:schema 适合上下文时内联、不适合时才延迟。若你想在升级间保证今天的行为,请固定 on 或 off。 |
threshold_pct | 5 | 列示预算占活动模型上下文长度的百分比。范围 0–100。 |
search_default_limit | 5 | 模型不带 limit 调用 tool_search 时每查询返回的命中数。 |
max_search_limit | 25 | 模型通过 limit 可请求的硬上限(按查询)。范围 1–50。 |
listing | auto | 在 tool_search 桥接描述中内嵌每份延迟工具的 skill 风格清单(名称 + 描述首句,≤60 字符,按 MCP 服务器分组)。auto 在预算允许时纳入(回退到仅名称,再到层 2 的服务器摘要);on/off 强制其一。 |
listing_max_tokens | 4000 | 内嵌列示的绝对上限,与上下文大小无关。范围 200–60000。大目录降级为仅名称或逐服务器摘要,完整 schema 仍可通过搜索获得。 |
defer | 精选列表 | 默认被桥接替换的工具名。列表可含冷内置工具及 MCP/插件工具;显式列表替换它,[] 对每个工具禁用延迟。 |
按调用的数组上限是内部安全边界,非配置。超上限调用返回错误,使模型可用更小批次重试。
列示为何存在
没有它,延迟能力是不可见的——在线基准显示,模型会替换为可见的核心工具(在终端跑 gh 而非搜索延迟的 GitHub 工具),或宣称能力不存在,而不调用 tool_search。列示把 skill 模式应用到工具上:每个能力始终可按名称发现,而完整参数 schema 保持延迟。若模型在列示中看到确切工具名,它可跳过 tool_search 直入 tool_describe,省一次往返。
你也可以切换旧的布尔形态:
tools:
tool_search: true # 等价于 {enabled: auto}
连接器(远程工具)
当你登录 Nous Portal 时,桥接还额外触及连接器——由托管工具网关提供的远程工具。它们从不在本地注册:tool_search 把每个查询发给网关,把网关命中作为文档加入本地目录(标记 source: "connectors",命名为 connectors__<连接器>__<工具>),并用同一 BM25 过程和同一最稀有 token 规则对二者排序,因此 limit 对整组设上限,一个能回答查询的连接器工具绝不会被与之共享一个词的本地工具挤出。网关调用限时 30 秒;缓慢或不可达的网关降级为仅本地结果。tool_describe 从网关获取连接器 schema,tool_call 把批次中的每个连接器条目作为自己的网关请求、按输入顺序发送(网关在其常规 slug 下不认识的工具名会按字面 slug 重试一次,因此一个条目可能花两次请求)。若某连接器同时提供 GMAIL_X 和字面的 X,二者都会组合为 connectors__gmail__X,它运行 GMAIL_X;搜索保留这一对中的前者、丢弃另一个并记录警告。结果按重算后的计数拼回批次原始顺序。
tools:
connectors:
enabled: true # false——绝不触碰连接器路由;桥接
# 表现得与该特性不存在时完全一致
未登录(或网关不为你的账户提供连接器)时,上述一切均不可见:本地搜索表现与本页其余部分描述完全一致,不向模型显示错误。
需要你尚未关联账户的连接器调用会返回 CONNECTION_REQUIRED 错误。manage_connections 工具列出连接器及其连接状态并启动授权:在桌面应用中该调用显示一张卡片,阻塞直到每个应用已连接或被跳过,并报告结果;在别处它为每个应用返回一个连接链接供用户打开。断开账户由用户在 Portal 中完成。同一工具还从目录安装、启用并授权本地 MCP 服务器(mcp: true 的目标),因此无论你是否登录它都存在;只有托管连接器操作需要登录。
桌面后端的账户列表和断开 API 使用 Portal 的账户管理服务,包括其组织成员检查和断开审计。Portal 不可用时不会回退到直接网关账户管理。工具发现、执行和连接状态观察继续通过网关进行;模型工具不能断开账户。
tool_call 接受批次:calls 是 {name, arguments} 条目数组(单个调用是长度为 1 的数组)。批次中的每个连接器条目作为自己的网关请求依次派发;本地延迟工具在每个 tool_call 中仍为一项。命名了本地工具的多条目批次会被拒绝,并给出纠正:用调用方自己的首条目重述有效形态;作为 JSON 字符串发出的 calls 值会按数组形式解析。审批在派发前按条目结算,条目之间的 /stop 使未开始的条目不发送(其槽位报告 INTERRUPTED)。
何时不该用它
工具搜索以固定的每回合 token 成本(三个桥接工具 schema 加目录列示)以及冷工具上至少一次额外往返(describe → call),换取延迟 schema 的节省。在层 1,列示保持每个能力可见,因此发现往返通常消失——模型直入 tool_describe。在线基准显示列示模式与热切加载的任务成功率相当,而成本低于裸桥接。
若你希望小工具集恢复旧的永远热切行为,设 enabled: off。
不会消失的权衡
这些来自提示词缓存完整性不变式——它们是任何渐进式披露设计固有的,并非本实现特有:
- 冷工具上多一次往返。 模型第一次需要延迟工具时,会多花一两次模型调用来找到并加载 schema。静态侧的 token 节省是真实的,但一部分在运行时偿还。
- 延迟 schema 无缓存收益。 加载的
tool_describe结果进入对话历史(因此后续回合确实会被缓存),但它永远享受不到系统提示词缓存前缀。 - 延迟 schema 无提供商原生校验。
tool_describe让模型读取延迟工具的 schema,但提供商仍只看到通用的tool_call.arguments对象。因此 Hermes 在派发前在本地强制转换并校验底层参数;具体工具或 MCP 服务器仍对 Hermes 无法安全校验的 schema(如格式错误的 schema 或外部引用)负责。 - 依赖模型质量。 工具搜索假设模型能为它想要的工具写出合理搜索查询。较小模型做得较差;已公布的 Anthropic 数据(Opus 4 上有 vs. 无工具搜索:49% → 74%)显示了上行空间,但也表明约 26 个百分点的准确率仍是检索失败。
- 工具集编辑使缓存失效。 会话中途增删工具会改变桥接工具的描述(其中含延迟工具计数)和目录,因此提示词缓存失效。这与任何工具集编辑是同一权衡。
实现细节
- 检索: 对分词后的工具名、来源名(工具所属的 MCP 服务器或插件工具集,因此搜索
"linear"能找到该服务器的工具,即便工具自己的名不带服务名)、描述和参数名做 BM25,索引和查询都施加 Snowball 词干提取(英文),使形态变体匹配("issues" 找到create_issue)。一个工具只有在包含查询最稀有 token(出现于最少工具文档中的那个,即命名意图的词:gmail、github、incident,而非send或create)时才作为结果。最稀有 token 不出现在任何工具中的查询返回空分组(含已连接来源和重试提示),而非共享一个常见词的limit个工具。 - 相关性下限: 一个工具必须至少匹配查询中一半的可回答词(目录中任何位置出现的词)才被提供——与长查询共享一个偶然的词不算匹配。搜索不存在的能力时返回无结果,而非一份看似合理、模型会反复改写的列表。下限只从四个可回答词起生效,因此 "list issues" 这类短查询保持全召回,而精确工具名查询始终命中。
- 并行执行解开桥接。 批次规划器在
tool_call的底层工具上决定并发,而非按字面桥接名——因此通过supports_parallel_tool_calls: true选择加入的 MCP 服务器在其工具经桥接调用时保留并发,而tool_search/tool_describe查询像任何只读工具一样并发批量进行。 - 目录跨回合无状态。 它每次组装都从当前工具定义列表重建——没有按会话键控的
Map。这避免了存储目录与活动工具注册表漂移失同步那类 bug。 - 目录作用域为会话的工具集。
tool_search、tool_describe、tool_call只看到并调用会话实际被授予的工具。被限制为工具集子集的子 agent、看板 worker 或网关会话不能用桥接发现或调用子集之外的工具——延迟目录是会话自身启用/禁用工具集中可延迟的切片,而非整个进程注册表。 - 无 JS 沙箱。 Hermes 使用更简单的"结构化工具"模式(search / describe / call 作为普通函数)。某些其他实现提供的 JS 沙箱"代码模式"表面积很大;我们跳过它。
另见
tools/tool_search.py——实现tests/tools/test_tool_search.py——回归测试套件- 原始实现 PR 中的
openclaw-tool-search-reportPDF——塑造设计的研究