Hermes Agent 源码专题【左扬精讲】—— AIAgent 核心循环:消息处理与工具调用
Hermes Agent 核心原理专题【左扬精讲】—— AIAgent 核心循环:消息处理与工具调用
本文深入剖析 AIAgent 的核心对话循环:消息如何组织、工具调用如何解析、迭代如何控制、错误如何恢复。
agent/conversation_loop.py ← 核心对话循环逻辑
run_agent.py ← AIAgent 主类(含 _execute_tool_calls 等方法)
agent/iteration_budget.py ← 线程安全的迭代预算计数器
agent/error_classifier.py ← API 错误分类与恢复策略
agent/tool_executor.py ← 工具调用执行器(并发/顺序)
消息格式 工具调用 迭代控制 错误恢复
学习重点提示
必须掌握
- 理解 AIAgent.run_conversation 是核心循环入口
- 理解消息格式与 OpenAI 兼容的实现方式
- 理解工具调用的解析与分发机制
- 理解 IterationBudget 如何控制迭代次数
需要了解
- 错误分类与智能切换策略
- 并发工具执行与顺序执行的选择逻辑
一、消息格式与 OpenAI 兼容
What — 消息格式是什么?
Hermes Agent 使用 OpenAI 兼容的消息格式,所有消息都是标准的字典结构,包含 role、content 等字段。这种设计使得 Hermes 可以对接任何兼容 OpenAI API 协议的模型提供商。
Why — 为什么需要 OpenAI 兼容的消息格式?
Hermes 是一个多提供商聚合的 Agent 框架,需要同时支持 Anthropic、OpenRouter、Ollama、本地模型等多种后端。如果每种后端都使用自己的消息格式,代码复杂度将指数级增长。
没有 OpenAI 兼容格式会发生什么?
- 每新增一个模型提供商,都需要重写消息构建逻辑
- 无法复用 OpenAI SDK,增加开发成本
- 跨提供商切换变得极其困难
源码视角:agent/conversation_loop.py 第 712-755 行,消息构建逻辑:
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 消息结构
当模型决定调用工具时,返回的消息包含 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_calls 和 tool 消息构成工具调用闭环
二、核心对话循环:run_conversation
What — run_conversation 是什么?
run_conversation 是 AIAgent 类的核心方法,驱动一次用户对话经过整个 Agent 循环(模型调用、工具分发、重试、压缩、后置钩子)。函数定义位于 agent/conversation_loop.py 第 469 行。
Why — 为什么需要 run_conversation?
Agent 的本质是一个"推理-执行"循环:模型输出决定是否需要调用工具,工具结果又反馈给模型继续推理。这个循环需要精细控制:迭代次数限制、中断处理、错误恢复、上下文压缩等。
没有 run_conversation 会发生什么?
- 无法限制 Agent 的最大迭代次数,可能陷入无限循环
- 无法处理 API 错误和重试,稳定性无法保证
- 无法支持上下文压缩,长对话会超出模型上下文限制
2.1 循环入口条件
源码视角:agent/conversation_loop.py 第 563 行的循环条件:
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 snapshot
agent._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...")
break
api_call_count += 1
agent._api_call_count = api_call_count
agent._touch_activity(f"starting API call #{api_call_count}")
三个退出条件:
- 迭代次数耗尽:api_call_count >= max_iterations
- 预算耗尽:iteration_budget.remaining <= 0
- 用户中断:_interrupt_requested == True
2.2 API 调用与响应处理
源码视角:agent/conversation_loop.py 第 969-1006 行的 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 = False
if agent.api_mode == "chat_completions":
_ctv = agent._get_transport()
if not _ctv.validate_response(response):
response_invalid = True
2.3 响应解析与工具调用判断
源码视角:agent/conversation_loop.py 第 3717-3733 行的工具调用判断:
# 检查工具调用
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:
print(f"{agent.log_prefix}🔧 Auto-repaired tool name: '{tc.function.name}' -> '{repaired}'")
tc.function.name = repaired
# 执行工具调用
agent._execute_tool_calls(assistant_message, messages, effective_task_id, api_call_count)
小结
- run_conversation 是 Agent 的核心循环,驱动整个推理-执行流程
- 循环受三个条件控制:迭代次数、预算耗尽、用户中断
- API 响应需要验证格式有效性,再判断是否包含工具调用
三、工具调用的解析与分发
What — 工具调用是什么?
工具调用是模型决定执行某个动作(如读写文件、搜索网络、执行代码)的机制。Hermes 通过 tools/registry.py 注册工具,通过 run_agent.py 的 _execute_tool_calls 执行工具。
Why — 为什么需要工具调用?
纯语言模型只能输出文本,无法操作外部系统。工具调用扩展了模型的能力边界,使其可以:读取文件、执行命令、访问网络、操作数据库等。
没有工具调用会发生什么?
- Agent 只能回答知识性问题,无法执行实际操作
- 无法构建真正有用的自动化流程
- 失去了 Agent 与传统聊天机器人的本质区别
3.1 工具注册机制
源码视角:tools/registry.py 第 151 行的 ToolRegistry 类:
class ToolRegistry:
"""Singleton registry that collects tool schemas + handlers from tool files."""
def __init__(self):
# ... 初始化逻辑
def register(
self,
name: str,
toolset: str,
# ... 其他参数
) -> None:
# 注册工具 schema 和处理器
每个工具文件在导入时通过 registry.register() 注册自己的 schema 和处理函数。自动发现机制确保无需手动维护导入列表。
3.2 工具执行器
源码视角:run_agent.py 第 5157-5251 行的工具执行方法:
def _execute_tool_calls(self, assistant_message, messages: list,
effective_task_id: str, api_call_count: int = 0) -> None:
"""Execute tool calls from the assistant message and append results to messages.
仅当批次看起来是独立的情况下才分并发执行:
- 只读工具始终可以并行
- 文件读写仅在目标路径不重叠时并行
"""
# ... 路径重叠检测逻辑 ...
# 根据依赖关系选择执行模式
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,定义于 run_agent.py 第 203 行和 agent/tool_executor.py 第 52 行)。
3.3 工具名称验证与修复
源码视角:agent/conversation_loop.py 第 3726-3760 行的工具名称验证逻辑:
# 验证工具调用名称 - 检测模型幻觉
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_calls
if tc.function.name not in agent.valid_tool_names
]
if invalid_tool_calls:
agent._invalid_tool_retries += 1
if agent._invalid_tool_retries >= 3:
# 连续 3 次无效工具调用后停止
agent._vprint(f"{agent.log_prefix}❌ Max retries (3) for invalid tool calls exceeded.")
return {
"final_response": None,
"messages": messages,
"completed": False,
"partial": True,
"error": f"Model generated invalid tool call: {invalid_preview}"
}
自动修复失败时,返回错误给模型,让模型在下一轮修正。这是"Agent 自我纠正"机制的一部分。连续 3 次无效工具调用后,Agent 会停止并返回错误。
小结
- 工具通过注册表模式管理,支持动态注册
- 执行器根据工具依赖关系选择并发或顺序模式
- 工具名称自动修复机制减少模型幻觉影响
四、迭代预算与中断机制
What — 迭代预算是指什么?
IterationBudget 是线程安全的迭代计数器,控制 Agent 的最大执行轮次。默认主 Agent 90 次,子 Agent 50 次。
Why — 为什么需要迭代预算?
没有预算限制的 Agent 可能陷入无限循环(如模型不断调用工具但不产生有效输出)。迭代预算是防止资源耗尽的安全机制。
没有迭代预算会发生什么?
- 模型可能陷入无限循环,耗尽 API 配额
- 用户无法预测 Agent 的最大运行时间
- 无法支持定时任务等需要确定性预算的场景
4.1 IterationBudget 实现
源码视角:agent/iteration_budget.py 第 17-60 行的完整实现:
class IterationBudget:
"""Thread-safe iteration counter for an agent."""
def __init__(self, max_total: int):
self.max_total = max_total
self._used = 0
self._lock = threading.Lock()
def consume(self) -> bool:
"""Try to consume one iteration. Returns True if allowed."""
with self._lock:
if self._used >= self.max_total:
return False
self._used += 1
return True
def refund(self) -> None:
"""Give back one iteration (e.g. for execute_code turns)."""
with self._lock:
if self._used > 0:
self._used -= 1
@property
def remaining(self) -> int:
with self._lock:
return max(0, self.max_total - self._used)
注意 execute_code 工具调用可以退还预算,因为它本质上是程序化调用,不消耗外部资源。
4.2 中断机制
源码视角:run_agent.py 第 2363 行的 interrupt 方法:
def interrupt(self, message: str = None) -> None:
"""
Request the agent to interrupt its current tool-calling loop.
Call this from another thread (e.g., input handler, message receiver)
to gracefully stop the agent and process a new message.
"""
self._interrupt_requested = True
self._interrupt_message = message
# 向所有工具发送中止信号
# 将中断范围限定在此 Agent 的执行线程
if self._execution_thread_id is not None:
_set_interrupt(True, self._execution_thread_id)
self._interrupt_thread_signal_pending = False
else:
# 中断请求在 run_conversation() 完成绑定前到达
# 延迟到启动完成后再发送工具级中断信号
4.3 优雅退出
源码视角:agent/conversation_loop.py 第 563-573 行的中断检查点:
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 行的 FailoverReason 枚举:
class FailoverReason(enum.Enum):
"""Why an API call failed — determines recovery strategy."""
# 认证 / 授权
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" # 安全过滤拒绝 - 不要重试
model_not_found = "model_not_found" # 404 - 回退到不同模型
# 未知
unknown = "unknown" # 不可分类 - 退避重试
5.2 重试策略
源码视角:agent/conversation_loop.py 第 952 行附近的重试循环:
api_start_time = time.time()
retry_count = 0
max_retries = agent._api_max_retries # 最大重试次数
_RETRY = TurnRetryState()
max_compression_attempts = 3
while 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 = True
elif classified.should_fallback:
# 切换到备用 provider
if agent._try_activate_fallback():
retry_count = 0 # 重置重试计数
continue
elif not classified.retryable:
# 不可重试的错误,直接失败
break
retry_count += 1
5.3 上下文压缩
当上下文超限时,Hermes 会尝试压缩历史消息:
if _RETRY.restart_with_compressed_messages:
api_call_count -= 1
agent.iteration_budget.refund() # 退还预算,因为这是压缩重启
# 统计压缩重启次数,防止无限循环
# 当压缩后仍无法放入上下文时,会话将中止
压缩尝试最多 3 次(max_compression_attempts = 3)。
5.4 工具调用错误的特殊处理
源码视角:agent/conversation_loop.py 第 3726-3760 行的无效工具调用处理:
# 验证工具调用名称 - 检测模型幻觉
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
invalid_tool_calls = [
tc.function.name for tc in assistant_message.tool_calls
if tc.function.name not in agent.valid_tool_names
]
if invalid_tool_calls:
agent._invalid_tool_retries += 1
if agent._invalid_tool_retries >= 3:
# 连续 3 次无效工具调用后停止
agent._vprint(f"{agent.log_prefix}❌ Max retries (3) for invalid tool calls exceeded.")
return {
"final_response": None,
"messages": messages,
"completed": False,
"partial": True,
"error": f"Model generated invalid tool call: {invalid_preview}"
}
小结
- 错误分类体系覆盖认证、计费、服务端、传输、上下文等多种场景
- 每个错误类型有对应的恢复策略(重试/压缩/轮换/中止)
- 上下文压缩最多尝试 3 次,防止无限循环
- 工具调用错误有特殊处理,支持模型自我修正
六、FAQ 20 问
FAQ 分组说明
以下 20 组 FAQ 涵盖 AIAgent 核心循环的常见问题,按主题分为四组:消息格式(1-5)、循环控制(6-10)、工具系统(11-15)、错误处理(16-20)。
Q1. Hermes 的消息格式与 OpenAI 完全一致吗?
不完全一致,但高度兼容。Hermes 在 OpenAI 格式基础上扩展了一些字段(如 reasoning_content、reasoning_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 被过滤了吗?
是的,特殊处理。纯思考消息(只有 thinking block,没有可见内容和工具调用)会被丢弃,防止某些 API 报错。
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_file、web_search)始终可并发。文件写入工具仅在目标路径不重叠时并发。
Q13. 工具执行结果为什么必须是 JSON 字符串?
API 协议要求。OpenAI 兼容的 tool 消息格式要求 content 是字符串。工具处理函数应返回 json.dumps(result)。
Q14. 什么是 Housekeeping Tools?
轻量级工具子集。如 todo_tool、memory 等。当一轮所有工具调用都是 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 会话持久化
- 插件系统与生命周期钩子
欢迎持续关注!

浙公网安备 33010602011771号