Hermes Agent Loop 技术知识与执行流程

一句话理解

Hermes 的 agent loop 本质上是:

模型想一步,Hermes 执行一步;执行结果再交给模型;直到模型不再需要工具、可以直接回答用户。

它不是一个神秘的“智能循环”,更像一个不断往返的工作流:

用户提出问题

Hermes 把问题和历史对话发给模型

模型判断:
  - 能直接回答:返回最终答案,循环结束
  - 需要工具:返回 tool_calls

Hermes 执行工具

Hermes 把工具结果作为 tool message 放回对话

再次调用模型

生活化比喻

可以把 Hermes 想成“模型的执行秘书”。

模型负责思考:

我需要看文件。
我需要跑测试。
我需要查历史会话。
我需要让子 agent 处理一部分任务。

Hermes 负责执行:

好,我去读文件。
好,我去跑命令。
好,我去查 session。
好,我去调 delegate_task。

工具执行完以后,Hermes 会把结果告诉模型:

文件内容是……
测试结果是……
命令输出是……

模型再继续判断下一步,直到给出最终回答。

核心代码位置

Hermes agent loop 的主入口:

run_agent.py
└── AIAgent.run_conversation()

核心工具执行入口:

run_agent.py
├── _execute_tool_calls()
├── _execute_tool_calls_sequential()
├── _execute_tool_calls_concurrent()
└── _invoke_tool()

普通工具分发入口:

model_tools.py
└── handle_function_call()

主流程

1. 初始化本轮对话

run_conversation() 开始时会做几件事:

  • 生成或接收 task_id
  • 重置各种 retry 计数器
  • 创建本轮 IterationBudget
  • 拷贝历史对话 conversation_history
  • 把用户当前消息 append 到 messages

可以理解为:先准备一张新的“工作记录纸”。

2. 进入 agent loop

主循环大致长这样:

while api_call_count < self.max_iterations and self.iteration_budget.remaining > 0:
    # 1. 准备 api_messages
    # 2. 调用模型
    # 3. 解析模型回复
    # 4. 如果模型要调用工具,就执行工具
    # 5. 如果模型给出最终答案,就结束

真实代码里还多了一个 _budget_grace_call,用于在预算耗尽时给模型一次收尾机会。

3. 构造发给模型的消息

Hermes 内部维护的是 messages

但每次真正发给模型前,会复制出一份 api_messages,并做一些处理:

  • 注入 system prompt
  • 注入 memory/context/plugin 提供的临时上下文
  • 清理模型 API 不接受的内部字段
  • 规范化 tool call 参数
  • 处理 provider 特有格式,例如 reasoning 字段、prompt caching、Codex Responses 字段等

关键点:

messages 是 Hermes 内部保存的真实对话状态;api_messages 是每次发给模型的临时副本。

这样做可以避免污染历史记录,也能保护 prompt caching。

4. 调用模型

Hermes 把 api_messages 和可用工具 schema 发给模型。

模型可能返回两种结果。

第一种:普通文本。

这是最终答案……

这种情况下,没有 tool_calls,循环结束。

第二种:工具调用。

{
  "tool_calls": [
    {
      "function": {
        "name": "terminal",
        "arguments": "{\"command\":\"scripts/run_tests.sh\"}"
      }
    }
  ]
}

这种情况下,Hermes 不会直接结束,而是进入工具执行阶段。

5. 校验工具调用

执行工具前,Hermes 会先做安全和格式检查:

  • 工具名是否存在
  • 工具参数是不是合法 JSON
  • 是否需要修复工具名
  • 是否有重复工具调用
  • 是否超过 delegate_task 限制
  • 是否被 plugin hook 阻止

如果工具名不存在,Hermes 会把错误作为 tool result 返回给模型,让模型自己修正。

也就是说,模型调用错工具时,Hermes 不会马上崩掉,而是给模型一次“你调错了,请重试”的反馈。

6. 执行工具

工具执行入口是:

_execute_tool_calls(assistant_message, messages, effective_task_id, api_call_count)

它会判断这批工具能不能并行:

  • 如果工具之间相互独立,可以走 _execute_tool_calls_concurrent()
  • 否则走 _execute_tool_calls_sequential()

并行执行常用于读文件、搜索等互不冲突的工具。

顺序执行更适合会改变状态的工具,比如写文件、patch、terminal 等。

7. 工具结果回填给模型

工具执行完以后,Hermes 会把结果 append 回 messages

{
    "role": "tool",
    "content": function_result,
    "tool_call_id": tool_call.id,
}

这一步非常关键。

模型并不会自动知道工具执行结果。Hermes 必须把结果作为新的上下文发回给模型。

于是下一轮模型看到的是:

用户问题
模型:我要调用工具
工具结果:测试失败,错误是……

模型再基于这个结果继续思考。

8. 重复循环,直到结束

如果工具结果还不够,模型会继续请求工具。

例如:

模型:我要读失败测试对应的源码文件。
Hermes:读取文件。
模型:我要修改代码。
Hermes:写文件或 patch。
模型:我要重新跑测试。
Hermes:跑测试。
模型:测试通过,可以回答用户。

当模型最终不再返回 tool_calls,而是返回普通文本,Hermes 就把这个文本作为最终回答。

一个完整例子

用户说:

帮我看看为什么测试失败

流程可能是:

第 1 轮
用户 → Hermes → 模型
模型:我要跑 scripts/run_tests.sh
 
第 2 步
Hermes 执行 terminal 工具
工具结果:test_xxx failed,错误在 foo.py:42
 
第 2 轮
Hermes 把工具结果发给模型
模型:我要读取 foo.py
 
第 3 步
Hermes 执行 read_file 工具
工具结果:foo.py 内容……
 
第 3 轮
Hermes 把文件内容发给模型
模型:我知道问题了,需要修改 foo.py
 
第 4 步
Hermes 执行 patch 工具
工具结果:修改成功
 
第 4 轮
Hermes 把修改结果发给模型
模型:我要重新跑测试
 
第 5 步
Hermes 执行 terminal 工具
工具结果:测试通过
 
第 5 轮
Hermes 把测试结果发给模型
模型:最终回答用户

Mermaid 流程图

flowchart TD
    A["用户输入"] --> B["run_conversation 初始化本轮状态"]
    B --> C["进入 while agent loop"]
    C --> D["构造 api_messages"]
    D --> E["调用模型"]
    E --> F{"模型返回 tool_calls?"}
    F -- "否" --> G["返回最终回答"]
    F -- "是" --> H["校验工具名和 JSON 参数"]
    H --> I{"校验通过?"}
    I -- "否" --> J["把错误作为 tool message 回填给模型"]
    J --> C
    I -- "是" --> K["_execute_tool_calls 执行工具"]
    K --> L["工具结果 append 到 messages"]
    L --> M{"需要压缩上下文或处理预算?"}
    M --> C

例子:写入一个文件

假设用户说:

我需要写入文件到 /tmp/hello.txt,内容是 hello

这句话不会让 LLM 自己直接写文件。LLM 只是读到需求,然后基于 agent 提供的工具列表,决定下一步需要调用写文件工具。

第一次模型调用:

Agent → LLM:
用户想写入文件。
你现在可用的工具里有 write_file。
请决定下一步。

LLM 可能返回一个结构化的工具调用请求:

{
  "tool_calls": [
    {
      "id": "call_123",
      "function": {
        "name": "write_file",
        "arguments": "{\"path\":\"/tmp/hello.txt\",\"content\":\"hello\"}"
      }
    }
  ]
}

这个返回值的意思是:

我要调用 write_file 工具。
参数是 path=/tmp/hello.txt,content=hello。

然后 agent 脚本进入判断:

response = call_llm(messages, tools)
 
if response.tool_calls:
    for tool_call in response.tool_calls:
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)
 
        result = handle_function_call(name, args)
 
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": result,
        })

真正写文件的是本地工具函数,不是 LLM 本体:

write_file(path="/tmp/hello.txt", content="hello")

工具执行完以后,可能返回:

{
  "success": true,
  "path": "/tmp/hello.txt",
  "bytes_written": 5
}

Agent 会把这个结果作为 tool 消息放回 messages

{
  "role": "tool",
  "tool_call_id": "call_123",
  "content": "{\"success\":true,\"path\":\"/tmp/hello.txt\",\"bytes_written\":5}"
}

然后第二次调用 LLM:

Agent → LLM:
刚才 write_file 工具执行成功。
结果是:写入 /tmp/hello.txt,写入 5 字节。
请继续。

LLM 这次看到工具已经成功,就返回最终自然语言:

已经写入 /tmp/hello.txt。

于是 agent loop 结束。

完整链路:

用户

Agent

LLM 返回 tool_calls

Agent 判断并调用 write_file

本地文件系统完成写入

Agent 把工具结果发回 LLM

LLM 生成最终回答

Agent 回复用户

例子:通过 Obsidian CLI 写入笔记

假设用户说:

把 agent loop 的知识写进 Obsidian

如果当前环境有 obsidian CLI,并且 agent 暴露了 terminal 工具,那么 LLM 可能会决定:

我需要调用 terminal 工具,执行 obsidian create。

它返回的工具调用可能类似:

{
  "tool_calls": [
    {
      "id": "call_obsidian_1",
      "function": {
        "name": "terminal",
        "arguments": "{\"command\":\"obsidian create name=\\\"Hermes Agent Loop\\\" content=\\\"# Hermes Agent Loop\\\\n...\\\"\"}"
      }
    }
  ]
}

Agent 收到后,不是把这段 JSON 当作回答展示给用户,而是进入工具执行流程:

Agent 解析 tool_calls

发现工具名是 terminal

解析 command 参数

在本机 shell 中执行 obsidian create ...

如果命令成功,本机 shell 返回:

Created note: Hermes Agent Loop

Agent 再把这个结果作为 tool message 回填给 LLM:

{
  "role": "tool",
  "tool_call_id": "call_obsidian_1",
  "content": "Created note: Hermes Agent Loop"
}

然后 LLM 基于这个工具结果生成最终回复:

已经写入 Obsidian。

所以 Obsidian CLI 这条链路是:

用户:写进 Obsidian

Agent 把任务 + 工具列表发给 LLM

LLM 返回:我要用 terminal 执行 obsidian create

Agent 调用 terminal 工具

terminal 调用本机 obsidian CLI

Obsidian CLI 写入 vault

terminal 返回执行结果

Agent 把结果发回 LLM

LLM 生成最终回答

如果环境中没有 obsidian CLI,agent 需要根据实际情况调整策略。例如这次实际执行时,obsidian 命令不在 PATH 中,所以流程变成:

Agent 检查:which obsidian

发现没有 obsidian CLI

读取 Obsidian 本地配置

找到 vault 路径

直接把 Markdown 文件写入 vault 目录

返回用户:已写入,但不是通过 obsidian CLI 命令完成

这也说明 prompt 里为什么不能假设运行环境固定。

贯通版:中间到底发生了什么

用 Obsidian 例子串起来看,完整过程不是:

用户 → LLM → Obsidian

而是:

用户

Agent 程序

LLM
 ↓  返回 tool_calls
Agent 程序
 ↓  调用 terminal/write_file 等工具
本机系统 / Obsidian CLI / 文件系统
 ↓  返回执行结果
Agent 程序
 ↓  把工具结果发回 LLM
LLM
 ↓  生成最终回答
Agent 程序

用户

这里有两次关键的 LLM 调用:

第一次 LLM 调用:
LLM 决定“我要用工具”。
 
第二次 LLM 调用:
LLM 看完工具结果,决定“我可以回复用户了”。

三者分工:

LLM:负责判断下一步该做什么。
Agent:负责解析 tool_calls、校验参数、调用工具、维护循环。
Tool:负责真实执行,例如读写文件、运行命令、调用 Obsidian CLI。

和普通函数调用的区别

普通函数调用通常是:

调用函数 → 得到结果 → 结束

Agent loop 是:

调用模型 → 模型决定下一步 → Hermes 执行 → 结果回给模型 → 模型再决定下一步

所以 agent loop 的关键不是某一个工具,而是这个“模型决策 + 工具执行 + 结果回填”的闭环。

为什么需要预算和中断

如果不加限制,模型可能一直调用工具,导致死循环或成本失控。

所以 Hermes 有几个保护:

  • max_iterations:最多循环多少轮
  • IterationBudget:本轮可用预算
  • _interrupt_requested:用户打断时退出循环
  • retry counter:避免无效工具名、坏 JSON、空回复无限重试
  • context compression:上下文太长时压缩历史

这些机制保证 agent 能持续工作,但不会无限跑下去。

工具分发机制

普通工具最终会走:

handle_function_call()

它会调用:

registry.dispatch(function_name, function_args, ...)

也就是说,工具不是写死在 agent loop 里的。

工具通过 registry 注册,模型只要返回对应工具名,Hermes 就能通过 registry 找到具体实现。

少数 agent 内部工具例外,例如:

  • todo
  • memory
  • clarify
  • delegate_task
  • session_search

这些工具需要访问 agent 内部状态,所以在 run_agent.py 中有特殊处理。

记忆口诀

模型负责想,
Hermes 负责做,
做完告诉模型,
模型继续想。

再压缩成一句:

Agent loop = LLM 决策循环 + 工具执行循环 + 消息回填循环。

延伸阅读入口

在 Hermes 项目中可以优先看这些位置:

/Users/huapai/OpenSourceProject/hermes-agent/run_agent.py
  - AIAgent.run_conversation()
  - AIAgent._execute_tool_calls()
  - AIAgent._execute_tool_calls_sequential()
  - AIAgent._execute_tool_calls_concurrent()
  - AIAgent._invoke_tool()
 
/Users/huapai/OpenSourceProject/hermes-agent/model_tools.py
  - handle_function_call()
 
/Users/huapai/OpenSourceProject/hermes-agent/tools/registry.py
  - registry.register()
  - registry.dispatch()