先复习一下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_completions、codex_responses、anthropic_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_callsdict)。
模式解析顺序:
- 显式
api_mode构造函数参数(最高优先级) - Provider 特定检测(例如
anthropicprovider →anthropic_messages) - Base URL 启发式规则(例如
api.anthropic.com→anthropic_messages) - 默认:
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 鉴权错误):
- 检查配置中的
fallback_providers列表 - 按顺序尝试每个回退 provider
- 成功后,使用新 provider 继续对话
- 遇到 401/403 时,在故障转移前尝试刷新凭据
回退系统也独立覆盖辅助任务——视觉、压缩和网页提取各自拥有独立的回退链,可通过auxiliary.*配置节进行配置。
压缩与持久化
压缩触发时机
- 预检(API 调用前):对话超过模型上下文窗口的 50%
- Gateway 自动压缩:对话超过 85%(更激进,在轮次之间运行)
压缩过程
- 首先将内存刷写到磁盘(防止数据丢失)
- 将中间对话轮次摘要为紧凑的摘要内容
- 保留最后 N 条消息完整不变(
compression.protect_last_n,默认:20) - 工具调用/结果消息对保持完整(不拆分)
- 生成新的 session 血缘 ID(压缩会创建一个"子" session)
Session 持久化
每轮结束后:
- 消息保存到 session 存储(通过
hermes_state.py使用 SQLite) - 内存变更刷写到
MEMORY.md/USER.md - 可通过
/resume或hermes 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 连接
2. 消息历史恢复与 用户消息注入(~L501–600)
- 复制 conversation_history → messages(避免修改调用者列表)
- 从历史中恢复 todo store 和 nudge 计数器(gateway 每次创建新 AIAgent,这些是内存状态)
- 追加用户消息,记录索引用于后续插件上下文注入
- 系统提示词缓存:_restore_or_build_system_prompt — 从 session DB 恢复(保持 Anthropic prefix cache 命中)或重新构建
3. 预检上下文压缩(~L603–701)
- 模型切换后检查消息列表是否超过 context window
- 如果需要则调用 _compress_context 主动压缩,最多 3 轮
- 压缩后重建 session、重置重试计数器
4. Plugin Hook: pre_llm_call(~L703–739)
- 插件可以返回 context 字符串,注入到当前轮的 user 消息中
- 注入 user message 而非 system prompt(保持 prompt cache 不变)
5. 主循环 — 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 时跳出
6. 后循环处理(~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 调用次数、预算使用情况