乐于分享
好东西不私藏

看Hermes 源码前两天,终于看懂 Agent 为什么不是聊天框

看Hermes 源码前两天,终于看懂 Agent 为什么不是聊天框

Hermes run_conversation 主循环示意

Hermes run_conversation 主循环示意

我这两天看 Hermes Agent 源码,第一反应其实是,wc,一个class这么长,有点吓人。

CLI、Gateway、Cron、ACP、Tools,每个看起来都很吓人。

如果这时候直接打开 run_agent.py 从第一行往下读,大概率会很快坚持不下去。

我第一天做的事,就是先忍住不去看逻辑实现。

读源码嘛,总觉得应该赶紧打开核心文件,看看里面到底写了什么。

但 Hermes 这种项目不太一样。

它不是一个单纯的命令行聊天工具,也不是一个工具集合。

它更像是很多入口,共用一套 Agent 内核。

先别急着读代码

我一开始以为,CLI 应该是中心。

毕竟平时打开 Hermes,最直接的入口就是终端。你输入一句话,它回答你,或者调用工具帮你做事。

但读完 Architecture 那一页以后,我发现这个理解太浅了。

CLI 只是入口。

Gateway 也是入口。

Cron 也是入口。

ACP 也是入口。

真正的问题不是「用户从哪里来」,而是「用户请求进来以后,谁负责把活都干完」。

这件事落在 AIAgent 身上。

你可以先把 Hermes 想成这样一张图:

Entry Points-> AIAgent-> Prompt / Provider / Tool-> Session / Memory / Platform Delivery

这张图不复杂,但它真滴很重要。

你看 gateway/run.py,会觉得 Gateway 是一套 Agent。

你看 cli.py,会觉得 CLI 才是主程序。

你看 tools/,又会觉得 Hermes 只是一个工具调用框架。

都对。

但都只对了一部分。

真正把这些东西串起来的,是 AIAgent

有个问题,如果同一个用户请求,分别从 CLI、Telegram、Cron 进来,哪些东西是不变的?

答案其实很明显。

要加载上下文。

要组装 messages。

要选 provider 和 model。

要把工具 schema 暴露给模型。

要执行 tool call。

要把工具结果放回消息里。

要保存 session。

这些都不是codex,claude专属的逻辑,是通用的逻辑。

所以 Hermes 的设计思路很清楚:平台只负责把消息接进来,再把结果送回去。

中间这段执行链,尽量共用。

这就是我第一天看了之后,第二天醒来,脑子里还留下来的东西。

不是某个函数名,是一个整体的地图。

真正的主线藏在 run_conversation() 里

第二天我才开始看 run_agent.py

这个文件太大了,直接从第一行往下读,看着看着就晕了😷。

我先看了两个主要的函数:chat() 和 run_conversation()

一开始我以为 chat() 是主入口。

后来发现,真正把一次任务跑完的,是 run_conversation()

可以先压缩成这样:

while 还有 iteration budget:    messages = 组装 system + history + 当前用户输入    response = 调用当前 provider/model    if response 有 tool_calls:        tool_result = self._invoke_tool(tool_call)        messages.append({"role": "tool", "content": tool_result})        continue    return 文本回复

这不是源码逐字翻译。是我给自己画的一张简单的逻辑图。

看到这里,Agent 和聊天框的区别就出来了。

普通聊天框大概是:

用户输入 -> 模型回答 -> 结束

Agent 不是这样。

Agent 允许模型先不回答。

它可以先判断,要不要查文件,要不要跑命令,要不要调用浏览器,要不要问用户确认。

模型只发起 tool call。

真正执行工具的是 Hermes runtime。

执行完以后,工具结果会变成一条 tool 消息,重新放回 messages。

然后模型再看这条结果,决定下一步。

所谓 Agent Loop,就是这条循环。工具也是主循环的一部分

这里我之前也容易误解。

我会下意识把 tools 理解成「模型旁边挂了一堆能力」。

比如文件读写、终端、浏览器、图片生成、搜索。

但看 run_conversation() 的时候,这个理解要改一下。

工具是主循环的一部分。

如果模型发起 tool call,主循环不会结束。

它会暂停回答,去执行工具,把结果写回消息,再继续下一轮模型调用。

所以真正的调用链路是:

model tool_call-> Hermes runtime 执行工具-> tool result 写回 messages-> 再次调用 model

这里最最最最关键的是最后一步,工具结果必须回到消息历史里。

否则模型就只知道自己请求了工具,却不知道工具到底返回了什么。

就像你让一个人去查资料,但查完以后不告诉他结果。那他当然会傻掉了,没法继续判断。

第二天还有一个让我改观的点,是 callback。callback 也不是单纯 UI

我原来以为 callback 更像界面层的东西。

比如显示正在思考,显示工具进度,弹出确认,或者把 reasoning 输出给用户看。

但放到 Agent Loop 里看,它就不是简单的 UI 装饰。

只有主循环知道什么时候该触发这些状态。

模型什么时候开始调用。

工具什么时候开始执行。

哪一步需要审批。

哪一步要澄清。

什么时候 fallback。

什么时候压缩上下文。

这些状态都发生在执行链里。

CLI、Gateway、ACP 只是用不同方式把它们呈现出来。

这也是为什么 callback 会出现在 Agent Loop 的理解范围里,而不是只放在 UI 里讲。

这两天最大的收获

第一,Hermes 的中心,是AIAgent 。

第二,AIAgent要先看 run_conversation(),不要先迷失在具体工具里。

第三,工具调用不是模型自己干活,而是 tool call、runtime 执行、tool message、再次模型调用这一整条的链路。