读 Hermes Agent 源码:一个会成长的 Agent Harness 是怎样设计的
最近在学习 Agent Harness。
这里的重点不是“模型 API 怎么调用”,而是一个真正能长期运行的 Agent 系统,如何组织:
- 系统提示词和上下文;
- 长期记忆;
- 工具及权限;
- 会话状态;
- 多平台消息入口;
- 历史检索;
- 技能沉淀和自我改进。
这次我选择阅读 Hermes Agent。
它把自己定位为一个 Self-hosted growing agent:不仅能完成当前任务,还试图将用户偏好、工作经验和会话历史沉淀下来,让 Agent 在长期使用中逐渐成长。
读完源码后,我觉得最值得学习的不是某个具体工具,而是下面几项 Harness 设计:
| 机制 | 解决的问题 | 核心思路 |
|---|---|---|
| 冻结快照记忆 | 记忆更新导致 Prompt Cache 失效 | 会话开始时注入,运行中保持不变 |
| 会话迁移 | 更换终端或平台后无法继续上下文 | 迁移 session 绑定,而不是复制摘要 |
| 主动技能沉淀 | 经验只停留在当前对话 | 后台 Agent 复盘并更新技能 |
| Toolsets | 工具数量和权限难以管理 | 工具集递归组合,禁用规则最终扣除 |
| SQLite + WAL + FTS5 | 长期会话难存储、难搜索 | 统一数据库、并发读取、全文检索 |
| 多平台 Gateway | 每个平台形成独立 Agent | 单进程适配多平台,共享会话系统 |
本文使用的源码版本为 Hermes Agent commit
210f4e7。
01、先理解 Agent Harness 在做什么
大模型只负责根据输入生成输出。
但一个可以真正工作的 Agent,还需要外部系统解决大量工程问题:
用户消息
↓
消息平台 / CLI
↓
会话路由与状态恢复
↓
组装 System Prompt、记忆和上下文
↓
模型推理
↓
工具权限检查与执行
↓
结果持久化、历史检索、后台复盘
↓
返回用户
这些围绕模型建立起来的运行环境,就是 Agent Harness。
因此,评价一个 Harness,不能只看“接入了多少工具”,还要看它如何处理长期运行中的几个矛盾:
- 上下文需要更新,但 Prompt Cache 希望前缀稳定。
- 会话需要跨平台继续,但不同平台有不同的用户和频道标识。
- Agent 需要学习经验,但学习过程不能干扰当前任务。
- 工具需要灵活组合,但权限边界必须能够收紧。
- 历史需要完整保留,但又不能每次全部塞进上下文。
Hermes 的几个关键机制,基本都围绕这些矛盾展开。
02、冻结快照记忆:为什么记忆更新后不立刻进入 System Prompt
第一次看到“冻结快照记忆”时,我觉得它有些反直觉。
假设用户在会话中告诉 Agent:
以后给我解释代码时优先使用中文,并先讲设计目的,再讲实现细节。
Agent 调用 memory 工具将它写入 USER.md。
直觉上,下一轮模型调用就应该重新读取 USER.md,把这条偏好加入 System Prompt。
但 Hermes 没有这么做。
它会立即把新记忆写入磁盘,却继续使用会话开始时的旧 System Prompt。新记忆通常要等到下一次会话,或者上下文压缩导致 Prompt 重建时,才会正式进入系统提示词。
这不是遗漏,而是一个明确的性能设计。
2.1 Prefix Cache 为什么要求前缀稳定
一次 Agent 请求通常可以简化为:
[System Prompt]
[Tool Definitions]
[Conversation History]
[Current User Message]
其中 System Prompt 和工具定义往往很长,但在多轮会话中变化不大。
大模型服务可以缓存已经计算过的公共前缀。下一轮请求只要前缀保持一致,就可以复用之前的计算结果,降低延迟和成本。
第 1 轮:
[稳定系统提示词 A][历史 1][用户问题 1]
└────── 可以缓存 ──────┘
第 2 轮:
[稳定系统提示词 A][历史 1][回答 1][用户问题 2]
└────── 命中旧缓存 ──────┘
如果每次写入记忆都重新生成 System Prompt,就会变成:
第 1 轮:[系统提示词 A] ...
第 2 轮:[系统提示词 B] ...
第 3 轮:[系统提示词 C] ...
即使只增加了一条用户偏好,整个前缀也可能无法继续命中缓存。
所以 Hermes 选择了一个很工程化的取舍:
记忆可以立即持久化,但当前会话的 System Prompt 尽量保持字节级稳定。
2.2 两份记忆状态
Hermes 的 MemoryStore 同时维护两份状态:
MEMORY.md / USER.md
↓ 会话开始加载
┌────────────────────┬────────────────────┐
│ 冻结 Prompt 快照 │ 实时记忆状态 │
│ snapshot │ live entries │
├────────────────────┼────────────────────┤
│ 注入 System Prompt │ memory 工具读写 │
│ 会话中不修改 │ 立即持久化到磁盘 │
└────────────────────┴────────────────────┘
对应源码在
tools/memory_tool.py。
下面是根据源码删减后的核心逻辑:
class MemoryStore:
def __init__(self):
self.memory_entries = [] # 实时状态
self.user_entries = []
self._prompt_snapshot = {} # 冻结快照
def load_from_disk(self):
self.memory_entries = read("MEMORY.md")
self.user_entries = read("USER.md")
self._prompt_snapshot = {
"memory": render(sanitize(self.memory_entries)),
"user": render(sanitize(self.user_entries)),
}
def add(self, target, content):
entries = self._entries_for(target)
entries.append(content)
self.save_to_disk(target)
# 不修改 _prompt_snapshot
def format_for_system_prompt(self, target):
return self._prompt_snapshot.get(target)
这里有三个值得注意的点。
第一,工具操作的是实时状态,因此 memory 工具执行后,可以立即返回最新结果。
第二,写入会立即落盘,所以即使程序随后退出,记忆也不会丢失。
第三,System Prompt 读取的始终是 _system_prompt_snapshot,而不是实时列表。源码中的
format_for_system_prompt()
明确保持了这个边界。
还有一个容易忽略的安全细节:Hermes 在生成冻结快照前,会扫描记忆条目中的提示词注入和 promptware 模式。
如果某条磁盘记忆命中风险规则:
原始内容 → 继续保留在实时状态中,用户可以查看和删除
Prompt 快照 → 替换成 [BLOCKED: ...] 占位信息
也就是说,它不会悄悄删除原始数据,让攻击痕迹从用户视野中消失;但也不会把可疑内容直接注入高权限的 System Prompt。
2.3 System Prompt 本身也只构建一次
冻结记忆只是第一层。
Hermes 还会将完整 System Prompt 缓存在 agent._cached_system_prompt 中。正常情况下,一个会话只构建一次,后续轮次直接复用。
核心逻辑可以概括为:
def build_system_prompt(agent):
stable = build_identity_and_rules()
context = build_context_files()
volatile = build_memory_user_date()
return join(stable, context, volatile)
def invalidate_system_prompt(agent):
agent._cached_system_prompt = None
agent._memory_store.load_from_disk()
这里源码把记忆、用户资料和日期放在 volatile 层,但要注意:
volatile 表示它们可能在不同会话或 Prompt 重建之间变化,并不表示每轮都会重新注入。
最终三层仍然会拼成一个完整字符串,并在整个会话中缓存。
对于长期运行的 Gateway,只依赖 Python 对象中的内存缓存还不够。
因为不同轮次可能会重新创建 AIAgent,如果每次都重新拼装 System Prompt,日期、插件状态或上下文扫描结果的细微变化仍然可能破坏前缀。
Hermes 因此还会把组装后的完整 System Prompt 保存到 session 数据库。继续旧会话时,优先恢复数据库中的原始字符串,只有旧会话没有保存过 Prompt 时才重新构建:
stored_prompt = session_db.get_system_prompt(session_id)
if stored_prompt:
agent._cached_system_prompt = stored_prompt
else:
agent._cached_system_prompt = build_system_prompt()
session_db.update_system_prompt(session_id, agent._cached_system_prompt)
对应逻辑位于
agent/conversation_loop.py。
这一层很重要:它让 Prefix Cache 的稳定性不依赖某个进程内对象一直存活,而是成为会话持久化协议的一部分。
2.4 什么时候才会刷新记忆快照
主要有两个时机:
- 开启一个新会话;
- 上下文压缩后重建 System Prompt。
上下文压缩发生时,Hermes 会:
清除旧 System Prompt 缓存
↓
重新读取 MEMORY.md / USER.md
↓
生成新的冻结快照
↓
为压缩后的新会话保存 System Prompt
对应代码在
agent/conversation_compression.py。
2.5 一个具体例子
假设会话开始时 USER.md 中只有:
- 用户是一名后端开发者。
因此冻结快照也是这条内容。
对话中,用户又说:
以后代码示例优先使用 Python。
Agent 调用 memory 工具后:
磁盘 USER.md:
- 用户是一名后端开发者。
- 代码示例优先使用 Python。
实时记忆:
- 用户是一名后端开发者。
- 代码示例优先使用 Python。
当前 System Prompt 快照:
- 用户是一名后端开发者。
本轮工具结果可以让模型知道写入成功,但接下来的请求仍使用旧 System Prompt。
到了下一个会话:
重新加载 USER.md
↓
生成新快照
↓
“代码示例优先使用 Python”正式成为系统上下文
这让我意识到,Agent 记忆并不一定追求“写入后立刻改变行为”。
对于长期偏好来说,稍晚一个会话生效通常可以接受;而稳定 Prefix Cache 带来的持续收益,可能更加重要。
2.6 外部记忆为什么不修改 System Prompt
Hermes 还支持外部记忆提供商。
外部记忆的召回结果可能每轮都不同。如果把它们直接拼进 System Prompt,同样会破坏稳定前缀。
因此 Hermes 将动态召回内容包装成 <memory-context>,在当前用户消息附近注入,而不是修改缓存好的 System Prompt。
相关流程在
agent/conversation_loop.py
和
agent/memory_manager.py。
这实际上形成了两条记忆通道:
| 记忆类型 | 注入位置 | 更新频率 | 适合内容 |
|---|---|---|---|
| 内置冻结记忆 | System Prompt | 会话级 | 用户偏好、长期事实 |
| 外部动态召回 | 当前调用上下文 | 每轮 | 与当前问题相关的历史 |
2.7 这个设计的收益与代价
收益:
- System Prompt 在会话中保持稳定;
- 更容易命中 LLM Prefix Cache;
- 降低重复计算和请求成本;
- 记忆写入仍然具备持久性;
- 动态召回不会污染稳定前缀。
代价:
- 新写入的偏好不会立即成为系统级指令;
- 同一会话中的模型行为可能暂时与最新记忆不一致;
- 需要明确区分实时状态、磁盘状态和 Prompt 快照。
这是一个典型的 Harness 设计:它没有追求单个模块的“绝对实时”,而是在行为一致性、性能和长期运行成本之间做平衡。
03、会话迁移:迁移的不是聊天摘要,而是会话身份
第二个让我印象很深的机制是 /handoff。
一个常见场景是:
白天在电脑 CLI 中工作
↓
需要离开电脑
↓
希望在 Telegram / Discord / Slack 中继续
最简单的实现可能是把当前对话总结一下,再发送到目标平台。
但摘要一定会损失信息,例如:
- 精确的用户原话;
- 工具调用和工具结果;
- 中间失败的尝试;
- 完整的角色结构;
- 当前任务尚未完成的细节。
Hermes 的做法不是复制摘要,而是把目标平台的会话路由,重新绑定到原来的 CLI session_id。
3.1 先区分 session key 和 session id
理解迁移前,需要区分两个概念。
session_key 表示“从哪里来”:
agent:main:telegram:dm:123456 agent:main:discord:thread:987654
它由平台、聊天类型、频道、线程和用户等信息确定,用来路由新消息。
session_id 表示“这是哪段会话”:
7d3b...a921
真正的消息历史、系统提示词和会话元数据都属于 session_id。
可以把两者理解成:
session_key = 入口地址 session_id = 会话实体
正常情况下:
Telegram 入口 ──→ Session A
CLI 入口 ──→ Session B
执行 handoff 后:
CLI 入口 ──→ Session B
Telegram 入口 ──→ Session B
迁移的核心就是修改这个绑定关系。
3.2 /handoff 的完整流程
Hermes 使用 SQLite 中的状态机协调 CLI 和 Gateway:
None
↓ CLI 请求
pending
↓ Gateway 原子领取
running
├── 成功 → completed
└── 失败 → failed
完整执行过程如下:
CLI: /handoff telegram
↓
state.db 写入 pending
↓
Gateway watcher 发现任务
↓
原子更新 pending → running
↓
找到 Telegram Home Channel
↓
尝试创建独立 Thread
↓
生成目标平台 session_key
↓
session_key 重新绑定原 CLI session_id
↓
清理目标入口的 Agent 缓存
↓
发送一条内部续接消息
↓
Agent 加载完整历史并主动回复
↓
completed
CLI 入口代码位于
cli.py。
状态机位于
hermes_state.py。
3.3 为什么需要原子 claim
Gateway 会定期扫描待处理迁移:
for row in list_pending_handoffs():
if not claim_handoff(row["id"]):
continue
process_handoff(row)
claim_handoff() 执行的本质是:
UPDATE sessions
SET handoff_state = 'running'
WHERE id = ? AND handoff_state = 'pending';
只有成功将 pending 改成 running 的 Gateway 才能继续处理。
这避免了:
- 同一个 Gateway 在两次轮询中重复处理;
- 多个 Gateway 同时领取同一个任务;
- CLI 已经重试时出现重复迁移。
这也是一个很典型的 Harness 问题:模型并不关心分布式任务领取,但运行模型的系统必须关心。
3.4 核心操作:重新绑定 Session
Gateway 确定目标平台后,会构造目标 session_key,然后执行:
session_store.get_or_create_session(destination)
session_store.switch_session(
destination_session_key,
original_cli_session_id,
)
对应源码在
gateway/run.py
和
gateway/session.py。
switch_session() 不会复制消息,而是让目标入口直接指向旧会话。
这样下一轮执行时,就可以通过原始 session_id 恢复完整的 role-aware transcript,包括:
user message
assistant message
tool call
tool result
reasoning metadata
3.5 为什么迁移后还要制造一条内部消息
只修改数据库绑定还不够。
如果目标平台没有新消息,用户不会立刻看到任何变化,也不知道迁移是否成功。
因此 Gateway 会创建一条 internal=True 的合成消息,大意是:
会话刚刚从 CLI 迁移到这里。
完整历史已经加载。
请确认已在当前平台继续工作,并简要总结此前任务。
然后把它交给正常消息处理管线。
源码见
gateway/run.py。
这个设计同时解决了三个问题:
- 验证新平台确实可以运行 Agent;
- 给用户一个明确的迁移成功反馈;
- 让模型主动恢复当前工作状态。
3.6 一个实际例子
假设我在 CLI 中让 Hermes 排查一个接口超时问题。
此前会话中已经发生:
1. 读取网关配置;
2. 搜索超时参数;
3. 运行测试;
4. 发现连接池配置不合理;
5. 正准备修改配置。
此时执行:
/handoff telegram
迁移成功后,Telegram 中不是只收到一句模糊的摘要,而是由同一个会话继续:
我们刚才正在排查接口超时。
已经确认问题更可能来自连接池配置,而不是路由层。
测试已经完成,下一步准备修改 pool timeout 并重新验证。
之后用户在 Telegram 回复“继续”,新的消息仍会进入原来的 session_id。
这就是“会话迁移”和“发送一份聊天摘要”的根本区别。
3.7 失败时为什么 CLI 会话仍然保留
目标平台可能没有启动、Home Channel 没配置,或者发送消息失败。
Hermes 会把状态改为 failed 并记录错误,但不会销毁 CLI 原会话。
因此失败后的语义是:
迁移失败 ≠ 原会话丢失
用户可以修复平台配置后重新执行 handoff,也可以继续留在 CLI 工作。
这说明一个成熟的迁移设计必须具备:
- 可观察的状态;
- 原子领取;
- 明确的成功和失败终态;
- 失败后的原状态保留;
- 幂等或可安全重试的操作。
04、主动技能沉淀:让经验从对话变成程序化记忆
长期记忆适合保存:
- 用户是谁;
- 用户偏好什么;
- 当前有哪些长期事实。
但“以后遇到同类任务应该怎么做”更适合保存成 Skill。
Hermes 对两者的区分可以概括为:
Memory:记住事实和用户 Skill:记住做事的方法
例如:
“用户喜欢简短回答” → Memory / USER.md “排查某类 API 超时的检查顺序” → Skill
4.1 后台 Review Agent
Hermes 在正常回答结束后,可以启动一个后台 Agent 复盘当前会话:
主 Agent 完成用户任务
↓
先把回答返回给用户
↓
后台复制会话快照
↓
Review Agent 判断是否需要:
- 保存用户记忆
- 更新已有 Skill
- 增加 reference / template / script
- 创建新的类别级 Skill
入口位于
agent/background_review.py。
它有几项重要约束:
review_agent = AIAgent(
model=parent.model,
provider=parent.provider,
skip_memory=True,
)
review_agent._cached_system_prompt = parent._cached_system_prompt
review_agent._memory_store = parent._memory_store
allow_tools(["memory", "skills"])
源码见
agent/background_review.py。
4.2 为什么使用独立 Agent
如果让主 Agent 在回答用户前先反思、写记忆、更新技能,会带来几个问题:
- 增加用户等待时间;
- 复盘任务可能抢占当前任务的注意力;
- 技能管理工具可能干扰主工具循环;
- 复盘失败可能影响正常回答。
Hermes 选择在回答发送后异步执行,因此自我改进是 best-effort:
复盘成功 → 沉淀经验 复盘失败 → 不影响用户当前任务
4.3 为什么继承完全相同的 System Prompt
后台 Agent 会直接继承父 Agent 的缓存 System Prompt。
这不仅保证复盘 Agent 知道当前环境,也是在继续维护 Prefix Cache:
父 Agent 请求前缀 ≈ Review Agent 请求前缀
甚至工具定义也尽量保持一致,然后在运行时使用白名单阻止无关工具。
如果一开始就删除工具定义,请求前缀会发生变化,反而可能无法复用已经预热的缓存。
所以这里又体现了 Hermes 的一条设计主线:
Prompt Cache 稳定性不是单个函数的优化,而是贯穿记忆、System Prompt、工具定义和后台任务的系统级约束。
4.4 技能不是无限增长
后台复盘并不是每次都创建一个新 Skill。
它的优先顺序大致是:
- 更新当前已经使用的 Skill;
- 更新已有的类别级 Skill;
- 给已有 Skill 增加 reference、template 或 script;
- 实在没有合适归属时,才创建新的类别级 Skill。
例如今天解决了一个 PostgreSQL 慢查询问题,不应该创建:
fix-order-query-2026-06-07
而应该更新更稳定的类别级技能:
postgres-performance-debugging
并把本次特殊执行计划、错误记录或复现步骤放进:
references/order-query-case.md
这可以避免技能库退化成一堆只适用于单次任务的碎片。
05、Toolsets:工具组合和权限收缩
随着 Agent 能力增加,直接维护一个巨大的工具列表会越来越困难。
Hermes 将工具组织成 Toolset:
web
files
browser
memory
skills
hermes-cli
hermes-telegram
...
一个 Toolset 可以包含具体工具,也可以继续包含其他 Toolset。
5.1 递归解析
核心逻辑位于
toolsets.py:
def resolve_toolset(name, visited=None):
visited = visited or set()
if name in visited:
return []
visited.add(name)
definition = get_toolset(name)
tools = set(definition["tools"])
for child in definition["includes"]:
tools.update(resolve_toolset(child, visited))
return sorted(tools)
visited 同时处理两种情况:
- 环形依赖;
- 菱形依赖导致的重复展开。
5.2 禁用规则为什么最后执行
Hermes 的工具计算过程可以简化为:
启用 Toolsets
↓
递归展开所有工具
↓
合并插件工具
↓
扣除 disabled Toolsets
↓
执行 check_fn 环境检查
↓
将最终工具 Schema 发给模型
源码见
model_tools.py。
禁用规则必须最后执行。
例如:
enabled = ["hermes-cli"]
disabled = ["browser"]
即使 hermes-cli 间接包含 browser,最终也必须将 browser 工具扣除。
这体现了一条安全原则:
权限收缩应该覆盖能力组合,禁止项的优先级高于便捷的聚合配置。
06、SQLite + WAL + FTS5:历史属于 Harness,而不是上下文窗口
一个长期 Agent 会积累大量会话。
不能把全部历史永远放进模型上下文,但也不能在结束会话后直接丢掉。
Hermes 使用 SQLite 保存:
- session 元数据;
- user / assistant 消息;
- 工具调用;
- 工具结果;
- System Prompt 快照;
- 父子会话关系;
- handoff 状态。
Gateway 和 CLI 使用的是同一套存储,因此会话恢复、历史搜索和跨平台迁移才有共同基础。
6.1 为什么使用 WAL
Gateway、CLI 和其他 Agent 进程可能同时访问 state.db:
Gateway:持续写入平台消息
CLI:写入当前会话
session_search:读取历史
后台任务:更新状态
Hermes 默认启用 SQLite WAL:
connection.execute("PRAGMA journal_mode=WAL")
WAL 允许读操作和写操作更好地并发,适合“多个读取者、少量写入者”的 Agent 状态系统。
源码还处理了两个工程细节:
- NFS、SMB 或部分 FUSE 文件系统不兼容 WAL 时,回退到 DELETE 模式;
- 多进程竞争写锁时,使用带随机抖动的应用层重试,减少同时重试形成的拥塞。
相关代码位于
hermes_state.py
和
SessionDB。
6.2 为什么需要 FTS5
长期会话的价值不只在于“恢复最近一次对话”,还在于搜索过去。
Hermes 为消息建立 FTS5 索引,索引内容包括:
消息正文 + 工具名称 + 工具调用参数
查询结果可以按 BM25 相关度排序,也可以按时间排序。
核心表结构位于
hermes_state.py。
6.3 中文搜索的 Trigram 索引
默认的全文分词器对中文短语并不总是理想。
Hermes 额外建立 trigram FTS5 表,将文本拆成连续的三字符片段,从而改善中文、日文、韩文等文本的子串搜索。
CREATE VIRTUAL TABLE messages_fts_trigram
USING fts5(content, tokenize='trigram');
搜索时:
普通英文查询 → 标准 FTS5
较长 CJK 查询 → trigram FTS5
很短的 CJK 查询 → LIKE 兜底
这说明所谓“Agent 记忆”不只是向量数据库。
在本地 Harness 中,SQLite、关键词检索、时间排序和上下文窗口组合,往往已经能解决大量实际问题。
07、多平台 Gateway:平台只是入口,不应该拥有 Agent
Hermes Gateway 在一个长期运行进程中加载多个平台适配器。
每个平台负责:
- 接收平台事件;
- 转换成统一 MessageEvent;
- 发送消息;
- 处理线程、频道等平台特性。
Gateway 负责:
- 构造确定性的 session_key;
- 找到或创建 session_id;
- 恢复完整历史;
- 创建 Agent;
- 执行工具循环;
- 保存消息;
- 将回复交还对应适配器。
Telegram ─┐
Discord ─┤
Slack ─┼→ Gateway → Session Store → Agent Loop
Matrix ─┤
CLI ─┘
确定性的 Session Key 构造位于
gateway/session.py。
这个架构使平台成为“输入输出适配器”,而不是各自维护一套 Agent 状态。
也正因为 Session Store 位于平台之下,/handoff 才能把同一个会话绑定到另一个平台入口。
08、把这些机制连起来看
单独看每个功能,它们似乎互不相关:
- Memory 管记忆;
- Gateway 管消息;
- SQLite 管数据;
- Toolsets 管工具;
- Background Review 管技能。
但从 Agent Harness 的角度看,它们其实围绕同一个目标:
让 Agent 在长时间、多轮次、多终端环境中持续工作,同时控制成本、权限和状态复杂度。
完整关系可以概括为:
┌─────────────────────┐
多平台消息 ─────→ │ Gateway / Session Key│
└──────────┬──────────┘
↓
┌─────────────────────┐
│ SQLite Session Store │
│ WAL + FTS5 │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ System Prompt │
│ Frozen Memory │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ Agent Tool Loop │
│ Toolset Permissions │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ Background Review │
│ Memory + Skills │
└─────────────────────┘
09、我从 Hermes 学到的 Agent Harness 设计原则
原则一:稳定上下文和动态上下文要分层
不是所有信息都应该每轮重新注入。
可以将上下文拆成:
稳定层:身份、行为规则、工具说明
会话层:冻结记忆、用户资料、上下文文件
动态层:当前消息、外部记忆召回、工具结果
变化越少的内容越靠前,越有利于缓存复用。
原则二:会话身份应该独立于传输平台
Telegram、CLI 和 Discord 只是入口。
真正的会话应该拥有独立 ID,并由路由键指向它。这样才能实现:
- 恢复;
- 分支;
- 压缩;
- 迁移;
- 跨平台继续。
原则三:自我改进不能阻塞主任务
记忆整理、技能复盘和经验沉淀都很重要,但它们不应该增加用户当前任务的关键路径。
比较合理的方式是:
先完成任务 → 返回结果 → 后台复盘 → best-effort 持久化
原则四:工具权限要在运行时再次收紧
只改变 Prompt 中展示的工具并不够。
真正安全的 Harness 还需要在工具执行时检查:
- 当前 Agent;
- 当前平台;
- 当前用户;
- 当前工具集;
- 白名单或黑名单;
- 环境是否满足;
- 后台任务是否具有更窄权限。
原则五:历史检索是 Harness 的职责
上下文窗口不是数据库。
长期历史应该持久化在外部存储中,通过:
- 全文搜索;
- 时间窗口;
- 会话浏览;
- 向量召回;
- 摘要;
按需返回给模型。
10、总结
Hermes Agent 最吸引我的地方,是它没有把“长期 Agent”简单理解成:
模型 + 更多工具 + 一个向量数据库
它真正处理的是长期运行中逐渐暴露出来的系统问题:
- 记忆更新与 Prefix Cache 的冲突;
- 终端切换与会话连续性的冲突;
- 自我学习与前台响应速度的冲突;
- 工具组合与权限收缩的冲突;
- 完整历史与有限上下文窗口的冲突。
其中我最喜欢的两个设计是:
- 冻结快照记忆:将“立即持久化”和“立即改变 Prompt”拆开,以会话级一致性换取缓存稳定性。
- 会话迁移:不复制一份缩水的摘要,而是迁移会话入口与原始 session_id 的绑定。
这两个机制让我更清楚地理解了 Agent Harness 的价值:
Harness 不只是替模型调用工具,而是在模型之外维护时间、状态、身份、权限和连续性。
模型负责完成当前一步。
Harness 决定这个 Agent 能不能稳定地工作几天、几个月,甚至更久。
参考资料
- Agent-Learning-Hub Stage 3
- Hermes Agent
- MemoryStore
- System Prompt 构建与失效
- 会话迁移状态机
- Gateway Handoff
- 后台技能与记忆复盘
- Toolsets 递归解析
- SQLite、WAL 与 FTS5
最近一条
更多推荐



所有评论(0)