Hermes Agent 3.x 源码专题【左扬精讲】—— AIAgent 核心循环:消息处理与工具调用

📅 2026/7/15 20:45:46 👁️ 阅读次数 📝 编程学习
Hermes Agent 3.x 源码专题【左扬精讲】—— AIAgent 核心循环:消息处理与工具调用

Hermes Agent 3.x 源码专题【左扬精讲】—— AIAgent 核心循环:消息处理与工具调用

本文介绍了 Hermes Agent 的整体架构,下一篇将深入剖析 AIAgent 的核心对话循环:消息如何组织、工具调用如何解析、迭代如何控制。敬请期待!

agent/conversation_loop.py   ← 核心对话循环逻辑(约 4400 行)
agent/iteration_budget.py   ← 迭代预算管理
agent/tool_executor.py      ← 工具调用执行器
model_tools.py              ← 工具注册与分发

消息格式 工具调用 迭代控制 错误恢复

学习重点提示

必须掌握

  • 理解 AIAgent.run_conversation 是核心循环入口
  • 理解消息格式与 OpenAI 兼容的实现方式
  • 理解工具调用的解析与分发机制
  • 理解 IterationBudget 如何控制迭代次数

需要了解

  • 错误分类与智能切换策略
  • 并发工具执行与顺序执行的选择逻辑

目录
  1. 消息格式与 OpenAI 兼容
  2. 核心对话循环:run_conversation
  3. 工具调用的解析与分发
  4. 迭代预算与中断机制
  5. 错误处理与恢复策略
  6. FAQ 20 问
  7. 后续 Roadmap

一、消息格式与 OpenAI 兼容

What — 消息格式是什么?

Hermes Agent 使用 OpenAI 兼容的消息格式,所有消息都是标准的字典结构,包含 rolecontent 等字段。这种设计使得 Hermes 可以对接任何兼容 OpenAI API 协议的模型提供商。

Why — 为什么需要 OpenAI 兼容的消息格式?

Hermes 是一个多提供商聚合的 Agent 框架,需要同时支持 Anthropic、OpenRouter、Ollama、本地模型等多种后端。如果每种后端都使用自己的消息格式,代码复杂度将指数级增长。

没有 OpenAI 兼容格式会发生什么?

  • 每新增一个模型提供商,都需要重写消息构建逻辑
  • 无法复用 OpenAI SDK,增加开发成本
  • 跨提供商切换变得极其困难
How — 消息格式的源码实现

agent/conversation_loop.py 第 712-755 行,消息构建逻辑:

# 构建发送给 API 的消息列表
api_messages = []
for idx, msg in enumerate(messages):api_msg = msg.copy()  # 复制消息,避免污染原始数据# 注入临时上下文到当前轮次的用户消息# 来源:内存管理器预取 + 插件 pre_llm_call 钩子if idx == current_turn_user_idx and msg.get("role") == "user":_injections = []if _ext_prefetch_cache:_fenced = build_memory_context_block(_ext_prefetch_cache)if _fenced:_injections.append(_fenced)if _plugin_user_context:_injections.append(_plugin_user_context)if _injections:_base = api_msg.get("content", "")api_msg["content"] = _base + "\n\n" + "\n\n".join(_injections)# 为所有 assistant 消息传递 reasoning 给 API# 这确保多轮推理上下文被保留agent._copy_reasoning_content_for_api(msg, api_msg)# 移除 'reasoning' 字段 - 仅用于轨迹存储if "reasoning" in api_msg:api_msg.pop("reasoning")# 移除 finish_reason - 严格 API 不接受(如 Mistral)if "finish_reason" in api_msg:api_msg.pop("finish_reason")# 移除内部 thinking-prefill 标记api_msg.pop("_thinking_prefill", None)api_messages.append(api_msg)

注意:这里使用的是消息副本 api_msg = msg.copy(),确保原始消息不被修改。

1.1 消息角色类型

Hermes 支持以下几种消息角色:

角色说明示例
system 系统提示词 Agent 行为规范、技能列表
user 用户消息 用户输入的问题或指令
assistant 助手回复 包含 content 和/或 tool_calls
tool 工具执行结果 包含 tool_call_id 和 content

1.2 工具调用消息结构

当模型决定调用工具时,返回的消息包含 tool_calls 字段:

How — tool_calls 消息结构
# assistant 消息中的 tool_calls 结构示例
{"role": "assistant","content": None,"tool_calls": [{"id": "call_abc123",           # 工具调用唯一 ID"type": "function","function": {"name": "read_file",        # 工具名称"arguments": '{"path": "/etc/passwd"}'  # JSON 字符串参数}}]
}

工具执行完成后,会追加一条 tool 角色的消息:

# tool 消息结构
{"role": "tool","tool_call_id": "call_abc123",     # 关联到哪个 tool_call"content": '{"success": true, "data": "..."}'  # 工具返回结果(JSON 字符串)
}

小结

  • Hermes 使用 OpenAI 兼容的消息格式,便于对接多种模型提供商
  • 消息通过副本传递,原始数据不受污染
  • tool_callstool 消息构成工具调用闭环

二、核心对话循环:run_conversation

What — run_conversation 是什么?

run_conversationAIAgent 类的核心方法,驱动一次用户对话经过整个 Agent 循环(模型调用、工具分发、重试、压缩、后置钩子)。代码位于 agent/conversation_loop.py,约 4400 行。

Why — 为什么需要 run_conversation?

Agent 的本质是一个"推理-执行"循环:模型输出决定是否需要调用工具,工具结果又反馈给模型继续推理。这个循环需要精细控制:迭代次数限制、中断处理、错误恢复、上下文压缩等。

没有 run_conversation 会发生什么?

  • 无法限制 Agent 的最大迭代次数,可能陷入无限循环
  • 无法处理 API 错误和重试,稳定性无法保证
  • 无法支持上下文压缩,长对话会超出模型上下文限制

2.1 循环入口条件

agent/conversation_loop.py 第 563 行的循环条件:

How — 循环退出条件
while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:# Reset per-turn checkpoint dedup so each iteration can take one snapshotagent._checkpoint_mgr.new_turn()# 检查中断请求(如用户发送了新消息)if agent._interrupt_requested:interrupted = True_turn_exit_reason = "interrupted_by_user"if not agent.quiet_mode:agent._safe_print("\n⚡ Breaking out of tool loop due to interrupt...")breakapi_call_count += 1agent._api_call_count = api_call_countagent._touch_activity(f"starting API call #{api_call_count}")

三个退出条件:

  • 迭代次数耗尽api_call_count >= max_iterations
  • 预算耗尽iteration_budget.remaining <= 0
  • 用户中断_interrupt_requested == True

2.2 API 调用与响应处理

每次循环迭代中,最关键的是 API 调用。看 agent/conversation_loop.py 第 969-1134 行:

How — API 调用流程
try:agent._reset_stream_delivery_tracking()# 为当前 provider 重新应用 reasoning echo# (provider 切换后可能需要不同的 reasoning 字段)agent._reapply_reasoning_echo_for_provider(api_messages)api_kwargs = agent._build_api_kwargs(api_messages)# 执行 API 调用(支持流式和非流式)if _use_streaming:response = agent._interruptible_streaming_api_call(api_kwargs, on_first_delta=_stop_spinner)else:response = agent._interruptible_api_call(api_kwargs)api_duration = time.time() - api_start_time# 验证响应格式response_invalid = Falseif agent.api_mode == "chat_completions":_ctv = agent._get_transport()if not _ctv.validate_response(response):response_invalid = True

2.3 响应解析与工具调用判断

API 返回后,需要判断是否包含工具调用。看 agent/conversation_loop.py 第 3717-3720 行:

How — 工具调用判断与分发
# 检查工具调用
if assistant_message.tool_calls:if not agent.quiet_mode:agent._vprint(f"{agent.log_prefix}🔧 Processing {len(assistant_message.tool_calls)} tool call(s)...")# 验证工具调用名称 - 检测模型幻觉for tc in assistant_message.tool_calls:if tc.function.name not in agent.valid_tool_names:# 尝试自动修复工具名称repaired = agent._repair_tool_call(tc.function.name)if repaired:tc.function.name = repaired# 执行工具调用agent._execute_tool_calls(assistant_message, messages, effective_task_id, api_call_count)

小结

  • run_conversation 是 Agent 的核心循环,驱动整个推理-执行流程
  • 循环受三个条件控制:迭代次数、预算耗尽、用户中断
  • API 响应需要验证格式有效性,再判断是否包含工具调用

三、工具调用的解析与分发

What — 工具调用是什么?

工具调用是模型决定执行某个动作(如读写文件、搜索网络、执行代码)的机制。Hermes 通过 model_tools.py 注册工具,通过 agent/tool_executor.py 执行工具。

Why — 为什么需要工具调用?

纯语言模型只能输出文本,无法操作外部系统。工具调用扩展了模型的能力边界,使其可以:读取文件、执行命令、访问网络、操作数据库等。

没有工具调用会发生什么?

  • Agent 只能回答知识性问题,无法执行实际操作
  • 无法构建真正有用的自动化流程
  • 失去了 Agent 与传统聊天机器人的本质区别

3.1 工具注册机制

Hermes 使用注册表模式管理工具。看 model_tools.py 第 32 行:

How — 工具注册表
from tools.registry import discover_builtin_tools, registry
from toolsets import resolve_toolset, validate_toolset# 工具注册表负责:
# 1. schema 收集
# 2. 分发
# 3. 可用性检查
# 4. 错误包装
# 所有处理器必须返回 JSON 字符串

每个工具文件在导入时通过 registry.register() 注册自己的 schema 和处理函数。

3.2 工具执行器

工具执行由 agent/tool_executor.py 处理,支持两种模式:并发和顺序。看 run_agent.py 第 5157-5252 行:

How — 工具执行入口
def _execute_tool_calls(self, assistant_message, messages: list, effective_task_id: str, api_call_count: int = 0) -> None:"""执行助手消息中的工具调用并追加结果到消息列表。仅当批次看起来是独立的情况下才分并发执行:- 只读工具始终可以并行- 文件读写仅在目标路径不重叠时并行"""# ... 路径重叠检测逻辑 ...# 根据依赖关系选择执行模式if all_independent:return self._execute_tool_calls_concurrent(assistant_message, messages, effective_task_id, api_call_count)else:return self._execute_tool_calls_sequential(assistant_message, messages, effective_task_id, api_call_count)

并发执行最多使用 8 个工作线程(_MAX_TOOL_WORKERS = 8)。

3.3 工具名称验证与修复

模型有时会"幻觉"出不存在的工具名,Hermes 有自动修复机制。看 agent/conversation_loop.py 第 3726-3733 行:

How — 工具名称自动修复
# 验证工具调用名称 - 检测模型幻觉
for tc in assistant_message.tool_calls:if tc.function.name not in agent.valid_tool_names:# 尝试自动修复工具名称repaired = agent._repair_tool_call(tc.function.name)if repaired:print(f"{agent.log_prefix}🔧 Auto-repaired tool name: '{tc.function.name}' -> '{repaired}'")tc.function.name = repaired# 检查是否还有无效工具调用
invalid_tool_calls = [tc.function.name for tc in assistant_message.tool_callsif tc.function.name not in agent.valid_tool_names
]
if invalid_tool_calls:# 返回错误给模型,让模型自己修正agent._invalid_tool_retries += 1available = ", ".join(sorted(agent.valid_tool_names))invalid_name = invalid_tool_calls[0]

自动修复失败时,返回错误给模型,让模型在下一轮修正。这是"Agent 自我纠正"机制的一部分。

小结

  • 工具通过注册表模式管理,支持动态注册
  • 执行器根据工具依赖关系选择并发或顺序模式
  • 工具名称自动修复机制减少模型幻觉影响

四、迭代预算与中断机制

What — 迭代预算是什么?

IterationBudget 是线程安全的迭代计数器,控制 Agent 的最大执行轮次。默认主 Agent 90 次,子 Agent 50 次。

Why — 为什么需要迭代预算?

没有预算限制的 Agent 可能陷入无限循环(如模型不断调用工具但不产生有效输出)。迭代预算是防止资源耗尽的安全机制。

没有迭代预算会发生什么?

  • 模型可能陷入无限循环,耗尽 API 配额
  • 用户无法预测 Agent 的最大运行时间
  • 无法支持定时任务等需要确定性预算的场景

4.1 IterationBudget 实现

agent/iteration_budget.py 第 17-60 行:

How — IterationBudget 源码
class IterationBudget:"""线程安全的迭代计数器。"""def __init__(self, max_total: int):self.max_total = max_totalself._used = 0self._lock = threading.Lock()def consume(self) -> bool:"""尝试消耗一次迭代。返回是否允许。"""with self._lock:if self._used >= self.max_total:return Falseself._used += 1return Truedef refund(self) -> None:"""退还一次迭代(如 execute_code 执行)。"""with self._lock:if self._used > 0:self._used -= 1@propertydef remaining(self) -> int:"""剩余可用的迭代次数。"""with self._lock:return max(0, self.max_total - self._used)

注意 execute_code 工具调用可以退还预算,因为它本质上是程序化调用,不消耗外部资源。

4.2 中断机制

用户可以随时中断 Agent 的执行。看 run_agent.py 第 2382-2430 行:

How — 中断请求处理
def interrupt(self, message: str = None) -> None:"""请求中断当前运行的 Agent。示例(消息场景):# 当活跃会话收到新消息时:if session_has_running_agent:running_agent.interrupt(new_message.text)"""self._interrupt_requested = Trueself._interrupt_message = message# 向所有工具发送中止信号# 将中断范围限定在此 Agent 的执行线程if self._execution_thread_id is not None:_set_interrupt(True, self._execution_thread_id)self._interrupt_thread_signal_pending = Falseelse:# 中断请求在 run_conversation() 完成绑定前到达# 延迟到启动完成后再发送工具级中断信号

4.3 优雅退出

中断发生时,Agent 会检查 _interrupt_requested 并优雅退出。看 agent/conversation_loop.py 第 568-573 行:

How — 中断检查点
while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:# ...# 检查中断请求(如用户发送了新消息)if agent._interrupt_requested:interrupted = True_turn_exit_reason = "interrupted_by_user"if not agent.quiet_mode:agent._safe_print("\n⚡ Breaking out of tool loop due to interrupt...")break  # 优雅退出循环

中断后,已完成的消息历史会保留,Agent 可以从中断点恢复。

小结

  • IterationBudget 线程安全地控制最大迭代次数
  • execute_code 调用可退还预算,不消耗资源配额
  • 中断机制确保用户可以随时停止 Agent
  • 中断后消息历史保留,支持恢复

五、错误处理与恢复策略

What — 错误处理是什么?

Hermes 内置智能错误分类与恢复系统 agent/error_classifier.py,根据错误类型选择最优恢复策略:重试、压缩、切换凭证、回退到备用 provider 等。

Why — 为什么需要智能错误处理?

API 调用可能因网络问题、限流、认证失败、上下文超限等多种原因失败。简单的"失败重试"往往不够,需要根据错误类型选择不同策略。

没有智能错误处理会发生什么?

  • 上下文超限时仍然不断重试,浪费资源
  • 认证失败时不尝试刷新凭证,体验差
  • 无法自动切换到备用 provider,容错能力弱

5.1 错误分类体系

agent/error_classifier.py 第 24-64 行:

How — FailoverReason 枚举
class FailoverReason(enum.Enum):"""为什么 API 调用失败 - 决定恢复策略。"""# 认证 / 授权auth = "auth"                          # 瞬时认证失败 - 刷新/轮换auth_permanent = "auth_permanent"      # 认证彻底失败 - 中止# 计费 / 配额billing = "billing"                    # 配额耗尽 - 立即轮换rate_limit = "rate_limit"              # 限流 - 退避后轮换# 服务端overloaded = "overloaded"              # 503/529 - 退避server_error = "server_error"          # 500/502 - 重试# 传输层timeout = "timeout"                    # 连接/读取超时 - 重建客户端 + 重试# 上下文 / 负载context_overflow = "context_overflow"  # 上下文过大 - 压缩而非切换payload_too_large = "payload_too_large"  # 413 - 压缩image_too_large = "image_too_large"     # 图片过大 - 缩小并重试# 模型 / provider 策略content_policy_blocked = "content_policy_blocked"  # 安全过滤拒绝 - 不要重试# 未知unknown = "unknown"                    # 不可分类 - 退避重试

5.2 重试策略

agent/conversation_loop.py 第 920 行:

How — 重试循环
api_start_time = time.time()
retry_count = 0
max_retries = agent._api_max_retries  # 最大重试次数
_retry = TurnRetryState()
max_compression_attempts = 3while retry_count < max_retries:try:# 执行 API 调用response = run_llm_execution_middleware(...)# 验证响应格式if agent.api_mode == "chat_completions":_ctv = agent._get_transport()if not _ctv.validate_response(response):response_invalid = True# ...except Exception as e:# 分类错误并确定恢复策略classified = classify_api_error(e, ...)if classified.should_compress:# 执行上下文压缩_retry.restart_with_compressed_messages = Trueelif classified.should_fallback:# 切换到备用 providerif agent._try_activate_fallback():retry_count = 0  # 重置重试计数continueelif not classified.retryable:# 不可重试的错误,直接失败breakretry_count += 1

5.3 上下文压缩

当上下文超限时,Hermes 会尝试压缩历史消息。看 agent/conversation_loop.py 第 3489-3491 行:

How — 压缩触发与重试
if _retry.restart_with_compressed_messages:api_call_count -= 1agent.iteration_budget.refund()  # 退还预算,因为这是压缩重启# 统计压缩重启次数,防止无限循环# 当压缩后仍无法放入上下文时,会话将中止

压缩尝试最多 3 次(max_compression_attempts = 3)。

5.4 工具调用错误的特殊处理

工具调用错误有特殊处理逻辑。看 agent/conversation_loop.py 第 3801-3865 行:

How — 无效 JSON 参数处理
# 验证工具调用参数是否为有效 JSON
invalid_json_args = []
for tc in assistant_message.tool_calls:args = tc.function.argumentsif isinstance(args, (dict, list)):tc.function.arguments = json.dumps(args)elif isinstance(args, str):try:json.loads(args)except json.JSONDecodeError:invalid_json_args.append((tc.function.name, args))if invalid_json_args:# 检查是否因截断导致(非模型格式错误)_truncated = any(not (tc.function.arguments or "").rstrip().endswith(("}", "]"))for tc in assistant_message.tool_callsif tc.function.name in {n for n, _ in invalid_json_args})if _truncated:# 截断导致的错误 - 返回提示让模型重试passelse:# 模型格式错误 - 注入恢复工具结果for tc in assistant_message.tool_calls:if tc.function.name in invalid_names:err = next(e for n, e in invalid_json_args if n == tc.function.name)tool_result = json.dumps({"error": f"Invalid JSON arguments: {err}","tool_call_id": tc.id})

小结

  • 错误分类体系覆盖认证、计费、服务端、传输、上下文等多种场景
  • 每个错误类型有对应的恢复策略(重试/压缩/轮换/中止)
  • 上下文压缩最多尝试 3 次,防止无限循环
  • 工具调用错误有特殊处理,支持模型自我修正

六、FAQ 20 问

FAQ 分组说明

以下 20 组 FAQ 涵盖 AIAgent 核心循环的常见问题,按主题分为四组:消息格式(1-5)、循环控制(6-10)、工具系统(11-15)、错误处理(16-20)。

Q1. Hermes 的消息格式与 OpenAI 完全一致吗?

不完全一致,但高度兼容。Hermes 在 OpenAI 格式基础上扩展了一些字段(如 reasoning_contentreasoning_details),用于支持模型的思考过程。这些扩展字段在发送给标准 OpenAI API 时会被自动清理。

Q2. 消息中的 tool_calls 和 tool 消息必须配对吗?

必须配对。每个 tool_calls 条目必须有对应的 tool 消息,通过 tool_call_id 关联。如果缺少对应消息,会导致角色交替错误。

Q3. 为什么需要消息副本(api_msg = msg.copy())?

避免污染原始消息历史。发送给 API 的消息需要做一些变换(如注入上下文、清理内部字段),这些变换不能影响会话历史。使用副本确保原始消息保持不变。

Q4. reasoning_content 和 reasoning 字段有什么区别?

用途不同。reasoning 用于轨迹存储,reasoning_content 用于 API 传输。有些 provider(如 Moonshot)要求 assistant 消息携带 reasoning_content 字段。

Q5. 模型输出的 thinking block 被过滤了吗?

是的,特殊处理。agent/conversation_loop.py 第 804-809 行,纯思考消息(只有 thinking block,没有可见内容和工具调用)会被丢弃,防止 Anthropic 400 错误。

Q6. max_iterations 和 iteration_budget 有什么区别?

本质相同,但 budget 更精细。max_iterations 是循环条件的硬限制,iteration_budget 额外支持退还机制(如 execute_code 不消耗预算)。

Q7. 什么是 grace call(宽限调用)?

预算耗尽后的最后一次调用。iteration_budget.remaining == 0 时,Agent 还会给模型一次额外机会(grace call)完成响应,确保用户体验。

Q8. 用户中断后消息历史会丢失吗?

不会丢失。中断发生时,当前轮次已完成的消息会被持久化到 SessionDB。用户可以随时恢复会话。

Q9. 循环退出后如何决定 final_response?

多级 fallback。优先使用最后一轮模型的实际回复,如果没有则使用 content_with_fallback(模型同时返回内容和工具调用时的文本 fallback),最后才是超时/中断提示。

Q10. step_callback 是什么?

Gateway 钩子事件。step_callback 在每个 API 调用前触发,用于 gateway 的 agent:step 事件,让外部系统可以监听 Agent 进度。

Q11. 工具调用名称自动修复的原理是什么?

编辑距离匹配。当模型输出的工具名不在有效列表中时,_repair_tool_call() 会计算与所有有效工具名的编辑距离,返回最接近的一个。

Q12. 哪些工具可以并发执行?

无依赖的工具。只读工具(如 read_fileweb_search)始终可并发。文件写入工具仅在目标路径不重叠时并发。

Q13. 工具执行结果为什么必须是 JSON 字符串?

API 协议要求。OpenAI 兼容的 tool 消息格式要求 content 是字符串。工具处理函数应返回 json.dumps(result)

Q14. 什么是 Housekeeping Tools?

轻量级工具子集。todo_toolmemory 等。当一轮所有工具调用都是 Housekeeping Tools 时,Agent 会跳过流式输出直接进入下一轮。

Q15. 工具调用的 token 消耗如何统计?

按实际 API 返回统计。每次 API 调用后,Hermes 从响应中提取 usage 信息,累加到会话统计中。

Q16. context_overflow 和 payload_too_large 错误有什么区别?

原因不同。context_overflow 是模型上下文窗口超限,payload_too_large 是 HTTP 413 错误。两者都触发压缩,但 payload_too_large 可能还需要分块处理。

Q17. rate_limit 错误会立即切换 provider 吗?

不会,先退避再轮换。限流错误先执行指数退避(jittered_backoff),退避后仍失败才尝试切换到备用 provider。

Q18. billing 错误为什么立即切换?

因为不可恢复。配额耗尽不是临时问题,等待毫无意义。Hermes 会立即尝试切换到有配额的 provider。

Q19. 压缩失败后 Agent 会怎样?

终止并返回错误。压缩尝试最多 3 次仍超限时,Agent 会中止并返回上下文超限错误,提示用户缩短对话或增加上下文窗口。

Q20. Nous Portal 限流有什么特殊处理?

全局感知与跳过。Hermes 维护一个全局限流状态,所有会话共享。当某个会话检测到限流时,其他会话会跳过 API 调用直接尝试备用 provider。

FAQ 总纲

  • 消息格式兼容 OpenAI,扩展字段自动清理
  • 循环控制通过 IterationBudget + 中断机制实现
  • 工具系统支持并发执行和自动名称修复
  • 错误处理根据类型选择最优恢复策略

七、后续 Roadmap

敬请期待

本篇介绍了 AIAgent 核心循环的架构设计。后续文章将深入剖析:

  • Agent 内存管理与上下文压缩
  • 工具系统的完整实现细节
  • Gateway 消息路由机制
  • SessionDB 会话持久化
  • 插件系统与生命周期钩子

欢迎持续关注!