先从一个很普通的使用场景开始。你让一个 agent 在仓库里改一个功能:它先读 README 和项目规则, 然后搜代码、改文件、跑测试。测试失败后,它查看日志、补一个边界 case,又跑了一轮。中途你告诉它, “以后这个项目里不要直接改生成文件”;过了一会儿,你又问它能不能把刚才失败的命令找回来。
如果这只是一个短聊天,最简单的做法是把所有消息都追加到 history,再把整段 history 送给模型。 但一个会长期运行的 agent 很快会撞上四个问题:history 会膨胀,工具输出会淹没真正的任务, 用户偏好和一次性失败会混在一起,多入口运行时还会出现“CLI 里记得,网关里忘了,cron 里又不能问人”的断裂。
可以先把 Hermes Agent 理解成包在模型外面的一套任务运行系统。模型负责判断下一步该做什么;工具负责真正读取文件、修改代码、运行命令; Hermes 负责把用户要求、项目规则和工具结果组织成一轮可以继续、可以恢复、也可以追溯的工作。这里说的“运行时”,就是这层负责协调模型、工具和状态的程序。
Hermes Agent 在 README 的功能地图 里把自己描述成 self-improving agent:它既能在终端和消息入口里工作,也能执行定时任务、把工作交给子任务,并从完成的任务里沉淀经验。 这些能力都在回答同一个更朴素的问题:一个 agent 该怎样让“当前任务”“真实历史”“长期知识”和“后台学习”各自待在正确的位置?
阅读契约。 全文只跟踪同一个“修复失败测试”的任务。读完后,你应该能用自己的话复述:一轮从哪里开始、在哪里结束;模型与工具怎样来回接力; 为什么原始记录和本轮工作副本必须分开;以及后续六篇会沿着哪些交接点继续深挖。
证据边界。本文只使用公开 README、配置文件和固定源码快照。直接事实来自文中的源码链接;设计意图只在调用链、
数据形状和源码注释能够支撑的范围内推断。README 是功能地图,具体机制以源码为准。例如 session_search 在当前快照中走
SQLite/FTS5 并返回真实消息,工具内部没有再调用一次模型。
接下来先不急着讨论“自进化”。这一篇只负责看清一句用户要求经过哪些位置,再完整走完一轮任务;长期规则、历史和后台学习会在后续篇章分别展开。 当这条主线连起来,Hermes 的“自进化”就不再像一个抽象口号,而是一套状态纪律:先交付当前任务, 再把可复用的东西沉淀到合适账本里;下一轮需要时再按来源、寿命和风险拿回来。
一、同一句话为什么要经过四个位置
继续沿用开头那句话:“以后这个项目里不要直接改生成文件。”它看起来只是一条用户消息,但 agent 要把它用好, 至少要让它经过四个位置。先别记源码名,我们只看这句话在每个位置承担什么职责。
第一站是门口。这句话可能来自终端,也可能来自网关或定时任务。门口只负责接住任务,并把它交给同一套回合流程; 如果每个入口都自己拼规则、自己保存历史,那么同一个 agent 很快就会在不同入口表现得像几个互不认识的助手。
第二站是本轮工作台。真正调用模型前,runtime 会把用户原话和项目规则、可用工具、这次临时召回的材料放在一起, 形成一份只服务当前推理的工作副本。模型需要看见“不要改生成文件”,也可能需要看见“哪些文件由生成器产出”; 但后者是 runtime 临时补充的材料,不能伪装成用户亲口说过的话。
第三站是档案。用户原话、agent 的回答、真实工具调用和结果都要按发生顺序保存。以后用户问“我当时到底怎么说的”, 系统应该能找到原始证据,而不是只剩一句模型总结。档案还必须尽早写入,不能等任务完全成功后才补记。
第四站才是以后还会用到的经验。当前任务交付后,后台流程可以判断这句话是不是稳定偏好,是否值得写入长期状态; 如果它只是本次任务的临时要求,就什么也不写。换句话说,收到一句话、让模型本轮看见它、保存它真实发生过、决定以后是否沿用, 是四个先后相连但不能合并的动作。
现在再给这四个位置加上源码里的名字:门口是入口面,本轮工作台是模型 view,
档案是真实 transcript,交付后提炼经验的动作是后台 review,结果才可能进入长期状态。AIAgent 站在中间,
是因为不同入口最终都要回到同一条回合生命周期,而不是各自维护一套大脑。
| 表面 | 刚才例子里的职责 | 典型失败 |
|---|---|---|
| 入口面 | 从终端、网关、定时任务或子任务接住用户要求。 | 每个入口自己拼上下文,行为很快分叉。 |
| 模型 view | 把用户原话和本轮需要的规则、工具、临时材料组装成工作副本。 | 把临时召回、时间、工具输出塞进稳定前缀,缓存和判断都会抖。 |
| 真实 transcript | 按发生顺序保存用户原话、回答、工具调用和工具结果。 | 只靠摘要恢复,丢掉以后排查需要的原始证据。 |
| 后台 review | 交付后再决定哪些小事实或流程值得在未来继续使用。 | 把一次性任务状态硬化成长期规则,下一次反而误导模型。 |
这张表只是把刚才的流程压缩成速查。这里先按职责拆开“保存原始证据”和“交付后提炼经验”;下一篇再把它们分别放回 SessionDB、Memory 和 Skills。开头总览图里最重要的也不是节点数量,而是方向:入口不直接写长期知识,后台 review 不抢前台回答。
二、跟着一次修复任务走完一轮
现在把开头的任务真正跑一次。你说“修复这个失败测试”,Hermes 最终回答“已修改两个文件,测试通过”。从收到这条用户消息到交付这段回答, 叫作一个 turn,也就是一轮。一轮不等于一次模型调用:为了完成任务,模型可能先要求读文件,看到结果后再要求改代码, 跑完测试后还可能根据错误继续修改。Hermes 要负责的是整段生命周期,而不是其中某一次思考。
最简单的实现会等一切成功后再保存历史,但模型报错、工具卡住或进程退出都可能让整轮消失。因此 Hermes 把一轮拆成四个容易检查的阶段: 先接住并保存任务,再准备模型本轮看到的材料,然后让模型和工具接力,最后交付回答并分发状态。下面逐段走。
2.1 接住任务:先建立本轮工作台,再保存用户原话
用户消息到达后,Hermes 先建立一份“本轮工作台”。它找回之前的会话、确定当前项目规则和可用工具、给这一轮分配标识, 同时把用户刚说的原话尽早写入持久化记录。这样即使后面的模型调用失败,系统仍然知道用户要求过什么,也能从这里恢复。
概念清楚后再看源码名:前台入口是
run_conversation,
每轮准备工作集中在 build_turn_context。它返回的
TurnContext
装着这一轮后续要用的消息、system prompt、任务标识和预取材料;
_persist_session
则让“回答成功”不再是保存用户原话的前提。
2.2 准备第一次模型调用:从档案复制出工作副本
真实档案不能直接等同于模型这一次要看的材料。修测试时,档案里保存用户原话和已经发生的操作;但模型做当前判断时,还可能临时需要 “这个仓库的生成文件清单”或插件补充的项目提示。Hermes 因此从真实消息复制出一份本轮工作副本,再把这些临时材料贴到副本上。
这份工作副本就是模型 view,真实档案就是 transcript。Hermes 在进入 provider 前复制 messages,
把外部 memory 预取和插件上下文只加到当前用户消息的 API 副本,不改原始列表。对应过程见
compose_user_api_content。
如果省掉这次复制,下一轮就无法分清哪些话来自用户,哪些只是 runtime 为上一次判断临时补充的材料。
2.3 模型和工具怎样来回接力
准备好第一次请求后,Hermes 才调用模型。模型本身不会直接打开文件或运行测试;它返回的是“请调用哪个工具、传入什么参数”。 Hermes 检查并执行这个请求,把成功结果或错误结果按原顺序追加到本轮消息里,再把更新后的工作副本交给模型。模型据此决定下一步: 继续调用工具,或者材料已经足够,给出最终回答。
因此,一轮内部其实是一个可能重复多次的小循环。源码里的主循环从
while 回合预算
开始;工具执行层负责真正运行调用,并把结果按模型请求的顺序放回 messages,这个顺序保证可从
tool_executor.py
看到。把它翻译成开头那个任务,就是:
用户:修复这个失败测试
Hermes:保存原话,准备项目规则和可用工具
模型:请读取测试文件和相关实现
Hermes:执行读取,把结果记回本轮
模型:请修改实现并运行测试
Hermes:执行修改和测试,把成功或错误记回本轮
模型:材料足够,给出最终回答
2.4 回答交付后:finalizer 才开始分发状态
当模型返回最终回答,前台任务可以交付,但 Hermes 还要收尾:确认返回给用户的内容,整理这一轮的真实消息, 同步外部状态,并检查是否达到后台 review 的触发条件。这个收尾者叫作 finalizer。 review 即使触发,也拿消息快照在回答之后运行;它不回头改变用户已经收到的结果。
这里还要分清最后两个动作。“同步状态”是把已经完成的回合送到会话存储或外部 memory provider,保证以后能恢复和检索; “启动 review”才是另开一条后台路径,判断其中有没有值得长期保留的经验,而且允许最后什么也不写。前者保存发生过的事,后者筛选未来可能复用的东西。
现在可以完整复述一轮了:接住并保存用户消息 → 组装模型工作副本 → 模型与工具反复接力 → 交付最终回答 → 同步状态并按条件复盘。
后面六篇都只是把这条路线上的一个交接点放大:下一篇先拆开 system prompt、Context、Memory、SessionDB 与 Skill;
第三篇再追踪交付后的 review、nudge 与 Curator;第四篇看 /learn 怎样主动生成 Skill;最后三篇才进入评测集、GEPA 和采用门。
先有整轮路线,后面的源码名才不会突然从半空落下来。
三、工具调用不是模型的私事
一个 agent 能调用工具之后,风险也会跟着上来。读文件、写文件、跑命令、查 session、改 memory、spawn 子任务, 看起来都是 tool call,但它们的 owner 不一样。有些只是普通函数;有些必须由 runtime 持有状态;有些还会带来副作用和恢复问题。
Hermes 的工具执行器先区分并发和顺序路径。
execute_tool_calls_concurrent
支持多个工具并发执行并保持原始 tool-call 顺序追加结果;
顺序路径
则处理交互类或需要更严格控制的工具。
更关键的是 runtime-owned 工具的分派:session_search 要拿 session DB,memory 要调用内置 memory store 并通知外部 provider,
delegate_task 要交给 agent 自己的 delegation dispatcher。这些分派在
tool_executor.py。
也就是说,模型可以请求工具,但工具的状态边界、审计和恢复语义仍然属于 runtime。
把 middleware、计时和展示代码拿掉,三个分支的差别会更直观:
if function_name == "session_search":
db = agent._get_session_db_for_recall()
result = session_search(..., db=db)
elif function_name == "memory":
result = memory_tool(..., store=agent._memory_store)
elif function_name == "delegate_task":
result = agent._dispatch_delegate_task(function_args)
模型给出的只有 function_name 和参数;当前 session 的数据库、真正的 memory store、父任务的 delegation dispatcher,
都是在执行层才绑定进去。这样一来,即使模型发出了格式正确的调用,也不能自行决定“读哪本历史账”或“把经验写进哪个用户的长期状态”。
这一步和前面的账本设计是连在一起的:如果 memory 只是普通工具,模型可能把临时观察写成长期事实; 如果 session_search 不知道 session DB,模型就只能靠摘要猜过去发生了什么;如果 delegation 不是 runtime-owned,父子上下文很容易互相污染。
四、多入口共享核心,但不能共享所有权限
到这里,我们已经能理解单个前台回合。接下来要看多入口运行。Hermes 不只是一个 CLI;README 和源码里还有 gateway、cron、delegation。 如果每个入口都自己实现一套 agent loop,状态纪律会很快崩掉:gateway 里的平台上下文、cron 的非交互限制、子 agent 的隔离边界, 都会变成互相不一致的特殊逻辑。
4.1 Gateway:把平台上下文变成 prompt 边界
gateway 的任务不是简单把 Slack 或其他平台消息转发给模型。它要告诉 agent 消息来自哪里、有哪些 connected platforms、
home channels 和 delivery targets。数据结构在
SessionContext,
渲染成 system prompt 段的逻辑在
build_session_context_prompt。
前台消息处理时,gateway 会按 session key 复用或新建 AIAgent,构造参数里包含 model、runtime、toolsets、platform、user/chat/thread 信息和 session DB;见
agent 构造段。
最终仍然调用
agent.run_conversation。
4.2 Cron:非交互入口必须更窄
cron 的压力不同:它在没有用户实时互动的情况下运行。它不能随便澄清问题,也不能打开 messaging 或再次创建 cronjob。
_resolve_cron_disabled_toolsets
明确让 cron-spawned agent 禁用 cronjob、messaging、clarify 三类 toolset,再叠加用户配置的 disabled toolsets。
cron 还有一个 no_agent 路径:如果 job 不需要 agent,它完全不构造 AIAgent,只执行脚本并交付 stdout 或错误;见
run_job。
如果走 agent,prompt 会在组装后再次扫描注入风险,尤其覆盖 runtime-loaded skill content 和脚本输出;见
_scan_assembled_cron_prompt。
4.3 Delegation:子任务隔离,父任务只拿结果
delegation 解决的是另一种压力:父 agent 不应该把所有探索细节都塞进自己的上下文。delegate_task 的模块说明写明,
子 agent 有隔离上下文、受限 toolsets 和自己的 terminal session;父 agent 阻塞直到子任务完成,但父上下文只看到 delegation call
和摘要结果,不看到子 agent 的中间工具调用和 reasoning。见
delegate_tool.py。
子 agent 默认不能使用 delegate_task、clarify、memory、send_message、execute_code,
这在
DELEGATE_BLOCKED_TOOLS
中定义。构造子 agent 时,还会 skip_context_files=True、skip_memory=True,并有独立 iteration budget;见
子 agent 角色、工具集与 AIAgent 构造段。
五、最后回到那个问题:信息到底该放哪里?
读完这些机制,再回到一开始的使用场景:用户偏好、测试失败、工具输出、可复用流程、一次性任务进度, 都不是“随便记一下”。Hermes 的源码给出的真正答案,是先判断信息类型,再决定它进入哪一本账,以及什么时候让模型重新看到它。
| 看到的信息 | 放哪里 | 为什么 | 放错会怎样 |
|---|---|---|---|
| 用户长期偏好、小型环境事实 | Memory | 小、稳定、可审计,适合作为会话快照进入 prompt。 | 如果只留在 transcript,下次需要时可能找不到。 |
| 过去某次工具输出、完整对话证据 | SessionDB / session_search | 需要保留真实消息窗口,而不是改写成总结。 | 如果写进 memory,会把一次证据伪装成长期规则。 |
| 以后同类任务可复用的方法 | Skills | 它是程序性记忆,需要索引、正文和 patch 语义。 | 如果写成 memory,模型只知道结论,不知道流程。 |
| 当前任务进度、临时失败、探索过程 | Transcript only | 它对恢复有用,但不应硬化成长期事实。 | 如果沉淀太早,下一次会被错误约束。 |
5.1 四个常见误读
第一种误读,是把 memory 当成所有历史的归宿。Hermes 源码不是这么做的。Memory 有预算、扫描、冻结 snapshot 和手动语义; session_search 才是查真实历史的工具。
第二种误读,是把 skills 当成“每次任务都创建一个经验条目”。技能系统强调 class-level、patch existing skill 和支持文件, 不是一轮一个窄技能。
第三种误读,是把 background review 想成主回答的一部分。finalizer 的触发位置说明它发生在 final response 之后; gateway 也会排队 review summary,主响应交付后再释放。
第四种误读,是把 cron 当成普通聊天自动化。cron 禁用交互类工具、扫描组装 prompt,并且支持 no-agent 脚本路径。 非交互入口的默认假设必须更窄。
六、结论:自进化靠状态纪律,不靠“反思”这个词
Hermes Agent 的源码价值,不在于它给“自进化”起了多少功能名,而在于它把长期运行 agent 最容易混乱的状态边界拆开了: 当前 turn、模型 view、真实 transcript、小型 facts、可搜索历史、程序性 skills、后台 review、空闲 curator、多入口策略。
一个完全不懂 Hermes 的读者,读到这里只需要抓住一句话:agent 的长期能力不是把所有东西都记住, 而是知道每种信息什么时候该出现、出现在哪个 view 里、结束后该不该沉淀。
事实要小而可审计;
证据留在 transcript;
流程变成 skill;
当前 turn 的 recall
不污染稳定 prompt;
后台学习发生在响应交付之后。
这也是 Hermes 最值得借鉴的地方:它没有把所有能力堆进一个越来越大的 prompt,而是给每种状态安排了 owner、生命周期和恢复路径。 一个可长期运行的 agent,真正难的是这些生命周期之间的边界。
不过,在讨论“怎样改进 Skill”以前,还要先回答一个更基础的问题:用户偏好、项目规则、历史证据和可复用流程为什么不能都写成 memory。 下一篇继续沿用这个修复任务,把 system prompt、Context、Memory、SessionDB 与 Skill 一层一层放回它们真正的 owner;只有知道改动落在哪里,后面的“进化”才知道自己究竟在改变什么。