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 调用次数、预算使用情况

相关推荐
say_fall1 小时前
可编程中断控制器8259A工作方式超详细解析
android·开发语言·学习·硬件架构·硬件工程
ting94520001 小时前
Superlog 开源自主可观测性工具全栈技术深度剖析
人工智能·架构·开源
学计算机的计算基1 小时前
2026 年 AI 助手三国杀:Claude Code vs 腾讯马维斯 vs MiniMax Mavis,我同时用了三周,结论很意外
java·人工智能·python·算法·langchain
_Aaron___1 小时前
Spring AI 应用上线前,先把大模型调用变成可观测链路
java·人工智能·spring
basketball6161 小时前
AI Infra 硬件体系与编程模型:6. Warp 调度器详解
人工智能
我有2只猫1 小时前
LabelStudio二次开发
人工智能·python·django·ocr
多年小白1 小时前
AI 日报 - 2026年6月7日
人工智能·量子计算
吃好睡好便好1 小时前
说说食物依赖性运动诱发过敏
学习·生活
前端的阶梯1 小时前
如何节省你的token,请看CodeGraph
前端·人工智能·后端