本文对Hermes的架构拆解是基于v2026.7.30 版本的源码解析
一条用户消息会经过 Platform Adapter、MessageEvent、GatewayRunner、Session 等模块,最终携带当前消息、历史记录、Session 信息等进入 AIAgent。
但到这里,其实只是走到了 Hermes 的"发动机舱门口"。
真正决定一个任务如何完成的,是 AIAgent 内部不断运行的 Agent Loop。
一次用户请求进入
AIAgent后,LLM、Tool 和 Conversation History 到底是如何不断协作,最终得到答案的?
一、从 Gateway 进入 AIAgent
先把前面的流程简单接起来。

Gateway 主要解决的是:
这条消息是谁发来的、属于哪个 Session,以及应该交给谁处理。
而进入 AIAgent 以后,问题发生了变化:
拿到用户的问题以后,我到底应该怎么把这个任务完成?
这就是 Agent Loop 要解决的问题。
官方将 AIAgent 定义为 Hermes 的核心编排引擎,其职责包括 Prompt 组装、Provider/API 模式选择、模型调用、Tool 执行、Conversation History 管理、Compression、Retry 和 Fallback 等。
二、AIAgent 并不是 LLM
理解 Agent Loop 之前,要先区分两个概念:
AIAgent ≠ LLM
LLM 只是整个 Agent 系统中的"推理核心"。
例如用户提出:
帮我看看 pom.xml 使用的 Spring Boot 是什么版本
LLM 自己并不能直接读取服务器上的:
pom.xml
真正的流程是:

因此可以这样理解:
LLM
负责判断:
"下一步应该做什么?"
AIAgent
负责组织:
"怎么让这件事情真正发生。"
比如 LLM 返回:
{
"tool_calls": [
{
"name": "read_file",
"arguments": {
"path": "pom.xml"
}
}
]
}
这句话真正表达的是:
Hermes,我需要
pom.xml的内容,请帮我调用read_file。
真正访问文件系统的是 Tool,而不是 LLM。
三、理解 Agent Loop 前,先区分 Turn 和 Iteration
这是看 Hermes 源码时非常重要的两个概念。
假设用户发送:
帮我读取 pom.xml,告诉我 Spring Boot 的版本
从用户发送消息,到 Hermes 最终回答,这整个过程可以理解为一个:
User Turn
但为了完成这个 Turn,Hermes 可能调用模型很多次。
例如:
User Turn
├── Iteration 1
│ ↓
│ 调用 LLM
│ ↓
│ LLM:我要读取 pom.xml
│ ↓
│ read_file
│
└── Iteration 2
↓
再次调用 LLM
↓
LLM:Spring Boot 是 3.5.1
↓
Final Answer
所以:
一个 Turn 内部,可以包含多个 Iteration。
复杂任务可能更加明显:
User:
帮我分析这个项目为什么启动失败并修复它
↓
Iteration 1
LLM → list_files
↓
Iteration 2
LLM → read_file(application.yml)
↓
Iteration 3
LLM → read_file(log.txt)
↓
Iteration 4
LLM → edit_file
↓
Iteration 5
LLM → run_command(mvn test)
↓
Iteration 6
LLM → Final Answer
这里用户只发送了一次消息,但 Agent Loop 已经运行了很多轮。
所以更准确地说:
Turn
= 一次用户任务
Iteration
= Agent 为完成这个任务进行的一轮
LLM → Tool → Observation
四、run_conversation:发动机启动入口
AIAgent 提供两个主要入口:
agent.chat(...)
以及:
agent.run_conversation(...)
其中 chat() 是比较简单的封装,而完整的 Conversation Loop 入口是:
run_conversation()
官方文档中给出的调用形式类似:
result = agent.run_conversation(
user_message="Fix the bug in main.py",
system_message=None,
conversation_history=None,
task_id="task_abc123"
)
run_conversation() 会负责整个 Turn,从加入用户消息,一直到模型最终返回答案。
如果把大量异常处理、Context Compression、Fallback 等逻辑暂时去掉,它最核心的结构其实非常简单:
def run_conversation(user_message, history):
messages = history
messages.append({
"role": "user",
"content": user_message
})
while True:
response = call_llm(
messages=messages,
tools=tools
)
if response.tool_calls:
results = execute_tools(
response.tool_calls
)
messages.append(results)
continue
return response.content
这段代码不是 Hermes 原始代码,而是 Agent Loop 的逻辑抽象。
真正的 Hermes,则是在这个循环周围增加了大量工程能力。
五、第一次调用 LLM 时发生了什么?
还是以这个问题为例:
帮我看看 pom.xml 使用的 Spring Boot 是什么版本
Hermes 不会只把这一句话发送给模型。
在调用 LLM 之前,AIAgent 会准备 Conversation。
概念上可能类似:
messages = [
{
"role": "system",
"content": "你是 Hermes Agent..."
},
# 之前的历史消息
{
"role": "user",
"content": "..."
},
{
"role": "assistant",
"content": "..."
},
# 当前消息
{
"role": "user",
"content":
"帮我看看 pom.xml 使用的 Spring Boot 是什么版本"
}
]
同时还会告诉模型:
tools = [
read_file,
terminal,
browser,
search,
...
]
也就是说模型得到两类非常重要的信息:
Conversation
+
Tool Schema
可以简单理解成:

Hermes 并不会提前写死:
用户提到 pom.xml
↓
一定调用 read_file
真正判断:
要不要调用工具?
调用哪个工具?
参数是什么?
的是 LLM。
六、模型返回 Tool Call
模型第一次看到:
帮我看看 pom.xml 使用的 Spring Boot 是什么版本
它会发现:
我不知道当前项目里的
pom.xml是什么内容。
因此可能返回:
{
"role": "assistant",
"tool_calls": [
{
"id": "call_001",
"function": {
"name": "read_file",
"arguments": {
"path": "pom.xml"
}
}
}
]
}
此时要特别注意:
这并不是最终回答。
它只是 Agent Loop 中间的一个动作决策。
于是执行权重新回到 Hermes:
LLM
│
│ Tool Call
▼
AIAgent
│
▼
Tool Runtime
│
▼
read_file
Hermes 的 Tool 执行流程还会经过 Handler 解析、Plugin Hook、安全审批、实际执行等步骤。
单个 Tool Call 可以直接执行;如果模型一次返回多个 Tool Call,Hermes 还支持通过线程池并发执行,交互类 Tool 等情况除外。
Tool Runtime 后续可以再单独拆一篇,这里先只理解它在 Agent Loop 中的位置。
七、Tool Result 为什么还要重新给 LLM?
假设:
read_file("pom.xml")
得到:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.1</version>
</parent>
Hermes 会把结果加入 Conversation:
{
"role": "tool",
"tool_call_id": "call_001",
"content": """
<parent>
...
<version>3.5.1</version>
</parent>
"""
}
于是 Conversation 就变成:
User
│
│ 帮我看看 pom.xml 使用什么 Spring Boot 版本
│
▼
Assistant
│
│ tool_call: read_file
│
▼
Tool
│
│ pom.xml 内容
│ <version>3.5.1</version>
│
▼
?
这里并不能直接结束。
原因是:
Tool Result 只是 Agent 获得的一次 Observation,而不是对用户问题的最终回答。
Tool 只知道:
这是 pom.xml 的内容
它并不知道:
用户究竟问了什么?
应该解释哪些字段?
还需不需要继续查其他文件?
应该怎样组织最终回答?
因此必须把:
Tool Result
重新交给 LLM。
让模型继续判断:
现在的信息够了吗?
如果够了:
Final Answer
如果不够:
继续 Tool Call
这就是 Agent Loop 真正形成"循环"的原因。
八、第二次调用 LLM
第二次调用模型时,它看到的内容已经发生变化。
第一次:
User:
帮我看看 pom.xml 使用什么 Spring Boot 版本
第二次:
User:
帮我看看 pom.xml 使用什么 Spring Boot 版本
Assistant:
我要调用 read_file
Tool:
pom.xml 内容如下:
<version>3.5.1</version>
于是模型现在可以理解:
用户的问题
+
自己之前为什么调用工具
+
工具真正获得的数据
然后返回:
这个项目使用的是 Spring Boot 3.5.1。
这一次:
tool_calls = null
Hermes 就知道:
当前任务已经不需要继续调用工具了。
Agent Loop 结束。
九、完整 Agent Loop
把整个过程画出来:

这其实就是整个 Agent 的核心。
不断重复:
LLM
↓
Tool Call
↓
Tool
↓
Tool Result
↓
LLM
↓
Tool Call
↓
...
↓
Final Answer
对应 Agent 中非常经典的思想:
Reason
↓
Act
↓
Observe
↓
Reason
↓
Act
↓
Observe
↓
Answer
十、Conversation 是 Agent Loop 的"工作记忆"
这里还有一个很重要的概念:
Agent Loop 能连续工作,是因为每一次结果都会持续追加到:
messages
里面。
例如:
messages
├── system
│
├── user
│ 帮我分析项目为什么启动失败
│
├── assistant
│ tool_call: read_file(application.yml)
│
├── tool
│ application.yml 内容
│
├── assistant
│ tool_call: read_file(error.log)
│
├── tool
│ error.log 内容
│
├── assistant
│ tool_call: edit_file(...)
│
├── tool
│ 修改成功
│
└── assistant
问题已经修复......
所以模型每进入下一次 Iteration,都能够看到前面已经发生过什么。
Hermes 内部统一使用接近 OpenAI 风格的消息结构:
{"role": "system", "content": "..."}
{"role": "user", "content": "..."}
{
"role": "assistant",
"content": "...",
"tool_calls": [...]
}
{
"role": "tool",
"tool_call_id": "...",
"content": "..."
}
并且 Tool Calling 期间会保持:
Assistant(tool_calls)
↓
Tool
↓
Assistant
这样的结构。
十一、为什么 Hermes 不只是一个简单 while 循环?
从原理上看,Agent Loop 确实可以压缩成:
while True:
response = llm(messages, tools)
if response.tool_calls:
tool_result = execute_tools(
response.tool_calls
)
messages.append(tool_result)
else:
return response.content
但真正上线运行的 Agent,远比这复杂。
Hermes 在这个循环外还增加了:
Provider Resolution
│
Prompt Assembly
│
Tool Registry
│
Tool Approval
│
Concurrent Tool Calls
│
Interrupt
│
Context Compression
│
Retry
│
Fallback Model
│
Memory
│
Session Persistence
│
Callbacks / Streaming
│
SubAgent
所以 Hermes 真正有价值的地方,并不是实现了:
LLM → Tool → LLM
这样一个循环。
而是:
把 Agent Loop 做成了一个可以长期运行、可以被打断、可以扩展工具、可以切换模型、可以持久化上下文的 Agent Runtime。
十二、Interrupt 为什么属于 Agent Loop 的重要能力?
前面讲 Gateway 时,我们已经知道用户可以:
/stop
打断正在执行的 Agent。
到了发动机内部,这个机制终于接起来了。
Hermes 的模型请求并不是:
调用 LLM
↓
一直阻塞
↓
等它返回
而是包装成可中断的 API Call。
官方文档描述的结构大致是:
Main Thread API Thread
等待:
response ready ───────→ HTTP Request
interrupt event
timeout
如果用户发出 /stop 或新的 interrupt:
API Thread
仍可能执行
但是结果会被丢弃
↓
不会写入 Conversation
↓
Agent 可以安全停止
这也是为什么前面的:
Gateway
和现在的:
Agent Loop
不能完全割裂来看。
Gateway 负责:
收到 /stop
AIAgent 负责:
真正让当前执行停下来
官方文档明确说明,模型调用通过 _interruptible_api_call() 包装,并监听 response、interrupt event 与 timeout;如果发生 interrupt,返回结果不会写入 Conversation History。
十三、把整个 Hermes 主干重新串起来
到这里,我们已经可以从用户输入一直追踪到发动机内部。

现在再回过头看整个 Hermes,可以发现它有非常清晰的职责边界。
Gateway 回答:
"这条消息应该交给哪个会话?"
Session 回答:
"这个会话以前发生过什么?"
AIAgent 回答:
"这个任务应该怎样执行?"
LLM 回答:
"下一步应该做什么?"
Tool 回答:
"真正执行后的结果是什么?"
然后 AIAgent 不断让:
LLM → Tool → Result → LLM
循环运行,直到模型认为:
任务已经可以回答用户。
十四、理解 Agent Loop 后,回看 Hermes
第一次打开 Hermes 的 run_agent.py,很容易被它庞大的代码量吓到。
该版本的 run_agent.py 有约 7410 行代码。
但如果已经建立 Agent Loop 的心智模型,就可以把这些代码重新分类:
Agent Loop
│
┌──────────────────┼──────────────────┐
│ │ │
Loop 前 Loop 中 Loop 后
│ │ │
▼ ▼ ▼
Prompt Build LLM Call Persistence
History Load Tool Call Memory Flush
Tool Schema Tool Result Callback
Provider Compression Final Answer
Context Interrupt
看起来复杂的 Hermes,本质上仍然围绕一个非常简单的问题:
Agent 下一步应该做什么?
如果需要外部信息:
调用 Tool
得到 Observation 以后:
重新判断
直到:
最终回答用户
这就是 Hermes 的"发动机"。
总结
如果只用一张图总结本文,就是:
┌──────────────────────┐
│ │
▼ │
User ──→ Conversation ──→ LLM │
│ │
▼ │
Tool Call? │
/ \ │
Yes No │
│ │ │
▼ ▼ │
Tool Final Answer │
│ │
▼ │
Tool Result │
│ │
└─────────────────────┘
而 Hermes 所做的,就是围绕这个 Loop 加上:
Session
Memory
Skills
Provider
Tool Runtime
Streaming
Interrupt
Compression
Retry
Fallback
SubAgent
安全控制
最终把一个简单的 Tool Calling Loop,变成一个真正可用的 Agent Runtime。
理解这一点以后,后面再拆 Hermes 的 Prompt、Provider、Tool Runtime、Memory 等模块时,它们的位置都会非常清晰:
它们不是相互独立的功能,而是在共同服务于同一个 Agent Loop。