docs: add algorithm documentation

This commit is contained in:
youzini
2026-05-31 17:02:21 +00:00
parent fa2a3bfd63
commit 98d35b0ea1
5 changed files with 514 additions and 0 deletions

View File

@@ -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 |

View File

@@ -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
```
> 说明:该边衰减辅助函数在当前检索/写入主路径中未见调用,属于预留能力。

View File

@@ -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.95edgeType 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.0200 字以内)。
> 当前默认后处理优先走**分层总结**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 | 提取时过滤 |

View File

@@ -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`(默认开)用贪心 DPPdeterminantal 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 多样性 |

View File

@@ -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 兼容请求 |