跳到主要内容《Agent Runtime 工程化》第十二章 hermes-agent 产品级实战 | 极客日志编程语言Agent Runtime工程化陈堂会开源二开
《Agent Runtime 工程化》第十二章 hermes-agent 产品级实战
第十二章 hermesagent 产品级实战 前面十一章讲的是方法。最后这一章,我们把方法放进一个真实开源项目里。 本章选择 NousResearch/hermesagent。原因不是它“功能多”这么简单,而是它已经把 Agent Runtime 从一个工具调用循环扩展成了产品系统:CLI/TUI、Desktop、messaging gateway、多 pr
站点编辑3 浏览

书名:《Agent Runtime 工程化:从工具调用循环到可恢复执行系统》
作者:陈堂会
平台:极客日志(https://zeeklog.com)
X:@Megick_com
联系邮箱:[email protected]
章节:第十二章 hermes-agent 产品级实战
第十二章 hermes-agent 产品级实战
前面十一章讲的是方法。最后这一章,我们把方法放进一个真实开源项目里。
本章选择 NousResearch/hermes-agent。原因不是它'功能多'这么简单,而是它已经把 Agent Runtime 从一个工具调用循环扩展成了产品系统:CLI/TUI、Desktop、messaging gateway、多 provider、MCP、工具注册、上下文压缩、skills、memory、cron、checkpoint、monitoring 都在一个项目里并存。读这样的项目,最容易看见 Runtime 工程化的真实样子。
本章基于截至 2026 年 8 月 14 日公开仓库 NousResearch/hermes-agent 的 main 分支,研究 commit 为 c69231270432914be78de4641651806f30654109。以下分析只基于 README、公开源码和项目文件结构,不推断未公开实现。

图 12-1:hermes-agent 的产品级 runtime 运行面示意。真实 Agent 产品不是一个 loop,而是入口、provider、工具、上下文、记忆、技能、权限、checkpoint 和观测系统围绕 loop 组成的运行面;具体模块名以源码文件为准。
12.1 读这种项目,先别急着找'主循环'
面对一个成熟 Agent 项目,新手最容易犯的错,是打开仓库就搜索 while、tool_call、chat.completions。这当然能找到东西,但很快会被分支、兼容层、回调、配置和历史包袱淹没。
更稳的读法是先画源码地图。你要先回答七个问题:
| 问题 | 在 hermes-agent 中的线索 |
|---|
| 用户从哪里进入 | cli.py、hermes_cli/*、gateway/*、apps/desktop/* |
| Agent 对象在哪里 | run_agent.py 的 |
AIAgent
| turn prologue 在哪里 | agent/turn_context.py |
| 主循环在哪里 | agent/conversation_loop.py |
| 工具怎样注册和执行 | tools/registry.py、model_tools.py、agent/tool_executor.py |
| 上下文怎样管理 | agent/context_engine.py、agent/context_compressor.py |
| 状态和恢复在哪里 | tools/checkpoint_manager.py、session DB、gateway session |
这个顺序比直接追模型调用更有效。真实项目里,模型调用通常只是中间一段;前面有上下文、记忆、权限、平台来源,后面有工具执行、持久化、压缩、回调、监控。只看模型调用,会漏掉 runtime 的大部分责任。
12.2 AIAgent 是门面,不是全部实现
run_agent.py 里保留了 AIAgent 主类。构造函数参数很长,这反而是一份 runtime 边界清单:provider、api mode、toolsets、callbacks、platform、session、memory、context files、checkpoint、fallback model、credential pool 都从这里进入。
不过,AIAgent 并不是所有实现都塞在一个类里。源码注释显示,许多大块逻辑已经抽到 agent/* 模块,并由 AIAgent 保留 forwarder 以兼容旧调用。
这是一种真实项目常见的演进形态:主类仍是公共门面,内部实现逐步模块化。二开时不要急着'重构主类'。先确认哪些方法只是 forwarder,真正逻辑在哪个模块。
run_agent.AIAgent.run_conversation
↓ forwarder
agent.conversation_loop.run_conversation
AIAgent._execute_tool_calls
↓ 判断 sequential / concurrent / segmented
agent.tool_executor.execute_tool_calls_*
这种结构告诉我们:如果要改工具执行证据、并发策略或结果预算,真正切入点很可能在 agent/tool_executor.py 和 agent/tool_dispatch_helpers.py,而不是在 run_agent.py 顶层硬插逻辑。
12.3 turn prologue 是产品 runtime 的重地
agent/turn_context.py 负责单个 turn 的前置准备。它的源码说明很明确:这里处理 stdio guarding、runtime wiring、retry counter reset、用户消息清洗、系统 prompt、session row、preflight compression、plugin hook、external memory prefetch 和崩溃韧性持久化。
这正好对应本书前面讲的 Context Builder 与 Session State。真实项目里,turn 开始前并不是简单追加一条用户消息。它要回答:
- 当前消息从 CLI 还是 gateway 来;
- 是否需要注入 memory context;
- 是否有 plugin user context;
- 是否要创建 session DB row;
- 是否触发上下文压缩;
- API-bound content 和用户可见 content 是否要分开;
- prompt cache 前缀能否保持稳定。
Hermes 用 api_content sidecar 处理'存储内容'和'发给模型内容'不完全一致的问题。这是非常值得学习的点:用户看到的 transcript 应保持干净,但 provider replay 又需要字节稳定。简单地把 memory 或 gateway note 拼到用户消息正文里,会污染会话;完全不持久化,又会破坏后续 replay。
产品级 runtime 经常就在这种细节里拉开差距。
12.4 工具系统:注册、筛选、调度三层分开
第一,tools/registry.py 是中心注册表。工具文件通过 registry.register() 声明 schema、handler、toolset、availability check 等信息。这个设计比'手写一个工具数组'更适合大项目,因为工具数量多、依赖多、可用性会变。
第二,model_tools.py 是 orchestration layer。它触发工具模块发现,提供 get_tool_definitions() 和 handle_function_call() 等公共 API,并处理 toolset 启用/禁用、同步/异步桥接。
第三,agent/tool_executor.py 负责工具调用执行。源码说明它支持 sequential 和 concurrent dispatch,还会处理分段执行。run_agent.py 中 _execute_tool_calls 的注释提到,segment planner 会把 batch 拆成可并发的连续片段和 sequential barriers。只读、非重叠文件目标、部分 MCP 工具可以并行;交互式、不安全或未识别工具形成顺序屏障。
这正好印证第四章的观点:模型可以提出多个 tool calls,但并行执行必须由 runtime 判断。
12.5 MCP:外部接入和反向暴露同时存在
第一种是接入外部 MCP。tools/mcp_tool.py 负责 MCP Client 支持。源码说明它支持 stdio、HTTP/StreamableHTTP、SSE transport,从 ~/.hermes/config.yaml 的 mcp_servers 读取配置,把外部工具发现并注册进 Hermes 工具系统。它还提到自动重连、环境变量过滤、credential stripping、per-server timeout、后台 event loop、sampling 支持和 per-server 并行工具调用开关。
第二种是把 Hermes 工具反向暴露为 MCP Server。agent/transports/hermes_tools_mcp_server.py 的注释写得很清楚:当 Codex app-server runtime 接管 loop 时,Hermes 的 web search、browser automation、vision、image generation、skills、TTS、kanban 等部分工具会通过 stdio MCP 暴露给 Codex 子进程。它也明确列出了不暴露的工具,例如 terminal、read/write file、patch、delegate_task、memory、session_search、todo 等,并说明原因。
这个设计很适合作为产品 runtime 案例:MCP 不只是'我能接外部工具',还可以成为 runtime 之间交换能力的边界。但边界要谨慎。哪些工具能暴露,哪些工具不能暴露,要看它们是否依赖当前 AIAgent 上下文、是否和宿主 runtime 的工具重复、是否会绕过审批。
12.6 上下文压缩:摘要必须是 reference-only
agent/context_engine.py 定义了可插拔 Context Engine。默认引擎是 compressor,也允许第三方引擎替换。接口包含 update_from_response()、should_compress()、compress(),还有 select_context() 这种每 turn 的上下文选择钩子。
agent/context_compressor.py 的注释非常有工程味。它强调:
- 用辅助模型总结中间 turn;
- 保护 head 和 tail;
- 做 tool output pruning;
- 使用结构化 summary;
- summary 是 reference-only,不应被当作新的用户任务;
- 压缩后要避免'摘要驱动下一轮模型继续旧任务'。
这正是第五章讲的风险:摘要不是事实替代品,也不是新的指令来源。Hermes 的 SUMMARY_PREFIX 设计把这个边界写得很明确:压缩摘要是背景引用,最新用户消息才是当前任务来源。
如果读者要二开上下文策略,建议不要直接改 summarizer prompt。先做三件事:
- 输出 context breakdown 或 debug report,证明哪些消息进入了模型;
- 构造长会话回归用例,覆盖'压缩摘要里有旧任务,但新用户要求停止';
- 确认工具调用和 tool result 配对在压缩后仍合法。
12.7 Memory 与 Skills 是运行时能力,不只是内容库
Hermes README 强调 memory 和 skills。源码里,这两类能力都有明确运行时边界。
agent/memory_manager.py 负责 memory provider 编排。它可以构建 system prompt、在 turn 前 prefetch、turn 后 sync,也能注入外部 memory provider 的 tool schema。源码中还有 context fencing 和 streaming scrubber,用于防止 memory context 标签或内部系统 note 泄漏到用户可见输出。
agent/skill_bundles.py 说明 skill bundle 是一组技能的别名,可以通过 slash command 一次加载多项 skill。tools/write_approval.py 则显示 memory 和 skill 写入有审批机制:memory 可以在交互式前台走 inline approval;skills 通常更大,适合 stage 后通过命令查看 diff 再批准。
这里有一个产品级经验:长期记忆和技能不是'多读几个文件'。它们会改变模型可见上下文,也可能改变用户长期行为。因此写入必须有来源、审批、待处理状态和 diff。否则 Agent 的自改进能力会变成不可控的自我污染。
12.8 Checkpoint:透明基础设施,不是模型工具
tools/checkpoint_manager.py 的开头说明很直接:Checkpoint Manager 通过单个共享 shadow git store 创建透明文件系统快照;它不是一个工具,LLM 不直接看见它;由 checkpoints config 或 --checkpoints CLI flag 控制。
这个设计和第八章高度一致。Hermes 的方案有几个工程点值得学:
- 使用共享 store,让不同项目或 worktree 的 git objects 可以去重;
- 使用
GIT_DIR、GIT_WORK_TREE、GIT_INDEX_FILE,避免 git 状态污染用户项目;
- 默认排除依赖目录、构建产物、缓存、虚拟环境、VCS、媒体、大压缩包、secret 和日志;
- 做 checkpoint pruning,清理不存在项目、过期引用和超出大小限制的快照;
- 对 commit hash 和文件路径做输入校验,避免 git 参数注入和路径穿越。
这也提醒我们:checkpoint 不应该完全交给模型控制。模型可以请求写文件,runtime 在文件变更前后自动建立恢复点。恢复能力越底层,越能保护上层不确定性。
12.9 产品改造实战:工具调用证据面板
现在设计一个可交付的产品级 runtime feature:工具调用证据面板。
它解决的问题很实际。用户看到 Agent 最终回答时,经常不知道它到底读了哪些文件、跑了哪些命令、哪些工具失败过、哪些输出被截断、哪些操作经过审批。工程师排障时也需要这些信息。我们不直接重写主循环,而是在现有工具执行和观测边界上加一层证据投影。
目标
为每个 turn 生成一份 Tool Evidence Report,记录工具调用证据,并可被 CLI、Desktop、gateway 或 debug 页面展示。
runId、turnId、toolCallId;
- 工具名、toolset、风险等级;
- 参数摘要,敏感字段脱敏;
- 是否经过审批;
- 执行状态、耗时、错误分类;
- 输出大小、是否截断、artifact 引用;
- 文件变更摘要或 checkpoint id;
- 并发 segment 信息;
- 这条证据是否进入模型上下文。
切入点
不要从 run_agent.py 顶层硬改。更合理的切入点是:
| 目标 | 可能文件 |
|---|
| 工具开始/结束事件 | agent/tool_executor.py |
| 工具结果归一化和预算 | tools/tool_result_storage.py、tools/budget_config.py |
| 工具注册元数据 | tools/registry.py、model_tools.py |
| 审批上下文 | tools/approval.py |
| 文件 checkpoint | tools/checkpoint_manager.py |
| 内容无关监控事件 | agent/monitoring/events.py |
第一版不要把完整工具结果写进报告。报告保存摘要和引用,完整输出仍走 artifact 或 session store。这样能避免泄露 secret,也避免报告本身变成上下文噪声。
数据结构
from dataclasses import dataclass, field, asdict
import time
from typing import Any, Optional
def now_ns() -> int:
return time.time_ns()
@dataclass(slots=True)
class ToolEvidenceEvent:
turn_id: str
tool_call_id: str
tool_name: str
toolset: str | None = None
risk_level: str | None = None
approval: str | None = None
status: str = "unknown"
duration_ms: int | None = None
error_code: str | None = None
output_chars: int | None = None
truncated: bool = False
artifact_ref: str | None = None
checkpoint_id: str | None = None
context_included: bool | None = None
ts_ns: int = field(default_factory=now_ns)
def to_dict(self) -> dict[str, Any]:
return {"event": "tool_evidence", **asdict(self)}
注意,这个事件不包含原始 prompt、完整参数、完整工具输出或用户私密内容。它是证据索引,不是内容仓库。
UI 表达
Tool evidence: 7 calls, 1 approval, 2 truncated outputs, checkpoint ckpt_42
| 工具 | 状态 | 耗时 | 审批 | 输出 | 证据 |
|---|
read_file | ok | 12ms | auto | 6.1K chars | included |
run_command | failed | 120s | approved | truncated | artifact |
apply_patch | ok | 34ms | approved | patch | ckpt_42 |
这比'Agent 完成了'更像产品。用户能看见执行证据,工程师能复现问题,eval 能读取结构化结果。
12.10 PR 级交付计划
一个真正能提交的改造,不应只包含代码。建议按以下 PR 结构交付。
第一部分:设计说明。说明为什么需要 Tool Evidence Report,哪些用户场景受益,哪些内容不会记录。
第二部分:数据模型。新增内容安全的 evidence event 类型,并写清楚字段脱敏策略。
第三部分:采集点。先只接 tool_executor 的开始、完成、失败,不碰 provider 调用和 memory/skills 写入。
第四部分:持久化。第一版可写 JSONL 或 session DB side table。长输出只保存 artifact ref。
第五部分:展示。CLI 先展示摘要,Desktop/gateway 后续再做表格。
第六部分:测试。覆盖正常工具、失败工具、输出截断、审批拒绝、并发工具和 checkpoint 写入。
第七部分:文档。说明如何打开、如何读取、隐私边界和已知限制。
12.11 验收标准
这章的实战验收不是'把 Hermes 跑起来'。那只是使用者层面的成功。工程实战要证明你读懂了 runtime,并做了一个边界清楚的产品改造。
| 项目 | 证据 |
|---|
| 能画出源码地图 | 入口、turn、tool、context、MCP、checkpoint 路径清楚 |
| 能定位改造点 | 不在 run_agent.py 里粗暴硬插 |
| 证据事件内容安全 | 不含完整 prompt、secret、长输出 |
| 工具结果可追踪 | tool call id、状态、耗时、截断、artifact ref 可见 |
| 审批和 checkpoint 可关联 | 高风险工具能看到 approval 与 checkpoint |
| 有测试计划 | 覆盖失败、截断、并发、拒绝和恢复 |
| 有发布风险说明 | 说明性能、隐私、存储增长和兼容性风险 |
完成这一章,读者应该能把前面十一章的知识迁移到真实大型项目:不是重写一个 Agent,而是在已有系统里找到 runtime 边界,做一项能被用户、维护者和测试共同验证的改造。
系列文章目录
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online