跳转到内容

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 使用双重保护防止循环失控:

  1. max_iterations:硬上限(默认 90 次 API 调用)
  2. 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 读取,复用 verbatim
stored_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 在 nullempty 状态下以 WARNING 级别记录日志。运维人员可通过 hermes logs 持续监控此类警告。


设计取舍

同步单线程 vs 异步事件驱动

方案优势代价
同步单线程(Hermes)实现简单、调试友好、无异步状态机阻塞期间无法处理其他事件
异步事件驱动可并发处理多个 turn、流式输出更灵活状态机复杂、调试困难

Hermes 选择同步模型是因为其目标场景是单用户对话式交互,用户等待回复期间的并发需求不高。网关层面通过 LRU 缓存和并发用户各自独立的 AIAgent 实例来实现多用户并发。

部分执行不回滚

方案优势代价
不回滚(Hermes)实现简单、已执行的有益操作不丢失中断后可能需要手动修复
事务回滚状态一致性好需要快照/副本机制、开销大

Hermes 的假设是:Agent 执行的多数工具调用(写文件、运行代码)是有目的的,即使被中断,部分结果通常也比完全没有好。


参考来源

  • 源码验证:agent/conversation_loop.pyagent/iteration_budget.pyagent/prompt_caching.py