# dsh-mneme 语义增强架构 — 设计文档 - **日期**:2026-08-15 - **状态**:✅ 已实现(本地 Embedding / Rerank / 聚类随 v0.2+ 落地;本文档转为历史设计存档,当前能力以 [README](../README.md) 正文为准) - **范围**:为 dsh-mneme 增加"完全离线的语义记忆引擎",覆盖本地 Embedding、Rerank 精排、autoDream 向量聚类三个阶段 - **相关文档**:[本地模型部署指南](LOCAL_MODEL.md) · [全键配置参考](CONFIGURATION.md) ## 1. 背景与目标 dsh-mneme v0.1 已具备**可选的向量搜索**:通过 `/api/dsh-mneme/vector-config` 配置 OpenAI 兼容 `/embeddings` 端点,命中"字面不同但语义相近"的记忆;未配置或失败时降级为 LIKE 子串搜索。 v0.2 的目标是把这条**可选、依赖外部 API** 的向量链路升级为**默认可用、完全离线**的语义记忆引擎: 1. **本地 Embedding**:ONNX(`Xenova/bge-small-zh-v1.5`)或 Ollama,记忆向量不再依赖外网 API 2. **Rerank 精排**:向量初筛后交叉编码精排,把 Top-K 准确率再抬一档 3. **autoDream 语义增强**:对记忆向量做聚类,让自动巩固(去重/合并/冲突裁决)从"主题相近"升级为"语义相近" ## 2. 总体架构 ``` ┌──────────────────────────────────────────────────────────────┐ │ 语义引擎(v0.2,完全离线可选) │ ├──────────────────────────────────────────────────────────────┤ │ ① Embedder 层(三选一,统一接口,可自动降级) │ │ LocalEmbedder(ONNX) │ OllamaEmbedder │ OpenAIEmbedder │ │ init() / embed(texts) / embedSingle() / dimension / hash │ ├──────────────────────────────────────────────────────────────┤ │ ② VectorIndex 层 │ │ SQLite memories.embedding 列(JSON 向量) │ │ + 余弦相似度检索(score 0..1)+ 缺失向量增量补建 reindex │ ├──────────────────────────────────────────────────────────────┤ │ ③ Rerank 层(可选) │ │ LocalReranker(Xenova/bge-reranker-base,交叉编码) │ │ 召回候选 → 精排 → Top-K │ ├──────────────────────────────────────────────────────────────┤ │ ④ autoDream 聚类(可选) │ │ clusterMemories(向量 K-Means) / findPotentialConflicts │ │ 语义分组 + 疑似矛盾检测 → 巩固决策更精准 │ └──────────────────────────────────────────────────────────────┘ ``` - **① Embedder**:`src/local-embedder.js`,三后端统一接口,`createEmbedderByProvider(provider, opts)` 按名创建 - **② VectorIndex**:复用 v0.1 的 `memories.embedding` 列与 `store.js` 的余弦检索,无新存储层 - **③ Rerank**:`src/reranker.js`(Phase 2),可选开启 - **④ 聚类**:`src/dream/clustering.js`(Phase 3),autoDream 调度器集成 ### 目录结构(新增/变化部分) ``` src/ ├── local-embedder.js # [新增] Local/Ollama/OpenAI 三 Embedder + createEmbedderByProvider ├── reranker.js # [新增·Phase2] LocalReranker 交叉编码精排 ├── dream/ │ └── clustering.js # [新增·Phase3] clusterMemories / findPotentialConflicts / kMeans └── embedding.js # [保留] v0.1 OpenAI 兼容客户端(作为降级路径之一复用) scripts/ ├── benchmark-embed.js # [新增] Embedding 吞吐量基准 └── benchmark-rerank.js # [新增] Rerank 延迟基准 docs/ ├── SEMANTIC.md # 本文档 ├── LOCAL_MODEL.md # 本地模型部署指南 └── CONFIGURATION.md # 全键配置参考 ``` ## 3. 搜索流水线 `memory_search` 在 v0.2 的完整链路(对应 `src/tools.js` 的 `mode: auto | keyword | vector | hybrid`): ``` query │ ┌───────────┴───────────┐ ▼ ▼ keyword 召回 vector 召回 (LIKE 子串 title/ (Embedder.embed(query) content/tags) → 余弦相似度 top-N → 缺失向量自动补建) │ │ └───── 混合融合 ─────────┘ (auto: 关键词命中优先 + 向量补足;分数归一化合并) │ ▼ [Rerank 开启?] ├─ 是 → LocalReranker.rerank(query, candidates, topK) │ 交叉编码精排,取 topK └─ 否 → 按 importance/score 直接截取 topK │ ▼ memory_search 结果 ``` - **召回阶段(候选)**:混合召回,宁多勿漏,候选集通常取向量 top-N(默认远超 Top-K,供精排筛选) - **精排阶段(Rerank)**:对候选逐条打分(query 与候选的交互编码),取 Top-K - **结果阶段**:返回条目 + `score`(召回分/精排分)+ `source` 时间戳,与 v0.1 返回结构保持一致 ## 4. 模块接口 ### 4.1 Embedder 统一接口(`src/local-embedder.js`) 三个后端共享同一接口,方法失败**抛错**,由上层编排降级链(与 v0.1 `embedding.js` 的"失败返回 null"不同)。 | 成员 | 说明 | |------|------| | `init()` | 加载模型 / 探测服务,失败抛错 | | `embed(texts: string[])` | 批量嵌入,返回 `number[][]`(本地模型 mean pooling + L2 归一化) | | `embedSingle(text)` | 单条嵌入 → `number[]` | | `dimension` | 向量维度(getter) | | `modelHash` | 模型指纹(`#`),用于向量索引一致性校验,模型换名/换维度时提示重建索引 | | `dispose()` | 释放模型资源 | **LocalEmbedder(ONNX)** ```js new LocalEmbedder({ model: "Xenova/bge-small-zh-v1.5", // 默认;中文优化,全离线 dimension: 512, // 默认与模型匹配 device: "cpu", // cpu | wasm | gpu(onnxruntime 后端) batchSize: 8, // 分批嵌入,控制峰值内存 cacheDir: "", // 自定义模型缓存目录,空则用 HF 默认缓存 useDtype: "q8", // 量化精度 q8/fp32/fp16 logger: null }) ``` **OllamaEmbedder** ```js new OllamaEmbedder({ baseUrl: "http://localhost:11434", // 默认 model: "nomic-embed-text", // 默认;维度从首次响应自动推断 logger: null }) ``` **OpenAIEmbedder(兼容 v0.1 行为)** ```js new OpenAIEmbedder({ baseUrl: "", // 例如 https://api.openai.com/v1,也支持 SiliconFlow/智谱/Ollama 代理 apiKey: "", model: "", timeoutMs: 15000 }) ``` **工厂** ```js import { createEmbedderByProvider } from "../src/local-embedder.js"; const embedder = createEmbedderByProvider("local", { device: "cpu" }); // local | ollama | openai await embedder.init(); const vecs = await embedder.embed(["你好", "记忆库"]); ``` ### 4.2 Reranker(`src/reranker.js`,Phase 2) | 成员 | 说明 | |------|------| | `constructor({ model, device, batchSize, cacheDir, logger })` | `model` 默认 `Xenova/bge-reranker-base` | | `init()` | 加载交叉编码模型 | | `rerank(query, candidates, topK?)` | 对候选逐条打分,返回按相关性降序的候选(附 `score`),`topK` 缺省返回全量排序 | 示例: ```js import { LocalReranker } from "../src/reranker.js"; const reranker = new LocalReranker({ model: "Xenova/bge-reranker-base" }); await reranker.init(); const top = await reranker.rerank("怎么部署本地模型", candidates, 5); ``` ### 4.3 autoDream 聚类(`src/dream/clustering.js`,Phase 3,已实现) | 成员 | 说明 | |------|------| | `cosineSimilarity(a, b)` | 余弦相似度,零向量/长度不齐返回 0,数值稳定 | | `kMeans(vectors, k, opts)` | K-Means(k-means++ 播种 + 空簇修复),返回每簇索引数组 | | `clusterMemories(memories, vectors, k)` | 按向量把记忆分组,返回簇(保留原对象引用) | | `findPotentialConflicts(memories, vectors, threshold = 0.85)` | 同类型、高相似(可能矛盾)的记忆对 | autoDream 调度器在触发巩固时:对候选记忆做 `clusterMemories` 分组 → 每组送 LLM 决策(merge/archive/conflict),并用 `findPotentialConflicts` 预先标出疑似矛盾对,减少漏判。 ## 5. 降级策略 ``` 首选 回退 1 回退 2 兜底 LocalEmbedder(ONNX) ──失败──► OllamaEmbedder ──失败──► OpenAIEmbedder ──失败/未配置──► 关键词 LIKE ``` - Embedder 方法**抛错**,由上层 `createEmbedderByProvider` 编排的降级链逐级捕获切换;全链路失败则回退到 v0.1 的 LIKE 子串关键词搜索(本库无 FTS5,关键词召回即 LIKE 子串扫描) - **原则**:语义链路任何一环失败都**静默降级、不阻断记忆读写**——写入照常落库,只是当次不补向量 - **降级是每请求动态的**:同一会话内 ONNX 偶发失败,下一次请求自动重试首选路径,不固化到降级档 - **索引一致性**:Embedder 的 `modelHash` 变化(换模型/换维度)时,旧向量与新向量不可混算余弦,需触发「重建索引」(复用 v0.1 的 `reindexMissing` 补建缺失向量) ## 6. 模型清单 | 用途 | 模型 | 后端 | 维度 | 说明 | |------|------|------|------|------| | Embedding | `Xenova/bge-small-zh-v1.5` | ONNX(默认) | 512 | 中文优化,全离线,体积小 | | Embedding | `bge-m3` | Ollama | 1024 | 多语言(中/英/日等),质量高 | | Embedding | `nomic-embed-text` | Ollama | 768 | 英文为主,通用 | | Embedding | `text-embedding-3-small` | OpenAI 兼容 | 1536 | v0.1 云端默认,升级后仍可用 | | Rerank | `Xenova/bge-reranker-base` | ONNX(Phase 2) | — | 交叉编码,首候选精排 | | Rerank | `bge-reranker-v2-m3` | 云/本地 | — | 更强精排,多语言 | > 维度随模型而定:换用不同维度的模型后,已存向量与模型指纹不匹配,需重建索引。 ## 7. 配置新增项 见 [CONFIGURATION.md](CONFIGURATION.md) 的检索与运行时键表。核心:`embedProvider`(`local` / `ollama` / `openai`,默认 `openai` 保持 v0.1 行为)、`localEmbedModel`、`localEmbedDimension`、`localEmbedDevice`、`localEmbedBatchSize`、`ollamaBaseUrl`、`ollamaModel`、`embedModelCacheDir`、`rerankEnabled`、`rerankModel`。 ## 8. 测试与基准 - 单元:`src/local-embedder.js` 三后端接口一致性、`clustering.js` 聚类正确性(node:test) - 降级链:mock 各后端抛错,断言逐级回退到 LIKE - 基准:`scripts/benchmark-embed.js`(嵌入延迟/吞吐)、`scripts/benchmark-rerank.js`(Rerank 延迟 vs 候选规模) ## 9. 反思更新(v0.2.1 `update` 决策 + 失败追踪) ### 9.1 `update` 决策类型 autoDream 整理时,LLM 可输出第 5 种决策 `update`,直接修正单条记忆的过时/错误内容: - **约束**:`ids` 只能含一个 id;必须产生实际字段变化;不能更新 `summary`;新建 < 24h 的记忆不可 update - **频率限制**:每次 autoDream 最多 `reflectionUpdateMaxPerRun`(默认 2)个 update,超限整单拒绝 - **幂等**:字段已与目标一致时跳过(可重放无副作用) - **审计**:`dream_runs` 记录 update 的 `_before` 快照,可回溯变更前内容 - **向量同步**:update 后删除旧向量、按新内容重嵌入,保持索引一致 ### 9.2 失败追踪(failure_memories) `failure_memories` 表记录记忆纠正/失败事件,为后续反思进化积累数据: ```sql CREATE TABLE failure_memories ( id TEXT PRIMARY KEY, query TEXT, -- 用户原始查询/意图(可选) expected TEXT, -- 期望结果 actual TEXT, -- 实际结果 failure_type TEXT, -- outdated / miss / wrong / user_correction memory_id TEXT, -- 关联的记忆 id created_at TEXT NOT NULL ); ``` - **触发**:用户调用 `memory_update` 且内容变化时,若 `reflectionFailureTracking` 开启则记录 `user_correction` 行(actual=旧值、expected=新值) - **查询**:`store.listFailures`(过滤/分页)、`store.getFailureStats`(按类型计数) ### 9.3 配置项 ```javascript reflectionUpdateEnabled: true, // update 决策总开关 reflectionFailureTracking: true, // 失败追踪总开关 reflectionUpdateMaxPerRun: 2, // 每次整理最多 update 数 reflectionUpdateMinAgeHours: 24 // 新建记忆保护期(小时) ```