diff --git a/docs/algorithms/consolidation-and-compression.md b/docs/algorithms/consolidation-and-compression.md new file mode 100644 index 0000000..d9d19e8 --- /dev/null +++ b/docs/algorithms/consolidation-and-compression.md @@ -0,0 +1,99 @@ +# 整合、压缩与分层总结 + +提取后处理链路里的三个维护算法:记忆整合/去重、压缩遗忘、分层总结。它们让图谱长期保持精简、不无限膨胀。 + +实现:`maintenance/consolidator.js`、`maintenance/compressor.js`、`maintenance/hierarchical-summary.js`。 + +## 整合 / 去重(Mem0 式) + +分两层:自动整合门 + 整合执行。 + +### 自动整合门(`analyzeAutoConsolidationGate`) + +决定要不要触发整合。默认 `conflictThreshold = 0.85`。对每个新节点: + +- 候选 = 活跃、未归档、非自身、且 scope/故事时间兼容(`canMergeTemporalScopedMemories`)的节点 +- 若类型是 `latestOnly`:对 `name`/`title` 做规范化精确匹配(去空白、折叠空格、小写),命中则触发,分数 1 +- 否则:对 scoped 候选做向量 top-1 相似度,分数 ≥ 阈值则触发 + +更高层门控:候选数 ≥ `consolidationAutoMinNewNodes`(默认 2)则无条件运行;不足且分析触发则运行。 + +### 整合执行(`consolidateMemories`) + +需要有效向量配置,否则跳过。 + +- **Phase 0**:收集有向量文本的活跃新节点。少于 2 个则全保留。 +- **Phase 1/2**:直连模式一次 `embedBatch()` 嵌入所有新节点;从有 embedding 的活跃节点建候选池;本地余弦 `searchSimilar()` 找邻居(默认 `neighborCount=5`)。 +- **Phase 3**:LLM 批量决策,每个新节点返回 `action: keep|merge|skip`、`merge_target_id`、`merged_fields`、`evolution`。 +- **Phase 4** 应用: + - `skip`:新节点归档 + - `merge`(目标活跃且兼容):用 `merged_fields` 或用新节点填补目标缺失字段;更新 `seq`/`seqRange`;复制缺失的 storyTime;清空目标 embedding;归档新节点 + - `keep`:保留;并对 evolution 建 `related` 边(strength 0.7)、更新邻居 state/summary、记录 `_evolutionHistory` + +> Mem0 式精确匹配主要体现在 `latestOnly` 类型的同名即时更新和整合门的精确匹配;更广的去重/合并是"LLM 在向量邻居上决策",不是纯确定性精确匹配。 + +| 参数 | 默认 | +| --- | --- | +| `enableConsolidation` | true | +| `consolidationNeighborCount` | 5 | +| `consolidationThreshold` | 0.85 | +| `consolidationAutoMinNewNodes` | 2 | + +## 压缩(分层) + +`compressAll()` 对每个 `compression.mode === "hierarchical"` 的 schema 类型运行 `compressType()`。 + +自动调度:`enableAutoCompression`(默认 true),`compressionEveryN`(默认 10,夹 1..500),当 `extractionCount % everyN === 0` 时调度。 + +压缩窗口参数来自各 schema 类型的 `compression` 配置: + +- `fanIn`(min 2,缺省 2):每组压缩多少个子节点 +- `threshold`(缺省 = fanIn):组多大才压缩 +- `keepRecentLeaves`(缺省 0):保留最近 N 个叶子不压缩 +- `maxDepth`(缺省 1):最大压缩层级 + +压缩流程:取某类型某层级的活跃节点按 seq 排序 → 按 POV owner / 客观区域分组 → 跳过太小的组 → 对 `fanIn` 个一批 LLM 总结成一个压缩节点(`level + 1`,importance 取子节点最大,seqRange 覆盖首末)→ 子节点归档并设 `parentId` → 外部边迁移到压缩节点(内部边忽略)。 + +## 遗忘 / 睡眠周期 + +`sleepCycle()`(`compressor.js`)。自动触发:`enableSleepCycle`(默认 false)且 `extractionCount % sleepEveryN === 0`(默认 10)。 + +跳过:`synopsis`/`rule`/`thread` 类型、`importance >= 8`、创建不足 1 小时的节点。 + +保留价值计算: + +``` +ageHours = (now - createdTime) / 3600000 +recency = 1 / (1 + log10(1 + ageHours)) +accessFreq = accessCount / max(1, ageHours / 24) +retentionValue = (importance / 10) × recency × (1 + accessFreq) +``` + +`retentionValue < forgetThreshold`(默认 0.5)的节点归档(不物理删除)。 + +> 已知边界:`sleepCycle` 直接用 `node.accessCount`,若未初始化会算出 NaN,使该节点不被遗忘(NaN < threshold 为 false)。 + +## 分层总结 + +`runHierarchicalSummaryPostProcess()`(`hierarchical-summary.js`)。默认启用(`enableHierarchicalSummary !== false`)。 + +### 小总结(small summary) + +`generateSmallSummary()`:阈值 `smallSummaryEveryNExtractions`(默认 3,夹 1..100)。当 `currentExtractionCount - lastSummarizedExtractionCount >= 阈值` 时,把自上次以来的提取切片(含当前批次)总结成一条 `synopsis`(80-220 字),记为 `level:0, kind:"small", status:"active"`,带 extractionRange/messageRange/sourceBatchIds 等。 + +### 卷积/折叠总结(rollup) + +`rollupSummaryFrontier()`:fan-in `summaryRollupFanIn`(默认 3,夹 2..10)。当同层活跃总结条目数 > fanIn 时,取前 fanIn 条 LLM 卷成一条更高层总结(120-260 字),源总结标记为 folded,新总结记为 `level + 1, kind:"rollup"`。循环直到没有可折叠的组。 + +这形成一个"小总结 → 折叠总结 → 更高层折叠"的金字塔,让久远的剧情用越来越浓缩的形式保留。 + +| 参数 | 默认 | +| --- | --- | +| `enableHierarchicalSummary` | true | +| `smallSummaryEveryNExtractions` | 3 | +| `summaryRollupFanIn` | 3 | +| `enableAutoCompression` | true | +| `compressionEveryN` | 10 | +| `enableSleepCycle` | false | +| `forgetThreshold` | 0.5 | +| `sleepEveryN` | 10 | diff --git a/docs/algorithms/diffusion-and-dynamics.md b/docs/algorithms/diffusion-and-dynamics.md new file mode 100644 index 0000000..0c3b95d --- /dev/null +++ b/docs/algorithms/diffusion-and-dynamics.md @@ -0,0 +1,114 @@ +# 图扩散与动态评分 + +检索管线里两个核心数值算法:图扩散(PEDSA 扩散激活)和混合评分(含访问强化/时间衰减)。 + +## 图扩散(PEDSA) + +实现在 `retrieval/diffusion.js`,是 PEDSA 式的扩散激活(spreading activation)。这是 JS 单线程的简化实现(无 Rayon/SIMD,见文件头注释)。 + +### 核心公式 + +``` +E_{t+1}(j) = Σ E_t(i) × W_ij × D_decay +``` + +能量从种子节点沿边扩散,每步衰减。 + +### 默认参数 + +| 参数 | 默认 | 含义 | +| --- | --- | --- | +| `maxSteps` | 2 | 扩散步数 | +| `decayFactor` | 0.6 | 每步衰减 | +| `topK` | 100 | 保留节点数(按绝对能量) | +| `minEnergy` | 0.01 | 能量过滤下限 | +| `maxEnergy` / clamp | 2.0 / -2.0 | 能量上下限 | +| `teleportAlpha` | 0.0(检索路径用 0.15) | PPR 式传送 | +| `inhibitMultiplier` | 2.0 | 抑制边放大 | + +### 种子构建 + +种子能量被夹在 `[-2, 2]`,同 ID 累加。 + +- **常规召回种子**:向量命中 `energy = score`,精确名称/标题锚点 `energy = 2.0`。 +- **交叉/残差种子**:向量命中、精确锚点 2.0、残差命中按 score、事件邻居 `1.5 × edge.strength`。 + +### 扩散步骤 + +对每个活跃节点的每条出边: + +- **正向传播**:`propagated = energy × strength × decayFactor × (1 - teleportAlpha)` +- **抑制边**(edge type 255):`propagated = -|energy| × strength × decayFactor × inhibitMultiplier` + +累加到下一步能量,夹紧并按 `|energy| >= minEnergy` 过滤。`teleportAlpha > 0` 时对初始种子做 PPR 式传送:`teleported = (1 - teleportAlpha) × current + teleportAlpha × seedEnergy`。动态剪枝保留绝对能量 top K。 + +### 排序 + +`diffuseAndRank()` 只保留正能量节点,按能量降序(同分按 nodeId)输出 `{nodeId, energy}`。 + +### 时序合成边 + +扩散使用 `buildTemporalAdjacencyMap()` 注入时序链接。默认 `enableTemporalLinks=true`、`temporalLinkStrength=0.2`——让时间上相邻的记忆之间有弱连接,帮助按时间线扩散。 + +## 混合评分 + +实现在 `retrieval/dynamics.js`。 + +### 公式 + +``` +FinalScore = (normGraph×α + normVec×β + normLexical×δ + normImportance×γ) / totalWeight × TimeDecay +``` + +> 注意:`dynamics.js` 头注释的公式省略了词法分,实际代码包含 `lexicalScore`(启用词法增强时)。 + +### 权重默认 + +| 权重 | 默认 | 信号 | +| --- | --- | --- | +| `graphWeight` (α) | 0.6 | 图扩散邻近度 | +| `vectorWeight` (β) | 0.3 | 向量相似度 | +| `importanceWeight` (γ) | 0.1 | 节点重要度 | +| `lexicalWeight` (δ) | 0(常规召回默认 0.18) | 词法匹配 | + +### 归一化 + +``` +normGraph = clamp(graphScore / 2.0, 0, 1) +normVec = clamp(vectorScore, 0, 1) +normLexical = clamp(lexicalScore, 0, 1) +normImportance = clamp(importance / 10.0, 0, 1) +``` + +### 时间衰减 + +``` +deltaDays = max(0, (now - createdTime) / 一天毫秒数) +factor = 0.8 + 0.2 / (1 + ln(1 + deltaDays)) +``` + +越新的记忆衰减因子越接近 1,越旧越接近 0.8(不会衰减到 0,保留底线)。 + +## 访问强化与边衰减 + +### 访问强化(`reinforceAccess`) + +被召回选中的节点: + +``` +accessCount += 1 +importance = min(10, (importance || 5) + 0.1) +lastAccessTime = now +``` + +经常被召回的记忆重要度缓慢上升——一种使用频率的正反馈。 + +### 边衰减(`reinforceEdge`,辅助) + +``` +被激活的边:strength = min(1.0, strength + decayRate × 0.5) +未激活的边:strength = max(0.1, strength - decayRate) +默认 decayRate = 0.02 +``` + +> 说明:该边衰减辅助函数在当前检索/写入主路径中未见调用,属于预留能力。 diff --git a/docs/algorithms/extraction.md b/docs/algorithms/extraction.md new file mode 100644 index 0000000..347b838 --- /dev/null +++ b/docs/algorithms/extraction.md @@ -0,0 +1,112 @@ +# 提取算法 + +写入链路的核心:助手回复落地后,把新对话提炼成图谱节点与关系。 + +运行时入口是自动提取计划 `resolveAutoExtractionPlanController()`(`maintenance/extraction-controller.js`),核心提取是 `extractMemories()`(`maintenance/extractor.js`)。 + +> `extractor.js` 头注释概括为"Mem0 精确对照 + Graphiti 时序边 + MemoRAG 全局概要"。这些技术名是灵感来源,实际实现见下,部分比原论文更简化。 + +## 1. 自动提取计划 + +`resolveAutoExtractionPlanController()` 决定是否运行: + +- 设置启用、自动提取启用(`extractAutoEnabled=true`) +- 上次处理之后的待处理助手楼层数 +- 可选 lag-one 策略(`extractAutoDelayLatestAssistant`,默认 false):延迟一层提取,避开最新一条还在变动 +- 可选智能触发(见下) + +**触发条件**:待处理数达到 `extractEvery`(默认 1,即每条助手消息都提取),或智能触发命中。 + +### 智能触发(`smart-trigger.js`) + +`enableSmartTrigger`(默认 false)启用时,给待处理消息打分: + +| 信号 | 加分 | +| --- | --- | +| 关键词命中(`DEFAULT_TRIGGER_KEYWORDS`) | `min(2, 命中数)` | +| 自定义正则(`triggerPatterns`)首个命中 | +2 | +| 角色切换 ≥ 2 次 | +1 | +| ≥ 2 个 `!?!?` | +1 | +| 实体型正则命中 | +1 | + +`triggered = score >= max(1, smartTriggerThreshold)`(默认阈值 2)。 + +## 2. 构建结构化提取输入 + +`buildExtractionInputContext()`(`extraction-context.js`): + +- 规范化角色/内容/说话者/序号 +- **先应用助手提取规则**,再应用排除规则 +- **过滤系统提取消息**和被排除标签的内容(默认排除 `think,analysis,reasoning`) +- 丢弃空消息 +- 构建原始和过滤后的 transcript + +可选近期消息上限 `extractRecentMessageCap`(默认 0 = 不限)。提示词模式 `extractPromptStructuredMode` 默认 `"both"`(可选 `transcript` / `structured` / `both`)。 + +## 3. 构建提取提示词 + +`buildTaskPrompt(settings, "extract", ...)` 分层组装: + +1. 当前对话(结构化 + transcript) +2. 图谱状态上下文(`buildTaskGraphStats()`,topK 12、diffusionTopK 48、多意图开、最大文本 1200) +3. 活跃总结(`extractIncludeSummaries !== false`,默认含) +4. 故事时间上下文(`extractIncludeStoryTime !== false`,默认含) +5. schema 定义 +6. 认知增强提示 + +LLM JSON 调用,maxRetries 2。 + +## 4. 规范化 LLM 操作 + +从多种可能的容器键里提取操作数组,规范化每个操作的 `action` / `type` / `nodeId` / `ref` / `links` / `clusters` / `scope` / `storyTime` / `fields`,以及 `cognitionUpdates` / `regionUpdates` / `batchStoryTime`。 + +## 5. 写入图谱 + +遍历规范化操作: + +- **create** → `handleCreate()`:新建节点。`latestOnly` 类型若同名节点已存在则直接更新(Mem0 式精确对照的一种)。 +- **update** → `handleUpdate()`:更新节点 + 时序边(见下)。 +- **delete** → 归档(archive,不物理删除)。 +- **`_skip`** → 忽略。 + +链接(links)先排队,所有节点操作后统一应用。可选在本批变更节点之间建默认 `related` 边(`extractDefaultBatchRelatedEdgeStrength`,默认 0.25)。直连向量模式下为缺向量的节点生成 embedding。最后应用认知更新、区域更新、批次故事时间。 + +## 时序边(Graphiti 式) + +update 操作触发时序处理: + +- 使旧 `updates` 边和旧 `temporal_update` 边失效 +- 若有 `sourceNodeId` 且与目标不同:建 `temporal_update` 边(`strength` 默认 0.95,edgeType 0) +- 若字段有变化:建一个描述"状态更新"的 `event` 节点(重要度夹在 `[4, 8]`),并从该事件节点建 `updates` 边指向被更新节点(strength 0.9) + +显式链接里 `contradicts` 映射为抑制边(edgeType 255,默认强度 0.8)——这就是扩散算法里的抑制边来源。 + +## 故事时间线(temporal metadata) + +`batchStoryTime` 和操作级 `storyTime` 规范化后应用: + +- `event` / `pov_memory` 用时间点 `storyTime` +- `thread` / `synopsis` / `reflection` 用时间跨度 `storyTimeSpan` +- 操作故事时间会解析/更新时间线段 + +## 全局概要(MemoRAG 式) + +遗留的 `generateSynopsis()` 在 ≥ 3 个活跃事件节点时,把所有活跃事件/角色/线程总结成一个 `synopsis` 节点(重要度 9.0,200 字以内)。 + +> 当前默认后处理优先走**分层总结**(hierarchical summary),而非 `generateSynopsis()`。分层总结见 [`consolidation-and-compression.md`](consolidation-and-compression.md)。 + +## 6. 后处理 + +`handleExtractionSuccessController()`(`maintenance/extraction-success-controller.js`)在提取成功后依次处理:整合去重 → 分层总结 → 反思 → 睡眠遗忘 → 压缩 → 向量同步。这些见 [`consolidation-and-compression.md`](consolidation-and-compression.md)。 + +## 关键默认参数 + +| 参数 | 默认 | 含义 | +| --- | --- | --- | +| `extractAutoEnabled` | true | 自动提取 | +| `extractEvery` | 1 | 每 N 条助手消息提取 | +| `extractContextTurns` | 2 | 上下文轮数 | +| `extractAutoDelayLatestAssistant` | false | lag-one 延迟提取 | +| `extractPromptStructuredMode` | "both" | 提示词模式 | +| `enableSmartTrigger` | false | 智能触发 | +| 排除标签 | think,analysis,reasoning | 提取时过滤 | diff --git a/docs/algorithms/retrieval.md b/docs/algorithms/retrieval.md new file mode 100644 index 0000000..e5df9f3 --- /dev/null +++ b/docs/algorithms/retrieval.md @@ -0,0 +1,110 @@ +# 检索 / 召回算法 + +读取链路的核心:在生成前,把与当前用户输入相关的记忆召回出来、注入提示词。这是一条多阶段混合管线,不是单一算法。 + +运行时入口是 `runRecallController()`(`retrieval/recall-controller.js`),核心检索是 `retrieve()`(`retrieval/retriever.js`)。 + +> 说明:`retriever.js` 头注释把它概括为"三层混合检索",但实际管线阶段更多(下列)。本文档以代码实际行为为准。 + +## 管线阶段(按顺序) + +``` +1. 控制器门禁与输入选择 recall-controller.js +2. 可复用持久召回?命中则跳过 recall-controller.js +3. retrieve 选项映射 index.js: buildRecallRetrieveOptions +4. Authority 候选预筛(可选) retriever.js +5. 向量预筛(多查询/多意图) shared-ranking.js: rankNodesForTaskContext +6. 图扩散(PEDSA) diffusion.js +7. 混合评分 shared-ranking.js / retriever.js +8. 认知边界过滤 retriever.js +9. 交叉召回 / 共现 / 残差(可选)retriever.js / retrieval-enhancer.js +10. DPP 多样性候选池 retriever.js / retrieval-enhancer.js +11. LLM 精排(可选) retriever.js: llmRecall +12. 访问强化 + 概率召回(可选)retriever.js / dynamics.js +13. 注入格式化 injector.js: formatInjection +``` + +## 1-2. 输入选择与持久复用 + +召回输入按优先级解析(`resolveRecallInputController`):override → 待发送意图(send intent)→ 聊天尾部用户楼层 → 已发送用户 → 最新用户楼层。 + +**持久召回复用**(`resolveReusablePersistedRecallRecord`):如果当前输入匹配某条已持久化的用户楼层召回记录,可直接复用已存的注入内容,**跳过全部新检索**,返回 `llm.status="persisted"`。这是 reroll 场景的关键优化(见 [`../architecture/control-plane.md`](../architecture/control-plane.md) 的 reroll 不变量)。 + +## 5. 向量预筛 + +`rankNodesForTaskContext()` 构建向量查询计划: + +- **上下文查询融合**:当前用户文本与近期上下文融合成查询。 +- **多意图拆分**(`enableMultiIntent`):`splitIntentSegments()` 按中英文标点和连接词(`顺便|另外|还有|对了|然后|而且|并且|同时`)拆分当前用户文本,最多 `maxSegments=4` 段,每段最小长度 3。 +- **多查询并发**:对各查询并发调用向量搜索,按 max score 合并命中。 + +## 6. 图扩散(PEDSA) + +种子来自向量命中和精确实体锚点,在图上做扩散激活。详细公式、参数、衰减见 [`diffusion-and-dynamics.md`](diffusion-and-dynamics.md)。 + +## 7. 混合评分 + +融合图分、向量分、词法分、重要度,乘以时间衰减。公式与权重见 [`diffusion-and-dynamics.md`](diffusion-and-dynamics.md)。 + +## 8. 认知边界过滤 + +两套机制: + +- **遗留可见性过滤**(`filterByVisibility`):按角色名匹配 `fields.visibility`,仅当 `enableVisibility && visibilityFilter` 时启用。 +- **认知记忆门**(`computeKnowledgeGateForNode`):评分时计算节点对当前视角是否可见,不可见的节点跳过。 + +注入时也会对客观节点重新应用可见性(`buildScopedInjectionBuckets`)。 + +## 9. 可选增强 + +默认全部关闭,按需启用: + +- **交叉召回**(`enableCrossRecall`):精确实体锚点存在时,把相连的 `event` 邻居作为扩散种子加入(能量 `1.5 × edge.strength`)。范围比"双记忆交叉检索"窄,只走"精确锚点 → 相连事件邻居"。 +- **共现增强**(`enableCooccurrenceBoost`):用精确锚点和补充向量锚点构建共现索引,给图分加 bonus。 +- **残差召回**(`enableResidualRecall`,需直连 embedding 模式):NMF 新颖度分析 + 稀疏编码残差,找出向量空间里"被现有基底节点覆盖不到"的新颖节点。参数:`residualBasisMaxNodes=24`、`residualNmfTopics=15`、`residualNmfNoveltyThreshold=0.4`、`residualThreshold=0.3`、`residualTopK=5`。 + +## 10. DPP 多样性 + +`enableDiversitySampling`(默认开)用贪心 DPP(determinantal point process)从候选池里选出既相关又互相多样的节点,避免召回一堆近义节点。 + +- 候选池大小 = `min(scoredNodes, max(target, target × dppCandidateMultiplier))`,`dppCandidateMultiplier=3`。 +- 候选数 ≤ target,或任一候选缺 embedding,则不应用。 +- 贪心选择:质量项 `q_i = max(score, 1e-10)^dppQualityWeight`(默认 `dppQualityWeight=1.0`),迭代选最大对角值并做 Cholesky 式更新。 + +## 11. LLM 精排(可选) + +`enableLLMRecall`(默认开):把候选节点描述(短键、类型、scope、故事时间、认知模式、可见性、字段、分数)交给 LLM,期望返回 `selected_keys` / `active_owner_keys` / `active_owner_scores`。无效/空/失败时回退到 top 评分候选。 + +候选池大小 `llmCandidatePool=30`。 + +## 12. 访问强化与概率召回 + +- **访问强化**(`reinforceAccessBatch`):被选中的节点 `accessCount += 1`、`importance += 0.1`(上限 10)、更新 `lastAccessTime`。见 [`diffusion-and-dynamics.md`](diffusion-and-dynamics.md)。 +- **概率召回**(`enableProbRecall`,默认关):在已选节点之外,从 `importance >= 6`、非 synopsis/rule 的活跃节点里按重要度取前 3,每个以 `probRecallChance`(夹在 `[0.01, 0.5]`)的概率额外纳入。 + +> 注意:没有"是否运行召回"的随机决策——召回是否运行只由确定性门禁决定(无图谱/未启用/空聊天/无用户输入/图谱不可读/历史恢复未就绪则跳过)。"概率"只作用于额外记忆的注入。 + +## 13. 注入格式化 + +`formatInjection()`(`injector.js`)把召回结果按以下顺序拼成提示词文本: + +1. 活跃总结段(`[Summary - Active Frontier]`) +2. 分桶段(若有 scope buckets):角色 POV / 用户 POV(带"非角色事实"警告)/ 客观当前区域 / 客观全局 +3. 仅当没有分桶段时,回退到遗留段 `[Memory - Core]` / `[Memory - Recalled]` + +表格按节点类型分组,单元格转义管道符、换行替空格、截断到 200 字符。最终注入顺序在 `buildResult()` 里按 `compareNodeRecallOrderWithContext()` 排序,全局客观桶上限 6,选中 ID 上限 `maxRecallNodes`(默认 8)。 + +## 关键默认参数 + +| 参数 | 默认 | 含义 | +| --- | --- | --- | +| `topK` | 20 | 排序候选数 | +| `maxRecallNodes` | 8 | 最终注入节点上限 | +| `diffusionTopK` | 100 | 扩散保留节点数 | +| `llmCandidatePool` | 30 | LLM 精排候选池 | +| `enableLLMRecall` | true | LLM 精排 | +| `enableVectorPrefilter` | true | 向量预筛 | +| `enableGraphDiffusion` | true | 图扩散 | +| `enableCrossRecall` | false | 交叉召回 | +| `enableProbRecall` | false | 概率召回 | +| `enableDiversitySampling` | true | DPP 多样性 | diff --git a/docs/algorithms/vector-and-embedding.md b/docs/algorithms/vector-and-embedding.md new file mode 100644 index 0000000..91ce6a2 --- /dev/null +++ b/docs/algorithms/vector-and-embedding.md @@ -0,0 +1,79 @@ +# 向量与 Embedding + +向量是召回质量的下限。本文档说明 embedding 路径、向量空间身份、维度门禁——尤其是"换 embedding 模型导致维度变化"时如何保证不崩、不误用旧向量。 + +实现:`vector/embedding.js`、`vector/vector-index.js`、`vector/vector-space.js`、`vector/vector-gate.js`。 + +## Embedding 执行位置 + +> `embeddingExecution` 默认 `client`——embedding 默认在客户端(浏览器)执行。 + +第三方自定义 URL 是一等公民:OpenAI 兼容 `/v1/embeddings`、one-api、new-api、litellm、vLLM、llama.cpp、Ollama 桥接等。Authority 不生成 embedding,只存/搜向量。 + +## 批量 Embedding + +`embedBatch()`(`vector/embedding.js`): + +- 默认按 `embeddingBatchSize=10` 分块(可配,上限 100) +- 直连模式发送分块 OpenAI 兼容 `/embeddings` 请求,`input: string[]`,含 `encoding_format: "float"` +- 后端模式发送分块 `/api/vector/embed`,`texts: string[]` +- **分块失败回退**:整块失败时降级到逐条 `embedText` +- **部分结果回退**:返回的向量里有 null/缺失项时,只重试缺失项,不重试整块 +- `AbortError` 继续向上传播(不吞) + +HTTP 错误(400/401/403/429/502 等)会带状态码和响应体抛出,而不是吞成"返回空结果"的泛化错误——这样用户能看到真实的 provider 错误(比如余额不足 403)。 + +## 向量空间身份(vectorSpaceId) + +换 embedding 模型会改变向量维度和语义空间。如果静默把新模型的查询向量拿去和旧模型的存储向量比,结果是垃圾。 + +> `vectorSpaceId` 由 provider 类型、embedding 模式、规范化 API URL、模型名、观测维度共同计算得出——**不是只看模型名**。API key 不参与计算。 + +实现见 `vector/vector-space.js` 的 `deriveVectorSpace(config, observedDim)`。 + +## 本地向量清单(manifest) + +直连/自定义 URL 路径维护一个本地向量清单,记录 `vectorSpaceId`、`observedDim`、模型、状态。 + +> 向量搜索按 manifest 兼容性门控:维度或向量空间不匹配时,标记 stale/dirty 并返回空向量结果,让召回回退到图/词法召回——**绝不静默复用不兼容的旧向量**。 + +这条保证了用户换模型时看到的是"记忆没丢,搜索索引在重建",而不是错误的召回结果或"数据丢失"的错觉。 + +## 维度门禁 + +`vector/vector-gate.js` 决定向量准备/修复前的动作:skip / repair / blocked / sync。 + +直连模型/源/集合变更时,不再把旧 `node.embedding` 当作干净可用。Authority 搜索在 BME apply/manifest 能力启用时也按 manifest 兼容性门控。 + +## 服务器端向量应用 + +启用 Authority `/bme/vector-apply` 时: + +> BME 在 payload 里发送 `vectorSpaceId` 和 `observedDim`(顶层 + 每项元数据)。DOA 按批校验 vectorSpaceId/observedDim 一致性,拒绝混合维度,返回带类型的校验错误。失败/404/旧 DOA 时回退到旧 Authority Trivium 路径或本地。 + +## 连接测试 + +`testVectorConnection()` 测的是**真实批量 embedding 路径**(走 `embedBatch`),而不是单条短文本——因为"测试通过但实际 embedding 失败"的根因就是测试只测了单条短文本而运行时用的是批量长文本。 + +- 测试按表单选择的传输模式(direct/backend)测试,不被 Authority 自动主路径劫持 +- 后端模式同时探测 `/api/vector/embed`(批量 embedding)和 `/api/vector/query`(向量存储健康) +- Authority 模式先用 embedding provider 生成批量向量,再检查 Trivium stat(避免暗示 Authority 生成 embedding) + +## 后台向量同步合并 + +后台向量同步任务通过 `runtime/vector-sync-coalescer.js` 合并: + +- 同 chat + 模型 scope 合并范围 +- 活跃同步期间最多保留一个待处理任务 +- 切换聊天时跳过陈旧任务 +- 队列拒绝时回滚待处理状态 + +这避免了"每次编辑/reroll 都产生独立任务、串行 FIFO 队列堆积"的放大问题。 + +## 关键默认参数 + +| 参数 | 默认 | 含义 | +| --- | --- | --- | +| `embeddingExecution` | client | embedding 执行位置 | +| `embeddingBatchSize` | 10(上限 100) | 批量分块大小 | +| `encoding_format` | float | 直连 OpenAI 兼容请求 |