最近在学习 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,不能只看“接入了多少工具”,还要看它如何处理长期运行中的几个矛盾:

  1. 上下文需要更新,但 Prompt Cache 希望前缀稳定。
  2. 会话需要跨平台继续,但不同平台有不同的用户和频道标识。
  3. Agent 需要学习经验,但学习过程不能干扰当前任务。
  4. 工具需要灵活组合,但权限边界必须能够收紧。
  5. 历史需要完整保留,但又不能每次全部塞进上下文。

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()

源码见
agent/system_prompt.py

这里源码把记忆、用户资料和日期放在 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 什么时候才会刷新记忆快照

主要有两个时机:

  1. 开启一个新会话;
  2. 上下文压缩后重建 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

这个设计同时解决了三个问题:

  1. 验证新平台确实可以运行 Agent;
  2. 给用户一个明确的迁移成功反馈;
  3. 让模型主动恢复当前工作状态。

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。

它的优先顺序大致是:

  1. 更新当前已经使用的 Skill;
  2. 更新已有的类别级 Skill;
  3. 给已有 Skill 增加 reference、template 或 script;
  4. 实在没有合适归属时,才创建新的类别级 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 状态系统。

源码还处理了两个工程细节:

  1. NFS、SMB 或部分 FUSE 文件系统不兼容 WAL 时,回退到 DELETE 模式;
  2. 多进程竞争写锁时,使用带随机抖动的应用层重试,减少同时重试形成的拥塞。

相关代码位于
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 的冲突;
  • 终端切换与会话连续性的冲突;
  • 自我学习与前台响应速度的冲突;
  • 工具组合与权限收缩的冲突;
  • 完整历史与有限上下文窗口的冲突。

其中我最喜欢的两个设计是:

  1. 冻结快照记忆:将“立即持久化”和“立即改变 Prompt”拆开,以会话级一致性换取缓存稳定性。
  2. 会话迁移:不复制一份缩水的摘要,而是迁移会话入口与原始 session_id 的绑定。

这两个机制让我更清楚地理解了 Agent Harness 的价值:

Harness 不只是替模型调用工具,而是在模型之外维护时间、状态、身份、权限和连续性。

模型负责完成当前一步。

Harness 决定这个 Agent 能不能稳定地工作几天、几个月,甚至更久。

参考资料

最近一条

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐