对标之后抄什么:四条做法的适配方案

八个框架横向对比挑出了几条别人做得比我们好的地方。 这一页只回答接下来的问题:放进我们的结构里,每一条该怎么落,或者为什么不落。 权威正文在overview.md,机制图在 记忆子系统:机制图解

第一层

我们现在

对着 openprogram/memory/ 里跑着的代码核实过,行号照录。带实测的地方写出数字。

第二层

人家怎么做

被抄的那家的原始形状,以及它为什么长成那样。形状不一定适用,动机通常适用。

第三层

我们打算

放进我们结构后的方案,或者不放的理由。每条一个判决,没有"值得研究"这种结论。

00 四条判决 01 检索不建索引 02 常驻块怎么裁 03 离开的分支 04 游标兜底 05 改动落在哪

00四条判决

先给结论。三条改造后采纳或直接采纳,一条否掉,理由都不是"不好",是我们的结构里那件事已经有别的东西在做,或者代价落在了错误的位置上。

借来的做法 出处 判决 一句话理由 检索不建索引:文件抬头写自述,模型从清单里挑五个 每轮检索多一次小模型调用,换掉索引 claude-code 否掉 那次调用买的是"没有索引也能语义匹配", 我们有索引,粒度还比文件细 常驻块按归属裁:只丢自动升上来的,人写的一律不动 并把丢掉的清单返回给调用方 openclaw 改造后采纳 · 已落地 归属之所以要紧是因为丢等于销毁。 让 core.md 变成派生视图,丢就不再销毁 离开一条分支时给它生成摘要,接在树上 被放弃的分支里说过的话不至于凭空消失 pi-mono 改造后采纳 目的照收,摘要节点不要。会话边界枚举全部 分支即可,走到有标记的前缀就停 游标找不到时,把全部消息重新当成没写过 返回零条会让抽取彻底停摆,重做一遍的代价有限 claude-code 采纳 迁移那条已覆盖一半,另一半没覆盖: 读不出来的账本现在会抛,然后无限重试

01检索不建索引否掉

claude-code的做法:每个记忆文件抬头写一句自述,检索时把这些描述列成清单,花一次便宜的小模型调用挑出五个文件名,全程不建索引。我们的检索是每轮都跑的BM25,跑在用户消息和模型调用之间。

第一层 · 我们现在 第二层 · claude-code 第三层 · 我们打算 用户发来一句话 provider.search() 每轮一次 retrieval/inspect.search,top_k=5,内部封顶10 BM25索引,persist=False 每轮把 topics/ 和 sources/ 全量重新哈希、重新解析 耗时与工作区体积成正比,见下图 五个段落,带块ID和脚注 直接围在 memory-context 里进提示词,零次模型调用 同一轮里的额外模型往返:0 每个记忆文件抬头一句自述 把全部自述列成一张清单 没有倒排表,没有向量,没有缓存要维护 一次便宜的小模型调用 判据比"相关就选"细:拿不准就不选,宁可返回空 findRelevantMemories.ts:18-24 五个文件名 agent 拿到名字,自己再 Read 一遍 同一轮里的额外模型往返:1,串在用户等待里 不引入这次调用 它换来的是"没有索引也能按意思找"。 我们已经有索引:BM25现成,语义那一路有 retrieval/embedding.py,都不占模型往返。 不引入文件自述 我们的返回单位是段落,不是文件。文件名 已经是自述:一个主题一个文件是写入契约, topics/people/dave.md 不需要再解释一遍。 只留下它的一条判据 "拿不准就不选,宁可返回空"。我们现在的 下限是 lexical<=0 且 adjustment<=0.02 才丢, 等于"共享一个词就算命中",所以和记忆无关的 一轮照样会被塞五段。分数下限值得单独加。
实测:在这份工作区里给 topics/ 插一段没有脚注的介绍性段落,严格解析直接拒绝 (TopicFormatError: memory block ID required),blockquote 同样被拒,只有HTML注释能过。 也就是说"抬头写一句自述"落到我们的格式里得先改解析契约,而换来的信息路径名已经给了。
每轮检索的实测耗时(本机,合成工作区,三次取平均) 这一栏是"多一次模型调用"要挤进去的地方。它现在已经不宽裕,而且宽裕不起来的原因是本地解析,不是排序。 0.50 MB 冷 66 ms 3.34 MB 冷 431 ms 9.80 MB 冷 1292 ms 9.80 MB 热缓存 981 ms(persist=True,缓存命中仍要重建评分语料) 0 500 ms 1000 ms
合成工作区:主题文件每个12个块,来源档案每个60条消息,按真实格式生成。 耗时随工作区总字节线性增长,因为 inspect.search 走的是 persist=False, 每轮把两棵目录树全量重新哈希和解析。结论的方向:每轮检索的预算问题是本地的, 本地问题能修;模型往返修不掉,只能不加。

否掉,不是因为它不好。 claude-code没有索引,这条路是它绕开索引的办法,同一份对比里也正是它把实现规模压到最小的原因。 我们已经付过建索引的代价,再花一次模型往返去买同一件事,买的是重复的东西,付的是每一轮的等待。 真要提升检索质量,先做的两件事都在本地:一是让每轮检索不再全量重解析,二是给结果加一个真正的分数下限。

02常驻块写满了怎么裁改造后采纳 · 已落地

openclaw裁 MEMORY.md 的时候只丢自己自动升上去的段落,人手写的一字不动,并把丢掉的日期列表返回给调用方。要抄这条得先答一个前置问题:core.md 在新结构里到底是什么。答完之后会发现,归属这件事本身可以不用再区分。

第一层 · 我们改之前 写入 agent 提示词:稳定信息 才更新 core.md core.md 追加一段 带块ID,带证据脚注 和主题文件同一套契约 2000 token 闸 block_views.py:27-36 超了就 raise 整笔事务被拒 这一轮的主题文件改动 一起回滚,工作区逐字节不变 修复话术:core.md is full. Leave it alone and put this in a Topic file. agent.py:96-99 从此以后每一条稳定事实都被劝走 core.md 停在它第一次装满时的样子,不再变化 没有任何东西在维护它 夜间重整只处理 topics/:organize_topics 把 topics/**.md 列进提示词,core.md 不在列表里(management/api.py:83-94)。 重构之前有一条确定性重建流程负责它,做法是"core.md 是某个指定页的镜像";重构时删掉了,没有补回来。
第二层 · openclaw:一个文件,丢就是销毁 第三层 · 我们现在:正本加派生视图,丢不销毁 MEMORY.md 就是全部,10000 字符上限 人手写的段落 · 一律保留 自动升上来的 · 按日期从老到新丢 被丢掉的 · 世界上不再有第二份 正因为丢等于销毁,才必须先分清归属。返回丢掉的日期列表是它的留痕。 memory-budget.ts:25,100-164 按归属,不按时间 topics/core.md 正本 一个主题文件,和别的主题 文件同一套契约,不设上限 段落 ^a1 · 常驻 段落 ^b2 · 常驻 段落 ^c3 · 超出预算,留在这里 渲染 core.md 派生 每次写入成功后重建,和 timeline/ 那些放在一起 段落 ^a1 段落 ^b2 装到 2000 token 为止,装不下的不报错 归属问题就此消失 被挤出去的段落还在正本里,检索照样找得到,脚注照样指着原话。 既然丢不销毁,就不需要先判断这段是谁写的才敢丢。 谁想让某段永远在,就把它排在前面 渲染按正本里的文件顺序装,装不下的是尾巴。人手编辑正本、夜间重整调整顺序, 都是已经存在的动作,不需要为常驻块单独发明一种标记。

前置问题的答案:core.md 是派生视图,不是独立内容。 它是 topics/core.md 这一个主题文件的渲染结果,和 timeline/recent_events.jsonlrelations.json 同一类,手工编辑它不起作用, 编辑要落在正本上。写入 agent 的提示词随之从"更新 core.md"变成"要每次都看见的稳定事实写进 topics/core.md",它于是只需要认识一种文件。老实现"core.md 是某个指定页的镜像"这句话仍然成立, 只是指定页从 wiki 页变成了主题文件。

为什么必须先变成派生视图,才谈得上裁 工作区有一条硬契约:编辑前存在过的块ID,编辑后必须还找得到,否则整笔拒绝。 workspace.py:147-162 取基线时把 core.md 的块ID并进集合;topic_normalization.py:296-322 比对后判 "block ID must not be removed"。 现在:core.md 里可能有独苗 core.md 段落 ^c3 topics/ 里没有同一个ID 裁掉它,^c3 从整个工作区消失,事务判定"块ID不得移除", 这一笔连同别的改动一起被拒。裁剪在今天是不合法的操作。 派生之后:core.md 的ID永远是子集 core.md 段落 ^c3 topics/core.md 里的 ^c3 裁掉渲染结果里的 ^c3,ID在正本里活着,契约满足。 顺带两处特判可以删掉:取基线和校验都不必再单独看 core.md。
格子我们改之前openclaw我们现在
谁写 写入 agent 直接编辑 core.md 自动升级流程加人手编辑,同一个文件 谁都不写它。写 topics/core.md,Runtime 渲染
闸门 2000 token,超了抛异常management/config.py:7 10000 字符 2000 token 变成渲染预算,不再是异常
满了怎么办 整笔事务拒绝,修复话术让写入方绕开它,core.md 从此冻结 丢自动段落,人写的不动 按正本文件顺序装到预算为止,装不下的留在正本
丢了会不会没 不适用,从来没丢过 会,所以必须先分归属 不会,正本是全量,检索照旧命中
什么时候跑 没有任何时候。夜间重整只列 topics/ 写入时按预算裁 每次写入事务安装成功后,跟着别的派生视图一起重建
说不说 reorganize 返回 {"status":"ok","topics":N},没有 core 的位置 返回丢掉的日期列表 返回渲染进去的token数和被挤出去的块ID

顺便补上"这次到底动没动"。夜间重整是模型判断, 而模型判断的失败模式是安静地什么都不做,并且在它自己的判据下它是对的: 同一套重整提示词,一个52万字符的单主题对话最后挤成一个34.4k字符的文件,重整跑了很多次一次都没拆它, 因为判据写的是"覆盖了两个主题"而不是大小。所以 reorganize 的返回要同时带上这一趟真正改过的文件列表, 空列表就是"这次什么都没动",让它可见。判据本身另说,但看不见的时候连该不该改判据都无从谈起。

03离开的分支改造后采纳 · 已落地

pi-mono在用户离开一条分支时给它生成一份摘要接在树上。这正对着 overview.md 承认的缺口:两条写入路径都只要 head 底下那条分支,用户中途放弃的分支再也没有人走回去。目的照收,摘要节点不要。

第一层 · 我们现在 第二层 · pi-mono 第三层 · 我们打算 起点 共享前缀 head,会被写 再没人走回来 两条写入路径都问 get_branch(session_id), 拿到的是 head 那一条,别的分支不在视野里。 分支摘要 它没有长期记忆层,摘要是被放弃的话唯一的去处。 branch-summarization.ts:200-262 会话边界枚举全部分支 list_branches(session_id) 给出每个分支尖端 对每个尖端走一次 get_branch(session_id, tip) 已写消息id的集合自动扣掉共享前缀 get_branch 已经过滤掉 rewound 的节点 非head分支欠够一批才写,头底下那条照旧强制 不新增节点类型,不新增事件,不多一次模型调用。 "故意放弃"不需要我们判断,会话存储已经记了 用户点回退,_rewind.py 给整段打上 rewound,节点留在图里但退出对话路径;get_branch 直接把它们滤掉(session_store.py:1030)。 重试产生的兄弟分支只有一两轮,够不上一批,自然不写。真正走了一段又折返的分支才够得上,而那正是缺口所指的那种。

为什么不做摘要节点。 pi-mono的会话JSONL树只为回放和分支服务,跨会话只剩 AGENTS.md,被放弃的分支不摘要就真的没了。 我们有一整层按主题存放的记忆,一条分支上说过的事实该去 topics/people/dave.md, 不该去挂在DAG上的一个摘要节点里,那里没有脚注、没有块ID、检索也进不去。 我们已经有一种挂在DAG上的摘要(context/summary 的压缩摘要),它服务的是上下文窗口,不是记忆, 再加一种只会让"这段话到底在哪"多一个答案。

而且"离开一条分支"在我们这里不是一个能观察到的事件。 事件注册表里没有任何 head 移动或者分支切换的事件;head 的写入散在五处 (set_headappend_message 的链式前进、SessionNodeWriter.appendupdate_session_rewind.py 直接改索引),它们唯一共同经过的地方是 SessionMemoryIndex.set_head,而那里把旧值直接丢掉了。 照抄 pi-mono 就得先在这条链路上开一个新事件、并让它记住上一个 head 是谁, 为的是触发一件在会话结束时做同样划算的事。会话边界那个入口已经在了: session_watcher.py:103 每次只问了 head 那一条分支。

一个要一起改的判据。 should_incremental_write 现在有两条:欠够 token 阈值,或者欠着东西且最后一条消息已经过去一小时。 被放弃的分支最后一条消息一定是旧的,第二条会让一轮的重试兄弟也被写进去。 所以非head分支只走第一条,把 idle_after 关掉。这是这条方案唯一需要新增的判断。

04游标兜底采纳 · 已落地

claude-code在游标指的那条消息找不到时,选择把全部消息重新当成没写过,注释写明返回零条会让抽取在这个会话剩下的时间里彻底停摆。我们的写入游标方案里"从原话档案播种"是同一个取舍。问题是它只覆盖了迁移那一次,日常运行里还有一条同形状的路没有堵。

第二层 · claude-code 堵的那条 第一层 · 我们现在同形状的那条 第三层 · 一条规则 游标是一条消息UUID,向后扫 那条消息在会话里找不到了 照直做的话:扫出零条 这个会话剩下的时间里再也抽不出东西 它选:全部当成没写过 重写一遍代价有限,安静停摆代价没有上限 游标状态在 runtime.json 里 文件读不出来或者不是合法JSON RuntimeStateStore.load 直接抛 state.py:51-55,json.loads 没有兜底 变成 WriteFailure(retryable=True) 看门狗不标记会话,每一轮回来重试,永远写不进去 读不出来的游标等于空集合 不是异常。一个读不出来的游标说的是 "我不知道哪些写过了",而集合形状下 "不知道"的安全解释只有一个:都没写过。 重写一批的代价是有限的,而且事务本来就会 合并说了同一件事的段落。 另一半已经被结构管住了 游标文件在工作区里面,不在旁边: memory/.scriptorium/cursors/。备份、git提交、 回滚都带着它一起走,不会出现"内容退回上周、 游标停在今天"这种把整周都标成已写的情形。

和迁移那条的分工。 overview.md 的「从位置游标迁移」处理的是一次性的:老状态没法反推出账,于是从原话档案播种, 宁可多写一批也不全丢。那是同一个取舍,但它只在升级那一刻发生。 这里说的是日常运行时账本本身出问题,和迁移无关,两条都要有,方向一致。

账本换了形状之后,这一条落在哪。 写入游标最后取的是在节点上打「已写」标记,不是记忆自己存一份 ID 集合, 所以没有一个会读不出来的游标文件了。这一条的方向仍然成立,只是换了对象: 账本读不出来时按「都没写过」处理,而不是抛异常让这个会话永远写不进去。 节点标记这边对应的是标记丢了就重做一遍,以及 RuntimeStateStore.load 对着一份解析不了的 runtime.json 应当读成空计数器而不是抛。 两种做法的并排对比在 written-marker.md 第三层。

05改动落在哪

三条采纳的方案,落到文件上是这些位置。否掉的那条不落任何文件。

方案 落点 这个位置现在做什么 core.md 变派生视图 · 已落地 02节 上面五行改行为, 下面五行是它不再 被当成正本之后, 顺带要拆掉的特判 runtime/derived_views.py 重建 timeline、recent_events、relations,core.md 加进来 management/block_views.py:24-36 _synchronize一超2000 token就raise,改成渲染预算并报出挤掉的块ID management/agent.py:96-99 修复话术"core.md 满了,别动它",删掉 prompts/write.py:15 让写入方直接编辑 core.md,改成写 topics/core.md prompts/system.py:7,15 同一句话在系统提示词里又说了两遍,一起改 management/transaction.py:27,169-190 WRITABLE_FILES把core.md列为可手改,派生之后它不再可写 management/patching.py:1-5 补丁的可写面同样写着topics/**和core.md workspace.py 与 transaction.py 的四处基线 取基线时把core.md的块ID并进校验集合,四处都可以去掉 topic_normalization.py:100,222,250,311 分配块ID时把core.md追加进文件列表,当成第五个主题文件 store.py:95 ensure()建工作区时塞一个空的# Core,派生之后由渲染负责 会话边界写全部分支 03节 memory/writing.py:231-291 _branch只取head那条,强制模式循环写到没有欠账 runtime/state.py:111-121 should_incremental_write的闲置分支要能关掉 memory/session_watcher.py:103 会话闲置时db.get_branch(sid)只拿head那条,这是方案的入口 游标读不出来等于空集合 runtime/state.py:51-55 load里的json.loads没有兜底,读不出来就抛

节点marker已经替换位置游标。 实现位于SessionDB节点metadata、runtime/online.pywriting.py_records边界,legacy迁移从严格解析的Source前缀恢复可验证节点。 会话边界现在先处理当前head,再枚举其他活分支;共享前缀由节点marker去重。 完整定义见 written-marker.md

实现状态(2026-08-11):01节的无索引模型选择按延迟实测拒绝; 02节topics/core.md正本与确定性core.md派生视图已实现;03节会话边界枚举 活分支已实现;04节损坏状态按空状态恢复并配合严格legacy迁移与节点marker实现。 当前仍使用轮后阈值和idle watcher,没有由head变化事件直接通知writer。