第4章:工具系统 —— 让模型有手脚,但每一步都可控
一、工具不是函数,而是一条管道
在最简单的 Agent 里,tool call 似乎只是:解析 JSON → 调 Python 函数 → 把字符串传回模型。Hermes 把它展开成:
发现 → 暴露 schema → 检查可用性 → 解析参数 → 搜索/解包
→ 请求级 middleware → 安全与审批 → checkpoint
→ 执行环境 → 结果裁剪/存储 → 回调与持久化 → tool result
这条管道的目的不是增加仪式感,而是让“模型请求的副作用”拥有多个可插入的保护层。
二、注册表:工具模块如何被发现
tools/registry.py 的文件头已经写明了依赖方向:
registry.py(无 model_tools / 工具模块依赖)
↑ tools/*.py(模块级 register)
↑ model_tools.py(加载 registry 与工具模块)
↑ run_agent.py / cli.py / batch_runner.py
ToolRegistry(约 tools/registry.py:290)维护 ToolEntry、toolset、check function、alias、generation 和 plugin override policy。工具文件在导入时注册 schema、handler、toolset 和可用性检查;运行时不需要再维护一份平行的工具清单。
为什么还要 AST discovery
discover_builtin_tools 先扫描模块顶层的 registry.register(...),再导入模块;扫描结果写入 ~/.hermes/cache/tool_discovery_cache.json,并以 mtime/size 作为缓存键。这样可以:
- 避免为了发现工具而导入所有重依赖;
- 把注册模块从非工具 helper 中区分出来;
- 在长期进程中缓存 discovery;
- 让模块自注册但不需要中央巨型注册表。
三、可用性检查不是静态 boolean
工具可能依赖 Docker daemon、Playwright、SSH、Modal、浏览器、MCP server 或外部凭证。注册表的 check_fn 通过约 30 秒 TTL 缓存,最近一次成功后短时间内的瞬态失败还会保留 last-good,避免一次探针超时就把整套工具从 prompt 中删掉。
这体现一个重要的运行时原则:能力广告要防抖,但不能永久撒谎。真实故障持续超过宽限期后,工具会变为 unavailable,并从当轮 schema 中消失。
四、一次 tool call 的五个安全边界
1. 参数边界
_parse_tool_arguments 将原始 arguments 解析成 dict;坏 JSON 不直接炸掉 Loop,而是产生可序列化的错误 tool result。工具搜索桥接还会先解包底层工具并校验延迟调用参数。
2. 请求级 middleware
在 agent/tool_executor.py:354 附近,_run_agent_tool_execution_middleware 负责把请求级、工具级和 plugin 级逻辑串联起来。它可以修改参数、阻止调用、记录 trace,最后才进入真正的 _invoke_tool。
3. 授权与审批
agent/tool_guardrails.py 将 tool call signature 和 decision 结构化;tools/approval.py、tools/write_approval.py、Gateway authorization mixin 处理需要人确认的路径。并发工具共享 _ConcurrentToolAuthorizationGate,避免多个 worker 同时弹出互相覆盖的审批。
4. Checkpoint
对 write_file、patch 或危险 terminal 命令,_begin_tool_execution 可以先让 checkpoint manager 保存工作区状态。它不是安全策略本身,而是把“允许执行”变成“允许执行且可回滚”。
5. 结果边界
工具结果不能无限进入上下文。tools/tool_output_limits.py、tool_result_storage.py 和 budget_config.py 负责限制字符/Token、把超长结果放到外部存储、或只把摘要/引用返给模型。
五、并行执行:顺序与并发分离
execute_tool_calls_concurrent、execute_tool_calls_sequential 和 execute_tool_calls_segmented 各自适合不同场景:
| 模式 | 适合场景 | 核心保证 |
|---|---|---|
| concurrent | 多个互不依赖的读取/查询 | 结果按原 tool-call 顺序入历史 |
| sequential | 写操作或需要稳定副作用顺序 | 前一步完成后才启动下一步 |
| segmented | 混合批次或需要特殊边界 | 由分段调度器掌管批次收尾 |
并发 worker 在 finally 中清理 worker tid、interrupt bit 和线程状态,这是容易被忽略但非常关键的恢复逻辑:线程池复用线程,如果中断标志泄漏到下一次任务,下一次工具会“无缘无故”被取消。
六、工具错误为什么要伪装成消息
工具执行失败通常不应该让整个 Python 进程崩溃。Hermes 把异常编码成 tool result,让模型获得纠错机会:缺少参数、权限不足、命令退出码非零、远端环境不可用和用户中断都能留在对话轨迹中。
但“变成消息”不等于“吞掉错误”:日志、telemetry、is_error 标记、显示回调和 Gateway 事件仍然保留失败事实。消息是给模型的恢复接口,日志是给人和运维的诊断接口。
七、工具与执行环境再次解耦
tools/environments/base.py:446 的 BaseEnvironment 定义统一的执行环境;实现包括 local、Docker、SSH、Singularity、Modal、Daytona、Vercel Sandbox。工具只调用环境接口,不直接假设命令一定运行在宿主机。
这带来两个收益:
- 同一个
terminal/read_file工具可以被不同 backend 承载; - 安全边界可以把 cwd、文件同步、网络出口、进程和清理集中在 environment 层。
八、工具系统的四个设计结论
- 工具 schema 是模型能力的广告;handler 是实际副作用;两者之间必须有授权层。
check_fn的稳定性直接影响模型上下文,所以要做缓存和瞬态故障宽限。- 并发优化不得改变消息协议顺序,也不得跳过审批、checkpoint 和中断清理。
- 工具结果是上下文资源;结果预算应在执行后立刻处理,而不是等窗口爆满。