进化从来不是一次顿悟,而是无数次微小改进的复利。
自我进化 = 三个工程组件
先把概念打碎。Hermes Agent 的"自我进化"不是一个神秘的 AI 能力,它首先体现为三个可配置、可观测的工程组件:
| 组件 | 触发方式 | 产出 | 代码入口 |
|---|---|---|---|
| 记忆 nudge | 每 N 轮用户消息 | MEMORY.md / USER.md 条目更新 | run_agent.py:8784 |
| 技能 nudge | 每 M 次主循环 / LLM 迭代 | SKILL.md 创建或补丁 | run_agent.py:11779 |
| 后台 review | nudge 命中后 fork Agent | 记忆/技能的实际写入 | run_agent.py:2458 |
如果你把 _memory_nudge_interval 和 _skill_nudge_interval 都设成 0,关闭的是"定期 nudge + 后台 review"这条在线闭环。 但这不等于整套沉淀能力彻底消失:轨迹采集仍然可以通过 save_trajectories 单独开启,记忆在 compression / session end 前的 commit_memory_session() flush 也仍然存在,只是它们不再由这两个定期 nudge 驱动。
这一讲我们先把这三个组件的全景串起来(前几讲各自拆过细节),然后进入新话题:轨迹数据和 RL 训练。
全景:一轮对话里闭环学习做了什么
把记忆、技能、后台 review 合在一起看,一轮完整的用户消息处理中,闭环学习系统的时序是这样的:
用户发消息
│
├─① 记忆 nudge 计数器递增(turn-based)
│ self._turns_since_memory += 1
│ 如果 >= _memory_nudge_interval → 设 _should_review_memory = True
│
├─② 主循环开始(while iteration_budget > 0)
│ │
│ ├─ 技能 nudge 计数器递增(iteration-based)
│ │ self._iters_since_skill += 1
│ ├─ API 调用
│ ├─ 工具执行
│ ├─ 如果 Agent 主动调了 memory → 重置 _turns_since_memory = 0
│ ├─ 如果 Agent 主动调了 skill_manage → 重置 _iters_since_skill = 0
│ └─ 继续循环...
│
├─③ 主循环结束,产出 final_response
│ 如果 _iters_since_skill >= _skill_nudge_interval → 设 _should_review_skills = True
│
├─④ 响应送达用户
│
└─⑤ 如果 _should_review_memory 或 _should_review_skills:
_spawn_background_review()
→ fork 静默 AIAgent(max_iterations=8)
→ 传入 _MEMORY_REVIEW_PROMPT / _SKILL_REVIEW_PROMPT / _COMBINED_REVIEW_PROMPT
→ review Agent 调 memory / skill_manage 工具写入
→ 扫描工具响应,打印 💾 摘要
几个关键的时序细节:
1. 记忆 nudge 在主循环之前检查,技能 nudge 在主循环内部递增、在主循环之后统一判定。 这不是随意的------记忆关注"用户说了什么"(输入侧),所以在输入处理阶段递增;技能关注"Agent 经历了多少轮推理与执行闭环"(执行侧),所以要等主循环跑完才知道这一轮累计是否命中阈值。
2. 主动调用 memory / skill_manage 会重置对应计数器 (run_agent.py:7821-7823、8141-8143)。这避免了"Agent 已经在主动管理记忆/技能了,nudge 还来催它"的冗余。
3. 两个计数器跨 run_conversation() 调用持久化 (run_agent.py:8745-8747)------CLI 多轮交互中不会重置,确保累积正确。
4. 后台 review 在响应送达后才执行------永远不会拖慢用户等待的响应时间。
两种 nudge 的本质区别
前两讲分别拆过记忆和技能的 nudge,这里用一张表把它们放在一起,突出设计选择背后的思考:
| 记忆 nudge | 技能 nudge | |
|---|---|---|
| 计什么 | 用户消息轮次 | 主循环 / LLM 迭代 |
| 默认间隔 | 10 轮 | 10 次迭代 |
| 配置路径 | memory.nudge_interval |
skills.creation_nudge_interval |
| 为什么这么计 | 记忆来自用户输入------用户说的越多,潜在新信息越多 | 技能来自执行经验------迭代越多,任务越复杂,越可能值得沉淀 |
| 典型场景 | 用户闲聊 10 轮,可能说了名字、偏好、项目 | 用户一句话触发 Agent 跑了很多轮"思考 → 调工具 → 再思考" |
| 不触发的场景 | Agent 跑了很多轮执行闭环但用户只说了 1 句话 | 用户和 Agent 聊了 20 轮但几乎没有进入工具执行闭环 |
为什么不统一用一种计数方式? 因为信息密度的分布不同。用户的 10 句话可能每句都包含新的偏好信息(记忆的原材料)。但一个只有文字回复的 10 轮对话,Agent 并没有"做什么"------没有试错、没有策略调整、不产生值得沉淀的方法论。
反过来,一个用户只说了"帮我把这个项目迁移到 Go"的单轮任务,可能触发 Agent 走很多轮推理与执行闭环------这些迭代里的试错和调整,才是技能的原材料。
这里要特别注意:技能 nudge 不是"一个 tool call 算一次" 。一次 assistant 响应即便发出多个并发工具调用,对 _iters_since_skill 通常也只记 1 次,因为它统计的是主循环 / LLM 迭代,而不是原子工具数。
记忆关注信息输入密度,技能关注执行复杂度。两种不同维度的信号。
_spawn_background_review() 的完整机制
第 06 讲拆过这个函数的骨架,这里补全它的后处理逻辑------review Agent 做完之后,怎么把结果展示给用户。
review Agent 运行完毕后(run_agent.py:2506-2542),主 Agent 会扫描 review Agent 的 _session_messages,从工具响应中提取成功动作:
# Scan the review agent's messages for successful tool actions
actions = [ ]
for msg in getattr(review_agent, "_session_messages", [ ]):
if not isinstance(msg, dict) or msg.get("role") != "tool":
continue
try:
data = json.loads(msg.get("content", "{}"))
except (json.JSONDecodeError, TypeError):
continue
if not data.get("success"):
continue
message = data.get("message", "")
target = data.get("target", "")
if "created" in message.lower():
actions.append(message)
elif "updated" in message.lower():
actions.append(message)
elif "added" in message.lower() or (target and "add" in message.lower()):
label = "Memory" if target == "memory" else "User profile" if target == "user" else target
actions.append(f"{label} updated")
elif "removed" in message.lower() or "replaced" in message.lower():
label = "Memory" if target == "memory" else "User profile" if target == "user" else target
actions.append(f"{label} updated")
if actions:
summary = " · ".join(dict.fromkeys(actions))
self._safe_print(f" 💾 {summary}")
这段扫描逻辑做了三件事:
-
只看
role == "tool"的消息(工具返回),跳过 assistant 的推理和用户消息 -
只看
"success": true的结果,忽略失败的尝试 -
用
dict.fromkeys()去重后用·拼接------如果 review Agent 往 MEMORY 和 USER 各加了一条,用户看到的就是💾 Memory updated · User profile updated
用户在正常使用中唯一能感知到后台 review 的,就是这行 💾 摘要。 没有弹框、没有确认流程、没有额外输出。
如果配置了 background_review_callback(Gateway 模式下常用),摘要还会通过回调推送给消息平台------比如在 Telegram 里多发一条消息。
从自我进化到训练数据:轨迹采集
到这里为止,我们聊的都是 session 级别的自我改进------Agent 在工作中积累记忆和技能,下次更好。但 Hermes Agent 的野心不止于此。
README 里反复强调的一个特性是 Batch Trajectory Generation ------用 Agent 的真实工作轨迹生成训练数据。这不是给用户的功能,而是给模型训练者的功能。
为什么 Agent 的工作轨迹是宝贵的训练数据
想想当前 tool-calling 模型是怎么训练的:
-
人工标注团队构造"用户问题 + 正确工具调用 + 工具返回 + 最终回答"的样本
-
样本质量参差不齐,覆盖面有限
-
标注成本极高,规模上不去
Hermes Agent 的思路是:让 Agent 自己跑任务,把完整的对话轨迹(包括推理、工具调用、工具返回、最终回答)保存下来,作为训练下一代模型的数据。
这些轨迹比人工标注有一个巨大优势:它们都包含真实的工具执行闭环。但要分清两种来源:
-
交互轨迹 :
save_trajectories保存真实会话,用户真的问了,Agent 真的跑了工具。 -
批量轨迹 :
batch_runner.py读取 JSONL 数据集逐条跑 prompt,它不一定来自真实用户会话,但工具执行和返回仍然是真实发生的。
换句话说,Hermes 既能采集在线交互轨迹,也能批量生成离线任务轨迹;它们的共同点不是"都来自真实用户",而是"都保留了真实执行过程"。
轨迹格式:ShareGPT 兼容
轨迹文件格式定义在 website/docs/developer-guide/trajectory-format.md。每条轨迹是一个 JSONL 行:
{
"conversations": [
{"from": "system", "value": "You are a function calling AI model..."},
{"from": "human", "value": "What Python version is installed?"},
{"from": "gpt", "value": "<think>\nI should run python3 --version.\n</think>\n<tool_call>\n{\"name\": \"terminal\", \"arguments\": {\"command\": \"python3 --version\"}}\n</tool_call>"},
{"from": "tool", "value": "<tool_response>\n{\"tool_call_id\": \"call_abc\", \"name\": \"terminal\", \"content\": \"Python 3.11.6\"}\n</tool_response>"},
{"from": "gpt", "value": "<think>\nGot the version.\n</think>\nPython 3.11.6 is installed."}
],
"timestamp": "2026-03-30T14:22:31.456789",
"model": "anthropic/claude-sonnet-4.6",
"completed": true
}
ShareGPT 是一个被广泛支持的对话训练数据格式------HuggingFace datasets、Axolotl、LLaMA-Factory 都能直接读。角色映射是:system → system,user → human,assistant → gpt,tool → tool。
归一化规则:让不同模型的输出长得一样
不同模型的推理输出五花八门------Anthropic 有 native thinking token,OpenAI 有 reasoning items,有的模型用 <REASONING_SCRATCHPAD> 标签。轨迹保存时,_convert_to_trajectory_format()(run_agent.py:2693)把所有变体归一化为 **<think>** 标签:
# 1. 原生 thinking token → <think> 包裹
if msg.get("reasoning") and msg["reasoning"].strip():
content = f"<think>\n{msg['reasoning']}\n</think>\n"
# 2. <REASONING_SCRATCHPAD> → <think> 替换
content += convert_scratchpad_to_think(msg["content"]) + "\n"
# 3. 没有推理也插一个空 <think> 块
# 确保每个 gpt turn 都有一致的格式
工具调用也做了归一化------从 API 的 tool_calls 数组转成 XML 包裹的 JSON:
<tool_call>
{"name": "terminal", "arguments": {"command": "ls -la"}}
</tool_call>
归一化的目的是让训练数据的格式完全一致,不管原始对话用的是哪个模型、哪种 API 协议。 这样训练时不需要针对不同模型做格式适配。
Batch Runner:批量生成轨迹
单次对话的轨迹保存通过 save_trajectory()(agent/trajectory.py:30-56)完成。但如果你想大规模生成训练数据 ,需要用 batch_runner.py。
Batch Runner 的核心流程(batch_runner.py:1-20):
1. 加载 JSONL 数据集(每行一个 prompt)
2. 分成多个 batch,用 multiprocessing.Pool 并行处理
3. 每个 prompt:创建 AIAgent → 跑 run_conversation() → 提取轨迹 + 工具统计
4. 写入 batch_{N}.jsonl
5. 所有 batch 合并成 trajectories.jsonl
6. 支持 --resume 断点续传(基于内容的去重检测)
Batch 格式比 CLI 格式多了几个字段:
{
"prompt_index": 42,
"conversations": [...],
"completed": true,
"partial": false,
"api_calls": 7,
"toolsets_used": ["code_tools", "file_tools"],
"tool_stats": {
"terminal": {"count": 3, "success": 3, "failure": 0},
"read_file": {"count": 2, "success": 2, "failure": 0},
"write_file": {"count": 0, "success": 0, "failure": 0}
}
}
tool_stats 被归一化到所有可能的工具名 (从 model_tools.TOOL_TO_TOOLSET_MAP 自动导出),未使用的工具填零。这保证了所有 JSONL 行的 schema 一致------加载到 HuggingFace Datasets 时不会因为 Arrow schema 不匹配而报错。
Atropos RL 集成:用轨迹训练模型
轨迹采集回答了"数据从哪来"的问题。下一步是"数据怎么用"。
Hermes Agent 的源码里实现了与 Tinker-Atropos RL 环境的集成接口(tools/rl_training_tool.py)。但这一节有个前提必须说清:要真正跑通这条链路,必须先初始化 tinker-atropos/ 子模块并安装对应依赖。 当前主仓只提供接线层,训练环境和脚本本体在子模块里。
在这个前提成立后,Tinker-Atropos 这条链路由两个组件构成:
-
Atropos:轨迹协调 API------接收环境产生的样本、分发给训练器、追踪指标
-
Tinker:训练服务------用 LoRA 适配器做 GRPO(Group Relative Policy Optimization),同时提供推理 API
10 个 RL 工具
rl_training_tool.py 注册了 10 个工具,让 Agent 可以在满足前置条件时驱动 RL 训练:
| 工具 | 功能 |
|---|---|
rl_list_environments |
发现 tinker-atropos 里的 BaseEnv 子类 |
rl_select_environment |
选择一个环境,加载其配置字段 |
rl_get_current_config |
查看可配置 vs 锁定的参数 |
rl_edit_config |
修改可配置的训练参数 |
rl_start_training |
启动三进程训练 |
rl_check_status |
监控进度(WandB 指标) |
rl_stop_training |
终止训练 |
rl_get_results |
获取最终指标 |
rl_list_runs |
列出所有活跃和已完成的训练 |
rl_test_inference |
快速验证(默认 3 步 × 16 completions × 3 模型) |
这里还有三个运行前提不能省:
-
rl_list_environments扫描的是tinker-atropos/tinker_atropos/environments/,不是主仓根下这个environments/目录。 -
这 10 个
rl_*工具都挂在check_rl_api_keys后面,至少要求有TINKER_API_KEY和WANDB_API_KEY。 -
rl_test_inference()还会额外检查OPENROUTER_API_KEY,因为它默认走 OpenRouter 做快速推理验证。
所以它们不是"仓库里有 10 个工具,Agent 就默认随时能用",而是"满足密钥、子模块和依赖前提后才会暴露的一组训练工具"。
训练的三进程架构
rl_start_training() 会启动三个子进程(rl_training_tool.py:314-428):
进程 1: run-api(Atropos API server,端口 8000)
↓ 等待 5 秒
进程 2: launch_training.py(Tinker trainer + 推理,端口 8001)
↓ 等待 30 秒(初始化 LoRA + FastAPI)
↓ 再等待 90 秒(确保推理服务就绪)
进程 3: environment.py serve(环境任务生成和评分)
启动是严格顺序的------每个进程起来后要等够时间再启动下一个,因为它们之间有 HTTP 依赖。如果任何进程异常退出,会调 _stop_training_run() 反向终止所有进程。
锁定字段:防止模型改坏基础设施配置
LOCKED_FIELDS(rl_training_tool.py:72-106)定义了一组不可修改的训练参数:
LOCKED_FIELDS = {
"env": {
"tokenizer_name": "Qwen/Qwen3-8B",
"rollout_server_url": "http://localhost:8000",
"max_token_length": 8192,
"total_steps": 2500,
...
},
"tinker": {
"lora_rank": 32,
"learning_rate": 0.00004,
...
},
}
Agent 可以通过 rl_edit_config 修改数据集路径、评分权重等"任务相关"参数;但 tokenizer、端口、LoRA rank、学习率这些"基础设施"参数是锁死的。
这是一个很好的"AI 自治边界"设计:让 Agent 在安全的范围内自主探索,但不允许它碰基础设施配置。
闭环的闭合:从 session 改进到模型改进
现在我们能看到完整的闭环了:
┌─────────────────────────────────────────────────────────────────┐
│ SESSION 级自我改进 │
│ │
│ 用户交互 → nudge 触发 → 后台 review → 记忆/技能写入 │
│ ↑ │ │
│ └────────── 下次 session 自动加载 ←──────────┘ │
│ │
│ 效果:单个 Agent 实例越用越好(同一用户、同一机器) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ MODEL 级自我改进 │
│ │
│ 批量任务 → 轨迹采集 → Batch Runner → trajectories.jsonl │
│ ↑ │ │
│ │ Atropos RL 环境 → GRPO + LoRA 训练 │ │
│ │ │ │ │
│ └────── 更新后的模型部署回 Agent ←──┘ │ │
│ │
│ 效果:所有 Agent 实例的底层模型变强(惠及所有用户) │
└─────────────────────────────────────────────────────────────────┘
上半圈(session 级)是在线的、增量的、个性化的------你的 Agent 记住你的偏好、沉淀你的工作方法。
下半圈(model 级)是离线的、批量的、普惠的------大量 Agent 的工作轨迹被收集起来,训练出更强的 tool-calling 模型,所有用户受益。
这两个闭环是正交的------session 级改进靠记忆/技能文件,model 级改进靠轨迹数据/RL 训练。它们可以独立工作,也可以叠加。
边界讨论:哪些"改进"可以自动化,哪些不能
闭环学习不是银弹。我们需要清醒地讨论它的边界。
可以自动化的
| 类型 | 示例 | 为什么可以 |
|---|---|---|
| 事实记忆 | "用户的时区是 UTC+8" | 确定性信息,错了也容易改 |
| 偏好记忆 | "用户讨厌 verbose 输出" | 用户会在后续交互中纠正 |
| 过程性技能 | "日志迁移的标准步骤" | 有明确的输入/输出/验证标准 |
| 踩坑经验 | "macOS 上 Docker 权限需要额外配置" | 事实性的、可验证的 |
不能自动化的(当前实现的安全边界)
1. 技能不能绕过安全扫描。 120 条威胁正则 + 四级信任策略,每次写入都扫描。Agent 无法创建包含已知危险模式的技能。
2. RL 训练不能修改锁定参数。 LOCKED_FIELDS 防止 Agent 改动基础设施配置。
3. 后台 review 不产生用户可见输出。 除了一行 💾 摘要,review Agent 的所有行为都对用户不可见------它不能代替主 Agent 说话。
4. 记忆有硬性空间限制。 2,200 + 1,375 = 3,575 字符。Agent 不能无限膨胀记忆来获取更多系统提示词空间。
应该但尚未实现的治理机制
诚实地说,当前的自我进化系统还缺几个治理能力:
1. 审计日志。 后台 review 写入了什么记忆、创建了什么技能------除了 💾 摘要,没有结构化的审计记录。如果你发现记忆里出现了奇怪的条目,很难追溯是哪次 review 写入的。
2. 记忆/技能的过期机制。 三个月前写入的"当前项目用 Webpack 4"可能早就过时了,但没有自动过期。依赖 Agent 在使用中发现过时并手动更新------这在实践中经常滞后。
3. 人在回路审批。 INSTALL_POLICY 里 agent-created + dangerous 的策略是 ask------理论上应该弹确认框。但后台 review 运行时用户不在交互循环里,所以当前实现把 ask 当 block 处理。真正的 human-in-the-loop 审批还没做。
这些不是"缺陷"------它们是有意识的取舍。 Hermes Agent 选择了"先让闭环跑起来,安全靠确定性规则兜底"的路线。在单用户 CLI 场景下,这个取舍是合理的------你自己的 Agent 往你自己的文件里写东西,风险可控。但如果要用在多租户 SaaS 场景,治理层需要补齐。
实战:观察一次完整的闭环学习
第一步:创造一个会触发两种 nudge 的场景
启动 CLI,给一个需要多轮工具调用的复杂任务:
hermes chat
帮我把当前目录下所有 .py 文件的 print() 替换成 logging.info(),
每个文件都要先读取、再修改、再验证。完成后告诉我改了几个文件。
这个任务通常会触发很多轮"读文件 → patch → 验证 → 再调整"的主循环往返,足以逼近甚至命中技能 nudge 的迭代阈值。
第二步:多聊几轮
任务完成后,继续在同一 session 聊几轮,分享一些偏好:
以后修改 Python 文件时,请保持 Black 格式化风格。
我的项目用 Poetry 管理依赖。
如果总轮次达到 10,记忆 nudge 也会触发。
第三步:观察 💾 输出
在某一轮响应结束后,你可能会看到类似:
💾 Skill 'print-to-logging-migration' created. · Memory updated · User profile updated
这就是后台 review 的产出------它同时触发了记忆和技能的写入。
第四步:验证写入
# 看记忆
cat ~/.hermes/memories/MEMORY.md
# 看技能
find ~/.hermes/skills -name SKILL.md
cat ~/.hermes/skills/print-to-logging-migration/SKILL.md
第五步:在新 session 中验证复用
退出 session,开一个新的,给一个类似任务:
hermes chat -q "帮我把另一个目录的 print() 也改成 logging"
Agent 应该会先 skills_list() 发现相关技能,再 skill_view() 加载完整步骤,然后按技能里记录的方法执行。第二次比第一次更快、更一致------这就是"自我进化"的可观测效果。
小结
这一讲我们从全景视角看了闭环学习系统的三个工程组件:
一,两种 nudge 的本质区别。 记忆 nudge 是 turn-based(关注信息输入密度),技能 nudge 是 iteration-based(关注主循环 / LLM 迭代,也就是执行复杂度)。两种维度的信号驱动两种类型的学习。
二,后台 review 的完整机制。 fork 静默 AIAgent → 传入 review prompt → 写入记忆/技能 → 扫描工具响应 → 打印 💾 摘要。永远在响应送达后执行,不阻塞主任务。
三,轨迹数据采集。 ShareGPT 格式 + 推理/工具调用归一化 + Batch Runner 大规模并行生成。Hermes 既能保存真实交互轨迹,也能批量生成离线任务轨迹;轨迹不只是调试产物,更是训练下一代 tool-calling 模型的原材料。
四,Atropos RL 集成。 10 个 RL 工具 + 三进程训练架构 + LOCKED_FIELDS 锁定基础设施参数。但这条链路建立在已初始化 tinker-atropos/ 子模块、配置好密钥和依赖的前提上;满足条件后,Agent 可以自己驱动 RL 训练,但不能碰锁定的配置。
五,两个闭环。 Session 级(记忆/技能,在线、个性化) + Model 级(轨迹/RL,离线、普惠)。两者正交,可独立工作。
六,边界是清晰的。 安全扫描、空间限制、锁定参数是已有的治理手段。审计日志、过期机制、真正的 human-in-the-loop 是还需要补齐的。
到这里,核心篇的四讲(记忆上下 + 技能 + 自我进化)全部完成。下一讲,我们进入安全篇。第 09 讲:技能安全------120 条威胁正则是怎么组织的?四级信任策略在安装流程里怎么生效?为什么选择确定性扫描而不是 LLM 语义审计?当 Agent 能写代码时,安全就不只是一个功能------它是整个系统的底线。我们下一讲见。