dsh-memory 是一个可安装的记忆 bundle:它观察对话中的信号,把值得留下的经验沉淀入库,并在未来的每一次会话里,把相关的记忆精准地递回给模型。本文拆解它的完整流水线——从捕获、存储、检索到注入与衰亡。
记忆不是一张静态的表,而是一个有进、有出、有衰亡的循环系统。
每一次会话都会产生信号——一句「记住」、一次纠正、一段连败后终于成功的调试。dsh-memory 把这些信号收集起来,交给 LLM 提炼成条目,过安全闸后写入持久化的 KV 存储;等到下一次会话,检索平面再按相关性把它们送回上下文。被反复使用的记忆保持新鲜,无人问津的则 quietly 退场。
实现上,它是七行 Cordis 编排:六个功能插件各司其职——memory-store 打开存储域并注册 ctx.memory 服务;tool-memory 暴露九个模型工具;memory-review 负责自动提取;memory-context 管理系统提示注入;memory-notes 把约定与踩坑渲染成 project-notes 提示词段落(不向仓库写任何文件);memory-remote 支撑设置界面里的记忆管理中心(远程写默认关闭,需显式开启)——外加一个只做客户端模块发现的 memory-root 根条目。存储介质只是 ~/.dsh/storages/memory.json 一个文件——卸载插件也不会丢失记忆。
一个纯同步的投影累加器折叠会话事件流,攒够信号才打扰一次 LLM。
每条用户消息和工具结果都会流过一个零 LLM 成本的投影累加器(session-projection)。它只收集三类候选片段——收集层刻意放宽漏斗,准入的保守由提取提示词负责:漏收是白损失,误收几乎免费。
用户直接要求记住。中英文的「记住 / 以后都 / 帮我记 / remember that / from now on」等模式命中即入候选。
用户推翻之前的说法。「不对 / 其实是 / actually / I meant」——修正意味着旧认知是错的,新结论值得沉淀。
同一签名的工具调用连续失败(默认 ≥2 次)后终于成功——这次成功就是被验证过的 workaround,蒸馏成踩坑条目。
两条 flush 路径保证信号不丢失:压缩结束(compaction/end)从被遮蔽的原始事件里提取;会话销毁(session/disposed)对全会话未压缩的消息做最后一次提取(5 秒超时、fire-and-forget)。
周期评审、压缩兜底、销毁兜底、定期整理共用每会话 20 次提取预算:每触发一次 drain 计 1——批次内部的多次 LLM 调用、去重裁决都不计数,设 0 不限。耗尽后本会话不再自动提取,这是成本的硬顶。
提取提示词本身是道闸:仓库已记录的内容、瞬时状态与未验证假设、未被工具验证过的流程都不记;偏好类需用户明确提出或主题重复出现。日期前缀在解析层剥离——时间戳永远由程序盖戳;输出走 scope: content 行协议,片段先压平成单行,防止伪造行污染协议。
每一次写入——无论来自模型工具还是后台提取——都必须穿过同一道闸门。
记忆条目主表。读取同步走内存权威态,写在域写链上串行化,先达后端再更新内存。写后超出软上限(entriesCap,可配)即按使用信号淘汰:pinned 免疫,accessCount / lastRecalledAt 最低者先走;全部豁免时允许超限,绝不硬删健康数据。
每次成功变更追加一条审计:操作、来源(tool/review/flush/ui/janitor)、单调 seq、内容预览。谁写的、何时写的,永远可查。
人审模式的待确认队列。提议不是记忆——不注入、不搜索、不衰减,只等待人的裁决(见 06)。
存储还有一层跨进程单写者检测:宿主的存储后端假设每个进程独占写入——两个 DSH 进程共享同一存储根时,各自持有内存权威态并整文件回写,互相静默覆盖。memory-store 因此每次启动把一枚 owner stamp(pid + bootId)写进存储域的 global 槽,并周期性回读:自己写的印章读回来变成别家的,即证明存在并发发布者,立即上报。它只检测、不加锁——pid 已死或干净退出的前代印章不算活威胁,重启场景不会误报。
没有向量库、没有嵌入模型——一套约 270 行的零依赖 Okapi BM25,配上 CJK 感知分词与拉丁保守词干。
检索分两步:结构化过滤先行(scope / category / projectName),再对幸存候选做 BM25 打分。分词器对拉丁文按词切分,并剥掉一个保守的规则后缀收敛词形——uses→use、studies→study,而 class、basis 这类词干永不受损(文档与查询共用同一分词器);对中文同时产出单字一元 + 相邻二字 bigram——bigram 让「记忆」不再命中所有只含「记」的条目,一元保留单字查询的召回。summary 与正文一起入索引,短摘要同样能召回条目。排序依次按:相关性 ↓、置顶 ↓、新近度 ↓。
记忆如何进入上下文,决定了它为模型省下的每一个 token。
每个会话在创建时冻结一份记忆快照,整个会话期间复用——这是为了 KV-cache 前缀稳定:前缀不变,缓存全中。唯一被打破的时刻是压缩(反正提示词都要重建),中途学到的记忆恰好在此刻补入。快照有双重预算:字符上限 5000 + 条目上限 20,尾部永远附一行 ≈N tokens 估算,注入成本始终可见。
index 模式——出厂默认(0.8 起)——是渐进式披露的核心:每个条目只渲染一行存在性索引(作用域/类别 · id · 摘要),按 project → user → global 的相关性层级排序;预算耗尽时尾部折叠成类别计数行(project/convention ×12)——索引体积随类别数增长,而非条目数。模型看见「存了什么」,按需用 memory_get 取全文。
可选的步级自动召回则走另一条路:每个 agent step 用当步用户文本对库做一次 BM25,命中条目以 <recalled-memory> 围栏追加为一条用户消息——不碰系统提示,前缀缓存分毫不动。加载时,被纠正条目还会带上冲突标注(⚠ 与更新的修正相矛盾),让模型自己权衡新旧。
janitor 在每次会话创建时巡场。拖动时间轴,看三种条目的不同命运。
"部署脚本在 scripts/deploy.sh,端口 8080"
"代理环境下 pnpm 需先设置 registry 镜像"
"代码评审意见必须逐条给出理由"
与衰减相对的是凝练:每 20 个会话,curator 会把超长(≥400 字符)的条目挑出来——每批最多 5 条——交给 LLM 改写成简洁的单行;低频、有预算约束、同样可进入人审队列。记忆库因此既会遗忘,也会沉淀。
记忆会被重新读进未来的上下文——所以它本身就是攻击面,必须双面设防。
[BLOCKED: …] 占位符打开 confirmBeforeWrite,系统的信任模型彻底改变:所有提取与工具写入都降级为提议,进入待确认队列。同一提议被反复观察到会累计 hits 并置顶——频率即信号。人可以编辑后再采纳,也可以一键拒绝;对既有条目的修改提议在采纳前绝不触碰原文。模型永不自我提升。
最后,记忆还会流回每次会话:convention / preference 条目与 failure / procedure / tool-quirk 踩坑日志渲染进 project-notes system prompt 段落。不向你的仓库写入任何文件——记忆保存在 host 侧存储中,在 Memory 设置 UI 中管理(0.6 起;≤0.5.x 留下的仓库内笔记文件会被自动清理)。已渲染进笔记的条目会从 memory 段落中剔除——同一份内容绝不在提示词里出现两次。存储是事实源,笔记段落只是它的只读投影。
被反复观察到的提议累计 hits 自动置顶;被反复召回的条目自动保鲜。使用痕迹本身就是最重要的元数据。
快照为 KV-cache 冻结整个会话,宁可延迟一拍也不抖动前缀——缓存命中省下的成本远超即时的收益。
每个注入面都声明:当前用户请求、仓库文件与工具输出永远优先于记忆。冲突时,相信当下。
37 条扫描规则、audit / suggestions 各 200 条滚动、entries 软上限 500、可选的人审队列——记忆系统必须能回答「这条记忆是谁、在何时、为什么写下的」。