芥末
发布于 2026-01-28 / 0 阅读
0
0

Clawdbot 本地持久记忆系统的工程设计与实践

Clawdbot 是一个面向现实任务的 AI Agent(人工智能代理),可以处理邮件、日历、航班值机、定时后台任务等操作。和普通聊天机器人相比,这类 Agent 最大的难点不是“能不能回答问题”,而是“能不能长期记住和用户、任务、工具相关的信息”。

如果 Agent 每次启动都像第一次见到用户,它就很难完成长期任务:

  • 用户已经说过的偏好会丢失;
  • 之前做过的决策需要反复确认;
  • 工具怎么用、哪些坑踩过,下一次还会再踩;
  • 长对话超过模型上下文窗口后,旧信息会被挤掉。

Clawdbot 的做法是把记忆放在本地工作区,而不是放进云端黑盒服务。记忆以 Markdown 文件保存,用户可以直接查看、修改、备份、版本控制;检索时再通过索引找到相关片段,而不是把所有历史一股脑塞进模型上下文。

这个设计的核心思想很简单:

上下文负责一次请求的即时推理,记忆负责跨会话、跨天、跨月保存知识。


一次请求里模型实际能看到什么

理解记忆系统之前,需要先弄清楚“模型看到的上下文”由哪些部分组成。Clawdbot 在每次请求时,大致会把信息按这样的顺序组织起来:

[0] 系统提示:静态规则、条件指令、工具说明
[1] 项目上下文:AGENTS.md、SOUL.md、USER.md、TOOLS.md 等
[2] 对话历史:消息、工具调用结果、压缩摘要
[3] 当前消息:用户刚刚输入的内容

可以画成一个请求上下文栈:

flowchart TB
    A[当前用户消息] --> B[对话历史]
    B --> C[项目上下文 Markdown 文件]
    C --> D[系统提示与工具定义]
    D --> E[发送给 LLM 的完整上下文]

LLM(大语言模型)并不会自动知道磁盘上所有文件的内容。真正进入请求的,只有被系统加载、被工具读取、或被检索命中的部分。

Clawdbot 把一组可编辑的 Markdown 文件作为项目上下文注入请求,其中几类文件尤其重要:

文件作用
AGENTS.mdAgent 行为规则,包括什么时候读取记忆、什么时候写入记忆
SOUL.mdAgent 的个性、语气、交互风格
USER.md和用户相关的长期信息
TOOLS.md外部工具的使用方式、注意事项、经验教训

这些文件不是数据库里的隐藏配置,而是普通 Markdown。用户可以像维护项目文档一样维护 Agent 的行为和记忆规则。


上下文和记忆不是一回事

很多 Agent 设计会把“记忆”简单理解成“把更多历史塞进上下文”。这会带来三个问题:上下文窗口有限、成本变高、模型注意力被无关信息稀释。

Clawdbot 把上下文和记忆分开处理。

上下文可以写成:

上下文 = 系统提示 + 项目上下文 + 对话历史 + 工具结果 + 当前消息 + 附件

记忆可以写成:

记忆 = MEMORY.md + memory/*.md + 会话记录 + 派生索引

两者区别如下:

维度上下文记忆
生命周期只服务当前请求保存在磁盘上,可跨会话存在
容量受模型上下文窗口限制理论上可以持续增长
成本token 会影响 API 调用费用和响应速度文件存储本身不产生模型调用费用
使用方式直接提供给模型推理先检索,再把相关片段放入上下文
典型内容当前任务、最近消息、工具返回用户偏好、长期决策、经验教训、历史事实

API(应用程序编程接口)计费通常和 token 数量相关。把所有记忆都塞进上下文,看起来简单,但会让每次调用都变重。更合理的方式是:记忆在磁盘上长期保存,真正需要时再搜索相关内容。


记忆如何存储:两层 Markdown 结构

Clawdbot 的记忆系统建立在一个非常工程化的原则上:

记忆的真实来源是工作区里的 Markdown 文件,数据库索引只是为了检索。

默认工作区可以理解成类似这样的目录:

~/clawd/
├── AGENTS.md
├── SOUL.md
├── USER.md
├── TOOLS.md
├── MEMORY.md
├── memory/
│   ├── 2026-06-07.md
│   ├── 2026-06-08.md
│   └── ...
└── sessions/
    ├── session-xxx.jsonl
    └── ...

其中记忆分成两层。

每日日志:memory/YYYY-MM-DD.md

每日文件用于记录当天发生的事情,偏“流水但有用”。当用户明确说“记住这个”,或者 Agent 判断某个信息对当天任务有价值时,可以追加到当天日志。

示例:

# 2026-06-07

## 任务记录
- 用户希望把部署脚本从 Bash 改成 Python,原因是需要更好的错误处理。
- 排查 PostgreSQL 连接失败时,发现环境变量名应该是 `POSTGRES_URL`,不是 `DATABASE_URL`。

## 待跟进
- 明天继续检查定时任务是否会重复触发。

这一层适合保存细节,但不一定每条都值得进入长期记忆。

长期记忆:MEMORY.md

MEMORY.md 存放经过筛选的长期知识。它更像一本持续维护的用户手册或项目事实库,适合保存稳定偏好、重要决策、长期约束和已经验证过的经验。

示例:

# Long-term Memory

## User Preferences
- 用户更喜欢直接给出可执行命令,而不是只解释概念。
- 用户希望部署方案优先考虑本地可控性,避免依赖太多云服务。

## Project Decisions
- 数据库连接环境变量统一使用 `POSTGRES_URL`。
- 后台任务需要具备幂等性,避免重复执行造成副作用。

## Lessons Learned
- 修改定时任务前,需要先检查已有 cron 配置,避免同一任务被注册两次。

这种两层结构解决了一个常见问题:所有东西都写进长期记忆会污染知识库,只写摘要又容易丢细节。每日文件保留过程,长期文件保留筛选后的事实。


写入记忆:不用专门的 memory_write

Clawdbot 没有设计一个特殊的 memory_write 工具。记忆文件就是普通文件,所以 Agent 可以用标准的 writeedit 工具修改 Markdown。

写到哪里,主要由 AGENTS.md 里的规则决定:

触发条件推荐写入位置
用户说“记住这个”memory/YYYY-MM-DD.md,必要时同步到 MEMORY.md
当天任务记录、临时线索memory/YYYY-MM-DD.md
稳定偏好、长期事实、关键决策MEMORY.md
工具使用经验、踩坑记录AGENTS.mdTOOLS.md
会话结束前的关键信息由会话钩子写入每日记忆或长期记忆

可以把 AGENTS.md 中的记忆规则写成这种形式:

# Memory Guidelines

- 当用户明确要求记住某件事时,将其写入当天的 `memory/YYYY-MM-DD.md`。
- 如果信息属于长期偏好、稳定事实或重要决策,同时整理到 `MEMORY.md`。
- 工具使用方式、错误处理经验、外部系统限制,优先写入 `TOOLS.md`。
- 写入长期记忆前尽量压缩表达,避免保存无关对话。
- 处理新任务前,先搜索可能相关的记忆,而不是假设自己已经知道。

重点不在工具名,而在“记忆也是文件”。这让记忆系统天然具备透明性:可以查看 diff,可以用 Git 管理,也可以手动修正错误记忆。


读取记忆:先搜索,再读取

读取记忆时,Clawdbot 使用两个工具:

工具作用
memory_search在记忆文件中搜索相关内容
memory_get根据搜索结果读取具体文件或片段

调用顺序通常是这样:

sequenceDiagram
    participant U as 用户
    participant A as Agent
    participant S as memory_search
    participant G as memory_get
    participant M as Markdown 记忆文件

    U->>A: 提出新任务
    A->>S: 搜索相关记忆
    S-->>A: 返回候选片段和分数
    A->>G: 读取最相关的文件片段
    G->>M: 获取 Markdown 内容
    M-->>G: 返回内容
    G-->>A: 注入当前上下文
    A-->>U: 基于当前任务和记忆回答

伪调用可以理解成:

memory_search("用户之前关于数据库部署的偏好")
memory_get("MEMORY.md#Project Decisions")

两阶段读取有一个好处:搜索结果只告诉 Agent 哪些片段可能相关,真正放入上下文的只有少量命中的内容。这样不会因为历史记忆太多,把当前任务的上下文挤掉。


记忆如何索引:Markdown 到 SQLite

Markdown 适合人类阅读和编辑,但不适合直接做快速语义检索。Clawdbot 的做法是:Markdown 作为真实来源,SQLite 作为派生索引。

保存记忆文件后,后台会把文件内容转换成可搜索结构:

flowchart LR
    A[保存 Markdown 记忆文件] --> B[文件监听器捕获变化]
    B --> C[解析 Markdown]
    C --> D[切分为记忆片段]
    D --> E[生成向量 embedding]
    D --> F[写入全文索引]
    E --> G[(SQLite + sqlite-vec)]
    F --> H[(SQLite FTS5)]
    G --> I[混合检索]
    H --> I

这里有两个关键组件:

  • sqlite-vec:SQLite 的向量搜索扩展,可以在 SQLite 内部做向量相似度检索,不需要单独部署向量数据库。
  • FTS5(Full Text Search 5,SQLite 全文搜索模块):SQLite 内置全文搜索能力,支持基于关键词的 BM25 排名。

BM25(Best Matching 25)是一种经典关键词相关性评分算法,适合查找精确词、变量名、日期、接口名等内容。

索引和 Markdown 的关系可以这样理解:

部分角色
Markdown 文件真实数据源,可以手动编辑
SQLite 向量索引语义搜索,用来找“意思相近”的内容
SQLite FTS5 索引关键词搜索,用来找“字面匹配”的内容
会话 JSONL历史消息记录,JSONL(JSON Lines,逐行 JSON)便于追加写入

如果索引损坏,理论上可以从 Markdown 重新构建;如果 Markdown 丢失,索引不能替代真实记忆。


混合搜索:语义检索和关键词检索一起用

只用向量搜索,会漏掉精确名称。比如用户想查 POSTGRES_URL,向量检索可能知道它和“数据库连接字符串”相关,但不一定把这个精确变量名排在最前。

只用关键词搜索,也会漏掉语义相近的表达。比如用户搜索“那个数据库配置”,关键词里没有“POSTGRES_URL”,BM25 就很难命中。

Clawdbot 同时跑两种搜索:

flowchart LR
    A[查询内容] --> B[向量检索]
    A --> C[BM25 关键词检索]
    B --> D[vectorScore]
    C --> E[textScore]
    D --> F[加权融合]
    E --> F
    F --> G[过滤低于 minScore 的结果]
    G --> H[返回候选记忆片段]

默认融合公式类似:

finalScore = 0.7 * vectorScore + 0.3 * textScore

语义相似度占 70%,关键词匹配占 30%。低于 minScore 的结果会被过滤,默认阈值是 0.35,这些参数可以按使用场景调整。

查询类型向量检索表现BM25 表现混合搜索效果
“上次那个数据库配置”能理解大概语义可能命不中向量兜底
POSTGRES_URL可能不稳定精确命中关键词兜底
“用户喜欢什么部署方式”能找偏好类记录依赖关键词两者互补
“2026-06-07 的决定”一般日期精确命中更容易定位

对 Agent 记忆来说,混合搜索比单一路线更稳,因为真实记忆里既有自然语言,也有变量名、路径、日期、命令、错误码。


多 Agent 记忆隔离

Clawdbot 支持多个 Agent,每个 Agent 可以有自己的工作区和索引。这样可以把不同身份、不同任务域的记忆分开,比如一个处理个人通信,一个处理工作协作。

结构可以表示为:

flowchart TB
    subgraph A1[个人 Agent]
        W1[workspace: ~/clawd/personal]
        M1[MEMORY.md + memory/*.md]
        I1[(SQLite index)]
        W1 --> M1
        M1 --> I1
    end

    subgraph A2[工作 Agent]
        W2[workspace: ~/clawd/work]
        M2[MEMORY.md + memory/*.md]
        I2[(SQLite index)]
        W2 --> M2
        M2 --> I2
    end

    K[Memory Manager] --> A1
    K --> A2

记忆管理器通常用 agentId + workspaceDir 作为键,确保不同 Agent 不会自动搜索彼此的记忆。

但要注意,工作区隔离更像“默认工作目录隔离”,不是天然的强安全边界。如果没有启用严格沙箱,Agent 理论上仍可能通过绝对路径访问其他目录。需要强隔离时,应开启更严格的文件系统限制,尤其是处理个人隐私、公司内部资料、密钥配置时。


压缩:上下文窗口满了怎么办

任何 LLM 都有上下文窗口限制。对话足够长之后,旧消息、工具结果、附件和当前任务会抢同一块上下文空间。Clawdbot 使用压缩来处理这个问题:把旧对话总结成更短的摘要,同时保留最近消息。

flowchart LR
    A[长会话历史] --> B{接近上下文上限?}
    B -- 否 --> C[继续使用完整近期上下文]
    B -- 是 --> D[压缩旧消息]
    D --> E[生成摘要]
    E --> F[保留最近消息]
    F --> G[用压缩后的上下文重试请求]
    E --> H[写入会话 JSONL]

压缩有两种触发方式。

自动压缩:当上下文接近模型限制时触发。压缩后,原请求会使用新的上下文重试。

手动压缩:使用 /compact 命令,并可以指定压缩重点。

/compact 关注决策和未决问题

压缩结果会持久化到会话 JSONL 文件中,而不只是临时存在于当前请求。这样后续会话加载历史时,可以从压缩后的记录开始。

压缩的代价是信息损失。摘要再好,也不可能保留旧对话中的所有细节,所以 Clawdbot 还需要记忆刷新。


记忆刷新:压缩前先保存关键信息

压缩是有损的。重要事实如果只存在于旧对话里,压缩时可能被概括掉,甚至完全消失。

Clawdbot 用“记忆刷新”缓解这个问题:在压缩之前,让 Agent 先检查当前会话里有哪些应该长期保留的信息,并写入 Markdown 记忆。

流程如下:

flowchart TD
    A[会话变长,准备压缩] --> B[扫描旧对话]
    B --> C{是否有长期有用信息?}
    C -- 有 --> D[写入 memory/YYYY-MM-DD.md]
    D --> E{是否属于稳定事实或决策?}
    E -- 是 --> F[整理到 MEMORY.md]
    E -- 否 --> G[保留在每日日志]
    C -- 无 --> H[直接压缩]
    F --> H
    G --> H
    H --> I[生成压缩摘要]

配置文件可以是 clawdbot.yamlclawdbot.json。字段名需要以所用版本为准,但需要关注的开关通常包括:

# 示意配置
memoryRefresh:
  beforeCompact: true
  onSessionEnd: true

compaction:
  enabled: true
  preserveRecentMessages: true

记忆刷新和压缩的职责不同:

机制目标是否有损结果保存在哪里
记忆刷新把重要事实写入长期文件尽量无损MEMORY.mdmemory/*.md
压缩减少上下文 token有损会话 JSONL 摘要
搜索找回相关记忆无损读取命中文件当前请求上下文

关键点是:重要信息不要只留在会话历史里。只要写进 Markdown,后续即使会话被压缩,也可以通过搜索找回来。


修剪:处理庞大的工具输出

Agent 经常调用工具。一次 exec 命令可能输出几万字符日志,一次网页抓取可能带回大量 HTML,一次测试失败可能产生很长的堆栈信息。

这些工具结果如果一直保留在上下文中,会让后续请求变慢、变贵,还会挤占有效信息。Clawdbot 使用修剪来裁掉旧工具输出。

修剪和压缩不同:

机制处理对象是否重写语义是否可恢复
压缩旧对话消息会总结、改写原始细节不再完整进入上下文
修剪旧工具输出直接截断或裁剪被裁掉的旧输出不可恢复
记忆刷新关键事实写入文件可通过文件找回

修剪适合处理“体积大但价值低”的内容,比如:

  • 已经分析过的长日志;
  • 重复的命令输出;
  • 大段构建信息;
  • 旧网页正文;
  • 对当前任务已经没有作用的测试输出。

但修剪前要确认关键信息已经被提炼出来。如果错误码、失败原因、最终路径只存在于旧输出里,直接修剪会造成信息丢失。


缓存 TTL 修剪:降低提示重新缓存成本

Anthropic 的提示前缀缓存可以在短时间内复用相同提示前缀,TTL(Time To Live,存活时间)大约是 5 分钟。在 TTL 窗口内,如果提示前缀相同,重复 token 的成本和延迟会降低很多;TTL 过期后,下一次请求需要重新写入缓存。

问题在于,长会话空闲一段时间后,下一次请求可能要重新缓存完整历史。如果历史里有大量旧工具输出,重新缓存成本会很高。

缓存 TTL 修剪的思路是:检测到缓存过期后,在下一次请求前裁掉旧工具结果,让重新缓存的提示更小。

flowchart TD
    A[长会话进入空闲] --> B[提示缓存 TTL 过期]
    B --> C[用户发起新请求]
    C --> D{是否存在大量旧工具输出?}
    D -- 是 --> E[修剪旧工具输出]
    D -- 否 --> F[直接发送请求]
    E --> G[较小上下文重新缓存]
    F --> H[完整上下文重新缓存]

这个机制不改变记忆文件,只影响会话上下文中的冗余工具结果。它解决的是成本和上下文体积问题,不负责保存长期知识。


会话生命周期:给记忆设置自然边界

会话不能无限延续。Clawdbot 用可配置规则重置会话,让记忆有自然边界。常见模式如下:

模式行为
daily每天固定时间重置,例如本地时间凌晨 4 点
idle空闲 N 分钟后重置
daily+idle两种条件谁先满足就触发重置

当用户运行 /new 开始新会话时,可以触发会话记忆钩子,把当前会话中的重要内容保存下来。

sequenceDiagram
    participant U as 用户
    participant A as Agent
    participant H as 会话记忆钩子
    participant M as Markdown 记忆

    U->>A: /new
    A->>H: 触发会话结束处理
    H->>H: 提取决策、偏好、待办、工具经验
    H->>M: 写入每日记忆或长期记忆
    H-->>A: 保存完成
    A-->>U: 开始新会话

这让“会话结束”不只是清空聊天窗口,而是一个整理记忆的机会。


一套可落地的记忆写入策略

设计 Agent 记忆时,最容易犯的错是“什么都记”。记忆不是越多越好,关键是可检索、可维护、可信。

一套实用规则可以这样定:

信息类型是否记忆存放位置示例
用户稳定偏好要记MEMORY.md用户喜欢命令优先、解释跟在后面
项目长期决策要记MEMORY.md统一使用 POSTGRES_URL
当天排查线索要记memory/YYYY-MM-DD.md某次失败由端口占用导致
外部工具用法要记TOOLS.md某工具必须先登录再调用
一次性闲聊通常不记不写入临时寒暄
大段日志不直接长期记提炼后写入保留错误原因,不保存整段日志
密钥、令牌、密码谨慎处理避免明文只记录变量名,不记录真实值

Markdown 记忆虽然透明,但透明也意味着容易把敏感信息暴露给文件系统、备份系统或版本库。长期记忆里不应保存真实 token、密码、私钥,除非有明确的加密和访问控制策略。


Clawdbot 记忆系统的工程取舍

Clawdbot 的记忆系统不是单纯加一个向量数据库,而是把文件、索引、上下文管理、会话生命周期放在一起设计。

它的关键取舍可以概括为四点。

透明文件优先于黑盒记忆

记忆是 Markdown,用户可以直接阅读和编辑。相比只能通过接口访问的云端记忆,本地文件更容易审计,也更容易修正错误。

搜索相关内容优先于全部注入

Agent 不需要每次都读取全部记忆。通过 memory_searchmemory_get,只把命中的片段放入上下文,可以减少 token 成本,也能让模型更专注当前任务。

长期文件优先于会话历史

会话历史会被压缩、修剪、重置。真正重要的事实应该进入 MEMORY.md 或每日记忆文件,而不是只留在聊天记录里。

混合检索优先于单一检索

向量检索适合理解语义,BM25 适合命中精确词。Agent 记忆里既有自然语言,也有变量名、路径、日期和错误码,混合搜索更适合这种数据形态。


适合借鉴的架构模式

如果要给自己的 AI Agent 做本地记忆系统,可以直接借鉴这条链路:

flowchart TD
    A[用户输入] --> B[加载系统提示和项目上下文]
    B --> C[判断是否需要记忆]
    C -->|需要| D[memory_search]
    D --> E[memory_get]
    E --> F[相关记忆进入当前上下文]
    C -->|不需要| F
    F --> G[LLM 推理与工具调用]
    G --> H{产生新记忆?}
    H -->|当天线索| I[追加到 memory/YYYY-MM-DD.md]
    H -->|长期事实| J[整理到 MEMORY.md]
    H -->|工具经验| K[更新 TOOLS.md 或 AGENTS.md]
    H -->|无| L[结束本轮]
    I --> M[后台更新 SQLite 索引]
    J --> M
    K --> M

这套架构把“记忆”拆成了几个清晰职责:

  • Markdown 文件负责可控存储;
  • SQLite 索引负责快速检索;
  • 混合搜索负责召回相关内容;
  • 压缩负责控制上下文长度;
  • 刷新负责避免重要信息被压缩丢失;
  • 修剪负责降低旧工具输出的上下文成本;
  • 会话钩子负责在边界处整理记忆。

Agent 真正需要的不是“无限上下文”,而是能把长期信息放在合适的位置,并在需要时准确找回来。Clawdbot 的设计价值就在这里:让记忆既能被模型使用,也能被人类维护。


评论