系列:Hermes Agent 源码探秘 作者:元思未来 字数:约3500字
这是整个系列最重要的一篇文章。
前两篇我们理解了 AI Agent 的概念,亲手跑起来体验了。现在,打开源代码,看它到底是怎么工作的。
一、找到核心代码
# 如果你已经安装过 Hermes
cd ~/.hermes/hermes-agent/
# 核心文件
ls -la run_agent.py
run_agent.py 大约 4100 行,是 Hermes 里最大的文件。但别怕——核心循环不靠行数多,靠逻辑清晰。
真正的循环逻辑已经抽离到了独立文件中:
ls agent/conversation_loop.py
这个文件约 4084 行,包含了 run_conversation() 的完整实现。
我们这次的拆解目标就是找出那不到 100 行的核心骨架,然后理解每个部分的作用。
二、核心骨架:Agent 循环的本质
不管外部包装得多复杂,所有 AI Agent 的核心循环都是这样的:
# ===== 核心骨架(伪代码) =====
def run_conversation(self, user_message):
# 1. 准备
messages = self._build_messages(user_message)
# 2. 循环
while self._should_continue():
# 3. 调用LLM
response = self._call_llm(messages)
# 4. 判断
if response.has_tool_calls():
# 5. 执行工具
for tc in response.tool_calls:
result = self._execute_tool(tc)
messages.append(result)
# 继续循环(让LLM看到结果后决定下一步)
else:
# 6. 返回最终回复
return response.text
# 7. 超时/达到限制,返回当前结果
return self._finalize(messages)
Hermes 的实现遵循这个骨架。我们现在一行行看真实代码。
三、真实代码拆解
3.1 循环入口
在 run_agent.py 中,AIAgent 类有两种调用方式:
# 简单接口——返回字符串
def chat(self, message: str) -> str:
result = self.run_conversation(message)
return result["final_response"]
# 完整接口——返回完整结果字典
def run_conversation(self, user_message: str,
system_message: str = None,
conversation_history: list = None,
task_id: str = None) -> dict:
...
外部使用者通常用 .chat(),内部核心逻辑全在 run_conversation() 里。
3.2 主循环
核心循环的条件是:
while (api_call_count < self.max_iterations
and self.iteration_budget.remaining > 0) \
or self._budget_grace_call:
if self._interrupt_requested:
break
# ... 循环体
几个关键控制变量:
| 变量 | 作用 | 默认值 |
|---|---|---|
max_iterations | 最大LLM调用次数 | 90(可配置) |
iteration_budget | Token预算跟踪 | 按模型上下文自动计算 |
_budget_grace_call | 预算耗尽后的一次"优雅结束"机会 | True |
_interrupt_requested | 用户中断标记 | 外部设置 |
设计亮点:
- 硬限制(max_iterations)+ 软限制(budget)+ 优雅降级(grace call)
- 多层保障,防止 Agent 无限循环
3.3 调用 LLM
循环体内第一件事:调用大模型。
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tool_schemas, # 工具定义列表
**kwargs # 其他参数
)
这里有个重要的设计:tools 参数。OpenAI-compatible API 都支持在请求中传入工具定义列表,LLM 会返回是直接回复还是调用工具。
tool_schemas 从哪里来?来自我们下一篇文章要拆解的工具注册系统。简单说就是:
# model_tools.py 中的代码
tool_schemas = get_tool_definitions(
enabled_toolsets=self.enabled_toolsets,
disabled_toolsets=self.disabled_toolsets
)
每个工具定义是一个 JSON Schema,描述工具名称、参数和功能描述。LLM 看到这些 Schema 后,就知道"哦,我有这些工具可以用"。
3.4 解析响应
LLM 返回的响应有两种可能:
if hasattr(response.choices[0].message, 'tool_calls') \
and response.choices[0].message.tool_calls:
# 情况1:LLM决定调用工具
tool_calls = response.choices[0].message.tool_calls
for tool_call in tool_calls:
# 记录思考过程
assistant_msg = {
"role": "assistant",
"content": response.choices[0].message.content or "",
"tool_calls": [tool_call]
}
messages.append(assistant_msg)
# 执行工具
result = self._execute_tool_call(tool_call)
# 重要:角色交替!
tool_result_msg = {
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
}
messages.append(tool_result_msg)
# 继续循环
api_call_count += 1
else:
# 情况2:LLM决定直接回复
return {
"final_response": response.choices[0].message.content,
"messages": messages
}
关键设计:消息角色交替
Assistant Message → Tool Result Message → Assistant Message → Tool Result Message → ...
OpenAI 的 API 要求 tool result 必须跟在对应的 assistant message 之后,不能连续两个 assistant 或连续两个 tool 消息。这是 LLM 调用的基本约束,违反了会导致 API 报错。
3.5 工具执行
_execute_tool_call 负责实际调用工具:
def _execute_tool_call(self, tool_call):
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
try:
result = handle_function_call(
function_name=function_name,
function_args=function_args,
task_id=self._current_task_id
)
return result # 返回JSON字符串
except Exception as e:
return json.dumps({"error": str(e)})
handle_function_call 来自 model_tools.py,它会:
- 根据函数名查找已注册的工具
- 执行工具的处理函数
- 将结果序列化为 JSON 字符串返回
四、循环之外的"周边能力"
上面的骨架看起来简单,但生产级 Agent 还有很多附加机制。
4.1 上下文压缩
随着对话进行,消息列表会越来越长,最终超过 LLM 的上下文窗口。Hermes 的处理策略:
# 每次循环前检查是否需要压缩
if self._should_compress(messages):
messages = self._compress_conversation(messages)
压缩策略包括:
- 丢弃最早的非关键消息
- 摘要历史对话
- 保留 system prompt 和最近的工具调用结果
压缩触发条件:当估算 token 数超过上下文窗口的 50% 时。
4.2 错误处理与重试
LLM API 调用可能失败(网络问题、限流、模型过载):
try:
response = client.chat.completions.create(...)
except Exception as e:
error_info = classify_api_error(e)
if error_info.should_retry:
# 指数退避重试
time.sleep(min(2 ** retry_count, 30))
continue
elif error_info.should_fallback:
# 切换到备用模型
model = self.fallback_model
continue
else:
# 不可恢复的错误,返回错误信息
return {"final_response": f"错误:{str(e)}"}
4.3 速率限制跟踪
# agent/rate_limit_tracker.py
class RateLimitTracker:
def __init__(self):
self.call_times = []
def wait_if_needed(self):
# 检查是否超过速率限制
# 如果超过,等待合适的时间
...
4.4 预算跟踪
# agent/iteration_budget.py
class IterationBudget:
def __init__(self, context_length):
self.total_budget = context_length
self.remaining = context_length
def deduct(self, tokens_used):
self.remaining -= tokens_used
预算跟踪确保 Agent 不会在耗尽上下文窗口前停下。
五、完整流程图
把上面所有内容整合起来,run_conversation() 的完整流程是这样的:
用户输入
│
▼
构建消息列表(System Prompt + 历史 + 新消息)
│
▼
┌─── 循环开始 ──────────────────────────────────────┐
│ │ │
│ ▼ │
│ 检查预算 & 中断标记 → 超限则退出循环 │
│ │ │
│ ▼ │
│ 检查上下文长度 → 需要则压缩 │
│ │ │
│ ▼ │
│ 调用 LLM → 失败则重试/降级 │
│ │ │
│ ▼ │
│ 解析响应 ──────────────────────┐ │
│ │ │ │
│ 有 tool_calls? 无 tool_calls? │
│ │ │ │
│ ▼ ▼ │
│ 逐个执行工具 返回文字响应 │
│ 结果追加到消息列表 → 结束 │
│ │ │
│ ▼ │
│ api_call_count++ │
│ │ │
│ ▼ │
│ 继续循环 ──────────────────────────────────────────┘
│
▼
循环结束(超限或中断) → 返回当前结果
六、给读者的思考
现在你知道了 AI Agent 的核心循环。它其实不复杂——一个 while 循环 + LLM 调用 + 工具执行,就是全部了。
但在这个简单骨架之上,Hermes 增加了:
- 多层退出条件(iteration限制 + budget限制 + 中断)
- 优雅降级(grace call 给LLM一次"说再见"的机会)
- 弹性错误处理(重试 + 降级 + 不可恢复错误分类)
- 资源管理(上下文压缩、预算跟踪)
- 速率控制(防止触发API限流)
这些能力不是 AI 的魔法,是工程设计的智慧。作为一个老程序员,最让我感慨的是:这个系统的核心逻辑如此简单,真正的复杂度都在"边缘"——错误处理、性能管理、安全控制。
好的架构就是把核心做简单,边界做扎实。 Hermes 做到了。
七、下一篇预告
核心循环拆解完了,你知道了 Agent 怎么"思考"和"决定"。下一个问题是:它拿什么"做事"?
第四篇我们拆解 工具系统——tools/registry.py 和 model_tools.py。
你会看到:
- 70+ 工具怎么自动注册
- 工具 Schema 怎么自动转成 LLM 能理解的格式
- 写一个新工具到底有多简单
代码位置:
~/.hermes/hermes-agent/run_agent.py
关键文件:run_agent.py(4104行),agent/conversation_loop.py(4084行)
系列目录: [Hermes Agent 源码探秘]
元思未来 · 行稳致远,进而有为