从一句「以后都用 pnpm」开始——它如何被捕获、过闸、入库、整合、召回、注入,又如何老去与告别。每一站都是源码里真实运行的机制;其中四个演示,此刻就在你的浏览器里跑。
Cairn(@chenhw7/dsh-memory)不是一张静态的表,而是一个有进、有出、有衰亡的循环系统。
实现上,它是八行 Cordis 编排:memory-store 打开存储域并注册 ctx.memory 服务;tool-memory 暴露九个记忆工具与 identity_update 身份工具;memory-review 负责自动提取与整合;memory-notes 把约定与踩坑渲染成 project-notes 段落(不向仓库写任何文件);memory-identity 播种并读取身份文档(SOUL.md / USER.md,默认关闭);memory-context 管理系统提示注入;memory-remote 支撑设置界面的记忆管理中心(远程写默认关闭);memory-root 只做客户端模块发现。所有插件共享同一个存储域。
数据落在 $DSH_HOME/storages/memory.json(默认的 host-medium 后端)或 $DSH_HOME/storages/memory.db(SQLite 后端)——卸载插件不会删除记忆。
一个 npm 包携带全部八行编排:宿主的服务以 peer 依赖声明,由已装的 DeepSeek Harness 提供,绝不重复安装;检索、分词、扫描内核手写零第三方依赖(记录在案的工程约束),SQLite 用 Node 内建 node:sqlite。tarball 预构建发布,安装环节零构建脚本——供应链越小,可信面积越小。
每条用户消息和工具结果都会流过一个零 LLM 成本的投影累加器——它只认三类信号。
用户直接要求记住。中英文的「记住 / 以后都 / 帮我记 / remember that / from now on」等模式命中即入候选。
用户推翻之前的说法。「不对 / 其实是 / actually / I meant」——纠正意味着旧认知是错的,新结论值得沉淀。
同一签名的工具调用连续失败(默认 ≥2 次)后终于成功——这次成功是被验证过的 workaround,蒸馏成踩坑条目。单次失败不入库。
两条 flush 路径保证信号不丢:压缩结束(compaction/end)从被遮蔽的原始事件里提取;会话销毁(session/disposed)对全会话做最后一次提取(5 秒上限、fire-and-forget,绝不阻塞事件)。
提取提示词本身是道闸:仓库已记录的内容、瞬时状态与未验证假设、未经工具验证的流程都不记;偏好类需用户明确提出或主题反复出现。输出行协议产出 [anchors:…] 与 [project] 标签,projectName 取最高优先级来源。身份层开启时另有一道机械 anti-echo 预过滤:与已注入身份文档的 IDF 加权重叠超过 0.6 的候选直接丢弃——身份文档自己的回声不会被抄进记忆库。
每 20 个会话,curator 把超长(≥400 字符)的条目挑出来——每批最多 5 条——交给提取模型改写成简洁单行。记忆库既会遗忘,也会沉淀。
收集层刻意放宽漏斗,保守由提取提示词承担:漏收是白损失——真信号一去不返;误收几乎免费——下一站的去重与整合会把噪音裁掉。把闸门放错位置,代价是不对称的。
每一次写入——无论来自模型工具还是后台提取——都必须穿过同一道闸门。亲手试试:
记忆主表。写在域写链上串行化;超出软上限按使用信号淘汰(pinned 免疫,accessCount / lastRecalledAt 最低者先走),全部豁免时允许超限——绝不硬删健康数据。
每次成功变更追加:操作、来源(tool / review / flush / ui / janitor)、内容预览。谁写的、何时写的,永远可查。
人审模式的待确认队列。提议不是记忆——不注入、不搜索、不衰减,只等待人的裁决(见站 07)。
系统元数据:SQLite 迁移水位线(防止二次迁移)、全库巡扫的进度与冷却、整合管线元信息。
身份文档(SOUL.md / USER.md)的当前记录:内容、版本号与种子版本,按 kind 主键。
身份文档的版本快照:每次改写留存一版;回滚只能恢复窗口内的版本,且不毁历史。
末尾两张表服务一个独立的可选层(identityEnabled)。SOUL.md 是助手的人格,USER.md 是助手对你的工作画像——内容由助手在对话中经 identity_update 整文重写(预算 soul 2000 / user 3000 字符),每次改写留一版快照;人从设置里的「身份」区审阅历史、回滚到任何保留版本,但不直接编辑。开启后两份文档以独立段落注入系统提示(见站 05);提取管线的 anti-echo 预过滤保证身份文档的回声不会被抄回记忆库(见站 01)。
存储有两种后端,一条配置切换:
| 维度 | host-medium(默认) | sqlite |
|---|---|---|
| 文件 | $DSH_HOME/storages/memory.json | $DSH_HOME/storages/memory.db |
| 引擎 | 宿主存储域 KV(与其他插件同构) | Node 内建 node:sqlite,WAL + busy_timeout,零额外依赖 |
| 写路径 | 域写链串行化,每次写整文件重新发布 | 每逻辑写 1 个事务(entries + audit 原子提交) |
| 写放大 A/B (226 次逻辑写) | 628 次全文件发布(2.78/写)· 累计 ~62 MB · 2.2 s | 0 次全文件发布 · 139 KB · 0.79 s(≈2.8×) |
| 切换 | 配置 storage: 'sqlite' 启动时一次性迁移并盖 migratedToSqlite 水位线;迁移后任一侧误启动都会 fail loud,绝不静默分叉 | |
还有一层跨进程单写者检测:存储后端假设每个进程独占写入——两个 DSH 进程共享同一存储根时会互相静默覆盖。memory-store 每次启动把一枚 owner stamp(pid + bootId)写进存储域的 global 槽并周期回读:读回别家的活印章即证明存在并发发布者,立即上报。它只检测、不加锁;已死 pid 或干净退出的前代印章不算威胁,重启不会误报。
host-medium 走宿主存储域,与其他插件同构、不引入额外文件,小写入量的记忆库完全够用;SQLite 服务于高写入场景(自动提取 + 巡扫 + 命中盖章),把整文件写放大压成每写一个事务。迁移只做一次且方向不可逆,两侧都会 fail loud——两条后端并存只会带来静默分叉,那是比慢更糟的故障。
新条目不直接堆进库里——它先和既有的记忆对质。
consolidation: 'legacy-judge' 保留。consolidation: 'legacy-judge' 保留旧路径作为开关。全库巡扫(可选,默认关):每 20 个会话创建时(外加首个会话的一次启动巡扫),按使用信号(hitCount / accessCount / 新近度)从全库挑出 top 20 条目并穷举配对(候选对上限 20),重新过一遍裁决协议。配对仍受词法门约束——重叠 > 0.2,零共享 token 的对照样被滤掉;巡扫真正的增益是「既有 vs 既有」的配对面与使用信号排序。进度与冷却(≥1 小时)持久在 meta 表。
命中信号(可选,默认关):每轮被注入的条目进入会话内台账;当助手的回答复述了某条目的 token 或 anchors,hitCount / lastHitAt +1——它只喂巡扫选对,从不参与删除。
被推翻的旧结论继续以「事实」身份被注入,是记忆库最危险的谎言——模型会拿着过期信息反驳当下的现实。取代不是删除:原文、审计与 supersededBy 指针全部保留,证据链完整可查;只是从此不再把它当作当前事实。
没有向量库、没有嵌入模型——一套零依赖的 Okapi BM25,配上 CJK 感知分词与拉丁保守词干。
检索分两步:结构化过滤先行(scope / category / projectName),再对幸存候选做 BM25 打分——文档频率按全语料统计,防止小候选池里的常见词虚高。分词器对拉丁文按词切分并剥一个保守后缀收敛词形(uses→use、studies→study,class、basis 永不受损);对中文同时产出单字一元 + 相邻二元——bigram 让「记忆」不再命中所有只含「记」的条目,一元保留单字查询的召回。summary 与正文同入索引。最终排序:相关性 ↓ → 置顶 ↓ → importance(缺省按 0,平票垫底)↓ → 新近度 ↓。
零命中回退:非空查询一个词法命中都没有时,memory_search 返回同过滤器下最近的条目并标记 fallback: true——只发生在工具返回里,注入面永不兜底。基准工具以 @chenhw7/dsh-memory/benchmark 导出,golden set 在 CI 里设下限门禁:success@5 ≥ 0.85、MRR ≥ 0.75、P@1 ≥ 0.6、中文切片 success@5 ≥ 0.8。
零依赖是记录在案的工程约束:BM25 可解释、零运行成本、离线可跑可测;中文二元分词补上了词级精度。诚实的代价也在明面上——没有跨语言与换说法的语义召回,golden set 的同义改写切片靠 summary 入索引兜住。等真实负载证明词法不够用时再评估升级,今天不为假设的需求付复杂度。
记忆如何进入上下文,决定了它为模型省下的每一个 token。
每个会话在创建时冻结一份记忆快照,整个会话期间复用——前缀不变,KV-cache 全中。唯一重新冻结的时刻是压缩(反正提示词都要重建),中途学到的记忆恰好在此刻补入。memory 段落位于系统提示 order 6000(宿主工具指引段 5000 之后),project-notes 紧随其后 @6001;身份层开启时,soul 与 user-profile 两段在更靠前的 @80 / @81。
可选的步级自动召回走另一条路:每个 agent step 用当步用户文本对库做一次 BM25,命中条目以 <recalled-memory> 围栏追加为一条用户消息——限 5 条、查询至少 12 字符、围栏 ≤1200 字符并带 token 尾注;不碰系统提示,前缀缓存分毫不动。加载时,与更新事实相矛盾的条目还会带上冲突标注(fresh / stale / conflicting 由一个纯 IDF 加权检测器判定),让模型自己权衡新旧。
常驻成本应与记忆库大小脱钩:policy-only 扁平约 344 tokens,digest 只在此基础上加一条一次性 ≈126 tokens 的类目与主题词清单——模型知道「库里大致有什么」,全文用 memory_search / memory_get 按需取。数据完全不进系统提示,KV 前缀最稳;需要更强存在感的库仍可切到 index 或 full(双预算 5000 字符 / 20 条)。
janitor 在每次会话创建时巡场。拖动时间轴,看三种条目的不同命运:
"部署脚本在 scripts/deploy.sh,端口 8080"
"代理环境下 pnpm 需先设置 registry 镜像"
"代码评审意见必须逐条给出理由"
project 作用域绑定仓库:仓库都换了、约定早改了,过期的项目记忆留着只会污染检索——过期即硬删。global / user 是跨项目资产,错删的代价远高于留存——盖章而非删除,一次召回就能翻案。两类记忆的错删成本不同,它们的死亡机制也应该不同。
删除是整个系统里最重的操作——所以它的每一步都被拦下来。
显式 confirm: true 双确认;超过检索上限一半的批次直接拒绝(失控保护);置顶条目跳过并在结果中报告;每一条删除独立写入审计。主题级遗忘不会顺手抹掉你置顶的镇馆之宝。
扫描器拦下的内容不落库;已入库后才发现的问题内容,管理界面通过 memory_get 的 raw: true 破窗读回修复——破窗本身进审计。原文永远可查,静默删除只会掩盖攻击。
最后是远程边界:记忆管理中心的 18 个 RPC 方法默认只读——10 读 8 写,写操作藏在一枚部署级开关(remoteWritesEnabled)后面;身份治理独占三个方法(identityList / identityHistory / identityRevert),其中回滚还要再过第二枚开关 identityRevertEnabled。信任的是传输围栏(回环 / 局域网 / 受信主机,由宿主持有),不是存储面:再宽的信任域也不会悄悄放大成整库写权限。
这个页面上的每个指标都有出处——要么在 CI 里设了门禁,要么跑在真实的 DeepSeek Harness 上。
| 层 | 测什么 | 怎么跑 |
|---|---|---|
| L0 | golden set 检索基准(35 条目 × 35 查询,en / zh / 混合,含同义改写与拉丁屈折切片) | CI 门禁:success@5 ≥ 0.85 · MRR ≥ 0.75 · P@1 ≥ 0.6 · 中文切片 success@5 ≥ 0.8 |
| L1 | 行为评估:core-v0 32 场景 132 题 + noise-v0 6 个长噪声场景 + identity-v0 3 场景 6 题 | 跑真实 harness + 确定性 mock 模型——无凭据随时可跑,每次 push 必绿 |
| L2 | 真模型判分,版本化 rubric(storage-v2 / recall-v2) | 环境门控;判分模型刻意不用被测模型——防自评偏置 |
| 单元 | 1096 个测试用例(1090 通过 + 6 需密钥跳过) | 存储契约测试跨三个后端跑:内存 / JSON 域 / SQLite |
被反复观察到的提议累计 hits 自动置顶;被反复召回的条目自动保鲜。使用痕迹本身就是最重要的元数据。
快照为 KV-cache 冻结整个会话,宁可延迟一拍也不抖动前缀——缓存命中省下的成本远超即时注入的收益。
每个注入面都声明:当前用户请求、仓库文件与工具输出永远优先于记忆。冲突时,相信当下。
37 条扫描规则、audit / suggestions 各 200 条滚动、entries 软上限 500、可选的人审队列——记忆系统必须能回答「这条记忆是谁、在何时、为什么写下的」。