Hermes Agent Loop 深度解读
一、整体架构
run_agent.py 是 hermes-agent 的核心文件(约 10,500 行),实现了 AIAgent 类------一个支持多模型、多 API 协议、多工具调用的 AI Agent 编排器。
用户输入 → run_conversation() → 主循环(while) → LLM API 调用 → 工具执行 → 循环/终止
其中Thought 藏在 "LLM API 调用" 这一步里面。
一次 API 调用,模型同时返回了 Thought + Action:
assistant_message = response.choices0.message
.content → Thought(推理文本)
.reasoning → Thought(结构化推理)
.tool_calls → Action(工具调用)
所以之前的流程图没有错,只是粒度不够细------LLM API 调用 = Thought + Action,它们不是两步,而是同一个 response 的不同字段。
主循环(while)
├── ① LLM API 调用 → 同时返回 Thought + Action
├── ② 工具执行 → 产生 Observation
└── ③ 回到循环顶部 → 带着 Observation 继续
二、Agent Loop 核心流程
2.1 入口:run_conversation() (行 7506)
这是 Agent Loop 的唯一入口,接收用户消息并返回完整对话结果。核心流程如下:
run_conversation(user_message)
│
├── 1. 初始化与预处理
│ ├── 安全 stdio 替换(防管道断裂)
│ ├── 恢复主运行时(如上一轮激活了 fallback)
│ ├── 清理 surrogates 字符(防止 JSON 序列化崩溃)
│ ├── 重置各类重试计数器
│ ├── 清理失效 TCP 连接
│ └── 重建 iteration budget
│
├── 2. 构建/恢复 System Prompt
│ ├── 首次会话:从零构建(_build_system_prompt)
│ └── 续接会话:从 SQLite 恢复(保持 cache prefix 一致)
│
├── 3. Preflight 上下文压缩
│ └── 若历史消息已超阈值 → 主动压缩(最多 3 轮)
│
├── 4. Plugin 钩子:pre_llm_call
│ └── 将插件上下文注入 user message(非 system prompt,保护 cache prefix)
│
└── 5. 主循环 while (api_call_count < max_iterations)
│
├── 检查中断请求
├── 消耗 iteration budget
├── 准备 API 消息(注入 memory、plugin 上下文)
├── 应用 Anthropic prompt caching
├── 消息规范化(JSON 排序、去除无效字段)
│
├── 内层 retry 循环 (最多 3 次)
│ ├── 构建 API 参数(_build_api_kwargs)
│ ├── 执行 API 调用(优先 streaming)
│ ├── 处理各类异常
│ │ ├── 429 限流 → 指数退避 + fallback
│ │ ├── 上下文长度溢出 → 压缩后重试
│ │ ├── 认证失败 → 特定 provider 重试
│ │ └── 空响应 → 重试 + fallback
│ └── 成功 → break 退出 retry 循环
│
├── 处理 API 响应
│ ├── 规范化响应(支持 chat_completions / codex_responses / anthropic_messages)
│ └── 标准化 content 为 string
│
├── 分支判断
│ ├── 【有 tool_calls】→ 工具执行路径
│ └── 【无 tool_calls】→ 最终响应路径
│
├── 工具执行路径
│ ├── 校验工具名称(自动修复 → 3 次无效则中止)
│ ├── 校验 JSON 参数(无效则重试 → 注入错误让模型自纠)
│ ├── 去重 + 限制 delegate_task 调用数
│ ├── 构建 assistant message 并追加到 messages
│ ├── 执行工具(_execute_tool_calls)
│ │ ├── 判断是否可并行(_should_parallelize_tool_batch)
│ │ ├── 并行路径:ThreadPoolExecutor
│ │ └── 串行路径:逐个执行
│ ├── 追加 tool results 到 messages
│ ├── 上下文压力预警(85% 橙色、95% 红色)
│ ├── 判断是否需要上下文压缩
│ └── continue → 回到主循环顶部
│
└── 最终响应路径
├── 检查空内容(优先使用前一轮附带内容)
├── thinking-only 响应 → prefill 继续最多 2 次
├── 真正空响应 → 重试 3 次 → 尝试 fallback
├── Codex 中间确认 → 继续推送
├── 截断续接(truncated_response_prefix)
├── 清理 think blocks
└── break → 退出主循环
2.2 循环退出条件
| 退出原因 | 触发条件 | 处理方式 |
|---|---|---|
text_response |
模型返回纯文本(无 tool_calls) | 正常结束,返回最终响应 |
interrupted_by_user |
用户发送中断信号 | 保存会话,返回中断状态 |
budget_exhausted |
iteration budget 用完 | 注入 grace 消息请求总结 |
max_iterations_reached |
API 调用次数达上限 | 调用 _handle_max_iterations 生成摘要 |
all_retries_exhausted |
3 次重试全失败 | 返回错误 |
empty_response_exhausted |
多次空响应 + fallback 耗尽 | 返回 "(empty)" |
error_near_max_iterations |
接近上限时出错 | 返回错误消息 |
| 各种 truncation | 输出被截断且无法恢复 | 返回 partial 结果 |
2.3 Grace Call 机制
当 iteration budget 耗尽时,不是直接退出,而是注入一条用户消息让模型总结:
"Your tool budget ran out. Please give me the information or actions you've completed so far."
给模型一次额外的 API 调用机会产出文本回复。
三、三大 API 协议适配
hermes 支持三种 API 协议,通过 api_mode 自动检测和切换:
| api_mode | 触发条件 | 响应解析方式 |
|---|---|---|
chat_completions |
默认;OpenAI 兼容端点 | response.choices[0].message |
codex_responses |
OpenAI GPT-5.x、直接 OpenAI URL | _normalize_codex_response() |
anthropic_messages |
api.anthropic.com、URL 以 /anthropic 结尾 | normalize_anthropic_response() |
自动检测逻辑在 __init__ 中,优先级:显式指定 > provider 名称 > URL 特征
四、工具执行系统
4.1 工具分发 (_execute_tool_calls)
_execute_tool_calls(assistant_message, messages, ...)
│
├── 判断是否可并行化 (_should_parallelize_tool_batch)
│ ├── 只读工具 → 允许并行
│ ├── 读/写工具路径不重叠 → 允许并行
│ └── 否则 → 串行执行
│
├── 并行路径 (_execute_tool_calls_concurrent)
│ └── ThreadPoolExecutor(max_workers=min(num_tools, _MAX_TOOL_WORKERS))
│ └── 每个工具一个 _run_tool 线程
│ └── _invoke_tool(name, args, task_id, call_id)
│
└── 串行路径 (_execute_tool_calls_sequential)
└── 逐个调用 _invoke_tool + 逐个追加结果
4.2 工具路由 (_invoke_tool)
| 工具名称 | 路由目标 | 说明 |
|---|---|---|
todo |
todo_tool | 内置 todo 管理 |
session_search |
session_search | 会话搜索 |
memory |
memory_tool | 内置记忆 + 通知外部 memory provider |
clarify |
clarify_tool | 交互式澄清(需 callback) |
delegate_task |
delegate_task | 子 Agent 委派 |
| 外部 memory 工具 | memory_manager.handle_tool_call | 外部记忆 Provider |
| 其他 | handle_function_call | 通用注册工具(文件、终端、浏览器等) |
4.3 Checkpoint 机制
对文件变更工具(write_file, patch)和破坏性终端命令,在执行前自动创建 checkpoint:
python
if function_name in ("write_file", "patch") and self._checkpoint_mgr.enabled:
self._checkpoint_mgr.ensure_checkpoint(work_dir, f"before {function_name}")
五、上下文管理
5.1 上下文压缩
由 ContextCompressor 驱动,在多个时机触发:
| 触发时机 | 条件 | 说明 |
|---|---|---|
| Preflight 压缩 | 进入主循环前,历史消息已超阈值 | 最多 3 轮压缩 |
| 运行中压缩 | 工具执行后 should_compress() 返回 True | 基于真实 token 计数 |
| API 错误触发 | 收到 context_length 错误 | 自动压缩后重试 |
压缩策略:保护最早 N 条 + 最近 N 条消息,中间部分被摘要替换。
5.2 Prompt Caching
对 Claude 模型通过 OpenRouter 使用时,自动注入 cache_control 断点(system + 最近 3 条消息),缓存命中率可达 ~75%。
关键设计:Plugin 上下文注入 user message 而非 system prompt,保持 system prompt 稳定以利用 prefix cache。
5.3 上下文压力预警
| 进度 | 等级 | 表现 |
|---|---|---|
| ≥ 85% | 🟠 橙色警告 | 提醒用户上下文即将满 |
| ≥ 95% | 🔴 红色严重 | 强烈警告,压缩即将触发 |
同一会话 300 秒冷却期,不重复警告。
六、容错与恢复
6.1 多层重试机制
| 错误类型 | 最大重试 | 恢复策略 |
|---|---|---|
| API 限流 (429) | 3 次 | 指数退避 + jitter (5s~120s) |
| 上下文溢出 | 3 次 | 压缩消息后重试 |
| 无效工具名 | 3 次 | 返回可用工具列表让模型自纠 |
| 无效 JSON 参数 | 3 次 | 重试 → 注入错误让模型自纠 |
| 空响应 | 3 次 | 重试 → fallback provider |
| 不完整 scratchpad | 2 次 | 不追加消息,直接重试 |
| Codex incomplete | 3 次 | 追加中间消息后继续 |
| Thinking-only | 2 次 | prefill 继续让模型产出文本 |
| 截断 (length) | 3 次 | 续接指令继续输出 |
6.2 Fallback Provider Chain
当主 provider 持续失败时,自动切换到 fallback 链中的下一个:
python
self._try_activate_fallback() # 切换到下一个 provider
self._restore_primary_runtime() # 下一轮恢复主 provider
触发场景:空响应、429 限流、上下文溢出无法压缩、认证失败等。
6.3 连接健康检查
每次对话开始前清理失效 TCP 连接:
python
self._cleanup_dead_connections() # 检测并清理僵尸 socket
七、Iteration Budget 管理
| 属性/方法 | 类型 | 说明 |
|---|---|---|
max_total |
int | 总预算(默认 90) |
used |
int | 已消耗次数 |
remaining |
int | 剩余次数 |
consume() |
方法 | 消耗 1 次,返回是否成功 |
refund() |
方法 | 退还 1 次(execute_code 调用免费) |
- 父 Agent 创建 budget,子 Agent(delegate_task)共享同一个实例
execute_code工具调用自动 refund(成本极低)- Budget 耗尽时触发 Grace Call 机制
八、记忆系统
8.1 内置记忆
| 工具 | 功能 | 说明 |
|---|---|---|
memory |
add / replace / read | 基于文件存储,写入时同步通知外部 provider |
session_search |
搜索历史对话 | 在会话数据库中检索 |
| 定期 nudge | 每隔 N 轮提醒 | 提醒模型检查记忆 |
8.2 外部记忆 Provider
通过 MemoryManager 接入:
| 操作 | 时机 | 说明 |
|---|---|---|
| Prefetch | 每轮开始前 | 基于用户查询预取相关记忆,注入到 user message |
| Write-back | 内置 memory 工具写入时 | 同步通知外部 provider |
九、插件系统
通过 hermes_cli.plugins.invoke_hook 实现:
| 钩子名称 | 触发时机 | 用途 |
|---|---|---|
on_session_start |
新会话创建时 | 初始化会话状态 |
pre_llm_call |
每轮 LLM 调用前 | 注入额外上下文到 user message |
pre_api_request |
每次 API 请求前 | 监控/计量 |
post_api_request |
每次 API 请求后 | 监控/计量 |
十、总结:Agent Loop 设计亮点
| # | 设计亮点 | 详细说明 |
|---|---|---|
| 1 | 🔵 三协议统一 | 用 api_mode 抽象层统一处理 OpenAI Chat Completions / Codex Responses / Anthropic Messages,上层逻辑无感知 |
| 2 | 🟢 防御性编程极强 | 几乎每个可能失败的操作都有 try/except、重试、fallback,从管道断裂到 JSON 解析失败到连接失效全覆盖 |
| 3 | 🟠 上下文生命周期 | Preflight 压缩 → 运行中压缩 → 压力预警 → Grace Call,形成完整的上下文溢出防护链 |
| 4 | 🔵 工具并行化 | 智能判断只读/读写工具的路径依赖,自动选择并行或串行执行 |
| 5 | 🟢 Cache-friendly | System prompt 只在首次构建后缓存复用,Plugin 上下文注入 user message,最大化 prefix cache 命中率 |
| 6 | 🟠 Budget 共享 | 父子 Agent 共享 iteration budget,子 Agent 不会无限消耗资源 |