从 76,175 字符到 4,507:我把 AI 的记忆拆成四层

16 阅读17分钟

从 76,175 字符到 4,507:我把 AI 的记忆拆成四层

一句话结论:AI 助手"记不住",多数时候不是模型的问题,是它的记忆文件被静默截断了——它从来没读到过那部分内容,而它自己不知道。

这不是"知识库"能解决的问题,是一份启动协议该解决的问题。


一、先说结论

如果你正在给自己的 AI 助手写记忆文件(CLAUDE.md、AGENTS.md、MEMORY.md、system prompt 里的画像……),下面三条可能对你有用:

  1. 记忆的注入量存在上限,而且远小于上下文窗口。 窗口有 20 万 token,不代表你能塞 20 万 token 的记忆。
  2. 超出部分通常是静默截断的。 AI 不知道自己缺了什么,会拿着半份规则继续工作,还会为自己的错误找出合理解释。
  3. 所以真正的解法不是"写更多",是"分层"。 一份极小的常驻协议 + 一个按需读取的仓库。

我按这个思路,把一套 76,175 字符的记忆系统重构成 4,507 字符常驻 + 14 个分片。


二、故障现场

我给自己的 AI 助手维护了一套记忆档案:

文件字符字节
归档正文57,694124,770
常驻 MEMORY7,17415,884
常驻 USER11,30727,825
合计76,175168,479

某天它建议我做一件事——而那件事在它自己的档案里被明确标注为禁止。

第一反应是"它忘了"。但翻记录发现,那条禁令写得很清楚,位置也没错。

于是去数了一下:它到底读到了多少。


三、这不是我一个人的问题

写这篇之前我搜了一圈,发现"记忆丢失"至少有三种形态,各有各的名字。

3.1 养 Agent 的人的日常:/compact

社区里有人记录过他养 Coding Agent 的经历:

屏幕上弹出一句「上下文已超出限制,请使用 /compact 压缩」。
这是本周第 11 次失忆。
一个 /compact 下去,方案丢了大半,回来之后它一脸懵地问我:"您之前说要先改哪一层来着?"

这是"压缩型失忆":内容真的进过上下文,但后来被摘要掉了。

它和本文要讲的"注入截断"是同一条链上的两个环节——一个在对话中途压缩,一个在开机时就已缺失。

3.2 群机器人的默认值

如果你养的是群里的机器人,问题更具体。以常见的 QQ 机器人插件为例:

参数默认值后果
sessionIdleTimeout1,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-Base440.2 个百分点
DeepSeek-V4-Pro-Base434.8 个百分点
DeepSeek-V4.1-Flash26.1 个百分点

⚠️ 这里有个常被写错的因果:后训练能收窄差距,但压不掉周期性;V4.1-Flash 的差距最小,同时它的压缩步长也从 4 改成了 2。 两个变量同时变了,不宜把 6.1 单独归给其中一个。

论文里最值得记住的一句:

高平均分可以与系统性的位置失效共存。

也就是说,跑分看不出来。平均分一拉平,周期性的忽高忽低就被抹掉了。用户实际撞上的,恰恰是被平均掉的那个低谷。

这也解释了一个长期困惑——为什么有时候它记得清清楚楚,换个位置(前面多几个 token、换个上下文长度)就全忘了。不是玄学,是位置。

3.4 三种失忆,一个共同点

失败类型触发条件表现
注入截断记忆文件 > 注入上限一部分规则从未被加载
压缩遗忘对话长度 > 上下文窗口早期内容被摘要/丢弃
相位敏感信息落在压缩窗口的坏位置内容在,但检索不到

三者触发条件不同,共同点却是一样的:全都是静默的。用户不知道丢了什么,AI 也不知道。

这才是这件事值得认真做的原因——它不是一个"优化项",是一个看不见的失效模式。


四、实测:到底丢了多少

先说方法,再说结论。 我没有平台侧的任何接口,测法是:

  1. 在会话里让助手报出"它看到的记忆文件第一行是什么、最后一行是什么";
  2. 拿到这两行后,和磁盘上的全文对照,就能定位到从哪一行开始没了;
  3. 按行累加字符数,得到注入量。

这个方法的边界必须说清楚:它只能定位到"从哪一行断的",字符数是按行累加出来的,误差在几十字符量级;它也无法排除"平台在更底层做过额外裁剪"。所以下面的 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,开机自动加载
失败方式查不到以为自己知道
容量原则越多越好越少越好

知识库追求覆盖率;启动协议追求关键路径上的确定性。前者装不下只是"少知道一些",后者装不下会"做错事"。


七、方案:四层

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 的配置目录里):

MEMORY.md    2,722 字符   操作规程(L0-A)
USER.md      1,785 字符   身份与状态(L0-B)

四层职责:

层文件注入容量装什么
L0MEMORY.md / USER.md✅ 每轮自动2,722 / 1,785行为规则、硬约束、指针
L1notes/index.md❌ 开工读一次2,006话题 → 文件、触发场景
L2notes/*.md ×14❌ 命中才读单个 ≤8,000具体内容(三区结构)
L3archive/*.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 处。它们不是笔误,是"修正只写在新处,旧结论原地不动"这个习惯导致的。

修法:分片内部分三区。

## 现状

当前唯一结论。可覆盖。修正 = 直接改写这里。

## 时间线

只追加。`## 2026-10-08  摘要` 一行一条。

## 已排除

试过不行的方案、被否掉的建议。防止 AI 重复提同一件早被否掉的事。

旧结论只作为"修正:A→B"留在时间线里,不再占据任何活跃位置。

坑 3:把系统目录当成自己的

我最初把分片建在 ~/.xxx/memory/,后来发现那个目录是工具自己用的——里面有个 memory.md 是云端画像的缓存,由系统定期覆写。

差一点就把 14 个分片塞进一个会被重写的目录。

修法:任何"我指定一个路径"的设计,先验证那路径没被别人占用。现在分片放 notes/,和系统目录完全隔离。

坑 4:手写的元数据必然漂移

一开始我在索引表里手写"首次讨论时间 / 最近更新时间 / 讨论次数",两周后就开始对不上。

后来我把这三列改成脚本扫出来的,以为解决了。

但没解决。写这篇文章时我发现了一个更隐蔽的同类问题:

L0 的首行探针写着「2,646 字符」,而它现在的实际大小是 2,722——差 76 字符。因为这中间我加了一行审计说明,改了正文,却忘了改首行那个数字。

探针本身,就是靠手写的元数据。

修法:所有可计算的数字都不手写。首行那个体积声明也应该由脚本生成,或者干脆只写"块数"和"末行标记"这种不会漂的东西。

# 检查:探针声明的字符数 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 首行声明体积,末行放一个标记:



... 正文 ...


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 助手)—— 结构设计、数据统计、文字初稿

数字口径说明:文中"我的实测"指我自己跑出来的观测(归档与常驻文件的字符/字节数、注入量、分片体积、渲染耗时),测量方法见第四节;论文数据、插件默认值、社区记录均为引用,出处已在正文标注。

分工:方向由我定;结构由她提出,我逐条确认或否决;文字由她起草,我核对、修改、重写。文中"我踩的四个坑",多数是她在实际运行中先撞到、然后我们一起改的。