Hermes架构拆解之Agent Loop

本文对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。

相关推荐
七夜zippoe1 小时前
Agent 上下文工程:Token 管理、上下文压缩与分层记忆设计
ai·agent·token·上下文压缩·分层记忆
DevNo1 小时前
我用AI工具辅助看盘的三段记录
人工智能
IT_陈寒1 小时前
React的useEffect依赖项居然骗了我三年
前端·人工智能·后端
HRaitest1 小时前
【架构拆解】从“外挂插件”到“原生基座”:2026 新一代全链路 AI 招聘系统底层技术演进
人工智能·ai·求职招聘
小尹哥-程序员1 小时前
第4集:让AI学会看文档:Spring AI RAG入门实战
java·人工智能·spring
“AI国潮设计-小江”1 小时前
【SDXL实战】Python自动化生成3D潮汕美食IP,附ComfyUI工作流与商用变现思路
开发语言·人工智能·python·prompt·aigc
墨染天姬1 小时前
[AI]BERT 详解:从起源到实战
人工智能
qyr67891 小时前
全球无硅导热垫片市场调研分析
大数据·人工智能·能源·无硅导热垫片
盼小辉丶1 小时前
PyTorch强化学习实战——分布式策略梯度
人工智能·pytorch·深度学习·强化学习