Hermes Agent 运行时:AIAgent、模型协议与工具执行

前言:入口把消息交给 Python 以后,真正决定 Agent 行为的是运行时:AIAgent 保存什么,Provider Transport 负责什么,Tool Registry 为什么不仅是函数表,以及大量工具 Schema 怎样避免撑爆上下文。本文只沿着一轮模型—工具循环向下追,不再讨论界面细节。


系列导航:这组文章按“总览 → 入口 → 运行时 → 扩展 → 状态 → 协作 → 安全”的顺序展开;第一次阅读可以从总览开始,带着具体问题也可以直接跳到专题。

  1. 01|Hermes Agent 架构总览:从 Agent Loop 到智能体平台
  2. 02|Hermes Desktop 架构:Electron 如何连接本地 Python Agent
  3. 03|Hermes Agent 运行时:AIAgent、模型协议与工具执行
  4. 04|Hermes Agent 扩展体系:Skill、Plugin 与 MCP
  5. 05|实战:同一个 Markdown 检查器的 Skill、Plugin 与 MCP 三种实现
  6. 06|Hermes Agent 的持续学习系统:Memory、Review、Skill 与 Curator
  7. 07|Hermes Agent 的长会话架构:Prompt Cache、压缩与 SessionDB
  8. 08|Hermes Agent 的多 Agent 与自动化:从 Goal 到 Kanban
  9. 09|Hermes Agent 安全架构:审批、沙箱、回滚与凭据隔离

Hermes 自带哪些工具

Hermes 当前源码的核心工具并不少,但不能把源码里的名称直接理解为“每次对话都会把全部 Schema 发给模型”。Toolset、Profile、Provider 配置、依赖探测、check_fn 和会话来源会逐层过滤;Plugin 与 MCP 还会在此基础上动态增减工具。下面列的是当前基线里的能力族,而不是一个永远不变的数量承诺。

能力族代表工具用途与启用说明
Webweb_searchweb_extract搜索与提取网页正文;依赖相应服务配置
终端与进程terminalprocess执行命令、管理长进程;落点由 Environment Backend 决定
文件与代码修改read_filewrite_filepatchsearch_files读取、搜索和修改工作区文件
浏览器browser_navigatebrowser_snapshotbrowser_clickbrowser_typebrowser_cdp页面导航、DOM/无障碍快照、交互、视觉与 CDP 控制
视觉与媒体vision_analyzeimage_generate、视频生成与查询、text_to_speech需要对应模型或服务 Provider
Skillskills_listskill_viewskill_manage发现、按需读取和管理操作知识
Agent 协作execute_codedelegate_taskclarifytodo程序化编排、子任务委派、澄清与计划维护
状态与自动化memorysession_searchcronjob跨会话记忆、历史搜索和定时任务
桌面专属read_terminalopen_previewfocus_panereact_to_messagesetup_mcp只向 Desktop 来源会话开放,通过反向事件桥执行
项目与可选集成project_listproject_createproject_switch,以及 Home Assistant、看板、Computer Use按会话表面、依赖和配置条件注册

工具最终在哪里执行

工具名相同,不代表执行机器相同。Terminal Environment 抽象目前可以落到 local、Docker、Singularity、Modal、Daytona、Vercel Sandbox 或 SSH。Desktop 选择远程模式后,Terminal、文件和代码工具应在远端项目环境工作;只有窗口、剪贴板、通知、Pane 状态这类 UI 能力留在本机。

能力本地模式SSH / Cloud 模式边界依据
模型与 Agent Loop本地 Python Backend远端 Backend当前 Gateway 连接
Terminal / 文件 / 普通 Python本机工作区远端工作区或沙箱Environment Backend
Plugin / MCP ServerBackend 所在机器或其配置目标远端 Backend 所在侧Profile 与进程生命周期
窗口 / Pane / 剪贴板 / 通知桌面 Renderer 与 Electron仍在用户桌面Desktop 反向事件桥

因此工具系统应该按三层理解:Registry 决定“有哪些能力”,会话过滤决定“模型本轮看见什么”,Environment 与 Surface 决定“调用最终在哪里发生”。Skill 提供怎么做的知识,Plugin 可以注册新的本地代码与生命周期扩展,MCP 把外部服务映射成动态工具;它们最后仍汇入同一套工具选择、审批、执行和结果回填流程。

AIAgent:会话运行时的组合根

run_agent.py 里的 AIAgent 仍然是最显眼的对象。它保存模型客户端、工具快照、上下文引擎、记忆管理器、SessionDB、迭代预算、回调、中断状态和一堆会话级计数。刚开始读时,很容易把所有行为都归到这个类上。

但当前代码已经在持续拆分:构造过程转发给 agent/agent_init.py,主循环转发给 agent/conversation_loop.py,工具批处理在 agent/tool_executor.py,上下文压缩和 Turn Finalizer 也有独立模块。所以我更愿意称它为组合根和会话级运行时容器

一轮请求到底怎么跑

sequenceDiagram
    participant U as 用户入口
    participant A as 智能体运行时
AIAgent participant L as 对话循环
Conversation Loop participant P as 模型提供方传输层
Provider Transport participant T as 工具执行管线
Tool Pipeline participant S as 会话数据库
SessionDB U->>A: 运行对话
run_conversation(message) A->>S: 获取会话租约并读取历史 A->>L: 进入本轮循环 L->>L: 恢复或构建冻结 Prompt L->>P: 转换并发送统一消息 P-->>L: 归一化响应 alt 模型返回工具调用 L->>T: 校验、审批、执行 T-->>L: 工具结果 L->>L: 追加消息并继续循环 else 模型返回最终文本 L->>S: 持久化消息和用量 L-->>A: 最终响应
final_response A-->>U: 展示结果 end

内部消息统一成 OpenAI 风格的 rolecontenttool_calls 字典。不同模型协议在边界上做转换,Conversation Loop 尽量不直接处理每家供应商的原始结构。

循环还维护一组很“死板”但非常必要的不变量:用户、助手和工具消息必须合法交替;工具调用和工具结果不能变成孤儿;临时展示字段、数据库行号和内部标记不能发给严格的模型 API;中断产生的半截消息也不能污染后续历史。模型服务对历史结构比人严格得多,少一个字段或多一个连续的 user message,都可能换来一次很难看懂的 400。

Provider Transport:把模型协议差异挡在边界上

多模型系统最容易长成一片 if provider == ...。Hermes Agent 用 ProviderTransport 把协议数据路径抽了出来,核心接口包括:

  • convert_messages():把内部消息转换成供应商格式;
  • convert_tools():转换工具 Schema;
  • build_kwargs():拼出最终 API 参数;
  • normalize_response():把响应归一化回内部对象。

当前代码里能看到 Chat Completions、Responses、Anthropic Messages 和 Bedrock Converse 等 Transport。它们处理工具格式、停止原因、缓存统计和响应字段差异;客户端生命周期、凭据刷新、流式中断和模型回退仍由 Agent 运行时统一管理。

这个边界我很喜欢,因为它没有走两个极端:Transport 如果太薄,协议判断会重新泄漏到主循环;如果太厚,每个 Transport 又会复制一套凭据、重试和中断逻辑。现在的划分大致是“协议格式归 Transport,执行生命周期归 Agent”。

模型供应商本身还有另一套 ProviderProfile 注册机制,用来声明名称、别名、地址、凭据字段、默认模型和供应商特例。这样,“支持一个新的供应商目录”和“支持一种新的线协议”成了两个不同维度,不需要绑死在一起。

Tool Registry:一个小型应用服务器

工具系统主要由 tools/registry.pymodel_tools.pytoolsets.py 组成。一个工具注册时不只交出 handler,还要声明名称、toolset、JSON Schema、可用性检查、环境依赖、异步标记、结果大小限制,以及可选的动态 Schema 覆盖。

工具为什么还要分 Toolset

模型每轮请求都要看到工具 Schema,所以“注册了”不等于“每个会话都应该暴露”。Toolset 把 file、terminal、browser、memory、web 等能力组合起来,再根据入口、平台、Profile 和用户配置求出本会话的工具快照。

  • 成本:Schema 越多,每轮固定输入越大;
  • 安全:公开 Webhook 不该拿到本地终端的完整能力;
  • 可达性:只有带图形界面的会话,才应该看到需要界面响应的工具。

换句话说,能力属于 Session,不属于 Process。一个后端进程可能同时服务桌面客户端、终端和远程消息会话,不能靠一个全局环境变量推断所有会话都有哪些能力。

Tool Search:工具已经获准,不等于完整 Schema 必须常驻

当一个会话同时连接多个 MCP Server 和 Plugin 时,即使一次都不用,它们的 JSON Schema 也会在每轮占用上下文。当前源码的 tools/tool_search.py 因此在 Toolset 快照之后又加了一层渐进披露:Hermes 核心工具始终直接可见;只有 MCP 工具与非核心 Plugin 工具可以延迟。默认 tools.tool_search.enabled: auto,只要存在可延迟工具就启用桥接。

flowchart LR
    Snapshot[本会话 Toolset 快照] --> Split{工具来源}
    Split -->|Hermes 核心
Hermes Core| Eager[完整 Schema 直接进入请求] Split -->|MCP / 非核心 Plugin| Catalog[构建本轮临时 Catalog] Catalog --> Budget{目录是否装得下预算} Budget -->|可以| Tier1[Tier 1:Bridge + 分组名称清单] Budget -->|过大| Tier2[Tier 2:Bridge + Server 摘要] Tier1 --> Discover[工具搜索与描述
tool_search / tool_describe] Tier2 --> Discover Discover --> Call[工具调用
tool_call] Call --> Unwrap[解包为真实工具名] Unwrap --> Pipeline[统一 Hook / Approval / Dispatch 管线]

模型实际只常驻看到 tool_searchtool_describetool_call 三个桥接 Schema。Tier 1 会在预算内附上按 Server 分组的名称与短描述,模型看到精确名称时可以直接 describe;目录再大就退化到 Tier 2,只保留每个 Server 的工具数量与领域线索。目录预算取“模型上下文百分比”和绝对上限的较小值,每次组装请求都从当前工具定义重建,所以会话中增删 MCP 不会留下失真的旧 Catalog。

搜索使用工具名、描述和参数名上的 BM25,并带名称子串兜底。更重要的是,tool_call 不是一条绕过策略的暗门:它先验证该工具仍在本会话获准的可延迟集合里,缺少必填参数时直接返回参数 Schema 让模型修复;真正执行前再解包成原工具名,因此 Hook、Guardrail、审批、活动日志和结果限制看到的仍是真实工具。它节省的是“常驻 Schema”,不是削弱能力边界。

一次工具调用经过的管线

flowchart LR
    Call[模型工具调用] --> Coerce[参数修正与作用域检查]
    Coerce --> Middleware[请求中间件]
    Middleware --> Hook[工具调用前钩子
pre_tool_call Hook] Hook --> Approval{需要人工审批?} Approval -->|拒绝| Block[统一错误结果] Approval -->|通过| Dispatch[注册表分派
Registry Dispatch] Dispatch --> Result[结果规范化] Result --> Post[post Hook 与结果变换] Post --> History[写回 Tool Message]

所以 Tool Registry 远不只是“函数名到函数”的字典。参数契约、作用域、审批、插件决策、错误封装和审计都集中在这里。handler 正常只返回字符串,唯一的结构化例外是受支持的多模态 envelope,这让上层日志、预算和持久化不必猜工具会返回什么类型。

工具还可以声明 check_fn。如果依赖、凭据或外部服务不可用,它就不出现在模型 Schema 里。当前实现会短时缓存检查结果,并保留最近一次成功判断来吸收瞬时抖动。我以前容易把这理解成简单的功能开关,源码读下来才发现它还在处理探测成本和服务抖动。

参考资料与源码入口

题图来源:《瀑布、池塘与自然》,创作者 dimitrisvetsikas1969,来自 Pixabay;依据 Pixabay Content License(Pixabay 内容许可)使用。

下一篇:Hermes Agent 扩展体系:Skill、Plugin 与 MCP

Prometheus + Grafana 构建监控平台 Jenkins + CICD流水线构建指南 AI混合云架构设计
View Comments