
最近在做一个陪伴型智能体Demo我一直在折腾一个问题这玩意儿到底能不能记住上周聊过的事目前的方案大多是塞进向量数据库效果还行但有几个晚上让我睡不踏实——它记住的东西我看不见、摸不着也没法手动改。它记错了我不但纠正不了还得眼睁睁看着它在错误的前提上继续推理。后来我换了个思路顺手起了个项目名叫openclaw核心主张就一句话把记忆变成配置文件。所有长期记忆不管是一句话的事实、一段偏好、还是某个任务的关键结论都落到本地文件里用Markdown写、用YAML做元数据、用Git管理版本。这个思路听起来挺朴素但实际跑下来它解决了很多智能体项目里记忆不可控的顽疾。这篇文章就把我为什么这么做、文件体系怎么设计、数据怎么流转、踩过哪些坑完整分享出来。适合正在做智能体、RAG或任何需要长期记忆功能的开发者参考就算你还没到那个阶段这个设计思路对理解记忆系统的本质也有帮助。1. 记忆为什么需要文件化而不只是向量化多数人做智能体记忆第一反应都是上向量库。把对话切成片段embedding成向量问的时候做相似度检索拿top-k当上下文喂给模型。这套路成熟、见效快我最早也是这么干的但跑久了会碰到一组共性痛点。1.1 向量检索的三个盲区第一不可读。向量库里的记忆是一堆浮点数组你不能直接翻开看它到底存了什么。有一次我的智能体把用户的我喜欢喝美式和我不能喝咖啡因同时作为记忆存了进去谁都没错但它已经完全忽略后者因为相似度检索把咖啡这个话题优先级提到最高。你根本没法在检索结果之外发现这逻辑冲突。第二不可修正。用户在对话里明确说我之前说的不是那个意思这是一次标准的记忆修正。但向量库里旧向量还在新向量也进来了检索时旧的还在前排。要么你费劲做删除、重算要么听之任之。第三不可审计。智能体做的每个决定到底参考了哪条记忆向量检索是概率性的每次回来top-k都可能不一样推理链不稳定。一旦出问题想复现比登天还难。1.2 文件化记忆到底改变了什么文件化记忆的思路是把记忆从隐式的向量空间里拉回来变成显式的、人可读的数据文件。它不追求用相似度去猜哪条记忆相关而是用明确的规则、标签和索引去查哪条记忆应该生效。有点像你把记账从拍脑袋改成翻账本每笔都落纸账目才清楚。这个转变最重要的价值是让记忆从一个黑盒变成了白盒。系统记得什么白纸黑字写在那里用户想改打开文件改一行就行版本想回退Git checkout就完事。对于追求稳定、可控的智能体产品来说这个取舍价值极高。1.3 三种记忆方案的直观对比方案可读性可修正性可审计性适用范围对话缓冲差纯上下文窗口差基本靠遗忘差无痕单轮或短多轮对话向量数据库差浮点数组中可删可重写但麻烦中可记录未读内容大规模语义检索问答文件化记忆openclaw好纯文本好直接编辑文件好每步都留痕需要长期稳定事实、偏好、任务进度的场景我把这份对比摆在台面上不是说向量库没用。语义检索在模糊回忆场景里仍然不可替代比如我之前是不是提过某个关于旅行的想法这类连用户本人都说不清楚的问题。但智能体的稳定记忆不该只靠模糊回忆它更需要准确记录。这两者应该分工而不是互相替代。2. openclaw的记忆文件到底长什么样这个项目的核心是一套目录规范和文件格式约定。我不喜欢把记忆塞进一个大JSON或者SQLite因为那样又回到了不可读的老路。我的做法是每个角色一个配置目录目录里所有内容都是记忆目录本身就是一个人格的存档。2.1 目录设计人是目录记忆是文件profiles/ └── assistant/ ├── identity.md # 角色设定启动必读 ├── memories.md # 用户长期事实记忆核心文件 ├── contexts/ │ ├── 2024-06-01_todo.md │ ├── 2024-06-02_planning.md │ └── archive/ # 已归档的上下文 ├── skills/ │ ├── recipe.md # 用户关注的技能领域知识 │ └── work.md # 用户工作背景 └── state.yaml # 任务状态、进度标记目录按角色划分这样同一个智能体内核可以在不同场景加载不同人格和记忆。memories.md是主记忆文件存的是稳定不变、跨会话长期有效的事实contexts/下面按日期或事件存临时上下文比如一次头脑风暴的过程记录skills/存放的是这个用户世界里的领域知识state.yaml则记录当前任务进行到哪一步。2.2 记忆条目的元数据设计每条记忆不是光秃秃一句话我会用YAML frontmatter给它配上元数据目的就是为了后续能做精细化的读取与淘汰。--- id: mem-0012 timestamp: 2025-06-01T14:32:0008:00 source: chat#3412 importance: high ttl: 2025-12-31 status: active tags: [preference, coffee, health] conflict: mem-0007 --- 用户表示自己现在不能摄入咖啡因喝美式会有心悸反应。字段看起来简单但都是经过风沙的。id是记忆的唯一标识Git提交、冲突标记、回滚操作都靠它定位。timestamp记录写入时间用于后续随时间衰减的排序。source指向原始对话编号方便回溯到具体对话记录。importance控制这条记忆在上下文中的权重high的每次对话都要带low的只在相关时才加载。ttl设置过期时间时间一到就自动归档到contexts/archive/。tags用于按主题索引读取时能精准挑选相关条目而不是把整个文件塞进上下文。conflict标记与哪条记忆存在冲突这是留给下一节讲的冲突合并逻辑的入口。2.3 为什么选Markdown加YAML而不是JSON有人会问机器读取用JSON不香吗我的回答是这个文件的第一读者是人其次才是机器。Markdown让任何用户都能打开文件读懂智能体的记忆YAML frontmatter让解析逻辑依然简单可靠。多一层数据格式的人类可读性换回来的是用户对系统的信任感和可控感这笔买卖值。另外Markdown天然能表达结构比如用清单、表格来组织记忆内容。配置文件不像数据库表那么死板它允许同一个主题下有自然语言的描述模型加载后能直接理解不需要再转译。3. 记忆的完整生命周期写入、检索与淘汰文件格式只是骨架真正麻烦的是记忆怎么进来、怎么被用、怎么消失。我在这里把openclaw的记忆流程拆成三段。3.1 写入不是每句话都值得记写入判断是记忆系统的核心。我刚开始犯的错是把每轮对话都记下来结果memories.md变成了天书上下文窗口被垃圾填充。后来我总结了三个写入准则重复出现的信息。用户如果一件事说了两遍那基本是重要的值得记。明确修正的信息。用户说不对不是改成后面跟的必然是记忆更新点。带情绪或明确标记的信息。我特别讨厌我永远都不记得以后都要这些都是关键信号。写入动作分三步先查重再冲突检测最后落盘。查重很简单用hash或者语义相似度都行。冲突检测更关键——openclaw会读取tags相同的现有记忆如果新的陈述与旧条目在关键事实上相悖就会触发conflict标记而不是直接覆盖。比如用户新说我重新开始喝咖啡了旧条目mem-0007就会被标记为superseded新条目入列旧条目保留归档。落盘用的是临时文件原子rename。先写memories.md.tmp校验通过后rename替换原文件避免写入过程中进程崩溃导致文件半截。def write_memory(new_entry, pathprofiles/assistant/memories.md): content read_current(path) if detect_conflict(new_entry, content): mark_conflict(new_entry, content) write_conflict_log(new_entry) else: content dedup_and_append(content, new_entry) tmp_path f{path}.tmp with open(tmp_path, w, encodingutf-8) as f: f.write(content) f.flush() os.fsync(f.fileno()) os.replace(tmp_path, path)3.2 检索先把目录放在桌面上openclaw不用向量检索它的读取策略分三层。第一层固定载荷。identity.md和state.yaml每次都读这相当于人的我是谁、我现在在干嘛必须常驻上下文。第二层标签索引。根据当前对话的主题词提取标签去memories.md里挑选匹配的条目。比如用户聊到最近胃不太舒服标签命中health那么所有健康相关的记忆都会带上其他无关的就不占上下文。第三层按需深挖。如果当前任务涉及具体项目去contexts/下找对应日期或事件的文件。比如用户问我们上周说的那个迁移方案呢就去找2024-06-01_planning.md。这个分层策略最大的好处是上下文消耗可控。向量检索一次要带几十条候选而标签索引往往只需要精准的几条Token开销小一个量级。3.3 淘汰记忆不该只增不减文件化记忆最怕膨胀。一条记忆一直留在memories.md里最终会让加载成本失控。openclaw的淘汰机制有三层TTL过期凡是带临时属性的记忆都设ttl到期自动移入contexts/archive/不参与日常加载。重要性降级每隔一段时间扫描一次importance为low且长时间未被命中的条目标记为cold默认不参与检索。归并压缩同主题的零散条目超过5条时触发归并。比如用户不断细化喜欢的旅行方式最后就合并成一条结构化描述附上所有历史来源ID。淘汰不是删除。所有被淘汰的记忆都留在archive/里随时可以恢复。这符合配置文件可追溯的原则——你永远不会因为某次淘汰而永久丢失用户信息。4. 把记忆当代码管理Git版本化与多轮回滚文件化最爽的时刻是你发现可以像管理代码一样管理记忆。Git天然适合纯文本文件记忆目录就是一个仓库。我在这上面尝到了不少甜头也踩了几次坑。4.1 自动提交策略每个任务节点留下存档openclaw会在每个任务完成后自动执行一次Git提交。提交信息有固定格式feat(mem): 记录用户旅行偏好调整 - mem-0013: 新增喜欢靠窗座位 - mem-0007: 标记superseded咖啡因禁令解除 - contexts/2024-06-03_planning.md: 新增 - state.yaml: 进度更新为阶段2/4这套提交信息不是摆设。哪天用户自己都忘了自己说过什么你可以翻Git日志一点一点还原整个沟通过程。这种反推人格演化史的能力向量库给不了。我把提交时机定在节点而不是每条一次对话结束、一个任务里程碑达成、一次用户主动要求记住这些的时候。提交太频繁会让日志噪声爆炸太稀疏则失去版本意义。实践下来一个自然交互会话一至两次提交的节奏最合适。4.2 回滚实战话题跑偏的后悔药多轮任务最怕什么怕跑偏。智能体在第三轮脑补了一个错误前提然后顺着错误前提猛跑五轮你发现整个方向都不对了。文件化记忆的救法很直接回到跑偏之前的那次Git提交。git log --oneline # 找到方向拐弯前的那次提交a3f9c2e feat(mem): 完成需求确认 git checkout a3f9c2e -- profiles/assistant/只回滚记忆目录不动代码目录。然后重新启动对话上下文里加载的就是那个还没有跑偏的记忆状态。配合state.yaml任务进度同时回退到第五轮之前的形态。这套操作下来等于给了智能体一台时光机。我第一次在真实场景里跑通这个流程时真有点激动——原来智能体的性格和记忆是可以做快照的。用户A想要激进版本用户B想要保守版本同一套系统完全能靠不同分支并行。4.3 分支合并人格分裂与统一提到分支就得说合并。曾经有个项目让智能体同时在两个线程里与用户协作一个线程偏执行一个线程偏闲聊。结果两个线程分别往记忆目录里写产生了分叉。处理方式也借鉴Git合并时把两个版本的memories.md都打开按id逐条比对 HEAD --- id: mem-0033 --- 用户最近在准备一场公开演讲方向是AI科普。 --- id: mem-0033 --- 用户在准备技术分享主题可能是分布式系统。 mem-thread-2这种冲突手动解决最稳。让模型去自动合并风险很大它可能同时保留两条互相矛盾的事实。我的做法是把冲突展示给用户让用户选一个或重写一条。记忆是关于人的事实最终裁决权应该属于人这一点在我所有设计里都是不可动摇的底线。4.4 记忆Git化带来的额外收益版本管理跑通之后额外收获了不少能力Git能力记忆管理中的应用git diff对比两个时间点的记忆差异追溯用户偏好的演变git log查看完整交互史复盘智能体决策依据git branch同一角色在不同场景下的记忆分叉工作/生活git blame定位某条记忆是哪个对话、哪次操作引入的git revert单独撤销某次错误的记忆写入这些操作在传统记忆方案里要么做不到要么得专门开发一套系统现在托Git的福全部白送。5. 实战踩坑手动编辑、并发写入与记忆膨胀设计再完善落地总会有意外。这节是几个人工兜底的场景也是我被人问得最多的地方。5.1 用户手动改文件之后系统不知道怎么办文件化记忆最受人欢迎的特性——人能直接编辑——也是最容易出事故的源头。有一次用户在memories.md里手动删掉了一条high重要度的记忆但state.yaml里的任务状态还与那条记忆强相关。结果对话一开启智能体发现任务状态说已确认预算但记忆里找不到预算数字当场生成幻觉数据。对策是增加一个加载时一致性校验每次启动读取记忆时openclaw会扫描关键字段引用关系如任务状态引用的记忆ID是否存在发现断链就写入audit.log并在对话中主动提示用户我发现有两处信息对不上你确认一下。这种情况下宁可多问一句也不要让模型瞎猜。5.2 并发写入导致文件互相覆盖多个线程同时往memories.md里追加内容如果都走临时文件rename后完成的会覆盖先完成的丢失先写入的那批记忆。这是我踩过最疼的坑之一丢过一整轮规划讨论的记录。解决办法分两层。第一层是写入前持文件锁fcntl.flock保证同时只有一个进程在写第二层是写入后做source字段检查如果发现本次追加的记录在并发窗口内被覆盖缺失就把丢失记录补写到contexts/下的恢复文件下次合并。我现在的建议是单目录单写入者非要并发就按角色拆目录把冲突概率从源头降下来。5.3 记忆文件膨胀当金鱼变成象三条T恤的记忆不占地方但跑一个季度后memories.md很容易膨胀到上千行。每次对话都全量加载不可行标签索引也只能缓解。我的处理是给memories.md设一个单文件上限比如200条。超过后触发分层降级最老且importancelow的条目移入contexts/archive/中等重要的条目做归并压缩把多条同类描述合成一段概述。压缩后的内容虽然信息密度高但模型读起来反而更清晰。另外一个心得今日事今日毕的记忆不进主记忆区。临时任务记录直接写contexts/{date}.md每天结束后由清理脚本把关键结论沉淀到memories.md其余打上ttl然后归档。这样一来主记忆区永远只存这个世界里不会速朽的事实。5.4 敏感信息与安全边界把记忆变成明文文件安全责任也变大了。配置文件里如果存了密码、身份证号、医疗记录这类敏感数据一旦仓库泄露就是事故。现在的项目里openclaw会做两级处理写入时脱敏正则匹配疑似敏感字段手机号、邮箱、账号等替换为占位符并记入secrets_refs.yaml。仓库隔离记忆仓库只保存在本地或私有仓库不随代码库同步。state.yaml里的状态字段也尽量避免携带原始敏感值。但说句实话这套方案不适合需要强合规的场景。如果做面向公众的产品该上加密库的还是要上文件化记忆更适合个人工具、内部助手、可控环境这类场景。别把它当万能钥匙。最后提一嘴把记忆变成配置文件之后最大的变化不是技术上的而是我和智能体之间的信任关系变了。以前它记错了我只能接受现在它记错了我可以打开文件改一行或者直接git revert。记忆不再是藏在模型权重和向量空间里的黑箱而是一沓摊在桌面上的纸每张都可以翻阅、批注和撕掉重写。我个人的体会是做智能体不要只追求能记住要追求记得住、看得见、改得动、收得回。配置文件这条路恰好把这四件事都满足了。如果你也在折腾智能体记忆不妨从今天起把手里的向量库降级成检索辅助把主记忆区换成Markdown试试。