01 - Agent 核心循环:同步单线程 ReAct 模式
学习目标
本章聚焦 Hermes Agent 的 run_conversation() 函数如何实现同步单线程 ReAct 循环。你将了解:
- 迭代预算 + grace 机制如何防止循环失控同时保证回复完整
- 中断信号如何在同步循环中被检测和处理
- 系统提示的 SQLite 持久化如何与 Anthropic 前缀缓存配合
- 预飞行上下文压缩为何在进入循环前就瘦身
前置知识
本章涉及 Agent 核心循环的通用原理,建议先阅读:
下文直接聚焦 Hermes 的具体实现。
项目实践
同步主循环结构
Hermes 的核心循环位于 agent/conversation_loop.py:run_conversation(),采用同步单线程的 ReAct 模式:
while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) \ or agent._budget_grace_call: if agent._interrupt_requested: break
# 1. 构建 api_messages # 2. 调用 LLM API # 3. 处理工具调用(并发最多 8 线程) # 4. 追加结果到 messages api_call_count += 1与事件驱动架构不同,Hermes 的循环在一个函数内完成所有步骤,直到模型返回最终文本或触发终止条件。
迭代预算 + One-turn Grace
Hermes 使用双重保护防止循环失控:
max_iterations:硬上限(默认 90 次 API 调用)IterationBudget:线程安全的计数器,父 Agent 默认 90 次,子 Agent 委派默认 50 次
当预算耗尽时,不是立即终止,而是进入 grace 轮:
if agent._budget_grace_call: agent._budget_grace_call = False # 消耗 grace# 本轮继续执行,但下一轮无论结果如何都退出设计意图:避免模型在预算即将耗尽时被截断。Grace 轮让模型有机会完成当前工具调用序列或生成合理的终止回复。
中断处理
中断通过线程信号实现:
agent._execution_thread_id = threading.current_thread().ident# 用户按 Ctrl+C 或发送新消息时:_ra()._set_interrupt(True, agent._execution_thread_id)循环每轮开头检查 _interrupt_requested,如果为 True 则 break。已经执行的工具调用不会被回滚——这与通用指南中的”事务回滚”不同,Hermes 选择让部分执行保留。
系统提示的 SQLite 持久化
Hermes 的系统提示只构建一次,然后持久化到 SQLite:
# 首次:构建并持久化agent._cached_system_prompt = agent._build_system_prompt(system_message)agent._session_db.update_system_prompt(agent.session_id, agent._cached_system_prompt)
# 后续轮次:从 SQLite 读取,复用 verbatimstored_prompt = agent._session_db.get_session(agent.session_id)["system_prompt"]agent._cached_system_prompt = stored_prompt三种异常状态(用于调试前缀缓存失效):
| 状态 | 含义 | 行为 |
|---|---|---|
missing | 无会话记录 | 正常首次构建 |
null | 记录存在但为 NULL | 旧版本遗留,警告并重建 |
empty | 记录存在但为空串 | 持久化 bug,警告并重建 |
所有记忆 prefetch、插件上下文、技能内容都注入到用户消息而非系统提示,确保系统提示跨轮次字节级稳定 → Anthropic prefix cache 前缀匹配。
预飞行上下文压缩
在进入主循环前,Hermes 检测当前消息是否已超出模型上下文窗口:
if len(messages) > protect_first_n + protect_last_n + 1: tokens = estimate_request_tokens_rough(messages, system_prompt, tools) if compressor.should_compress(tokens): for _pass in range(3): messages, system_prompt = compress(...) if len(messages) >= _orig_len: break # 无法继续压缩这在用户切换到小上下文模型时尤其重要——历史对话可能已经超出新模型的窗口,如果不预先压缩,整个 turn 会在 API 调用时失败。
问题与规避
Grace 轮的边界情况
问题:如果模型在 grace 轮中再次触发工具调用,循环会在该工具执行完毕后必然退出,即使工具调用产生了新的待处理工作。
对策:模型需要在 grace 轮中优先完成最终回复,而不是启动新的工具调用序列。这通过系统提示中的行为约束实现。
中断后的状态不一致
问题:Turn 被中断时,已经执行的工具可能已修改了文件系统。Hermes 不回滚这些修改。
对策:依赖终端后端隔离(Docker 等)限制文件修改的范围。进程内的文件操作默认在用户自己的工作目录下。
系统提示缓存失效的诊断
问题:SQLite 写入失败会导致每轮重建系统提示,前缀缓存持续 miss,成本飙升。
对策:Hermes 在 null 和 empty 状态下以 WARNING 级别记录日志。运维人员可通过 hermes logs 持续监控此类警告。
设计取舍
同步单线程 vs 异步事件驱动
| 方案 | 优势 | 代价 |
|---|---|---|
| 同步单线程(Hermes) | 实现简单、调试友好、无异步状态机 | 阻塞期间无法处理其他事件 |
| 异步事件驱动 | 可并发处理多个 turn、流式输出更灵活 | 状态机复杂、调试困难 |
Hermes 选择同步模型是因为其目标场景是单用户对话式交互,用户等待回复期间的并发需求不高。网关层面通过 LRU 缓存和并发用户各自独立的 AIAgent 实例来实现多用户并发。
部分执行不回滚
| 方案 | 优势 | 代价 |
|---|---|---|
| 不回滚(Hermes) | 实现简单、已执行的有益操作不丢失 | 中断后可能需要手动修复 |
| 事务回滚 | 状态一致性好 | 需要快照/副本机制、开销大 |
Hermes 的假设是:Agent 执行的多数工具调用(写文件、运行代码)是有目的的,即使被中断,部分结果通常也比完全没有好。
参考来源
- 源码验证:
agent/conversation_loop.py、agent/iteration_budget.py、agent/prompt_caching.py