mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
docs: add algorithm documentation
This commit is contained in:
99
docs/algorithms/consolidation-and-compression.md
Normal file
99
docs/algorithms/consolidation-and-compression.md
Normal 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 |
|
||||
114
docs/algorithms/diffusion-and-dynamics.md
Normal file
114
docs/algorithms/diffusion-and-dynamics.md
Normal 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
|
||||
```
|
||||
|
||||
> 说明:该边衰减辅助函数在当前检索/写入主路径中未见调用,属于预留能力。
|
||||
112
docs/algorithms/extraction.md
Normal file
112
docs/algorithms/extraction.md
Normal 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.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 | 提取时过滤 |
|
||||
110
docs/algorithms/retrieval.md
Normal file
110
docs/algorithms/retrieval.md
Normal 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`(默认开)用贪心 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 多样性 |
|
||||
79
docs/algorithms/vector-and-embedding.md
Normal file
79
docs/algorithms/vector-and-embedding.md
Normal 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 兼容请求 |
|
||||
Reference in New Issue
Block a user