从 76,175 字符到 4,507:我把 AI 的记忆拆成四层
一句话结论:AI 助手"记不住",多数时候不是模型的问题,是它的记忆文件被静默截断了------它从来没读到过那部分内容,而它自己不知道。
这不是"知识库"能解决的问题,是一份启动协议该解决的问题。
一、先说结论
如果你正在给自己的 AI 助手写记忆文件(CLAUDE.md、AGENTS.md、MEMORY.md、system prompt 里的画像......),下面三条可能对你有用:
- 记忆的注入量存在上限,而且远小于上下文窗口。 窗口有 20 万 token,不代表你能塞 20 万 token 的记忆。
- 超出部分通常是静默截断的。 AI 不知道自己缺了什么,会拿着半份规则继续工作,还会为自己的错误找出合理解释。
- 所以真正的解法不是"写更多",是"分层"。 一份极小的常驻协议 + 一个按需读取的仓库。
我按这个思路,把一套 76,175 字符的记忆系统重构成 4,507 字符常驻 + 14 个分片。
二、故障现场
我给自己的 AI 助手维护了一套记忆档案:
| 文件 | 字符 | 字节 |
|---|---|---|
| 归档正文 | 57,694 | 124,770 |
| 常驻 MEMORY | 7,174 | 15,884 |
| 常驻 USER | 11,307 | 27,825 |
| 合计 | 76,175 | 168,479 |
某天它建议我做一件事------而那件事在它自己的档案里被明确标注为禁止。
第一反应是"它忘了"。但翻记录发现,那条禁令写得很清楚,位置也没错。
于是去数了一下:它到底读到了多少。
三、这不是我一个人的问题
写这篇之前我搜了一圈,发现"记忆丢失"至少有三种形态,各有各的名字。
3.1 养 Agent 的人的日常:/compact
社区里有人记录过他养 Coding Agent 的经历:
屏幕上弹出一句「上下文已超出限制,请使用 /compact 压缩」。
这是本周第 11 次失忆。
一个 /compact 下去,方案丢了大半,回来之后它一脸懵地问我:"您之前说要先改哪一层来着?"
这是"压缩型失忆":内容真的进过上下文,但后来被摘要掉了。
它和本文要讲的"注入截断"是同一条链上的两个环节------一个在对话中途压缩,一个在开机时就已缺失。
3.2 群机器人的默认值
如果你养的是群里的机器人,问题更具体。以常见的 QQ 机器人插件为例:
| 参数 | 默认值 | 后果 |
|---|---|---|
sessionIdleTimeout |
1,800,000 ms(30 分钟) | 闲置半小时后会话被回收,再问就像失忆 |
| 会话历史保留轮数 | 由调用方自己定 | 更早的内容根本不进请求 |
另外还有一个容易混淆的点:textChunkLimit(默认 4,500 字符)控制的是长回答怎么切分,不影响记忆。这两件事经常被写在一起,但性质完全不同。
真正的问题是第二行:会话窗口该留多少轮,没有标准答案,而大多数实现停在"够用就行"。
3.3 最硬的一条:位置决定命运
这一节值得单独讲,因为它有论文。
Periodic Weak Spots: Phase Sensitivity from Chunked KV-Cache Compression
arXiv:2609.36322,2026-09-28 提交,65 页
Xingyu Zhu, Pu (Luke) Yi, Ziheng Cheng, Ang Lv, Jing Liu, Lexing Ying, Yiyuan Ma, Xin Dong
ByteDance Seed(参与作者另有 Princeton / Stanford / UC Berkeley)
它研究的是分块 KV 缓存压缩(CSA) 带来的一个副作用:压缩把连续的 token 按固定步长打包成更少的缓存条目,这引入了一个新坐标------相位,也就是 token 相对于压缩窗口边界的位置。
结果:同一段信息,落在不同的相位,模型能不能找回来,差别极大。
最生动的实验:
让 DeepSeek-V4-Flash-Base 补全一段 FP8 量化函数,正确答案是 8。然后只在代码前面加一段纯装饰性的注释,里面放重复的等号,调整等号数量------代码一个字没改,正确答案也没改。
| 填充长度 mod 4 | 模型给出 |
|---|---|
| 0 或 1 | 错误答案 32 |
| 2 或 3 | 正确答案 8 |
它会跟着注释里的等号数量,在正确答案和错误答案之间来回翻转,周期正好 4 个 token------等于压缩步长。
在 128K 长上下文检索里,这个效应被量化了:
| 模型 | 步长 | 不同相位间最大准确率差 |
|---|---|---|
| DeepSeek-V4-Flash-Base | 4 | 40.2 个百分点 |
| DeepSeek-V4-Pro-Base | 4 | 34.8 个百分点 |
| DeepSeek-V4.1-Flash | 2 | 6.1 个百分点 |
⚠️ 这里有个常被写错的因果:后训练能收窄差距,但压不掉周期性;V4.1-Flash 的差距最小,同时它的压缩步长也从 4 改成了 2。 两个变量同时变了,不宜把 6.1 单独归给其中一个。
论文里最值得记住的一句:
高平均分可以与系统性的位置失效共存。
也就是说,跑分看不出来。平均分一拉平,周期性的忽高忽低就被抹掉了。用户实际撞上的,恰恰是被平均掉的那个低谷。
这也解释了一个长期困惑------为什么有时候它记得清清楚楚,换个位置(前面多几个 token、换个上下文长度)就全忘了。不是玄学,是位置。
3.4 三种失忆,一个共同点
| 失败类型 | 触发条件 | 表现 |
|---|---|---|
| 注入截断 | 记忆文件 > 注入上限 | 一部分规则从未被加载 |
| 压缩遗忘 | 对话长度 > 上下文窗口 | 早期内容被摘要/丢弃 |
| 相位敏感 | 信息落在压缩窗口的坏位置 | 内容在,但检索不到 |
三者触发条件不同,共同点却是一样的:全都是静默的。用户不知道丢了什么,AI 也不知道。
这才是这件事值得认真做的原因------它不是一个"优化项",是一个看不见的失效模式。
四、实测:到底丢了多少
先说方法,再说结论。 我没有平台侧的任何接口,测法是:
- 在会话里让助手报出"它看到的记忆文件第一行是什么、最后一行是什么";
- 拿到这两行后,和磁盘上的全文对照,就能定位到从哪一行开始没了;
- 按行累加字符数,得到注入量。
这个方法的边界必须说清楚 :它只能定位到"从哪一行断的",字符数是按行累加出来的,误差在几十字符量级;它也无法排除"平台在更底层做过额外裁剪"。所以下面的 4,120 是个观测值,不是理论值。
结果:
| 项目 | 数值 |
|---|---|
| 文件全量 | 7,174 字符 |
| 观测到的注入量 | 4,120 字符 |
| 缺失 | 约 43% |
| 断点 | 第 69 行 / 共 138 行 |
而丢掉的这 43% 里,包括:
- 核心工作约束("时间预算为 0,不要给行动项")
- 输出格式要求("公式必须写成分式")
- 当前任务清单
- 档案索引本身("需要时去读哪个文件")
最后一条最要命:告诉它"该去查档案"的那一行,本身就在被截断的那一侧。
所以它不知道要查,因为它没见过"要查"这句话。
五、为什么"多写记忆"是错的
大多数人遇到这个问题,第一反应是"那我写得更精炼一点",或者"换个窗口更大的模型"。
这两条都没用,因为混淆了三个数量级。
| 概念 | 我这里的量级 |
|---|---|
| 本次重构涉及的档案 | 76,175 字符 |
| 把 61 个工作区的历史日志也算上 | 约 45--54 万字符(含/不含备份) |
| 压缩后存下来的核心 | 51,145 字符 / 14 个分片 |
| 每次开机加载 | 4,507 字符 |
上下文窗口那 20 万 token,是"这一次对话能装多少",不是"每次开机自动加载多少"。
而且常驻部分要按轮次计费:4,507 字符约 2,800 token,一次 20 轮的对话,累计就是 5.6 万 token 的输入开销。如果把常驻层做成 5 万字符,20 轮累计约 66 万 token 的输入------这只是账单,不是单次窗口,但足以让成本失控。
所以那个上限小,不是设计得抠,是因为它是每轮都要背的包,不是仓库。
六、重新定义:它不是知识库
| 知识库 | 启动协议 | |
|---|---|---|
| 追求 | 全 | 够用,且一定装得下 |
| 谁来读 | 人,主动查 | Agent,开机自动加载 |
| 失败方式 | 查不到 | 以为自己知道 |
| 容量原则 | 越多越好 | 越少越好 |
知识库追求覆盖率;启动协议追求关键路径上的确定性。前者装不下只是"少知道一些",后者装不下会"做错事"。
七、方案:四层
bash
notes/ ← 注意:不是 memory/,原因见坑 3
├── index.md 2,006 字符 话题索引(脚本生成)
├── tasks.md 1,222 字符 任务看板
├── academics.md 2,712 字符
├── health.md 3,393 字符
├── devices.md 6,742 字符
├── network.md 1,956 字符
├── ...
└── tasks/archive/2026-Q4.md 已完成归档
外加两个自动注入的文件(在 Agent 的配置目录里):
css
MEMORY.md 2,722 字符 操作规程(L0-A)
USER.md 1,785 字符 身份与状态(L0-B)
四层职责:
| 层 | 文件 | 注入 | 容量 | 装什么 |
|---|---|---|---|---|
| L0 | MEMORY.md / USER.md |
✅ 每轮自动 | 2,722 / 1,785 | 行为规则、硬约束、指针 |
| L1 | notes/index.md |
❌ 开工读一次 | 2,006 | 话题 → 文件、触发场景 |
| L2 | notes/*.md ×14 |
❌ 命中才读 | 单个 ≤8,000 | 具体内容(三区结构) |
| L3 | archive/*.md |
❌ 几乎不读 | 不限 | 冻结的历史 |
补充一句口径:框架每轮还会注入
SOUL.md(1,524)和IDENTITY.md(596)。所以真正"每轮都在"的其实是四个文件、合计 6,627 字符。本文说的 L0 只指我自己控制的那两份。
核心原则:L0 只装"算法",不装"数据"。
✅ 装「他问进度就去读 academics.md」 → 算法,不过期
❌ 不装「他学到第三章」 → 数据,一周就过期
结论只存在于一个地方(分片的现状区),L0 永远只指路。这样"信息过期"在架构层面就基本消失了------L0 里没有会高频过期的内容("年龄""报名月份"这类低频字段仍在,但它们一年才变一次,且变化本身是可预见的)。
一个结构上的印证
写到这里我去查了 DeepSeek V4 的长上下文架构,发现它内部的策略,和我这套外部记忆在结构上是同构的:
| 模型内部 | 我的外部记忆 |
|---|---|
| 本地窗口:近处内容逐字细看 | L0:规程完整在场,一个字不省 |
| CSA:中远距离看摘要、挑重点回看 | L1 / L2:索引定位 + 按需读分片 |
| HCA:超远距离只留一个整体轮廓 | L3:冻结档案,只在查细节时翻 |
近处保真、中距离摘要、远处轮廓------这是同一个约束(信息量 > 容量)逼出来的同一种策略。
当然,尺度和实现差得很远:模型的本地窗口是 token 级、由注意力机制自动完成;我的 L0 是 4,500 字符、靠人写规则。同构的是约束,不是实现。
但区别在于关键的一点:模型能做的只有压缩,压完就真没了;而在外部,被压掉的东西还躺在硬盘上,随时可以取回来。
八、三条判据
结构可以抄,判据不能抄。但推导用的这三条是通用的:
1. 「这条不在场,我会不会做错事?」
会 → 进 L0。只是"回答得不够丰富" → 进分片。这比"重要性"好判断,因为它可判定。
2. 「我什么时候需要它?」
决定分片怎么切。按主题切永远有归属争议("算学业还是算方法?");按处境切,每条只有一个正确答案。
3. 「以后用得着吗?不是有没有意思。」
决定归并时写不写。
第 2 条最值钱。索引里那一列不叫"关键词",叫**"什么处境读它"** ------因为它描述的是 Agent 的处境,不是用户的用词。漏查永远发生在用户没用那个关键词的时候。
九、我踩的四个坑
坑 1:把会变的数据写进常驻层
最初 L0 里写着"他昨晚睡了 7 小时 15 分""某个视频播放 6.5 万"。
一个多月后,这些数字全过期了,但还在最显眼的位置。AI 读到就当真。
修法:常驻层禁止出现高频变动的数字(时长、播放量、版本号、进度)。数字进分片,L0 只留判断。
坑 2:新旧结论并存,谁都不算错
我在归档文件里写了「A」,后来发现 A 错了,在另一个文件里写了「B 才对」。
结果两份都在,AI 读到哪个用哪个。更糟的是:自动注入的是 A(旧的那份),B 在需要主动读的文件里。
类似的矛盾我找出了 4 处。它们不是笔误,是"修正只写在新处,旧结论原地不动"这个习惯导致的。
修法:分片内部分三区。
shell
## 现状
当前唯一结论。可覆盖。修正 = 直接改写这里。
## 时间线
只追加。`## 2026-10-08 摘要` 一行一条。
## 已排除
试过不行的方案、被否掉的建议。防止 AI 重复提同一件早被否掉的事。
旧结论只作为"修正:A→B"留在时间线里,不再占据任何活跃位置。
坑 3:把系统目录当成自己的
我最初把分片建在 ~/.xxx/memory/,后来发现那个目录是工具自己用的 ------里面有个 memory.md 是云端画像的缓存,由系统定期覆写。
差一点就把 14 个分片塞进一个会被重写的目录。
修法 :任何"我指定一个路径"的设计,先验证那路径没被别人占用。现在分片放 notes/,和系统目录完全隔离。
坑 4:手写的元数据必然漂移
一开始我在索引表里手写"首次讨论时间 / 最近更新时间 / 讨论次数",两周后就开始对不上。
后来我把这三列改成脚本扫出来的,以为解决了。
但没解决。写这篇文章时我发现了一个更隐蔽的同类问题:
L0 的首行探针写着「2,646 字符」,而它现在的实际大小是 2,722 ------差 76 字符。因为这中间我加了一行审计说明,改了正文,却忘了改首行那个数字。
探针本身,就是靠手写的元数据。
修法:所有可计算的数字都不手写。首行那个体积声明也应该由脚本生成,或者干脆只写"块数"和"末行标记"这种不会漂的东西。
python
# 检查:探针声明的字符数 vs 实际
import re
t = open('MEMORY.md', encoding='utf-8').read()
declared = int(re.search(r'|([\d,]+) 字符|', t.split('\n')[0]).group(1).replace(',', ''))
print('声明', declared, '实际', len(t), '差值', len(t) - declared)
十、两个机制,成本几乎为零
探针
L0 首行声明体积,末行放一个标记:
html
... 正文 ...
AI 每次读到的第一行就是这个。如果它数得到 7 块却看不到 L0-END,就说明被截断了------这时应该主动去读全文,而不是拿半份规则干活。
成本:两行注释。收益:把"静默失败"变成"可发现"。
(至于那个字符数会不会漂------见坑 4。)
定期归并
日常对话产生的信息不会自动进分片,所以需要一个周期性动作,把散落的日志归并进对应分片。
我搭好了这套东西,但还没跑过第一次------首次执行排在本周日,方式是定时任务 + 邮件通知:
- 无异常不发信,避免每周一封"一切正常"把它变成噪音
- 任务自身失败也要发信(这一条是设计意图,等首跑后验证)------因为"没收到邮件"和"任务挂了"看起来一模一样,不做这一条,你会在几个月后才发现它早就停了
十一、什么时候不该用这套
这条比"怎么用"更重要。
判断依据不是某个百分比,而是一个观察信号:当你开始遇到"AI 建议你做它自己档案里禁止的事",或者怀疑"它是不是没看到那一部分"时,就该考虑分层了。 在那之前,一个大文件反而是最省事的形式。
| 场景 | 判断 |
|---|---|
| 记忆 < 5 万字符,一次能全塞进上下文 | ❌ 别分层,纯属自找麻烦 |
| 短期项目(一两周结束) | ❌ 建造成本收不回来 |
| 单次任务型 Agent | ❌ 没有"下次"就不需要长期记忆 |
| 纯网页版、无文件读写 | ❌ 整套依赖读写本地文件 |
| 长期陪伴 / 跨月协作 / 大量硬约束 | ✅ 值 |
十二、最后一条诚实的
这套设计能保证**「规则在场」** ,但保证不了**「规则被执行」**。
探针能发现文件被截断,索引能让查阅变便宜,定期归并能发现矛盾------但没有任何机制能强制 Agent 在回答之前真的去读那两个文件。
这是整套东西唯一没有机械保障的地方,也是它最可能失败的地方。
承认这一点,比假装设计完美有用。
附:改造前后的数字
| 项 | 改造前 | 改造后 |
|---|---|---|
| 常驻层(我控制的两份) | 18,481 字符(实际注入约 14,175) | 4,507 字符(完整注入) |
| 四个注入文件合计 | 约 20,601 字符 | 6,627 字符 |
| 内容总量 | 76,175 字符(三文件) | 51,145 字符 / 14 个分片 |
| 查一次资料的读取量 | 57,694 字符(全量归档) | 1--7 千字符(多数 2--4 千) |
| 已知矛盾 | 4 处 | 0 处 |
"完整注入"这个判断的依据是:新 L0 的 4,507 字符小于旧版观测到的 4,120+ 那个量级的上限假设,且新文件能读到末行
L0-END。但平台的确切上限我没有接口可查,这一条属于观测,不属于定义。
如果这篇对你有用,欢迎在评论区说说你的记忆文件现在多大了、有没有遇到"AI 建议你做它自己禁止的事"这种情况。我很好奇这个问题有多普遍。
作者:墨流觞雪
协作:望舒(AI 助手)------ 结构设计、数据统计、文字初稿
数字口径说明:文中"我的实测"指我自己跑出来的观测(归档与常驻文件的字符/字节数、注入量、分片体积、渲染耗时),测量方法见第四节;论文数据、插件默认值、社区记录均为引用,出处已在正文标注。
分工:方向由我定;结构由她提出,我逐条确认或否决;文字由她起草,我核对、修改、重写。文中"我踩的四个坑",多数是她在实际运行中先撞到、然后我们一起改的。