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.md | Agent 行为规则,包括什么时候读取记忆、什么时候写入记忆 |
SOUL.md | Agent 的个性、语气、交互风格 |
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 可以用标准的 write、edit 工具修改 Markdown。
写到哪里,主要由 AGENTS.md 里的规则决定:
| 触发条件 | 推荐写入位置 |
|---|---|
| 用户说“记住这个” | memory/YYYY-MM-DD.md,必要时同步到 MEMORY.md |
| 当天任务记录、临时线索 | memory/YYYY-MM-DD.md |
| 稳定偏好、长期事实、关键决策 | MEMORY.md |
| 工具使用经验、踩坑记录 | AGENTS.md 或 TOOLS.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.yaml 或 clawdbot.json。字段名需要以所用版本为准,但需要关注的开关通常包括:
# 示意配置
memoryRefresh:
beforeCompact: true
onSessionEnd: true
compaction:
enabled: true
preserveRecentMessages: true
记忆刷新和压缩的职责不同:
| 机制 | 目标 | 是否有损 | 结果保存在哪里 |
|---|---|---|---|
| 记忆刷新 | 把重要事实写入长期文件 | 尽量无损 | MEMORY.md、memory/*.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_search 和 memory_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 的设计价值就在这里:让记忆既能被模型使用,也能被人类维护。