DeepSeek Harness 记忆召回插件的"PRD"设计拆解
目标读者 :正在给 AI 工具做记忆系统的工程师------这活儿有两半:从会话中沉淀 ,与从沉淀中复用 。
核心价值 :看一份 PRD 背后的设计取舍------记忆怎么从会话里被沉淀成可信条目、怎么挡住提取噪声、怎么在需要时被召回并注入,以及两次被追问出来的重大转向。
阅读时间:约 8 分钟
记忆的价值不在于被存下来,而在于被想起来的那一刻。

一个熟悉的尴尬:记忆库建好了,却没人用
你可能也经历过这个循环。
花了一个月,把 AI 会话里值得留的东西都沉淀了下来:技术选型的理由、你说过的纠正、踩坑后的洞察、团队的流程约定。面板里整整齐齐------按项目、按会话、按类型排成三级树,点开每一条都是干净的 Markdown,带置信度和来源标注。
然后呢?
下一个相似问题来的时候,你依然从零开始讲一遍背景。
问题不在于沉淀做得不好,而在于沉淀和复用之间少了一段路:记忆停留在"档案室"里,没有一条路径把它送回真正需要它的地方------正在进行的这场对话。而少了这一段,前一半的沉淀工作也就白做了。
这篇文章拆解的设计,就是补上这条路:一个运行在 DeepSeek Harness(下称 DSH)主会话里的插件 dsh-memory-recall,用 /memory-recall 一条斜杠指令,从记忆库里召回 top-5,预览勾选后注入当前上下文。
目标很具体:输入查询后 3 秒内返回结果,每条带相关度分数,注入内容严格隔离,且整个链路在向量模型缺失时依然可用。
先想清楚:这不是再做一个记忆管理器
做设计最容易掉进去的坑,是"把已有的东西再做一遍"。
DSH 生态里已经有 dsh-memory-manager、dsh-dev-memory 这类负责沉淀的插件,Markdown 目录、分类体系、面板展示都被解决过了------这些约定直接沿用,不重造轮子。
但边界要划清:这个插件额外补上、也是它真正的新增价值,是从沉淀中复用 那一段------召回与注入。而为了让它成立,从会话中沉淀那一段(提取 → 草稿箱 → 采纳建索引)也得由它端到端跑通,否则闭环是断的。
把职责拆开,其实是四层,每层只回答一个问题:
| 层 | 回答的问题 | 关键约束 |
|---|---|---|
| 指令层 | 什么时候触发召回? | 只响应显式指令,不产生模型消息 |
| 召回层 | 哪些记忆和当前问题相关? | 混合检索 + 过滤 + 融合 + 去重,可观测 |
| 闸门层 | 这条记忆配不配进召回池? | 人工审核优先,自动提取不算数 |
| 注入层 | 怎么送进去才不污染上下文? | 格式隔离 + 字符预算 + 可裁剪 |
四层串起来才是完整的闭环。先看全景------矩形是数据,菱形是唯一的人工决策点,虚线是自动净化:
#mermaid-svg-YsGdFVjPz59iao5B{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YsGdFVjPz59iao5B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YsGdFVjPz59iao5B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YsGdFVjPz59iao5B .error-icon{fill:#552222;}#mermaid-svg-YsGdFVjPz59iao5B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YsGdFVjPz59iao5B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YsGdFVjPz59iao5B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YsGdFVjPz59iao5B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YsGdFVjPz59iao5B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YsGdFVjPz59iao5B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YsGdFVjPz59iao5B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YsGdFVjPz59iao5B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YsGdFVjPz59iao5B .marker.cross{stroke:#333333;}#mermaid-svg-YsGdFVjPz59iao5B svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YsGdFVjPz59iao5B p{margin:0;}#mermaid-svg-YsGdFVjPz59iao5B .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YsGdFVjPz59iao5B .cluster-label text{fill:#333;}#mermaid-svg-YsGdFVjPz59iao5B .cluster-label span{color:#333;}#mermaid-svg-YsGdFVjPz59iao5B .cluster-label span p{background-color:transparent;}#mermaid-svg-YsGdFVjPz59iao5B .label text,#mermaid-svg-YsGdFVjPz59iao5B span{fill:#333;color:#333;}#mermaid-svg-YsGdFVjPz59iao5B .node rect,#mermaid-svg-YsGdFVjPz59iao5B .node circle,#mermaid-svg-YsGdFVjPz59iao5B .node ellipse,#mermaid-svg-YsGdFVjPz59iao5B .node polygon,#mermaid-svg-YsGdFVjPz59iao5B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YsGdFVjPz59iao5B .rough-node .label text,#mermaid-svg-YsGdFVjPz59iao5B .node .label text,#mermaid-svg-YsGdFVjPz59iao5B .image-shape .label,#mermaid-svg-YsGdFVjPz59iao5B .icon-shape .label{text-anchor:middle;}#mermaid-svg-YsGdFVjPz59iao5B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YsGdFVjPz59iao5B .rough-node .label,#mermaid-svg-YsGdFVjPz59iao5B .node .label,#mermaid-svg-YsGdFVjPz59iao5B .image-shape .label,#mermaid-svg-YsGdFVjPz59iao5B .icon-shape .label{text-align:center;}#mermaid-svg-YsGdFVjPz59iao5B .node.clickable{cursor:pointer;}#mermaid-svg-YsGdFVjPz59iao5B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YsGdFVjPz59iao5B .arrowheadPath{fill:#333333;}#mermaid-svg-YsGdFVjPz59iao5B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YsGdFVjPz59iao5B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YsGdFVjPz59iao5B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YsGdFVjPz59iao5B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YsGdFVjPz59iao5B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YsGdFVjPz59iao5B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YsGdFVjPz59iao5B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YsGdFVjPz59iao5B .cluster text{fill:#333;}#mermaid-svg-YsGdFVjPz59iao5B .cluster span{color:#333;}#mermaid-svg-YsGdFVjPz59iao5B div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YsGdFVjPz59iao5B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YsGdFVjPz59iao5B rect.text{fill:none;stroke-width:0;}#mermaid-svg-YsGdFVjPz59iao5B .icon-shape,#mermaid-svg-YsGdFVjPz59iao5B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YsGdFVjPz59iao5B .icon-shape p,#mermaid-svg-YsGdFVjPz59iao5B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YsGdFVjPz59iao5B .icon-shape .label rect,#mermaid-svg-YsGdFVjPz59iao5B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YsGdFVjPz59iao5B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YsGdFVjPz59iao5B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YsGdFVjPz59iao5B :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} turn-stopping + 入队
采纳 / 编辑后采纳
丢弃
注入 memory_context
标记低价值候选
用户消息
模型回复
提取队列 .extraction-queue.jsonl
串行消费:提取 + 分类 + 脱敏
草稿箱 .drafts/ 不建索引
人工闸门
Markdown 真相源 + zvec insertSync
丢弃
面板:项目 › 会话 › 类型
/memory-recall 召回
混合检索 → RRF → 精排 → MMR
预览勾选 → agent.steer 注入
召回历史:命中率 M/N
对照前面的四层:指令层 是 C 的触发方式,召回层 是 V,闸门层 是那个菱形(全流程唯一需要人点头的地方),注入层 是 S。

四种记忆,和为什么不多分几类
管道有了,接下来要定义流动的东西:一条"记忆"到底是什么?
提取器每轮产出 3--10 条,全部落进四个大类。这不是按内容主题分的,而是按认知类型分的:
| 大类 | 收敛什么 | 对应认知层 |
|---|---|---|
| decision | 技术选型、架构决策、方案取舍 | 程序记忆 |
| correction | 用户纠正、偏好表达、风格要求 | 程序记忆 |
| insight | 语义发现、缺口、模式种子、开放问题 | 语义记忆 |
| workflow | 流程偏离、自动化机会、工具链约定 | 程序记忆 |
前三个大类回答"这件事该怎么做",insight 回答"世界是什么样的"。
这四类是从十类砍下来的,砍的过程比结果更有意思。参考方案的原始分类有 decision / insight / confidence / pattern_seed / commitment / learning / correction / cross_agent / workflow_note / gap 十个桶,看上去更"全",实际站不住:decision 天然包含 learning(你在做决策时学到的推理本身就是学习),correction 只是特定场景下的 learning,pattern_seed 和 insight 在"跨会话发现"这个维度上几乎无法区分,commitment 更像待办事项而非记忆。最离谱的是 confidence------置信度是对所有类型的评估维度,它压根不是一种类型。
真正的理由不在分类学的美感,而在工程现实:**类型越多,结构化提取的准确率越低。**要求模型在十个桶里做精确选择,等于给它加认知负荷。四类正好对应认知科学里久经检验的"程序记忆 / 语义记忆"二分,每一类都能映射到不同的检索与生命周期策略。
省下来的表达力交给字段,而不是新分类:gap(发现的认知缺口)与 pattern_seed(尚未验证的模式)被压成 insight 的 subtype,另配一个 maturity 字段(seed / open / stable)表达成熟度;confidence_score 也只当元数据。主分类永远是四类,zvec 的标量过滤字段不会被持续膨胀,而表达力一分没少------子类只做标签,不参与过滤。
还有一条容易被忽略:提取优先级不是平均的。有一组"意外触发器"会给候选加权------从失败中恢复、用户纠正方向、期望被违背、重复出现类似请求 。这些信号在 DSH 里都能落到具体钩子上(工具报错对应 tools/post-execute,请求失败对应 agent/request-error),成为异步提取任务的优先级加权因子。
好的分类学不是能装下所有东西,而是知道什么不该单独立类。
召回层:把"可观测"当成一等公民
召回是整个插件的心脏。管线的形状与每一步的动作都是定死的,没有留给"实现时再说":
#mermaid-svg-oDbdkGFgiw9n4OQe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oDbdkGFgiw9n4OQe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oDbdkGFgiw9n4OQe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oDbdkGFgiw9n4OQe .error-icon{fill:#552222;}#mermaid-svg-oDbdkGFgiw9n4OQe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oDbdkGFgiw9n4OQe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oDbdkGFgiw9n4OQe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oDbdkGFgiw9n4OQe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oDbdkGFgiw9n4OQe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oDbdkGFgiw9n4OQe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oDbdkGFgiw9n4OQe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oDbdkGFgiw9n4OQe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oDbdkGFgiw9n4OQe .marker.cross{stroke:#333333;}#mermaid-svg-oDbdkGFgiw9n4OQe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oDbdkGFgiw9n4OQe p{margin:0;}#mermaid-svg-oDbdkGFgiw9n4OQe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oDbdkGFgiw9n4OQe .cluster-label text{fill:#333;}#mermaid-svg-oDbdkGFgiw9n4OQe .cluster-label span{color:#333;}#mermaid-svg-oDbdkGFgiw9n4OQe .cluster-label span p{background-color:transparent;}#mermaid-svg-oDbdkGFgiw9n4OQe .label text,#mermaid-svg-oDbdkGFgiw9n4OQe span{fill:#333;color:#333;}#mermaid-svg-oDbdkGFgiw9n4OQe .node rect,#mermaid-svg-oDbdkGFgiw9n4OQe .node circle,#mermaid-svg-oDbdkGFgiw9n4OQe .node ellipse,#mermaid-svg-oDbdkGFgiw9n4OQe .node polygon,#mermaid-svg-oDbdkGFgiw9n4OQe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oDbdkGFgiw9n4OQe .rough-node .label text,#mermaid-svg-oDbdkGFgiw9n4OQe .node .label text,#mermaid-svg-oDbdkGFgiw9n4OQe .image-shape .label,#mermaid-svg-oDbdkGFgiw9n4OQe .icon-shape .label{text-anchor:middle;}#mermaid-svg-oDbdkGFgiw9n4OQe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oDbdkGFgiw9n4OQe .rough-node .label,#mermaid-svg-oDbdkGFgiw9n4OQe .node .label,#mermaid-svg-oDbdkGFgiw9n4OQe .image-shape .label,#mermaid-svg-oDbdkGFgiw9n4OQe .icon-shape .label{text-align:center;}#mermaid-svg-oDbdkGFgiw9n4OQe .node.clickable{cursor:pointer;}#mermaid-svg-oDbdkGFgiw9n4OQe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oDbdkGFgiw9n4OQe .arrowheadPath{fill:#333333;}#mermaid-svg-oDbdkGFgiw9n4OQe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oDbdkGFgiw9n4OQe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oDbdkGFgiw9n4OQe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oDbdkGFgiw9n4OQe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oDbdkGFgiw9n4OQe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oDbdkGFgiw9n4OQe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oDbdkGFgiw9n4OQe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oDbdkGFgiw9n4OQe .cluster text{fill:#333;}#mermaid-svg-oDbdkGFgiw9n4OQe .cluster span{color:#333;}#mermaid-svg-oDbdkGFgiw9n4OQe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oDbdkGFgiw9n4OQe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oDbdkGFgiw9n4OQe rect.text{fill:none;stroke-width:0;}#mermaid-svg-oDbdkGFgiw9n4OQe .icon-shape,#mermaid-svg-oDbdkGFgiw9n4OQe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oDbdkGFgiw9n4OQe .icon-shape p,#mermaid-svg-oDbdkGFgiw9n4OQe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oDbdkGFgiw9n4OQe .icon-shape .label rect,#mermaid-svg-oDbdkGFgiw9n4OQe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oDbdkGFgiw9n4OQe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oDbdkGFgiw9n4OQe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oDbdkGFgiw9n4OQe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Query 预处理
去填充词 · 展开缩写 · 补项目同义词
不调用 LLM
zvec 混合检索
Dense + Sparse + FTS 并行
标量过滤 status=active
RRF 融合
只依赖排名,无需分数归一化
精排(可选)
粗召回 topK×20 → 输出 topK
MMR 去重
插件侧后处理,压掉语义重复
Top-K 结果
三个细节值得单独说。
第一,过滤发生在检索内,而不是检索后。 默认标量条件是 status == 'active',被取代的记忆根本不进候选集------比"先召回 100 条再筛掉过时的"干净,也不会让过时记忆挤占候选位。若用户手动勾选了带 superseded_by 的记忆,卡片上会标注"已被 mem_xxx 取代"并给跳转。
第二,测试台和生产走同一条管线。 面板的"召回测试"会展示六个阶段各自的候选、分数与排名变化(↑3 / ↓2 / ---),并标注每条是"进入下一阶段"还是"被淘汰、为什么"。实现上强制复用 recallPipeline.search(),生产与调试只差一个开关:
typescript
const testOptions = buildRecallOptions(formState, config, {
collectStages: true, // 只测试模式收集中间阶段快照
writeHistory: false, // 测试模式不写召回历史
})
一旦测试台自己实现一套"简化版检索",它给出的结论就不再可信------可观测性的前提是同一套代码。
第三,降级是设计的一部分,不是兜底。 Embedding 模型不可用时管线退化为纯 FTS 检索,面板明确提示"向量检索未启用"。功能不阻断,用户知道自己处在降级态。
为什么是 zvec? 这个选型在需求阶段就定了,理由不是"它比较快",而是它跟插件的存在形态契合:进程内嵌入 ,无独立服务进程、零配置,正好满足"插件在 host 侧进程内运行"的约束;单次查询混合召回 Dense + Sparse + FTS,省掉插件侧手动编排多路检索的复杂度;RRF 融合器内置 ,不用自己实现算法;WAL 保证崩溃与断电不丢索引 。最妙的一条是多进程并发读、单进程写------它直接解释了后面提取队列为什么必须串行消费。
值得一提的是,这版设计并不是一开始就选 zvec。更早的方案是 sqlite-vec + FTS5 + onnxruntime 的组合,引擎换掉之后,召回管线、集合 Schema、持久化、生命周期、依赖清单五处跟着重写。
一条管线的价值,一半在它给出的排序,另一半在它能解释自己为什么这么排。

闸门层:提取是概率事件,准入必须是确定事件
如果只让我保留一个设计决策,我会留这个:草稿箱。
原因是提取这件事的本质------它是一个概率过程。LLM 从一轮对话里"读"出 3 到 10 条记忆,必然会带噪声:把一次闲聊当成决策、把临时妥协当成偏好、把某次试验当作约定。如果这些噪声直接进召回池,那么召回质量会随着记忆库增长而下降 。这是记忆系统最反直觉的地方:数据越多,不一定越好用。
所以在"提取"和"进入召回池"之间插一道人工闸门:
#mermaid-svg-uVsXT1m5AiNJcMTZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uVsXT1m5AiNJcMTZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uVsXT1m5AiNJcMTZ .error-icon{fill:#552222;}#mermaid-svg-uVsXT1m5AiNJcMTZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uVsXT1m5AiNJcMTZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uVsXT1m5AiNJcMTZ .marker.cross{stroke:#333333;}#mermaid-svg-uVsXT1m5AiNJcMTZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uVsXT1m5AiNJcMTZ p{margin:0;}#mermaid-svg-uVsXT1m5AiNJcMTZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uVsXT1m5AiNJcMTZ .cluster-label text{fill:#333;}#mermaid-svg-uVsXT1m5AiNJcMTZ .cluster-label span{color:#333;}#mermaid-svg-uVsXT1m5AiNJcMTZ .cluster-label span p{background-color:transparent;}#mermaid-svg-uVsXT1m5AiNJcMTZ .label text,#mermaid-svg-uVsXT1m5AiNJcMTZ span{fill:#333;color:#333;}#mermaid-svg-uVsXT1m5AiNJcMTZ .node rect,#mermaid-svg-uVsXT1m5AiNJcMTZ .node circle,#mermaid-svg-uVsXT1m5AiNJcMTZ .node ellipse,#mermaid-svg-uVsXT1m5AiNJcMTZ .node polygon,#mermaid-svg-uVsXT1m5AiNJcMTZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uVsXT1m5AiNJcMTZ .rough-node .label text,#mermaid-svg-uVsXT1m5AiNJcMTZ .node .label text,#mermaid-svg-uVsXT1m5AiNJcMTZ .image-shape .label,#mermaid-svg-uVsXT1m5AiNJcMTZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-uVsXT1m5AiNJcMTZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uVsXT1m5AiNJcMTZ .rough-node .label,#mermaid-svg-uVsXT1m5AiNJcMTZ .node .label,#mermaid-svg-uVsXT1m5AiNJcMTZ .image-shape .label,#mermaid-svg-uVsXT1m5AiNJcMTZ .icon-shape .label{text-align:center;}#mermaid-svg-uVsXT1m5AiNJcMTZ .node.clickable{cursor:pointer;}#mermaid-svg-uVsXT1m5AiNJcMTZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uVsXT1m5AiNJcMTZ .arrowheadPath{fill:#333333;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uVsXT1m5AiNJcMTZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uVsXT1m5AiNJcMTZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uVsXT1m5AiNJcMTZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uVsXT1m5AiNJcMTZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uVsXT1m5AiNJcMTZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uVsXT1m5AiNJcMTZ .cluster text{fill:#333;}#mermaid-svg-uVsXT1m5AiNJcMTZ .cluster span{color:#333;}#mermaid-svg-uVsXT1m5AiNJcMTZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uVsXT1m5AiNJcMTZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uVsXT1m5AiNJcMTZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-uVsXT1m5AiNJcMTZ .icon-shape,#mermaid-svg-uVsXT1m5AiNJcMTZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uVsXT1m5AiNJcMTZ .icon-shape p,#mermaid-svg-uVsXT1m5AiNJcMTZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uVsXT1m5AiNJcMTZ .icon-shape .label rect,#mermaid-svg-uVsXT1m5AiNJcMTZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uVsXT1m5AiNJcMTZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uVsXT1m5AiNJcMTZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uVsXT1m5AiNJcMTZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 采纳
编辑后采纳
丢弃 / 超 7 天未处理
turn-stopping
提取 → 写入 .drafts/ 目录
不建索引
面板草稿箱
写 Markdown + insertSync
改后写 Markdown + insertSync
删除
进入召回池
草稿默认不参与召回,只有采纳后才写 Markdown 并执行 insertSync。配置项 extraction.requireReview 默认 true------想走直写路径可以关掉,但默认值必须是"先审核"。草稿超过 7 天未处理自动丢弃,避免草稿箱变成第二个垃圾场。
配套的还有一条容易被忽略的工程线:提取任务队列。
agent/turn-stopping 触发时不能同步做提取(会让用户等待),只能入队。队列持久化在 .extraction-queue.jsonl,每条任务带 {sessionId, turnIndex, enqueuedAt, attempts},幂等键是 sessionId + turnIndex,单消费者串行处理------因为 zvec 的写入是单进程独占的,并发只会制造冲突。
失败策略也很坦白:单任务重试上限 3 次,超过就跳过并记到 .extraction-errors.jsonl。某一轮提取彻底失败,那条记忆就永久丢失。这个丢失是被接受的,因为记忆系统的正确目标不是"零丢失",而是"不因为追一条记忆而拖垮主会话"。
提取是概率,准入是确定;把概率的东西挡在确定的东西前面,库才不会腐化。

注入层:记忆是参考,不是指令
注入是风险最高的一步。你往上下文里塞一段文本,模型很可能把它当成新的用户指令来执行------尤其当记忆内容本身就是"你应该先读文件再改"这类祈使句时。
设计上用三重手段隔离:
第一,格式边界。 注入内容包在 <memory_context> 标签里,只有 <current_user_request> 标签内的内容才是用户真实请求:
<memory_context source="recall" query="用户查询" count="3">
<memory id="mem_001" type="decision" relevance="0.92"
session="会话标题" created="2026-08-15" confidence="92"
status="active">
[记忆内容]
</memory>
</memory_context>
<current_user_request>
[当前用户的实际请求]
</current_user_request>
第二,字符预算。 injection.maxChars 默认 2000,预览卡片底部直接显示"本次注入约 N tokens"。当记忆内容可能无限长时,预算是唯一能防住上下文爆炸的闸门。
第三,可裁剪模式。 /memory-recall --brief 或 injection.briefMode 打开时只注入 summary 字段,预算降到 600 字符。这个模式的存在本身就是承认:不是每次召回都值得付全额成本。
还有一个设计细节值得一提:注入前必须预览 。召回结果先以 UI 卡片展示,用户勾选后再确认,最后才调用 agent.steer() 注入。命令本身不产生模型消息。这既是体验设计(用户拥有最终决定权),也是数据设计------勾选与注入行为被记进召回历史,成为后续命中率统计的原料。
从反馈到净化:M/N 是记忆库的排水阀
注入不是终点。召回历史里记录两个字段:userSelected(用户是否勾选)与 injected(是否最终注入)。
有了它们就能算出每条记忆的命中率 M/N------被召回 N 次,真正被选中注入 M 次。然后机制开始自动工作:
M/N < 0.1且N ≥ 10的记忆被标记为低价值候选,集中列出供清理;- 配套"高频召回榜"与召回时间线,让"哪条记忆真的在干活"变成看得见的事实。
为什么这条值得单列一节?因为它把"清理记忆"这个反人性的动作自动化了。没有人会主动点开一条每次都被召回、每次都被跳过的记忆------但把 M/N 做成显式阈值,等于给记忆库装了一个排水阀:噪声不用你去发现,它自己会浮出来。
再往前一步的迭代方向也留了位置:注入消息带了 source.kind: 'memory-recall' 标记,配合 turn-stopping 时的会话分析,可以推断模型后续输出是否真的引用了这条记忆------把"用户勾选了"升级为"确实起作用了",全程不需要用户多做一件事。
召回率衡量系统找得准不准,命中率衡量记忆值不值得留着。

触发策略与生命周期:该闭嘴时就闭嘴
触发方式 采用了"手动为主、提示为辅"。除了 /memory-recall,插件还在 agent/pre-step 钩子里跑一次轻量检测:用户消息 ≥ 20 字符、不以 / 开头、且与某条 active 记忆相似度 ≥ 0.85 时,在输入框下方浮出一条"发现 N 条可能相关的记忆"。
关键在于这条提示的成本约束 :单次检测目标耗时 < 50ms,超时立即静默放弃 ;embedding 结果按消息哈希做 LRU 缓存(容量 100);提示条 30 秒自动消失;可通过 autoHint.enabled 一键关闭。
异步、有预算、可关闭------任何自动行为都该有这三条。
生命周期 解决"记忆会过期"这个现实问题。状态只有三个,而被取代不等于被删除:
#mermaid-svg-P4DeKfu1Lqx92Nni{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P4DeKfu1Lqx92Nni .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P4DeKfu1Lqx92Nni .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P4DeKfu1Lqx92Nni .error-icon{fill:#552222;}#mermaid-svg-P4DeKfu1Lqx92Nni .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P4DeKfu1Lqx92Nni .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P4DeKfu1Lqx92Nni .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P4DeKfu1Lqx92Nni .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P4DeKfu1Lqx92Nni .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P4DeKfu1Lqx92Nni .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P4DeKfu1Lqx92Nni .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P4DeKfu1Lqx92Nni .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P4DeKfu1Lqx92Nni .marker.cross{stroke:#333333;}#mermaid-svg-P4DeKfu1Lqx92Nni svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P4DeKfu1Lqx92Nni p{margin:0;}#mermaid-svg-P4DeKfu1Lqx92Nni defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-P4DeKfu1Lqx92Nni g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-P4DeKfu1Lqx92Nni g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-P4DeKfu1Lqx92Nni g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-P4DeKfu1Lqx92Nni g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-P4DeKfu1Lqx92Nni g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-P4DeKfu1Lqx92Nni .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-P4DeKfu1Lqx92Nni .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-P4DeKfu1Lqx92Nni .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-P4DeKfu1Lqx92Nni .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-P4DeKfu1Lqx92Nni .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-P4DeKfu1Lqx92Nni .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-P4DeKfu1Lqx92Nni .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-P4DeKfu1Lqx92Nni .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P4DeKfu1Lqx92Nni .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-P4DeKfu1Lqx92Nni .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P4DeKfu1Lqx92Nni .edgeLabel .label text{fill:#333;}#mermaid-svg-P4DeKfu1Lqx92Nni .label div .edgeLabel{color:#333;}#mermaid-svg-P4DeKfu1Lqx92Nni .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-P4DeKfu1Lqx92Nni .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-P4DeKfu1Lqx92Nni .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-P4DeKfu1Lqx92Nni .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-P4DeKfu1Lqx92Nni .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-P4DeKfu1Lqx92Nni .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P4DeKfu1Lqx92Nni .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P4DeKfu1Lqx92Nni #statediagram-barbEnd{fill:#333333;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P4DeKfu1Lqx92Nni .cluster-label,#mermaid-svg-P4DeKfu1Lqx92Nni .nodeLabel{color:#131300;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-P4DeKfu1Lqx92Nni .note-edge{stroke-dasharray:5;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-note text{fill:black;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram-note .nodeLabel{color:black;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagram .edgeLabel{color:red;}#mermaid-svg-P4DeKfu1Lqx92Nni #dependencyStart,#mermaid-svg-P4DeKfu1Lqx92Nni #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-P4DeKfu1Lqx92Nni .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-P4DeKfu1Lqx92Nni :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 草稿采纳 / 手动新建
标记已过时并指定取代者
归档
撤销标记
恢复
软删除
软删除
恢复
彻底删除
回收站 .trash
active
superseded
archived
召回默认只返回 active,
其余状态在 zvec 保留但被过滤排除
软删除进 .trash/ 保留 60 天,且不自动清空------超期只在面板标记,删除权始终在人手里。自动清理是数据安全事故的常见来源,这 60 天是为了让人来得及反悔。
写路径上还有一条铁律:索引坏了不该丢数据。
#mermaid-svg-HsRiLeuSzJj5FClp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HsRiLeuSzJj5FClp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HsRiLeuSzJj5FClp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HsRiLeuSzJj5FClp .error-icon{fill:#552222;}#mermaid-svg-HsRiLeuSzJj5FClp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HsRiLeuSzJj5FClp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HsRiLeuSzJj5FClp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HsRiLeuSzJj5FClp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HsRiLeuSzJj5FClp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HsRiLeuSzJj5FClp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HsRiLeuSzJj5FClp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HsRiLeuSzJj5FClp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HsRiLeuSzJj5FClp .marker.cross{stroke:#333333;}#mermaid-svg-HsRiLeuSzJj5FClp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HsRiLeuSzJj5FClp p{margin:0;}#mermaid-svg-HsRiLeuSzJj5FClp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HsRiLeuSzJj5FClp .cluster-label text{fill:#333;}#mermaid-svg-HsRiLeuSzJj5FClp .cluster-label span{color:#333;}#mermaid-svg-HsRiLeuSzJj5FClp .cluster-label span p{background-color:transparent;}#mermaid-svg-HsRiLeuSzJj5FClp .label text,#mermaid-svg-HsRiLeuSzJj5FClp span{fill:#333;color:#333;}#mermaid-svg-HsRiLeuSzJj5FClp .node rect,#mermaid-svg-HsRiLeuSzJj5FClp .node circle,#mermaid-svg-HsRiLeuSzJj5FClp .node ellipse,#mermaid-svg-HsRiLeuSzJj5FClp .node polygon,#mermaid-svg-HsRiLeuSzJj5FClp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HsRiLeuSzJj5FClp .rough-node .label text,#mermaid-svg-HsRiLeuSzJj5FClp .node .label text,#mermaid-svg-HsRiLeuSzJj5FClp .image-shape .label,#mermaid-svg-HsRiLeuSzJj5FClp .icon-shape .label{text-anchor:middle;}#mermaid-svg-HsRiLeuSzJj5FClp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HsRiLeuSzJj5FClp .rough-node .label,#mermaid-svg-HsRiLeuSzJj5FClp .node .label,#mermaid-svg-HsRiLeuSzJj5FClp .image-shape .label,#mermaid-svg-HsRiLeuSzJj5FClp .icon-shape .label{text-align:center;}#mermaid-svg-HsRiLeuSzJj5FClp .node.clickable{cursor:pointer;}#mermaid-svg-HsRiLeuSzJj5FClp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HsRiLeuSzJj5FClp .arrowheadPath{fill:#333333;}#mermaid-svg-HsRiLeuSzJj5FClp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HsRiLeuSzJj5FClp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HsRiLeuSzJj5FClp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HsRiLeuSzJj5FClp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HsRiLeuSzJj5FClp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HsRiLeuSzJj5FClp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HsRiLeuSzJj5FClp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HsRiLeuSzJj5FClp .cluster text{fill:#333;}#mermaid-svg-HsRiLeuSzJj5FClp .cluster span{color:#333;}#mermaid-svg-HsRiLeuSzJj5FClp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HsRiLeuSzJj5FClp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HsRiLeuSzJj5FClp rect.text{fill:none;stroke-width:0;}#mermaid-svg-HsRiLeuSzJj5FClp .icon-shape,#mermaid-svg-HsRiLeuSzJj5FClp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HsRiLeuSzJj5FClp .icon-shape p,#mermaid-svg-HsRiLeuSzJj5FClp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HsRiLeuSzJj5FClp .icon-shape .label rect,#mermaid-svg-HsRiLeuSzJj5FClp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HsRiLeuSzJj5FClp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HsRiLeuSzJj5FClp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HsRiLeuSzJj5FClp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败
启动对账:比对三元组
面板编辑 / 草稿采纳
Markdown 真相源
revision +1,updated_at 更新
zvec insertSync
indexed_at = now
绿点:已同步
.pending-sync.jsonl
定时重试,红点:同步失败
revision(单调递增)+ updated_at + indexed_at 三个字段共同判定同步状态。特意加 revision 是为了防时钟异常------updated_at 不可信时,还有一个单调计数器兜底。
剩下两条不那么显眼但必要:
- 敏感信息脱敏 :提取阶段用正则(OpenAI key、AWS AKIA、password 赋值、Bearer token)替换为
[REDACTED],frontmatter 标记contains_redaction: true。刻意不做 LLM 判断------脱敏是安全边界,需要确定性。 - 配置分层 :存储与索引、Embedding 参数属部署级(
cordis.patch.yml,重启生效);召回参数、注入控制、提取阈值、面板行为属行为级(Settings 页热生效)。
为什么真相源一定要是 Markdown,而不是 JSON? 三个理由,从弱到强:
- 给模型看的和给人看的,需求不同。 记忆最终要注入上下文被模型阅读,JSON 的花括号与引号只会白烧 token;有格式对比测试称,JSONL 被模型准确理解的比例明显低于 Markdown 表格(这个数字建议自己复测,别直接引用)。
- 索引会坏,schema 会变。 zvec 支持标量字段的 schema 演进,但向量字段的增删尚不支持。一旦索引需要重建,Markdown 就是那张不能丢的底牌。
- 人要能改。 高级用户可以直接编辑 Markdown 修正一条记忆,或把它纳入 Git 版本控制------JSON 的 diff 可读性远不如 Markdown。调试提取质量时,这个能力比什么都重要。
JSONL 没有被抛弃,它退到了更合适的位置:召回历史、提取队列、待同步队列------这些都是"程序读、连续追加写"的场景,正是 JSONL 的主场。
明确不做的部分
设计里最容易被跳过的一节,是不做什么。
- 默认不做自动注入 :检测到相关记忆只提示,不擅自塞进上下文。唯一的例外是一个默认关闭的逃生舱
injection.autoOnError------当工具执行失败时(往往正是"上次也这么错过"的场景),自动触发一次轻量召回,在tools/post-execute的 waterfall 里附加上下文。 - 不做手动补提取指令 :
/catch-memories明确标记"暂不实现",避免首个版本摊子铺得过大。 - 不做提取失败补偿:单轮提取失败即永久丢失,接受这个损失。
- 不做自动清空回收站:只标记过期,删除权始终在人手里。
- 不追求实时索引同步:Markdown 先落盘,zvec 异步跟随,用待同步队列 + 定时重试收敛。
这套设计是怎么长出来的
回头看,这份 PRD 不是一次写成的。它经历过两次比较大的转向,而两次都不是因为"想到了更好的",是因为被追问了。
第一次转向:记忆类型从十类砍到四类。 起点是一份成熟的记忆提取指令(十类记忆 + 十二种实体类型,还带着 source_chunk 审计追踪和"意外触发器")。照抄看着最省事,但十个桶里有五个是伪分类------同行、包含,或者根本不是类型。砍到四类之后,gap 与 pattern_seed 降级为子类,source_chunk 与意外触发器反而被吸收进来。
第二次转向:检索引擎从 sqlite-vec + FTS5 换成 zvec。 早期方案是 SQLite 全家桶(sqlite-vec 做向量、FTS5 做全文、onnxruntime 跑本地 embedding),可行,但插件得自己编排多路检索、融合与降级。换成 zvec 之后这些能力收进引擎内部,插件侧只剩 MMR 后处理和标量过滤条件的构造。
两次转向的共同点是:先问"这个设计要解决什么问题",再问"这个工具能不能接住"。 顺序反过来,就会得到一份看起来齐全、实际处处打补丁的方案。
设计的成熟度不体现在方案有多少层,体现在砍掉了多少层。
留几个启发式探索
PRD 把"是什么"和"为什么"讲清楚了,但"怎么做到底"还留着大量空白。下面这些是我在拆解时反复卡住、也觉得最有意思的地方------它们不适合在需求文档里拍板,适合在实现阶段被反复追问。
探索一:召回管线的具体算法设计
管线的形状 和每一步的动作都已经定了,但参数与边界还没收口:
- FTS 与 Sparse 向量都在做词汇层面的匹配,职责边界在哪?三路结果的相对权重该怎么定?
mmrLambda默认 0.7 意味着"相关性优先"还是"多样性优先"?在一个以"原子笔记"为原则的记忆库里,MMR 到底在去什么重------语义重复,还是同一主题的过多条目?scoreThreshold默认 0.3 作用在哪个阶段?RRF 之后的分数尺度和向量相似度完全不同,一个阈值能通用吗?- 要不要先做一次意图分类("这是在问怎么做" vs "这是在问以前怎么决定的"),把不同查询路由到不同的过滤条件?这一步会引入延迟,值不值?
探索二:UI 方案的详细设计
PRD 定义了四个标签页和一堆控件,但信息密度 和交互路径还没落笔:
- 召回预览卡片在一屏里要同时放下相关度、类型、来源会话、时间、置信度、token 估算和勾选框------哪个字段该退到悬停提示里?
- 分阶段视图的"淘汰原因"是学习工具,设计不好就是噪音。什么情况下该默认隐藏被淘汰项(
panel.recallTestShowEliminated默认true,但 20 条候选的淘汰项可能占满屏幕)? - 参数对比模式要做"同一记忆在不同快照中的排名连线",这是一个图布局问题,不是列表问题。三条快照的连线交叉不可避免时怎么处理?
- 阅读视图要做"渲染模式 ↔ 编辑模式双向定位"------在渲染后的 Markdown 里定位到光标对应的源码行,这个映射怎么建?
- 空状态、首次安装、零记忆的引导文案之后,用户点"导入示例记忆"会发生什么?导入的记忆是哪一类、能不能删?
探索三:提取器的质量验证
- 提取规则其实已经写得很具体了------跳过常规操作与格式更改、置信度低于 50 不提取、按 90--100 / 70--89 / 50--69 三档打分、意外触发器加权。但怎么验证它们真的有效?需不需要一份回归样本集,把"提取准确率"变成可测量的指标?
- 四类的判定边界在实操中如何避免摇摆?尤其"一次妥协到底是 correction 还是 decision"。
confidence_score是 zvec 的标量过滤字段,但它参与排序吗?如果参与,会不会和模型的自我评估偏差叠加?如果不参与,它是不是只剩面板筛选的作用?- 脱敏是"替换 + 标记",但被
[REDACTED]掉的上下文往往正是记忆的价值所在。脱敏发生在写入前还是索引前?原始片段要不要留一份只读副本?
探索四:一致性收敛的成本
三条同步路径(watcher / 启动对账 / 乐观并发)都指向同一个问题:对账是有成本的。
storage.maxMemoryFiles默认 10000。启动时扫 1 万个 Markdown 文件、逐个解析 frontmatter 并比对三个字段,耗时是多少?值得改成按目录 mtime 剪枝吗?revision作为并发栅栏,遇到冲突弹"查看差异 / 覆盖 / 放弃"。在一个单用户桌面插件里,冲突真的会发生吗?还是说它其实是为多窗口/多设备场景预留的?- watcher 监听
projects/**/*.md并 debounce 500ms,但 zvec 是单进程独占。当 watcher 发现外部编辑时,它和队列消费者之间的锁该怎么排?
探索五:资源预算与规模边界
- 1 万条记忆、384 维向量(PRD 默认
all-MiniLM-L6-v2),索引文件大概多大?稀疏向量是主要体积来源吗? - 本地 ONNX embedding 首次加载模型的冷启动时间是多少?它和"召回 3 秒内返回"的目标能同时成立吗?是否需要在插件加载时就预热?
zvec.queryThreads和memoryLimitMb默认 0(自动)。在一台同时跑着编辑器、浏览器、模型推理的机器上,"自动"的判断依据是什么?
探索六:多工作区与作用域的语义
/memory-recall --scope project|global|all 看着很清楚,但落到存储上是模糊的:
- 目录结构只有
projects/<project-slug>/,那scope=global的跨项目通用记忆存在哪里 ?根目录下再开一个global/吗?还是说 global 只是"所有项目库的并集"? - 多工作区场景下"记忆归属主工作区,子工作区合并到主项目",那么一个 monorepo 里主动切到子包工作时,
project_id是否应该变?如果变,scope=project会突然查不到刚才的记忆;如果不变,跨仓库复用的边界又在哪? project-slug由路径末段生成,"中文转拼音首字母或哈希"两种方案会给目录名带来完全不同的可读性------这件事未来还能改吗?改了要不要迁移?
这些问题的共同点是:它们都无法靠需求文档拍板,只能靠原型和实测收口。 这也正是从 PRD 走到可用插件之间,最有价值的那段路。
总结
回头看这个设计,真正起作用的东西并不复杂:
真相源唯一 (Markdown 权威、索引可重建)、同步可收敛 (三元组判定 + 待同步队列 + 启动对账)、准入有人工闸门 (草稿箱把 LLM 的概率性提取挡在召回池之外)、注入有边界 (格式隔离 + 字符预算 + 可裁剪)、自动行为有预算(50ms 超时即放弃)。五条加起来,就是一个能用、能坏的插件。
而最容易做错的选择,是把它做成"第二个记忆管理器"。两半都得跑通,但要用各自的机制跑:沉淀那半靠闸门与队列,复用那半靠召回与注入------混成一个模块,两半都会做坏。
实践建议
- 先定边界,再定功能:明确写出"不做什么"清单(本文那份),它比功能列表更能防住范围蔓延。
- 让测试复用生产管线 :任何"简化实现"都会让调试结论失效,
collectStages这类开关是最省事的做法。 - 给每个自动行为配一个预算:超时多久放弃、缓存多大、提示多久消失、重试几次,全部写进配置默认值。
- 把降级路径当主线开发:没有 embedding 也能召回(FTS),是这套设计敢上线的底气。