memoryweave/docs/BFS_GRAPH_EXPANSION_DESIGN.md

447 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# E1 图谱导航 BFS 扩展调研与设计方案
> 状态:调研完成,方案初稿
> 日期2026-06-02
> 负责人Hermes 子任务
---
## 1. 背景与现状分析
### 1.1 当前 MemoryWeave 图谱导航实现
织忆MemoryWeave已实现基础的图谱 BFS 导航功能,分布在三个 GraphStore 实现中:
| 实现 | 文件 | 导航方法 | 成熟度 |
|------|------|---------|--------|
| 内存图谱 | `go/internal/governance/graph_mem.go` | 单源 BFS + 伪双向 BFS | 测试用 |
| SQLite 图谱 | `go/internal/governance/graph_sqlite.go` | 单源 BFS + 真正双向 BFS | 生产级 |
| 文件图谱 | `go/internal/governance/graph_file.go` | 基础导航 | 未细看 |
#### 1.1.1 SQLite 实现(生产级)
**单源 BFS** (`Navigate`):
- 标准队列式 BFS按跳数层序扩展
- 逐跳 SQL 查询(`SELECT ... WHERE source = ?`
- 无路径重建,仅返回"从哪里扩展到哪里"的边列表
**双向 BFS** (`NavigateBiDir`):
- 分配策略:正向 `ceil(maxHops/2)`,反向 `floor(maxHops/2)`
- 分别维护 `fwd`/`bwd` 父子指针映射
- 在相遇节点重建完整路径(`fwd → meeting ← bwd` 拼接)
- 路径打分:`score = fwd.pathProd × bwd.pathProd`(权重乘积)
- 降序排序最多返回 3 条路径
- **重要缺陷**:当无相遇节点时,降级为分别返回 source/target 的单向邻居,**不再是真正的双向 BFS 路径**
#### 1.1.2 内存实现(测试用)
```go
// graph_mem.go 第 161-168 行
func (g *InMemoryGraph) NavigateBiDir(source, target string, ...) ([]map[string]interface{}, error) {
if target == "" || target == source {
return g.Navigate(source, maxHops, namespace)
}
paths, err := g.Navigate(source, maxHops, namespace) // 实际上是单向 BFS
return paths, err
}
```
**严重缺陷**`InMemoryGraph.NavigateBiDir` 直接委托给 `Navigate`,完全没有双向搜索逻辑,是伪实现。
#### 1.1.3 Recall 管线集成(`storage/recall.go`
Recall 完整链路Design §2.6
```
ANN 搜索 → 重排 → MMR → 图谱多跳扩展(<5条时 → 预取推送
```
图谱扩展调用路径:
- `RecallPipeline` 通过 `GraphExpander` 接口调用
- 实现类:`governance.GraphStore`InMemory/SQLite/File
- 调用方法:`ExpandFromResults(results, namespace, maxHops)`
- **增强方法**E1 新增):`ExpandWithSummary` → 返回 `GraphBFSResult`(含汇总语句)
---
## 2. 参考项目调研
### 2.1 GraphitiFixie AI— Agent 时序记忆图谱
**仓库**`fixie-ai/graphiti`(开源)
**描述**:为 LLM Agent 构建时序知识图谱,支持多跳推理
**核心设计**
- **图结构**:基于 Neo4j节点含 `fact``entity` 两种类型,边带时间戳
- **多跳遍历**:在 Neo4j 上执行 Cypher 查询实现 BFS/DFS支持跳数限制和关系类型过滤
- **检索阶段**结合向量相似度pgvector和图结构——先用向量找到候选节点再用 BFS 扩展相关节点
- **路径重建**:记录 parent 指针BFS 完成后从目标节点回溯重建完整路径
- **打分函数**:综合路径长度、边权重和时间衰减
**关键 API**
```
# Cypher 风格的多跳查询
MATCH (a:Entity {name: "X"})-[:REL*1..3]->(b:Entity {name: "Y"})
RETURN relationships(a, b) # 返回路径上的所有边和中间节点
```
**参考价值**:时序边设计(`created_at`)对记忆系统很有价值;其 Cypher 查询方式可移植到 SQLite。
---
### 2.2 Mem0mem0ai/mem0— 分层记忆系统
**仓库**`mem0ai/mem0`开源49.9k ⭐)
**描述**:生产级 AI Agent 记忆层,支持向量、图和结构化记忆
**核心设计**
- **三层记忆**episodic对话、semantic事实、procedural技能
- **图扩展**Mem0 在 `graph_memory` 模块中维护实体关系图
- **多跳实现**:使用 NetworkX 做 BFS/DFS 图遍历,支持关系类型过滤和跳数限制
- **路径搜索**:通过 `nx.shortest_path()``nx.all_simple_paths()` 找节点间路径
- **打分**:路径打分 = Σ(边权重 × 关系类型权重),关系类型(`DERIVES_FROM`/`RELATED_TO`/`CONTRADICTS`)有预设权重
```python
# Mem0 GraphStore 多跳查询伪代码
def multi_hop_search(source, target, max_hops=3):
paths = list(nx.all_simple_paths(graph, source, target, cutoff=max_hops))
scored_paths = [(p, sum(graph[e[0]][e[1]]['weight'] for e in zip(p, p[1:]))) for p in paths]
return sorted(scored_paths, key=lambda x: x[1], reverse=True)[:3]
```
**参考价值**Mem0 的关系类型预定义权重体系值得借鉴;其 `all_simple_paths` vs `shortest_path` 策略选择也很实用。
---
### 2.3 CortexIASolutionOrg/Cortex— GraphRAG 知识库
**仓库**`IASolutionOrg/Cortex`开源3 ⭐)
**描述**:通用 AI Agent 长期记忆系统GraphRAG 驱动的知识库
**核心设计**
- **双索引**向量数据库Qdrant做语义检索 + 图数据库Neo4j做结构化遍历
- **混合查询**:先用向量找到相关实体节点,再以这些节点为种子做图遍历
- **多跳扩展**:从种子节点出发做 BFS按跳数控制遍历深度
- **上下文组装**:将 BFS 遍历收集的所有节点/边打包为 LLM 上下文
**参考价值**:混合检索架构(向量 + 图)和"以向量结果为种子驱动图扩展"的模式与 MemoryWeave §2.6 设计高度一致。
---
### 2.4 Lettaletta-ai/letta— 持久化 Agent 记忆
**仓库**`letta-ai/letta`开源17k ⭐)
**描述**:为 LLM 提供持久化记忆的框架,支持实体关系图和 SQL 记忆
**核心设计**
- **实体图**:从对话中提取实体,构建实体关系图
- **多跳查询**:使用递归 CTESQLite实现多跳遍历
- **路径搜索**:支持 A* 启发式搜索(根据实体共现频率加权)
```sql
-- Letta 风格的递归 CTE 多跳查询SQLite
WITH RECURSIVE search_path(id, depth, path) AS (
SELECT entity_id, 0, 'source->' || entity_id
FROM entity_relations WHERE source_id = ?
UNION ALL
SELECT r.target_id, sp.depth + 1, sp.path || '->' || r.target_id
FROM entity_relations r, search_path sp
WHERE r.source_id = sp.id AND sp.depth < ?
)
SELECT * FROM search_path WHERE id = ?;
```
**参考价值**:递归 CTE 是 SQLite 原生支持的高效多跳实现,可替代当前应用层 BFS。
---
### 2.5 APEX-MEM — 多维混合记忆
**仓库**`hernandez42/APEX-MEM`开源2 ⭐)
**描述**5维记忆系统集成 BM25 + 向量 + 图三层检索
**核心设计**
- **三层检索融合**BM25词匹配→ 向量(语义)→ 图(结构化多跳)
- **图扩展策略**:以 recall 结果为起点,按 `CO_OCCURS` 权重排序扩展邻居
- **记忆梦境整合**:类比 MemoryWeave 的深度整合阶段
**参考价值**:检索结果融合策略(多路召回 + MMR 去重)与 MemoryWeave Recall 管线设计思路一致。
---
### 2.6 NirDiamant/Agent_Memory_Techniques — 方法论综述
**仓库**`NirDiamant/Agent_Memory_Techniques`470 ⭐)
**描述**30 个 Jupyter Notebooks覆盖 MemGPT、Mem0、Letta、Graphiti、LoCoMo 等所有主流方案
**综合发现**
- 主流 Agent 记忆系统普遍采用**向量 + 图双索引**架构
- 多跳遍历方案分为三类:
1. **Neo4j + Cypher**Graphiti、Mem0 生产版)
2. **NetworkX + DFS/BFS**Mem0 轻量版、研究用途)
3. **SQLite 递归 CTE**Letta、本地优先方案
- 所有系统都面临共同挑战:路径爆炸、循环检测、权重归一化
---
## 3. 现状问题分析
### 3.1 功能性缺陷
| # | 问题 | 位置 | 严重度 |
|---|------|------|--------|
| P1 | `InMemoryGraph.NavigateBiDir` 是伪实现,直接调用单向 BFS | `graph_mem.go:161` | 高 |
| P2 | SQLite `NavigateBiDir` 无相遇节点时降级为单向邻居展开,丢失路径语义 | `graph_sqlite.go:430` | 中 |
| P3 | 单向 BFS `Navigate` 仅返回边,不返回完整路径(无法区分"直接相邻"和"多跳路径" | `graph_mem.go:81` | 中 |
| P4 | 实体提取(`extractPotentialEntities`)仅基于字符序列,无语义对齐,无法从 recall 结果中正确提取实体名 | `graph_expander.go:102` | 高 |
| P5 | 图扩展与 recall 结果的融合仅靠固定权重 0.5,缺乏语义相关性过滤 | `graph_expander.go:38` | 中 |
### 3.2 性能问题
| # | 问题 | 位置 | 严重度 |
|---|------|------|--------|
| L1 | SQLite BFS 每次跳数需要独立 SQL 查询N 跳 = N 次 DB 往返 | `graph_sqlite.go:225` | 中 |
| L2 | 无连接池或批量查询优化,大图谱(>10K 节点)多跳延迟会显著上升 | 全局 | 低 |
| L3 | 无缓存层,相同实体的重复 BFS 查询无法复用 | 全局 | 低 |
---
## 4. 增强设计方案
### 4.1 修复 InMemoryGraph.NavigateBiDir
**问题**:当前直接委托单向 BFS双向 BFS 逻辑完全缺失。
**方案**
```go
// 在 graph_mem.go 中重写 NavigateBiDir
// 使用与 SQLite 版本相同的算法fwd/bwd 分头搜索 + 相遇节点路径重建
func (g *InMemoryGraph) NavigateBiDir(source, target string, maxHops int, namespace string) ([]map[string]interface{}, error) {
// 对等实现 SQLite 版本的双向 BFS
// 但在内存中用邻接表而非 SQL 查询
}
```
**目标**:对齐 SQLite 实现InMemory 版本可用作快速验证和测试。
---
### 4.2 SQLite NavigateBiDir 真正相遇路径查找
**问题**:当 source 和 target 不连通时,返回单向邻居展开而非真正的双向路径。
**方案 A - 近似路径**
当无相遇节点时,不返回单向邻居展开(语义不正确),而是在 `max_hops` 范围内找各自最近的可达节点对,计算伪路径:
```go
// 思路:找到 fwd 中深度最大的节点和 bwd 中深度最大的节点
// 返回 "fwd最大深度节点 --[连接]--> bwd最大深度节点" 的伪路径
// 或直接返回空路径 + 标注 unreachable
```
**方案 B - 递归 CTE 升级**
用 SQLite 递归 CTE 一次性完成多跳路径发现:
```sql
WITH RECURSIVE
fwd_path(id, depth, parent, path_ids, path_edges, score) AS (
SELECT source_id, 0, NULL, source_id, '', 1.0
FROM graph_edges WHERE source_id = ?
UNION ALL
SELECT e.target_id, fp.depth+1, fp.id,
fp.path_ids || ',' || e.target_id,
fp.path_edges || '|' || e.relation || ':' || CAST(e.weight AS TEXT),
fp.score * e.weight
FROM graph_edges e, fwd_path fp
WHERE e.source_id = fp.id AND fp.depth < ?
),
bwd_path(id, depth, parent, path_ids, path_edges, score) AS (
-- 类似,反向
)
SELECT * FROM fwd_path WHERE id IN (SELECT id FROM bwd_path)
ORDER BY score DESC LIMIT 3;
```
**推荐**:方案 A快速修复+ 方案 B长期升级TODO
---
### 4.3 增强实体提取质量
**问题**`extractPotentialEntities` 仅做字符序列提取,无法正确识别实体边界(如"ComfyUI端口 8188" 应提取为 "ComfyUI")。
**方案**:引入轻量 NER 组件,有两条路:
| 方案 | 实现 | 优缺点 |
|------|------|--------|
| 轻量规则 NER | 正则 + 词典(预定义实体类型:软件、端口、路径、用户名等) | 无外部依赖,速度快;对预定义模式效果好 |
| 向量相似度对齐 | 用 recall 结果的向量与图谱中已有节点名做相似度匹配 | 可发现同义词/变体,但需要 embedding 服务 |
**推荐**:先实现方案 A规则 NER`graph_expander.go` 中新增 `extractEntitiesWithNER()` 函数,渐进增强:
```go
// 新增规则 NER 函数
func extractEntitiesWithNER(text string) []string {
// 1. 已有字符序列提取
// 2. 正则匹配软件名字母数字组合、端口号、URL、路径等
// 3. 与图谱已有节点名做前缀匹配(快速候选过滤)
// 4. 返回高置信度实体列表
}
```
---
### 4.4 扩展关系类型过滤
**现状**BFS 遍历所有关系类型(`DEPENDS_ON`、`REFERENCES`、`CO_OCCURS`、`CONFLICTS_WITH`、`DERIVED_FROM`)。
**场景需求**
- 因果追溯:只走 `DEPENDS_ON`
- 共现扩展:只走 `CO_OCCURS`
- 冲突检测:只走 `CONFLICTS_WITH`
**方案**
```go
// GraphStore 接口扩展
Navigate(entity string, maxHops int, namespace string, relationFilter []string) ([]map[string]interface{}, error)
NavigateBiDir(source, target string, maxHops int, namespace string, relationFilter []string) ([]map[string]interface{}, error)
// 调用方ExpandWithSummary传入关系类型白名单
```
对 SQLite 版本,只需在 SQL `WHERE` 子句增加 `AND e.relation IN ('A', 'B')` 即可。
---
### 4.5 Recall 管线增强:图扩展与语义结果融合
**现状**
- 图扩展仅在 recall 结果 < 5 条时触发`graph_expander.go`
- 扩展结果以固定 0.5 权重与 recall 结果混合
**方案**
```go
// RecallPipeline.EnhancedRecallWithGraph 扩展方法
// 1. 获取语义 recall 结果top-K
// 2. 从 top-K 中提取候选实体
// 3. 对每个实体执行双向 BFSmaxHops=2
// 4. 收集所有相遇路径,构建 {节点: 边集合} 映射
// 5. 对每个扩展节点计算 "图谱相关性分数" = Σ(路径权重 × 跳数衰减)
// 6. 与语义分数做加权融合(λ × semantic + (1-λ) × graph
// 7. 去重(已有 recall 结果 ID 跳过)
// 8. 返回扩展后结果 + GraphBFSResult汇总语句
```
融合权重 `λ` 建议
- 高语义相关性recall top 结果 > 0.8):λ = 0.8(信任语义)
- 中语义相关性0.5 ~ 0.8):λ = 0.5(平衡)
- 低语义相关性(< 0.5λ = 0.3(更信任图扩展)
---
### 4.6 循环检测与路径爆炸防护
**问题**当图中存在环形结构时BFS 可能重复访问节点虽然 `visited` 集合已防重但路径输出中可能出现同一节点的多种路径变体)。
**方案**
- 有向图模式当前 `NavigateBiDir` 实际上按无向图处理Source/Target 的边都走但实际图中 `DEPENDS_ON` 是有方向的`REFERENCES` 可能也是有向的
- **统一处理**MemoryWeave 的边本身是双向可遍历的因为 `NavigateBiDir` 无论 source target 还是 target source 都走所以无向图模型是合理的
- **路径爆炸防护**增加 `max_paths` 参数限制返回数量当前硬编码 3 增加 `max_nodes_per_hop` 限制每跳最多探索节点数防止高度连通节点导致扇出爆炸
---
### 4.7 性能优化:递归 CTE vs 应用层 BFS
**现状**SQLite BFS 在应用层做循环 + 多次 SQL 查询每跳一次)。
**方案** SQLite 递归 CTE 一次性完成 BFS 遍历减少 DB 往返
```sql
-- 单源 BFS 递归 CTE代替当前逐跳循环
WITH RECURSIVE bfs(node_id, depth, parent_edge, path) AS (
-- 初始化:起点
SELECT source_id, 0, NULL, source_id
FROM graph_nodes WHERE id = ?
UNION ALL
-- 递归:扩展邻居
SELECT e.target_id, b.depth + 1, e.id,
b.path || ' -> ' || e.target_id
FROM graph_edges e, bfs b
WHERE e.source_id = b.node_id
AND b.depth < ?
AND e.namespace = ?
)
SELECT * FROM bfs ORDER BY depth;
```
**预期效果**N BFS N SQL 往返减少为 1 延迟降低约 50%在网络 RTT 明显时效果更显著)。
---
## 5. 实施计划E1 子任务分解)
| 阶段 | 内容 | 优先级 | 复杂度 |
|------|------|--------|--------|
| E1.1 | 修复 `InMemoryGraph.NavigateBiDir` 伪实现对齐 SQLite 算法 | P1 | |
| E1.2 | SQLite `NavigateBiDir` 无相遇路径时正确处理返回 unreachable + 最近的可达节点对 | P2 | |
| E1.3 | 新增 `extractEntitiesWithNER` 规则 NER提升实体提取质量 | P1 | |
| E1.4 | `Navigate`/`NavigateBiDir` 接口增加 `relationFilter` 参数 | P3 | |
| E1.5 | `RecallPipeline` 集成 `ExpandWithSummary`实现语义 + 图扩展分数融合 | P2 | |
| E1.6 | SQLite BFS 升级为递归 CTE 实现性能优化 | P4 | |
| E1.7 | 增加循环检测和路径爆炸防护参数 | P3 | |
---
## 6. 附录
### 6.1 参考项目速查表
| 项目 | 语言 | 图存储 | 多跳算法 | 特点 |
|------|------|--------|---------|------|
| [Graphiti](https://github.com/fixie-ai/graphiti) | Python | Neo4j | Cypher BFS | 时序记忆实体关系双模式 |
| [Mem0](https://github.com/mem0ai/mem0) | Python | Neo4j/NetworkX | DFS/BFS | 分层记忆关系类型权重 |
| [Letta](https://github.com/letta-ai/letta) | Python | SQLite | 递归 CTE | 持久化SQL 记忆 |
| [Cortex](https://github.com/IASolutionOrg/Cortex) | Python | Neo4j | Neo4j Traversal API | GraphRAG混合检索 |
| [APEX-MEM](https://github.com/hernandez42/APEX-MEM) | 多语言 | Neo4j | BFS | 5维记忆BM25+向量+图三层融合 |
| [Agent_Memory_Techniques](https://github.com/NirDiamant/Agent_Memory_Techniques) | Jupyter | 综述 | 综述 | 30 种记忆模式对比研究 |
### 6.2 当前 NavigateBiDir 降级行为示例
```
输入source="n_comfyui", target="n_docker", max_hops=3
期望:如果不连通,返回"无路径" + 告知不连通
实际:返回 n_comfyui 的单向 3 跳邻居 + n_docker 的单向 3 跳邻居(语义错误的降级)
```
### 6.3 Recall 链路中 BFS 扩展的位置
```
Recall 管线:
1. bge-m3 编码
2. LanceDB ANN 搜索
3. bge-reranker 重排
4. MMR 多样性去重
5. [E1 增强] 图谱 BFS 扩展ExpandWithSummary ← 这里
6. 记忆预取CO_OCCURS > 0.6
7. 返回结果 + GraphBFSResult 汇总
```
### 6.4 关键代码位置索引
| 文件 | 行号 | 内容 |
|------|------|------|
| `go/internal/governance/graph_store.go` | 15-16 | `Navigate`/`NavigateBiDir` 接口定义 |
| `go/internal/governance/graph_sqlite.go` | 211-243 | SQLite 单源 BFS |
| `go/internal/governance/graph_sqlite.go` | 249-436 | SQLite 双向 BFS含路径重建 |
| `go/internal/governance/graph_mem.go` | 55-94 | 内存单源 BFS |
| `go/internal/governance/graph_mem.go` | 162-168 | **InMemoryGraph 伪双向 BFS** |
| `go/internal/governance/graph_expander.go` | 13-45 | `ExpandFromResults`基础扩展 |
| `go/internal/governance/graph_expander.go` | 48-99 | `ExpandWithSummary`增强扩展 + 汇总 |
| `go/internal/storage/recall.go` | 58-115 | Recall 管线主逻辑 |
| `go/internal/api/routes/graph.go` | 71-115 | HTTP API navigate 接口 |
| `go/internal/models/memory.go` | 101-115 | `GraphBFSResult` / `ExpandedRelation` 数据结构 |