HuanCode Docs

Hermes Agent 是如何越用越聪明的

没有梯度更新,没有微调,Hermes 靠三个互相配合的持久化系统实现跨会话的知识积累——Memory、Skills、Session Search,加上一个后台自动复盘机制,让 Agent 真正做到"用得越多,懂得越多"。

上一篇文章我们打好了地基——搞懂了大模型 API 调用、多轮对话和 Tool Use。但一个只能"对话"的模型,充其量是个高级聊天机器人。真正好用的 Agent,应该越用越懂你:记住你的偏好、学会新的工作流、遇到老问题能翻出以前的解法。

Hermes Agent 就做到了这一点。但它不是通过机器学习(没有梯度更新、没有微调),而是靠 3 个互相配合的持久化系统 实现跨会话的知识积累。

本文带你从源码级别拆解这套机制。


总览:三个系统 + 一个触发器

先看全景图:

系统类比存储位置作用
Memory 工具随手记笔记MEMORY.md / USER.md记住环境事实和用户偏好
Skills 技能库写操作手册skills/*.md存储可复用的工作流
Session Search翻以前的日记SQLite FTS5全文检索历史对话
Background Review每隔一段时间主动复盘后台线程 + 子 Agent自动触发上述三者

三者各有分工:Memory 存结论,Skills 存方法,Session Search 存完整历史。而 Background Review 是让这一切自动运转的发动机。


系统一:记忆工具 (Memory Tool)

源码位置: tools/memory_tool.py(585 行)

两个纯文本文件

Hermes 的"记忆"本质上就是两个 Markdown 文件:

~/.hermes/memories/
├── MEMORY.md   ← Agent 的笔记(环境事实、项目约定、踩过的坑)
└── USER.md     ← 关于你的画像(偏好、工作风格、沟通习惯)

MEMORY.md 存的是客观信息——"这个项目用 pnpm 不用 npm"、"生产环境的数据库在 AWS us-east-1"。USER.md 存的是关于你的信息——"用户喜欢简洁的终端风格回答"、"用户是后端工程师,前端经验较少"。

LLM 通过一个 memory 工具来读写这两个文件:

# 记住用户偏好
memory(action="add", target="user",
       content="用户喜欢简洁的终端风格回答,不需要 markdown 格式")

# 记住项目事实
memory(action="add", target="memory",
       content="这个项目的 CI 用 GitHub Actions,不是 Jenkins")

# 修正过时信息
memory(action="replace", target="memory",
       old_text="数据库在 us-east-1",
       content="数据库已迁移到 ap-southeast-1")

冻结快照:保护 Prompt Cache

这里有一个精妙的设计:Memory 的写入和读取是异步的

class MemoryStore:
    def __init__(self, memory_char_limit=2200, user_char_limit=1375):
        self.memory_entries: List[str] = []
        self.user_entries: List[str] = []
        # 关键:冻结快照
        self._system_prompt_snapshot: Dict[str, str] = {"memory": "", "user": ""}

    def load_from_disk(self):
        self.memory_entries = self._read_file(mem_dir / "MEMORY.md")
        self.user_entries = self._read_file(mem_dir / "USER.md")
        # 拍一张快照,整个会话期间不变
        self._system_prompt_snapshot = {
            "memory": self._render_block("memory", self.memory_entries),
            "user": self._render_block("user", self.user_entries),
        }

流程是这样的:

  1. 会话启动时,从磁盘加载 MEMORY.mdUSER.md,生成一份冻结快照注入 system prompt
  2. 会话中间,如果 LLM 调用 memory(action="add"),内容立即写入磁盘文件
  3. 但当前会话的 system prompt 不变——快照是冻结的
  4. 下次会话启动时,重新加载磁盘文件,快照刷新

为什么要这样设计?因为 Anthropic 的 Prompt Cache

Anthropic 的 API 有一个缓存机制:如果请求的 system prompt 前缀和上一次完全相同,服务端可以复用缓存的 KV 状态,token 开销只有正常价格的 10%。如果你在会话中间改了 system prompt(哪怕加一行 memory),缓存就失效了,所有后续请求都要全价重算。

所以 Hermes 选择了"写入立即持久化,读取延迟到下次会话"的策略——牺牲了"本次会话立即生效"的即时性,换来了整个会话期间的缓存命中率。

安全扫描

写入 memory 之前,内容会经过安全扫描:

_MEMORY_THREAT_PATTERNS = [
    (r'ignore\s+(previous|all|above|prior)\s+instructions', "prompt_injection"),
    (r'you\s+are\s+now\s+', "role_hijack"),
    (r'do\s+not\s+tell\s+the\s+user', "deception_hide"),
    (r'curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET)', "exfil_curl"),
    (r'authorized_keys', "ssh_backdoor"),
]

因为 memory 内容会注入 system prompt,如果不过滤,恶意用户可以通过"让 agent 记住一段提示词注入"来劫持后续会话。这不是理论风险——在多用户共享 agent 的场景下,这是真实的攻击面。


系统二:技能库 (Skills)

源码位置: tools/skills_tool.py + tools/skill_manager_tool.py

Skills 是什么

如果说 Memory 是"我知道的事实",Skills 就是"我会做的事情"。

每个 Skill 是一个目录,核心是一个 SKILL.md 文件:

~/.hermes/skills/
├── deploy-to-aws/
│   ├── SKILL.md              # 主文档:操作步骤
│   ├── references/           # 参考文档
│   │   └── aws-cli-cheat-sheet.md
│   └── templates/            # 输出模板
│       └── cloudformation.yaml
└── debug-docker-network/
    └── SKILL.md

SKILL.md 使用 YAML frontmatter + Markdown 正文:

---
name: deploy-to-aws
description: 将 Next.js 项目部署到 AWS ECS
version: 1.0.0
platforms: [macos, linux]
prerequisites:
  commands: [aws, docker]
---

# Deploy to AWS

1. 构建 Docker 镜像...
2. 推送到 ECR...
3. 更新 ECS 服务...

三个工具

LLM 有三个工具来操作 Skills:

skills_list()                    # 列出所有技能标题和摘要
skill_view("deploy-to-aws")     # 读取某个技能的完整内容
skill_manage(action="create",    # 创建新技能
             name="xxx",
             content="...")

渐进式加载:两层索引

这里有一个很实际的工程问题:如果你有 50 个 Skills,每个 SKILL.md 几千字,全部塞进 system prompt 会吃掉大量 token。

Hermes 的解决方案是渐进式加载

第一层(System Prompt)——只放索引,每个 Skill 一行名称 + 摘要:

## Skills (mandatory)
Before replying, scan the skills below. If a skill matches...

  deployment:
    - deploy-to-aws: 将 Next.js 项目部署到 AWS ECS
    - deploy-to-vercel: 一键部署到 Vercel
  debugging:
    - debug-docker-network: 排查 Docker 容器网络问题

第二层(按需加载)——LLM 觉得某个 Skill 相关时,调用 skill_view() 读取全文。

第三层(深度参考)——Skill 目录下的 references/templates/ 等文件,只有在 LLM 明确需要时才读取。

这样,system prompt 里只占几百 token 的索引,但 LLM 随时可以"翻开手册"查看完整步骤。

两层缓存

索引的构建本身也做了优化——内存 LRU 缓存 + 磁盘快照

# 第一层:进程内 LRU 缓存(进程存活期间有效)
_SKILLS_PROMPT_CACHE = OrderedDict()  # 最多 100 条

# 第二层:磁盘快照(进程重启后仍有效)
# .skills_prompt_snapshot.json — 存储解析后的元数据
# 通过 mtime/size manifest 校验是否过期

# 第三层:完整文件系统扫描(两层缓存都 miss 时才触发)

只有当两层缓存都失效时,才会做完整的目录扫描。日常使用中,索引构建基本是零开销。


源码位置: hermes_state.py(1444 行)

为什么需要这个

Memory 有字符数限制(MEMORY.md 2200 字符,USER.md 1375 字符),不可能什么都记。Skills 存的是通用方法,不是具体的对话细节。

但你经常需要回顾"上次我们怎么解决那个问题的"——完整的对话上下文,包括试错过程、最终方案、中间的讨论。

Session Search 就是干这个的:每次对话完整存入 SQLite,支持全文检索

SQLite FTS5 全文搜索

-- 全文搜索虚拟表
CREATE VIRTUAL TABLE messages_fts USING fts5(
    content,
    content=messages,
    content_rowid=id
);

-- 自动同步:消息写入时自动更新索引
CREATE TRIGGER messages_fts_insert AFTER INSERT ON messages BEGIN
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

LLM 通过 session_search 工具来检索:

session_search(query="上次我们怎么修复的那个 docker 网络问题")

返回的不是孤立的匹配行,而是带上下文的片段——每个匹配结果包含前后各 1 条消息,让 LLM 能理解对话的来龙去脉。

中日韩文字的处理

FTS5 默认的分词器对中文不友好——会把每个汉字单独拆开。Hermes 的做法是:先尝试 FTS5 查询,如果检测到 CJK 字符且 FTS5 结果不理想,降级到 LIKE 查询:

def search_messages(self, query: str, ...):
    query = self._sanitize_fts5_query(query)
    # 先尝试 FTS5 MATCH
    # 如果 CJK 检测到且结果为空,降级到 LIKE

写入并发控制

多个进程(CLI、网关、子 Agent)可能同时写同一个 state.db。SQLite 的默认策略是确定性退避——但这会导致"护航效应"(convoy effect),所有竞争者同步等待、同步重试,导致终端明显卡顿。

Hermes 用随机抖动替代确定性退避:

_WRITE_MAX_RETRIES = 15
_WRITE_RETRY_MIN_S = 0.020   # 20ms
_WRITE_RETRY_MAX_S = 0.150   # 150ms

for attempt in range(self._WRITE_MAX_RETRIES):
    try:
        self._conn.execute("BEGIN IMMEDIATE")
        result = fn(self._conn)
        self._conn.commit()
        return result
    except sqlite3.OperationalError as exc:
        if "locked" in str(exc).lower():
            jitter = random.uniform(
                self._WRITE_RETRY_MIN_S,
                self._WRITE_RETRY_MAX_S
            )
            time.sleep(jitter)

随机 20-150ms 的抖动让竞争者错开重试时机,消除了可感知的 UI 卡顿。


触发器:后台自动复盘 (Background Review)

以上三个系统都需要 LLM 主动调用工具才能写入。如果完全依赖 LLM 的"自觉性",很多值得记录的内容会被遗漏。

Hermes 的解决方案是:自动触发后台复盘

源码位置: run_agent.py(2757-2895 行)

触发时机

两个独立的计数器,分别控制 Memory 和 Skills 的复盘频率:

# Memory 复盘:基于用户消息轮次(默认每 10 轮)
self._turns_since_memory += 1
if self._turns_since_memory >= self._memory_nudge_interval:  # 默认 10
    _should_review_memory = True
    self._turns_since_memory = 0

# Skills 复盘:基于工具调用迭代次数(默认每 10 次)
if self._iters_since_skill >= self._skill_nudge_interval:  # 默认 10
    _should_review_skills = True
    self._iters_since_skill = 0

注意两者的触发维度不同:Memory 按对话轮次(用户说了几轮话),Skills 按工具迭代次数(LLM 调了几次工具)。这是有道理的——用户偏好在对话中体现,可复用的工作流在工具使用中体现。

复盘流程

触发后,Hermes 会在后台线程启动一个全新的 Agent 实例:

对话进行中

  │  每10轮  ──→  后台线程启动
  │                  │
  │                  ↓ 创建新的 AIAgent 实例(同模型、同工具)
  │                  ↓ 把当前对话历史复制给它
  │                  ↓ 追加一条复盘提示词
  │                  ↓ 子 Agent 自主判断、自主调用工具
  │                  │
  │                  ├── 值得记?→ memory("add") → 写入磁盘
  │                  ├── 可复用?→ skill_manage("create") → 写入技能
  │                  └── 没什么特别的 → "Nothing to save."

  └──→ 主对话不受影响
       用户看到一行 "💾 Memory updated"

复盘提示词

复盘 Agent 收到的提示词长这样:

Memory 复盘:

_MEMORY_REVIEW_PROMPT = (
    "Review the conversation above and consider saving to memory "
    "if appropriate.\n\n"
    "Focus on:\n"
    "1. Has the user revealed things about themselves — their persona, "
    "desires, preferences, or personal details worth remembering?\n"
    "2. Has the user expressed expectations about how you should behave, "
    "their work style, or ways they want you to operate?\n\n"
    "If something stands out, save it using the memory tool. "
    "If nothing is worth saving, just say 'Nothing to save.' and stop."
)

Skills 复盘:

_SKILL_REVIEW_PROMPT = (
    "Review the conversation above and consider saving or updating a "
    "skill if appropriate.\n\n"
    "Focus on: was a non-trivial approach used to complete a task that "
    "required trial and error, or changing course due to experiential "
    "findings along the way, or did the user expect or desire a different "
    "method or outcome?\n\n"
    "If a relevant skill already exists, update it with what you learned. "
    "Otherwise, create a new skill if the approach is reusable.\n"
    "If nothing is worth saving, just say 'Nothing to save.' and stop."
)

关键词是"if appropriate"和"if nothing is worth saving, just say Nothing to save"——不是每次复盘都会写入,LLM 自己判断。

实现细节

def _spawn_background_review(self, messages_snapshot, review_memory, review_skills):
    def _run_review():
        review_agent = AIAgent(
            model=self.model,
            max_iterations=8,      # 限制迭代次数,轻量复盘
            quiet_mode=True,       # 静默运行
        )
        # 共享 Memory/Skills 存储(写入同一个磁盘文件)
        review_agent._memory_store = self._memory_store
        # 禁用复盘触发器(防止递归:复盘 Agent 又触发复盘)
        review_agent._memory_nudge_interval = 0
        review_agent._skill_nudge_interval = 0

        review_agent.run_conversation(
            user_message=prompt,
            conversation_history=messages_snapshot,
        )

        # 提取成功的写入操作,展示给用户
        if actions:
            self._safe_print(f"  💾 {summary}")

    t = threading.Thread(target=_run_review, daemon=True, name="bg-review")
    t.start()

几个设计要点:

  1. daemon=True——后台线程不阻塞主会话退出
  2. max_iterations=8——轻量级复盘,不是完整任务执行
  3. 共享存储——子 Agent 写入的 memory/skill 直接持久化到磁盘
  4. 禁用递归触发——复盘 Agent 不会再触发自己的复盘
  5. 响应已返回后才启动——不和主对话抢模型算力

完整数据流

把三个系统串起来,看一次完整的"学习 → 回忆"流程:

┌──────────────── 会话 N ────────────────┐
│                                         │
│  用户: "部署到 AWS 时 ECS 任务定义      │
│         的内存参数要设多少?"            │
│          ↓                              │
│  Agent: 调用工具、查文档、试错           │
│          ↓                              │
│  用户: "对,就用 512MB,另外记住        │
│         我们所有服务都跑在 ap-southeast-1"│
│          ↓                              │
│  Agent: 主动调用 memory("add") 保存     │
│         → 写入 MEMORY.md(立即持久化)  │
│         → 当前 system prompt 不变       │
│          ↓                              │
│  ... 继续对话 ...                       │
│          ↓                              │
│  [第 10 轮] 后台复盘触发               │
│     → 子 Agent 回顾对话                │
│     → 发现部署流程值得记录             │
│     → skill_manage("create",           │
│         name="ecs-deploy",             │
│         content="...")                  │
│     → 写入 skills/ecs-deploy/SKILL.md  │
│     → 用户看到 "💾 Skill created"      │
│                                         │
│  会话结束,完整对话存入 SQLite           │
└─────────────────────────────────────────┘
           ↓ 时间流逝 ↓
┌──────────────── 会话 N+1 ──────────────┐
│                                         │
│  启动时:                               │
│  ├─ MEMORY.md 重新加载 → 新快照注入     │
│  │  system prompt(含 ap-southeast-1)  │
│  ├─ Skills 索引重建 → 包含 ecs-deploy   │
│  └─ session_search 工具随时可用         │
│                                         │
│  用户: "帮我把新服务也部署一下"         │
│          ↓                              │
│  Agent: 看到 Skills 索引有 ecs-deploy   │
│     → skill_view("ecs-deploy") 读全文   │
│     → 按照之前的步骤操作               │
│     → 自动使用 ap-southeast-1 区域      │
│     → 如果细节不够,session_search      │
│        检索上次的完整对话               │
│                                         │
└─────────────────────────────────────────┘

三层存储各司其职:

  • MEMORY.md 提供事实("区域是 ap-southeast-1")
  • Skills 提供方法("部署 ECS 的完整步骤")
  • Session Search 提供细节("上次具体报了什么错、怎么修的")

System Prompt 的组装

最后看一下这些知识是怎么注入到 system prompt 中的。

源码位置: agent/prompt_builder.py

System prompt 的结构如下:

┌─────────────────────────────────────┐
│ 1. Agent 身份                       │
│    "You are Claude, made by..."     │
├─────────────────────────────────────┤
│ 2. Memory 冻结快照                  │
│    ══════════════════════           │
│    MEMORY (your personal notes)     │
│    ══════════════════════           │
│    - 项目用 pnpm                    │
│    - 数据库在 ap-southeast-1        │
│                                     │
│    USER PROFILE                     │
│    - 用户喜欢简洁回答               │
│    - 后端工程师,前端经验少         │
├─────────────────────────────────────┤
│ 3. Skills 索引                      │
│    ## Skills (mandatory)            │
│      deployment:                    │
│        - ecs-deploy: 部署到 ECS     │
│      debugging:                     │
│        - debug-docker: 排查网络     │
├─────────────────────────────────────┤
│ 4. 平台特定指令                     │
│    (CLI / Telegram / Discord)       │
├─────────────────────────────────────┤
│ 5. 工具使用指南                     │
├─────────────────────────────────────┤
│ 6. Memory/Skills 使用指南           │
│    "当发现值得记录的内容时..."      │
├─────────────────────────────────────┤
│ 7. Session Search 使用指南          │
│    "你可以搜索历史会话..."          │
├─────────────────────────────────────┤
│ 8. 上下文文件                       │
│    (SOUL.md, AGENTS.md 等)          │
├─────────────────────────────────────┤
│ 9. 环境信息                         │
│    (OS, Shell, 已安装工具等)        │
└─────────────────────────────────────┘

注意第 2、3 部分——Memory 快照和 Skills 索引是每次会话开始时重新构建的,所以上次保存的知识自然就出现在了新会话的"大脑"里。

构建代码的核心逻辑:

def build_memory_context_block(memory_store, user_profile_enabled=True):
    """用冻结快照构建 memory 上下文块"""
    blocks = []
    # 使用快照而非实时状态
    memory_block = memory_store.format_for_system_prompt("memory")
    if memory_block:
        blocks.append(memory_block)
    if user_profile_enabled:
        user_block = memory_store.format_for_system_prompt("user")
        if user_block:
            blocks.append(user_block)
    return "\n\n".join(blocks) if blocks else None

本质总结

Hermes 的"自主学习"没有任何黑魔法:

LLM 的理解能力 + 文件持久化 = 跨会话的记忆积累

它的精髓在于分层设计

层次容量速度用途
MEMORY.md2200 字符每次会话自动加载关键事实,高频使用
USER.md1375 字符每次会话自动加载用户画像,个性化
Skills每个 10 万字符按需加载可复用工作流
SQLite 会话无上限搜索时加载完整历史,长尾检索

小容量的 Memory 像 CPU 的 L1 缓存——快但小;大容量的 SQLite 像硬盘——慢但全。Skills 介于两者之间,是结构化的"操作手册"。

更重要的是自动触发机制。不需要用户说"记住这个",后台复盘 Agent 会定期回顾对话,自主判断什么值得保存。这让"学习"从手动变成了自动——用户只管用,Agent 自己变聪明。

而且这套机制是模型无关的——不依赖特定模型的微调能力,任何支持 Tool Calling 的 LLM 都可以做到。这也是为什么 Hermes 能支持 Claude、GPT、DeepSeek 等多种模型。


下一篇预告

这篇讲的是 Agent 如何积累知识。但知识要有用,首先得有一个高效的执行循环——Agent Loop。下一篇我们来拆解 Hermes 的 Agent Loop 实现:模型怎么决定调什么工具、工具执行失败怎么重试、上下文太长怎么压缩。

地基(API)打好了,记忆(Memory)有了,接下来该看引擎了。

On this page