hermes源码学习3-Agent Loop 内部机制

先复习一下hermes架构

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        Entry Points                                  │
│                                                                      │
│  CLI (cli.py)    Gateway (gateway/run.py)    ACP (acp_adapter/)     │
│  Batch Runner    API Server                  Python Library          │
└──────────┬──────────────┬───────────────────────┬───────────────────┘
           │              │                       │
           ▼              ▼                       ▼
┌─────────────────────────────────────────────────────────────────────┐
│                     AIAgent (run_agent.py)                          │
│                                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐               │
│  │ Prompt       │  │ Provider     │  │ Tool         │               │
│  │ Builder      │  │ Resolution   │  │ Dispatch     │               │
│  │ (prompt_     │  │ (runtime_    │  │ (model_      │               │
│  │  builder.py) │  │  provider.py)│  │  tools.py)   │               │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘               │
│         │                 │                 │                       │
│  ┌──────┴───────┐  ┌──────┴───────┐  ┌──────┴───────┐               │
│  │ Compression  │  │ 3 API Modes  │  │ Tool Registry│               │
│  │ & Caching    │  │ chat_compl.  │  │ (registry.py)│               │
│  │              │  │ codex_resp.  │  │ 70+ tools    │               │
│  │              │  │ anthropic    │  │ 28 toolsets  │               │
│  └──────────────┘  └──────────────┘  └──────────────┘               │
└─────────┴─────────────────┴─────────────────┴───────────────────────┘
           │                                    │
           ▼                                    ▼
┌───────────────────┐              ┌──────────────────────┐
│ Session Storage   │              │ Tool Backends         │
│ (SQLite + FTS5)   │              │ Terminal (7 backends) │
│ hermes_state.py   │              │ Browser (5 backends)  │
│ gateway/session.py│              │ Web (4 backends)      │
└───────────────────┘              │ MCP (dynamic)         │
                                   │ File, Vision, etc.    │
                                   └──────────────────────┘

核心编排引擎是 run_agent.py 中的 AIAgent 类------这是一个大型文件(15k+ 行 新版本已经重构 只有5k+),负责处理从 prompt(提示词)组装到工具分发再到 provider 故障转移的所有逻辑。

核心职责

AIAgent 负责:

  • 通过 prompt_builder.py 组装有效的系统 prompt 和工具 schema
  • 选择正确的 provider/API 模式(chat_completionscodex_responsesanthropic_messages
  • 发起支持取消操作的可中断模型调用
  • 执行工具调用(顺序执行或通过线程池并发执行)
  • 以 OpenAI 消息格式维护对话历史
  • 处理压缩、重试和回退模型切换
  • 跨父 agent 和子 agent 追踪迭代预算
  • 在上下文丢失前将持久化内存刷写到磁盘

两个入口点

复制代码
# 简单接口------返回最终响应字符串
response = agent.chat("Fix the bug in main.py")

# 完整接口------返回包含消息、元数据、用量统计的 dict
result = agent.run_conversation(
    user_message="Fix the bug in main.py",
    system_message=None,           # 省略时自动构建
    conversation_history=None,      # 省略时自动从 session 加载
    task_id="task_abc123"
)

chat() 是对 run_conversation() 的轻量封装,从结果 dict 中提取 final_response 字段。

API 模式

Hermes 支持三种 API 执行模式,通过 provider 选择、显式参数和 base URL 启发式规则来确定:

API 模式 用途 客户端类型
chat_completions 兼容 OpenAI 的端点(OpenRouter、自定义及大多数 provider) openai.OpenAI
codex_responses OpenAI Codex / Responses API openai.OpenAI(使用 Responses 格式)
anthropic_messages 原生 Anthropic Messages API 通过适配器使用 anthropic.Anthropic

模式决定了消息的格式化方式、工具调用的结构、响应的解析方式,以及缓存/流式传输的工作方式。三种模式在 API 调用前后均收敛到相同的内部消息格式(OpenAI 风格的 role/content/tool_calls dict)。

模式解析顺序:

  1. 显式 api_mode 构造函数参数(最高优先级)
  2. Provider 特定检测(例如 anthropic provider → anthropic_messages
  3. Base URL 启发式规则(例如 api.anthropic.comanthropic_messages
  4. 默认:chat_completions

单轮生命周期

agent loop 的每次迭代按以下顺序执行:

复制代码
run_conversation()
  1. 若未提供则生成 task_id
  2. 将用户消息追加到对话历史
  3. 构建或复用已缓存的系统 prompt(prompt_builder.py)
  4. 检查是否需要预检压缩(上下文超过 50%)
  5. 从对话历史构建 API 消息
     - chat_completions:直接使用 OpenAI 格式
     - codex_responses:转换为 Responses API 输入项
     - anthropic_messages:通过 anthropic_adapter.py 转换
  6. 注入临时 prompt 层(预算警告、上下文压力提示)
  7. 若使用 Anthropic,应用 prompt 缓存标记
  8. 发起可中断的 API 调用(_interruptible_api_call)
  9. 解析响应:
     - 若有 tool_calls:执行工具,追加结果,回到步骤 5
     - 若为文本响应:持久化 session,按需刷写内存,返回

消息格式

所有消息在内部均使用兼容 OpenAI 的格式:

复制代码
{"role": "system", "content": "..."}
{"role": "user", "content": "..."}
{"role": "assistant", "content": "...", "tool_calls": [...]}
{"role": "tool", "tool_call_id": "...", "content": "..."}

推理内容(来自支持扩展思考的模型)存储在 assistant_msg["reasoning"] 中,并可选择通过 reasoning_callback 展示。

消息交替规则

agent loop 强制执行严格的消息角色交替规则:

  • 系统消息之后:User → Assistant → User → Assistant → ...
  • 工具调用期间:Assistant(含 tool_calls)→ Tool → Tool → ... → Assistant
  • 不允许连续出现两条 assistant 消息
  • 不允许连续出现两条 user 消息
  • 只有 tool 角色可以连续出现(并行工具结果)

Provider 会验证这些序列,并拒绝格式错误的历史记录。

可中断的 API 调用

API 请求被封装在 _interruptible_api_call() 中,该方法在后台线程中执行实际的 HTTP 调用,同时监听中断事件:

复制代码
┌────────────────────────────────────────────────────┐
│  主线程                        API 线程             │
│                                                    │
│   等待:                        HTTP POST           │
│    - 响应就绪          ───▶    发送至 provider       │
│    - 中断事件                                       │
│    - 超时                                          │
└────────────────────────────────────────────────────┘

当发生中断(用户发送新消息、/stop 命令或信号)时:

  • API 线程被放弃(响应被丢弃)
  • agent 可以处理新输入或干净地关闭
  • 不会将部分响应注入对话历史

工具执行

顺序执行与并发执行

当模型返回工具调用时:

  • 单个工具调用 → 直接在主线程中执行
  • 多个工具调用 → 通过 ThreadPoolExecutor 并发执行
    • 例外:标记为交互式的工具(如 clarify)强制顺序执行
    • 无论完成顺序如何,结果均按原始工具调用顺序重新插入

执行流程

复制代码
for each tool_call in response.tool_calls:
    1. 从 tools/registry.py 解析处理器
    2. 触发 pre_tool_call 插件 hook
    3. 检查是否为危险命令(tools/approval.py)
       - 若危险:调用 approval_callback,等待用户确认
    4. 使用参数 + task_id 执行处理器
    5. 触发 post_tool_call 插件 hook
    6. 将 {"role": "tool", "content": result} 追加到历史

Agent 级工具

部分工具在到达 handle_function_call() 之前,由 run_agent.py 提前拦截:

工具 拦截原因
todo 读写 agent 本地任务状态
memory 向持久化内存文件写入内容(有字符限制)
session_search 通过 agent 的 session DB 查询 session 历史
delegate_task 以隔离上下文生成子 agent

这些工具直接修改 agent 状态,并返回合成的工具结果,不经过注册表。

回调接口

AIAgent 支持平台特定的回调,用于在 CLI、gateway 和 ACP 集成中实现实时进度展示:

回调 触发时机 使用方
tool_progress_callback 每次工具执行前后 CLI spinner、gateway 进度消息
thinking_callback 模型开始/停止思考时 CLI "thinking..." 指示器
reasoning_callback 模型返回推理内容时 CLI 推理展示、gateway 推理块
clarify_callback 调用 clarify 工具时 CLI 输入提示、gateway 交互消息
step_callback 每次完整 agent 轮次结束后 Gateway 步骤追踪、ACP 进度
stream_delta_callback 每个流式 token(启用时) CLI 流式展示
tool_gen_callback 从流中解析出工具调用时 CLI spinner 中的工具预览
status_callback 状态变更时(思考、执行等) ACP 状态更新

预算与回退行为

迭代预算

agent 通过 IterationBudget 追踪迭代次数:

  • 默认:90 次迭代(可通过 agent.max_turns 配置)
  • 每个 agent 拥有独立预算。子 agent 获得独立预算,上限为 delegation.max_iterations(默认 50)------父 agent 与子 agent 的总迭代次数可超过父 agent 的上限
  • 达到 100% 时,agent 停止并返回已完成工作的摘要

回退模型

当主模型失败时(429 限流、5xx 服务器错误、401/403 鉴权错误):

  1. 检查配置中的 fallback_providers 列表
  2. 按顺序尝试每个回退 provider
  3. 成功后,使用新 provider 继续对话
  4. 遇到 401/403 时,在故障转移前尝试刷新凭据

回退系统也独立覆盖辅助任务------视觉、压缩和网页提取各自拥有独立的回退链,可通过 auxiliary.* 配置节进行配置。

压缩与持久化

压缩触发时机

  • 预检(API 调用前):对话超过模型上下文窗口的 50%
  • Gateway 自动压缩:对话超过 85%(更激进,在轮次之间运行)

压缩过程

  1. 首先将内存刷写到磁盘(防止数据丢失)
  2. 将中间对话轮次摘要为紧凑的摘要内容
  3. 保留最后 N 条消息完整不变(compression.protect_last_n,默认:20)
  4. 工具调用/结果消息对保持完整(不拆分)
  5. 生成新的 session 血缘 ID(压缩会创建一个"子" session)

Session 持久化

每轮结束后:

  • 消息保存到 session 存储(通过 hermes_state.py 使用 SQLite)
  • 内存变更刷写到 MEMORY.md / USER.md
  • 可通过 /resumehermes chat --resume 恢复 session

关键源文件

文件 用途
run_agent.py AIAgent 类------完整的 agent loop
agent/prompt_builder.py 从内存、技能、上下文文件和个性组装系统 prompt
agent/context_engine.py ContextEngine ABC------可插拔的上下文管理
agent/context_compressor.py 默认引擎------有损摘要算法
agent/prompt_caching.py Anthropic prompt 缓存标记和缓存指标
agent/auxiliary_client.py 用于辅助任务的辅助 LLM 客户端(视觉、摘要)
model_tools.py 工具 schema 集合,handle_function_call() 分发

总结一下 run_conversation 方法源码

以下是 agent/conversation_loop.py 中 run_conversation 方法的主要环节分析(~4900行,是 Hermes Agent 核心循环的心脏):

  1. 前置初始化(~L364--500)

参数与守卫:

  • 接收 user_message、system_message、conversation_history、stream_callback 等

  • _install_safe_stdio() --- 保护 daemon/systemd 下 stdout 写崩溃

  • 确保 session DB 存在

运行时设置:

  • 向辅助客户端注册主 provider/model(set_runtime_main)

  • 设置 session 日志上下文、skill 写入来源标记

  • 从之前的 fallback 恢复主 runtime(_restore_primary_runtime)

  • 清理用户输入中的 surrogate 字符

重置状态:

  • 重置各类重试计数器(_invalid_tool_retries、_empty_content_retries 等 10+ 个)

  • 重置 iteration budget、vision 支持标记、tool guardrails

连接健康检查:

  • 检测并清理前次 provider 留下的死 TCP 连接
  1. 消息历史恢复与 用户消息注入(~L501--600)
  • 复制 conversation_history → messages(避免修改调用者列表)

  • 从历史中恢复 todo store 和 nudge 计数器(gateway 每次创建新 AIAgent,这些是内存状态)

  • 追加用户消息,记录索引用于后续插件上下文注入

  • 系统提示词缓存:_restore_or_build_system_prompt --- 从 session DB 恢复(保持 Anthropic prefix cache 命中)或重新构建

  1. 预检上下文压缩(~L603--701)
  • 模型切换后检查消息列表是否超过 context window

  • 如果需要则调用 _compress_context 主动压缩,最多 3 轮

  • 压缩后重建 session、重置重试计数器

  1. Plugin Hook: pre_llm_call(~L703--739)
  • 插件可以返回 context 字符串,注入到当前轮的 user 消息中

  • 注入 user message 而非 system prompt(保持 prompt cache 不变)

  1. 主循环 --- Agent Loop(~L814--4524)

这是最核心的部分,每次迭代执行:

5.1 中断检查与预算控制(~L818--838)

  • 检查 interrupt 标记

  • 消耗 iteration budget,耗尽时跳出

5.2 Step Callback & Steer Drain(~L841--924)

  • 向 gateway 回报当前步骤

  • 处理 /steer 指令(用户中途给模型的补充指令)

5.3 构建 API 请求(~L926--1122)

  • 修复 tool call 参数

  • 修复消息序列角色交替错误

  • 注入内存 prefetch 结果和插件上下文到当前 user 消息

  • 复制 reasoning 字段

  • 组装最终 system prompt(缓存 + 临时)

  • 应用 prompt caching

  • 规范化 JSON、去除 surrogate

  • 估算 token 数

5.4 API 重试循环(~L1178--3567, 最复杂的部分)

成功路径:

  • 调用 provider → 校验响应结构 → 提取 finish_reason → 记录 token 用量和成本

截断处理(length / truncation):

  • 检测 thinking-budget 耗尽

  • 输出截断 → 最多 3 次 continuation 重试

  • tool call 截断 → 最多 3 次重试,同时提升 max_tokens

异常恢复(巨大的 try/except):

  • Unicode 编码错误:去除 surrogate 字符 或 强制 ASCII 模式

  • 图片被拒:切换为纯文本模式

  • 图片过大:压缩图片后重试

  • Multimodal tool content 被拒:降级列表类型 tool 内容

  • Anthropic OAuth 1M context beta 被拒:禁用 beta header 后重建客户端

  • 认证过期:刷新 OAuth 令牌(Codex / Nous / Anthropic / Copilot)

  • Thinking 签名无效:清除 reasoning_details

  • 加密推理回放被拒:禁用回放

  • llama.cpp 语法错误:去除 regex pattern/format

降级策略链(按优先级):

  1. credential_pool 轮换 API key(429)

  2. 上下文压缩(413, context_overflow, long_context_tier)

  3. 切换到 fallback 模型/提供商

  4. 指数退避重试

  5. 最终报错返回

5.5 响应规范化(~L3608--3636)

  • 通过 transport 层将不同 API 模式(chat_completions / anthropic_messages / bedrock_converse / codex_responses)统一为标准 assistant_message 格式

5.6 Post-API Plugin Hook: post_api_request(~L3638)

5.7 处理 Tool Calls(~L3802--4131)

  • 验证 tool 名称是否存在(自动修复常见变形)

  • 验证 JSON 参数是否合法

  • 检测截断的 JSON → 拒绝执行

  • Post-call guardrails:限制 delegate_task 并行数、去重

  • 执行工具:_execute_tool_calls

  • 执行后检查上下文大小 → 按需压缩

  • 增量保存 session

5.8 处理无工具调用的最终响应(~L4133--4467)

  • 内容为空 → 多种恢复尝试:

  • 使用已流送的片段(partial stream recovery)

  • 使用前一轮的工具 + 内容组合(housekeeping fallback)

  • 追加 "nudge" 提示让模型继续

  • Thinking-only → prefill continuation

  • 空响应重试(最多 3 次)

  • fallback 后再试

  • 最终返回 "(empty)"

  • 组装最终 assistant 消息,清理临时 scaffolding

  • 截断片段拼接:把前几轮 continuation 的文本合并

5.9 外层异常捕获(~L4469--4524)

  • 填充未回答的 tool_call_id 的 error 结果

  • 接近 max_iterations 时跳出

  1. 后循环处理(~L4526--4901)

6.1 Budget 耗尽处理(~L4526--4595)

  • 若 iteration budget 耗尽且无最终回复 → 调用 _handle_max_iterations(去掉 tools 后单次求和式问答)

  • 如果是 Kanban Worker,记录 timeout 到 kanban DB

6.2 会话持久化(~L4615--4616)

  • 去除内部 scaffolding 消息

  • 保存到 JSON 日志 + SQLite

6.3 诊断日志(~L4618--4660)

  • 记录 _turn_exit_reason、API 调用次数、工具轮次、响应长度

  • 若以 tool result 结尾 → WARNING 级别

6.4 文件变更验证器(~L4662--4685)

  • 检测 write_file/patch 是否真的写入成功,附加 footer

6.5 Turn 结束解释器(~L4687--4734)

  • 如果响应为空或过短,用 _turn_exit_reason 生成可读解释

6.6 Plugin Hooks(~L4736--4779)

  • transform_llm_output:允许插件修改最终文本

  • post_llm_call:允许插件持久化对话数据

6.7 后台审查(~L4848--4874)

  • 检查是否需要触发 memory nudge 或 skill review

  • 如果需要,在后台 fork 一个独立 agent 线程执行

6.8 最终 Hooks & 返回(~L4886--4901)

  • on_session_end hook

  • 返回包含 final_response、messages、token 用量、cost、reasoning 等完整结果字典

总结

run_conversation 本质上是一个带有完整故障恢复链的 agent 工具调用循环,其核心阶段链:

Initialize → Build Messages → Tool Loop → Post-process → Return

↑ │

└── Retry / Compress / Fallback

关键设计特点:

  • 分层恢复:credential 轮换 → 压缩 → fallback → 退避重试 → 报错

  • 自适应输出处理:截断 continuation、thinking-aware、空响应指纹、prefill 桥接

  • 响应完整性保障:流中断恢复、片段拼接、tool call 截断检测

  • 插件化:4 个 hook 点,不侵入核心逻辑

  • 可观测性:每轮结束记录详尽诊断日志,包含退出原因、API 调用次数、预算使用情况

相关推荐
米小虾16 小时前
Loop Engineering —— 循环的设计与自主执行
人工智能·agent
米小虾16 小时前
Harness Engineering —— 系统的安全护栏
人工智能·agent
火山引擎开发者社区17 小时前
积分当钱花,火山引擎开发者激励计划首月消费双倍回馈
人工智能
aqi0017 小时前
15天学会AI应用开发(十)把文本嵌入模型换成国产模型
人工智能·python·ai编程
MobotStone18 小时前
为什么在AI时代,“好奇心”成了最值钱的能力?
人工智能
武子康19 小时前
调查研究-200 llama.cpp b9754:一次很小但很关键的 Agent 工具调用修复
人工智能·agent·llama
Ralph_Salar19 小时前
从0到1搭建AI智能支付风控助手Stage1-RAG知识库升级 — 元数据让检索更精准
人工智能
武子康19 小时前
调查研究-199 MCP Zero-Touch OAuth:为什么它是 MCP 进入企业生产的关键门槛?
人工智能·agent·mcp