F1-F4: 端到端管道修复 + 蒸馏激活 + 治理基础

修复内容:
- F1: Hermes 插件 commit/recall 加 agent_id/namespace,201 状态码兼容,namespace 推导
- F2: 存储后端确认在 LanceDB,graph API match 格式兼容,recall/debug 诊断端点
- F3: LLM 端点 URL 修复(缺 /chat/completions),API Key 前缀修复,中文实体提取启发式
- F4: 冲突检测加载实际记忆,图谱 API match 搜索,PassiveValidator 修正
- REPAIR-FULL.md + IMPLEMENTATION.md: G1-G9 全量修复计划
This commit is contained in:
xiaowei 2026-05-30 02:01:11 +08:00
parent 34329ddabb
commit f6f4c832fb
15 changed files with 1427 additions and 378 deletions

View File

@ -1,308 +1,48 @@
# 织忆 MemoryWeave 实施计划 v0.1
# 织忆 v3.8 修复进度
> **目标**Python 原型 → Go+Rust 生产级基础设施
> **代码生成**:全部由 opencode 生成,不做手工编码
> **仓库**http://192.168.123.11:3000/xiaoxue_admin/memoryweave
> 实时更新,每次 session 开始时或结束后更新
> 格式:[ ] 未开始 | [→] 进行中 | [✓] 完成 | [✗] 阻塞
## 当前阶段
- [→] **G1: Recall 管线完整化** — 开始日期: 待开始
## 总体进度
| Phase | 状态 | 完成度 |
|-------|------|--------|
| G1 Recall 管线完整化 | [ ] | 0% |
| G2 Embedding 本地化 | [ ] | 0% |
| G3 治理闭环 | [ ] | 0% |
| G4 共现+预取+WebSocket | [ ] | 0% |
| G5 评估+金标+V值 | [ ] | 0% |
| G6 聚类+蒸馏深化 | [ ] | 0% |
| G7 Skill+自动化流程 | [ ] | 0% |
| G8 基础设施加固 | [ ] | 0% |
| G9 集成拓展 | [ ] | 0% |
## 当前任务详情
### G1.1 MMR 多样性去重
状态: [ ] 未开始
细节: recall.go 添加 mmrDiversify 函数
### G1.2 多跳图谱扩展
状态: [ ] 未开始
细节: recall <5 时自动调用 navigate 扩展
### G1.3 图谱导航双向 BFS
状态: [ ] 未开始
细节: graph/navigate 加 source/target 参数
### G1.4 搜索缓存确认
状态: [ ] 未开始
细节: searchcache.go TTL 确认
## 最新状态
(留空,每次更新时填写)
---
## 里程碑概览
| 里程碑 | 天数 | 输出 | 验收标准 |
|--------|------|------|---------|
| **M1: 骨架** | 1-2 | go build + cargo build 通过 | `/health` 200 OK |
| **M2: 存储** | 3-5 | LanceDB 集成 + Embedding 管线 | recall 延迟 < 200ms本地 BGE |
| **M3: 核心 API** | 6-10 | /commit + /recall + /bootstrap | 对比 Python 版本结果一致 |
| **M4: 蒸馏** | 11-15 | 自动蒸馏 + 质量评分 | commit → distill → LanceDB 全链路 |
| **M5: 治理** | 16-20 | 冲突检测 + 遗忘 + 知识图谱 | /conflicts/scan 返回正确结果 |
| **M6: 自优化** | 21-25 | 仪表盘 + 缺口检测 + 因果追踪 | 7 项指标产出有效趋势 |
| **M7: 分布式** | 26-30 | Redis Streams + CRDT | 双实例 doc_count 一致 |
| **M8: 切换** | 31-35 | 影子部署 → 灰度 → 全量 | 7821 端口 Go 服务稳定 72h |
---
## M1: 项目骨架Day 1-2
### 任务
| ID | 任务 | 文件 | 预估 |
|----|------|------|------|
| M1.1 | Go `go mod tidy` 通过 | go/go.mod | 0.5h |
| M1.2 | Rust `cargo check` 通过 | rust/Cargo.toml | 0.5h |
| M1.3 | `/health` 端点 + Auth 中间件 | go/internal/api/server.go | 2h |
| M1.4 | Protobuf 定义 (Go↔Rust IPC) | go/proto/consolidate.proto | 1h |
| M1.5 | Rust main.rs 参数解析 | rust/src/main.rs | 1h |
| M1.6 | systemd unit 文件 | deploy/*.service | 0.5h |
| M1.7 | CI: build + test (Makefile) | Makefile | 1h |
### 验收
```bash
make build # go build + cargo build 通过
sudo make install # systemd 服务安装成功
curl http://localhost:7821/health # {"status":"ok"}
```
---
## M2: 存储层Day 3-5
### 任务
| ID | 任务 | 文件 | 预估 |
|----|------|------|------|
| M2.1 | LanceDB schema 创建memories/episodes/tombstones | go/internal/storage/lancedb.go | 3h |
| M2.2 | LanceDB 读写Insert/Search/Update | go/internal/storage/lancedb.go | 3h |
| M2.3 | bge-m3 编码客户端(本地 vLLM + 模力方舟 fallback | go/internal/storage/embedder.go | 3h |
| M2.4 | bge-reranker-v2-m3 客户端 | go/internal/storage/reranker.go | 2h |
| M2.5 | Recall pipeline: encode → LanceDB search → rerank | go/internal/storage/recall.go | 3h |
| M2.6 | JSONL 分片读写episodes 历史数据) | go/internal/storage/jsonl.go | 2h |
| M2.7 | Redis 客户端commit store + 事件流) | go/internal/storage/redis.go | 2h |
| M2.8 | SQLite 初始化distill_queue + graph + audit_log | go/internal/storage/sqlite.go | 2h |
| M2.9 | Rust LanceDB 原生读写lancedb crate | rust/src/lancedb.rs | 2h |
| M2.10 | Rust Embedding 管线Candle/ort | rust/src/embed.rs | 3h |
### 验收
```bash
# LanceDB 创建表 + 插入 10 条测试数据 + 搜索 → 返回正确结果
curl -X POST -H "X-API-Key: $KEY" localhost:7821/api/v1/recall \
-d '{"query":"牧尘的系统配置","limit":5}'
# → 返回 5 条相关记忆,延迟 < 200ms
```
---
## M3: 核心 APIDay 6-10
### 任务
| ID | 任务 | 预估 |
|----|------|------|
| M3.1 | `POST /api/v1/commit` — 写入 episode | 3h |
| M3.2 | `POST /api/v1/recall` — 混合检索(向量+图谱+MMR | 3h |
| M3.3 | `GET /api/v1/bootstrap` — 冷启动 | 1h |
| M3.4 | `POST /api/v1/batch-commit` — 批量提交 | 2h |
| M3.5 | `GET /api/v1/stats` — 统计 | 1h |
| M3.6 | 全端点 Auth 中间件 | 1h |
| M3.7 | API 对比测试Python vs Go | 3h |
| M3.8 | WebSocket `/api/v1/ws/{agent_id}` — 实时推送 | 3h |
### 验收
```bash
# 与 Python 版本相同输入 → 相同输出
python -m pytest tests/api_compat.py -v
# → 全部通过
```
---
## M4: 蒸馏引擎Day 11-15
### 任务
| ID | 任务 | 预估 |
|----|------|------|
| M4.1 | 硬规则过滤Layer 1 | 2h |
| M4.2 | LLM 评估蒸馏Layer 2— 5 维度评分 | 4h |
| M4.3 | commit → 自动触发蒸馏 | 2h |
| M4.4 | Distilled 写入 LanceDB | 2h |
| M4.5 | 批量蒸馏(攒 N 条一起处理) | 2h |
| M4.6 | 每日 LLM 调用限额50 次/agent | 2h |
| M4.7 | 降级策略LLM 不可用 → 规则提取) | 2h |
| M4.8 | `/api/v1/feedback/useful` + `/not-useful` 端点 | 2h |
| M4.9 | 质量评分quality_score = useful/(useful+not_useful) | 1h |
### 验证
```bash
# 提交 3 条 episode → 自动蒸馏 → 检查 distilled 表
curl -X POST /api/v1/commit -d '{...}' # x3
curl /api/v1/stats | jq '.distilled_count'
# → 3
```
---
## M5: 治理引擎Day 16-20
### 任务
| ID | 任务 | 预估 |
|----|------|------|
| M5.1 | 冲突检测 — entity/fact/decision 三种冲突 | 4h |
| M5.2 | `/api/v1/conflicts` 端点 | 2h |
| M5.3 | 冲突自动裁决latest_wins / primary_wins | 2h |
| M5.4 | 遗忘策略:线性衰减 + 核心记忆保护 | 3h |
| M5.5 | 知识图谱 SQL schema + 建图 | 4h |
| M5.6 | 多跳导航(双向 BFS + 打分) | 4h |
| M5.7 | 图谱修剪4 条规则) | 2h |
| M5.8 | Namespace 隔离shared vs agent-local graph | 2h |
| M5.9 | `/api/v1/graph/*` 端点 | 2h |
| M5.10 | WebSocket 冲突通知 → 牧尘裁决 | 2h |
### 验收
```bash
# 写入两条冲突事实 → 检测冲突
curl -X POST /api/v1/commit -d '{"content":"Docker used_by hermes"}'
curl -X POST /api/v1/commit -d '{"content":"Docker used_by openclaw"}'
curl /api/v1/conflicts | jq '.count'
# → 1
# 知识图谱多跳导航
curl /api/v1/graph/navigate -d '{"entity":"Docker","max_hops":2}'
# → 返回 Docker → hermes, Docker → openclaw 完整路径
```
---
## M6: 自优化引擎Day 21-25
### 任务
| ID | 任务 | 预估 |
|----|------|------|
| M6.1 | 自优化仪表盘 (`GET /api/v1/metrics/self`) — 7 项指标 | 3h |
| M6.2 | 知识缺口检测(连续 3 次 miss → gap | 3h |
| M6.3 | 缺口自动分类Type A/B/C/D | 3h |
| M6.4 | Type B/C 自动修复(同义词映射/阈值重试) | 2h |
| M6.5 | Type A 学习任务创建 + WebSocket 推送 | 2h |
| M6.6 | 记忆预取 — 共访关系图谱 | 3h |
| M6.7 | 记忆刷新 — freshness 字段 + stale 标记 | 2h |
| M6.8 | 溯源链 — version_history + source + trigger | 2h |
| M6.9 | 来源信任加权(牧尘 1.0 > 配置 0.7 > LLM 0.4 | 1h |
| M6.10 | Rust 深度整合DBSCAN + 衰减回归 + 质量回溯) | 5h |
| M6.11 | 蒸馏质量反向测试L0→L1→L0' cosine 检验) | 3h |
| M6.12 | 周期性触发器(条件触发 > 24h 定时) | 2h |
### 验收
```bash
# 仪表盘有数据
curl /api/v1/metrics/self | jq '.recall_useful_rate'
# → 0.85
# 缺口检测正常工作
curl /api/v1/gaps | jq '.count'
# → > 0
# Rust 深度整合
sudo systemctl start zhiyi-consolidate
journalctl -u zhiyi-consolidate | grep "completed"
# → "consolidation completed: 3 clusters, decay calibrated, quality score 0.92"
```
---
## M7: 分布式部署Day 26-30
### 任务
| ID | 任务 | 预估 |
|----|------|------|
| M7.1 | Redis Streams 事件传播XADD/XREAD | 4h |
| M7.2 | CRDT 合并策略(时间戳 + 来源优先) | 4h |
| M7.3 | 服务发现Redis Hash + TTL 心跳) | 2h |
| M7.4 | 速率限制per-agent 令牌桶) | 2h |
| M7.5 | Nginx 负载均衡配置 | 1h |
| M7.6 | 备份原子性LanceDB checkpoint + SQLite .backup | 2h |
| M7.7 | 灾难恢复脚本 | 1h |
| M7.8 | Prometheus metrics 端点 | 2h |
| M7.9 | 双实例 E2E 测试 | 3h |
### 验收
```bash
# 双实例同步
# Instance A commit → Instance B 30s 内可见
# Instance B 宕机重启 → 自动补齐缺失数据
# doc_count 一致
```
---
## M8: 灰度切换Day 31-35
| ID | 任务 | 预估 |
|----|------|------|
| M8.1 | FAISS → LanceDB 迁移脚本 | 3h |
| M8.2 | 影子部署Go 在 7822 运行 48h | — |
| M8.3 | 灰度Nginx upstream 10% → 50% → 100% | — |
| M8.4 | systemd 切换zhiyi.service → Go 二进制 | 1h |
| M8.5 | Hermes bridge 验证memory_search/write 正常 | 1h |
| M8.6 | 旧 Python 代码归档7 天可快速回滚) | 0.5h |
| M8.7 | Go 版本稳定运行 72h 无回滚 | — |
### 验收
```bash
curl http://localhost:7821/health
# → {"status":"ok","service":"zhiyid","version":"1.0.0","uptime":"72h"}
```
---
## 关键路径
```
M1 → M2 → M3 → M4 → M5 → M6 → M7 → M8
↘ M5可与 M4 并行)
↘ M6依赖 M5 图谱)
```
### 可并行
| 并行组 | 任务 |
|--------|------|
| M2.9-M2.10Rust | 与 M2.1-M2.8Go并行 |
| M5治理 | 可与 M4蒸馏部分并行 |
| M7.5-M7.8(运维) | 与 M7.1-M7.4 并行 |
---
## 风险
| 风险 | 概率 | 缓解 |
|------|------|------|
| opencode 限流 | 中 | 核心路径优先;多轮生成 |
| LanceDB Go 绑定不稳定 | 低 | 备选Rust `lancedb` crate 原生 |
| bge-m3 向量不一致 | 中 | cosine_sim ≥ 0.99 阈值检验 |
| FAISS 数据迁移丢失 | 低 | 备份 → 迁移 → 数目对比 → 抽查 |
| Rust Candle/ort 编译失败 | 中 | 先 HTTP 调用 vLLMRust 仅用 LanceDB |
---
## 分支策略
```
main ← 稳定版本,只接受 PR
develop ← 开发主线
feature/m1-* ← 各里程碑特性分支
release/v1.0 ← 发布分支
```
## Commit 规范
```
feat: M3.1 POST /api/v1/commit 实现
fix: LanceDB schema timestamp 类型修正
docs: IMPLEMENTATION.md M2 验收标准更新
refactor: recall pipeline 提取为独立模块
```
---
## 版本标签
| 标签 | 对应 | 日期目标 |
|------|------|---------|
| v0.1.0-dev | M1 骨架完成 | Day 2 |
| v0.2.0-dev | M2 存储完成 | Day 5 |
| v0.3.0-dev | M3 核心 API | Day 10 |
| v0.4.0-dev | M4 蒸馏 | Day 15 |
| v0.5.0-dev | M5 治理 | Day 20 |
| v0.6.0-dev | M6 自优化 | Day 25 |
| v0.7.0-dev | M7 分布式 | Day 30 |
| v1.0.0 | M8 全量切换 | Day 35 |
*进度文件。更新于: 2026-05-30*

530
REPAIR-FULL.md Normal file
View File

@ -0,0 +1,530 @@
# 织忆 v3.8 — 完整修复计划
> 版本1.0 | 2026-05-30
> 设计文档:`~/mc/小唯/07-Wiki/concepts/织忆(MemoryWeave)-v3.8-完整定稿.md`
> 项目路径:`~/projects/memoryweave/`
> 作战模式:我(规划+验收)→ opencode执行→ openclawUAT
> 禁止偷懒,禁止更改设计语言,逐条对照设计方案实施
---
## 当前状态基线
```
Go/Rust 服务: zhiyid:7821 ✅ running | sidecar ✅ running | Redis ✅
Hermes 插件: ✅ memory_search/write/stats 全部可用
数据: 20 memories, 3 episodes, 82节点/135边
蒸馏: ✅ LLM 5维评估 + 实体提取
触发器: ✅ 8个全部激活
L3世界模型: ✅ 已填充
自优化指标: ⚠️ 全部为0 (无数据反馈)
```
---
## Phase G1: Recall 管线完整化
> 目标:补齐 2.6 Recall 管线的缺失步骤,达到设计文档的 7 步完整链路
> 当前状态:只到 Step 3 (Rerank),缺 MMR/多跳扩展/预取/WebSocket
> 优先级:⚠️ 高recall 是整个系统的核心路径)
### G1.1 MMR 多样性去重
**设计依据**§2.6 Step 4 — MMR = (1-λ) × relevance + λ × (1 - max_sim_to_selected),λ=0.5
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/storage/recall.go` |
| 改动 | 在 rerank 结果之后,添加 MMR 去重步骤 |
| 输入 | rerank 后的 top-N 结果 (content + score) |
| 输出 | MMR 去重后的 top-K 结果 |
| 核心逻辑 | `mmrDiversify(results []Result, lambda float64) []Result` |
| 参数 | `mmr_diversity` 从 config 读取,默认 0.5 |
| 引用的记忆 | 取 `content` 做 bge-m3 编码比较相似度;或简单版用 Jaccard 字符 bigram |
| 边界 | 结果 ≤ 3 条时不触发 MMR太少没必要 |
| 验收 | recall 返回的相邻结果 content 相似度 < 0.85 |
### G1.2 多跳图谱扩展
**设计依据**§2.5.4 — recall 结果 < 5 条时从结果出发做双向 BFS 扩展
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/api/routes/core.go` + `go/internal/storage/recall.go` |
| 改动 | recall 返回结果 < 5 自动调用图谱 navigate 扩展 |
| 逻辑 | 取 recall 结果中的实体 → navigate 扩展 1 跳 → 去重 → 加入结果末尾 |
| 参数 | `max_hops=1`(只扩展 1 跳,避免漂移),`max_extra=3` |
| 触发 | recall 结果 count < 5 且图谱有节点 |
| 边界 | 扩展结果质量不降级 — 标注为 "graph_expanded" 并排在原始结果之后 |
| 验收 | 搜索冷门实体时,返回图谱关联结果 |
### G1.3 图谱导航双向 BFS
**设计依据**§2.5.4 — `POST /api/v1/graph/navigate` 使用 `source`/`target` 参数
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/api/routes/graph.go` |
| 改动 | 新增 `source`/`target` 参数支持(不删 `entity` 兼容旧格式) |
| 参数 | `{"source": "node-id", "target": "node-id", "max_hops": 3}` |
| 算法 | 双向 BFS从 source 扩展 2 跳 + 从 target 扩展 1 跳 → 汇合 |
| 路径打分 | Π(每个边的 weight) |
| 旧兼容 | `entity` 参数继续支持,内部转为 source=entity/target="" |
| 验收 | 两个已知实体之间返回路径 |
### G1.4 搜索缓存确认
**设计依据**§2.6 — 搜索缓存 Redis TTL 1h
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/storage/searchcache.go` |
| 检查 | 确认 query hash → Redis GET/SET 逻辑正确 |
| 修复 | 如果 key 没设 TTL → 加 EXPIRE 3600 |
| 验收 | 相同 query 2次请求第2次比第1次快 > 100ms |
---
## Phase G2: Embedding 本地化
> 目标:从模力方舟 API 切换到本地 vLLM消除外网依赖
> 当前状态:`BGE_ENDPOINT=https://ai.gitee.com/v1/embeddings`
> 优先级:⚠️ 高(依赖外网,延迟高)
### G2.1 下载 bge-m3 模型
**设计依据**§2.3
```bash
# ModelScope 国内快
pip install modelscope
python -c "from modelscope.hub.snapshot_download import snapshot_download; \
snapshot_download('BAAI/bge-m3', cache_dir='/home/muc/models/bge-m3')"
```
必须排除 onnx/ 子目录以节省空间:`--exclude "imgs/**"`
### G2.2 部署本地 vLLM (port 8000)
**设计依据**§2.3 — vLLM `--task embed --dtype half --host 0.0.0.0 --port 8000`
| 项目 | 内容 |
|------|------|
| 方式 | systemd 管理 `/etc/systemd/system/vllm-bge.service` |
| 二进制 | `/home/muc/.local/bin/python -m vllm.entrypoints.openai.api_server` |
| 参数 | `--model /home/muc/models/bge-m3 --task embed --dtype half --host 0.0.0.0 --port 8000` |
| 量化 | half (float16)RTX 3050 4GB 够用 |
| 验证 | POST /v1/embeddings `{"model":"bge-m3","input":["测试"]}` → 返回 1024 维向量 |
### G2.3 切换环境变量 + 验证一致性
| 项目 | 内容 |
|------|------|
| 改动 | `~/.hermes/.env` 或 zhiyi 启动环境:`BGE_ENDPOINT=http://127.0.0.1:8000/v1` |
| 一致性检查 | 用同一段文本分别请求模力方舟和本地 vLLMcosine_sim ≥ 0.99 |
| 回滚方案 | 如果一致性 < 0.99保持模力方舟排查编码参数差异 |
| 验收 | commit/recall 全部走本地 vLLM响应时间 < 200ms |
### G2.4 更新 deploy/ 配置
**设计依据**§7.4
- 创建 `deploy/vllm-bge.service`(如果不存在)
- 更新 `deploy/nginx-zhiyi.conf`(如果需代理)
- 更新 systemd 依赖:`zhiyid.service` After=vllm-bge.service
---
## Phase G3: 治理闭环
> 目标激活遗忘、冲突自动裁决、PassiveValidator、版本历史溯源
> 当前状态全部代码存在但从未运行0 次触发)
> 优先级:高
### G3.1 遗忘策略激活
**设计依据**§3.3.1
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/governance/governance.go` + `go/internal/scheduler/scheduler.go` |
| 当前 | 衰减扫描器 (`t_decay` 每 6h) 已在调度器注册,但 `applyDecay` 可能没加载实际数据 |
| 修复-1 | 确认 `t_decay` trigger 的 handler 调用 `applyDecay()` 时传入所有记忆列表 |
| 修复-2 | `applyDecay` 的实现需读 LanceDB memories 表,按 category 分组应用不同 `decay_rate` |
| 修复-3 | 淘汰工序按优先级执行,从 quality_score < 0.2 开始扫描 |
| 边界 | tier=core 免疫衰减volatile_flag=true 加倍 |
| 验收 | `GET /api/v1/metrics/self``deprecated_per_day > 0` |
### G3.2 冲突自动裁决
**设计依据**§3.3.2
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/api/routes/conflicts.go` + `go/internal/governance/governance.go` |
| 当前 | 冲突检测器 `detectConflicts``OnDistillComplete` 回调中接收空切片 |
| 修复-1 | `OnDistillComplete` 加载当前 namespace 的实际记忆列表 |
| 修复-2 | 检测到冲突后,自动检查自动裁决条件(信任差异 >0.5 / 时间戳差 >90天 / 已裁决过同类冲突) |
| 修复-3 | 可自动裁决 → 执行裁决 + 写入 version_history + 创建 CONFLICTS_WITH 边 |
| 修复-4 | 不可自动裁决 → 创建冲突记录等待人工裁决 |
| 验收 | 写入两条矛盾事实 → 自动创建冲突且 auto_resolve_rate > 0 |
### G3.3 PassiveValidator 激活
**设计依据**§3.3.4 — 三层匹配 (P1/P2/P3)
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/governance/governance.go` |
| 修改 | 确认每次 commit 后调用 `passiveValidate` |
| 逻辑 | 检查当前 episode 内容与已有 distilled 的匹配度 |
| 验收 | commit 后某些记忆的 quality_score 自动上升 |
### G3.4 版本历史溯源
**设计依据**§3.3.3 — `version_history` 字段 JSON 数组
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/storage/lancedb_ipc.go` + `go/internal/api/routes/core.go` |
| 当前 | `version_history` 字段在所有记忆中都为空 |
| 修复-1 | commit/feedback/correct 等修改操作时,检查是否有已有版本 → 追加 version_history 条目 |
| 修复-2 | `GET /api/v1/memory/{id}/versions` 端点解析并返回 version_history |
| 验收 | 修改一条记忆后versions 端点返回 2 条历史 |
### G3.5 衰减校准
**设计依据**§3.3.1 衰减模型校准
| 项目 | 内容 |
|------|------|
| 文件 | Rust sidecar `rust/src/decay_calibrate.rs` |
| 当前 | 代码存在但从未执行Rust sidecar 只被 consolidate.timer 触发,而 timer 条件不满足) |
| 修复-1 | 确认 Rust sidecar 的 `decay_calibrate` 实现是否可单独调用 |
| 修复-2 | 改为 Go 端每周运行一次,或通过 IPC 触发 Rust 执行 |
| 验收 | 衰减率 decay_rate 按类别更新 |
---
## Phase G4: 共现 + 预取 + WebSocket
> 目标:打通 CO_OCCURS 图谱 + 记忆预取 + WebSocket 8 种事件推送
> 当前状态:全部未实现
> 优先级:中高(依赖 G1 完成 recall 管线)
### G4.1 CO_OCCURS 共现追踪
**设计依据**§2.5.3 来源 2 + §6.7
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/storage/cooccur.go` |
| 当前 | 代码存在但 `cocoour` 模块没有任何触发器调用它 |
| 修复-1 | 每次 recall 后,取 top-5 结果 → 任意两条记录共现 → `evidence_count+1` |
| 修复-2 | `CO_OCCURS` 权重 = 共被recall次数 / min(A_recall_count, B_recall_count) |
| 修复-3 | 权重 > 0.6 → 加入预取 map< 0.3 14 天无更新 修剪 |
| 存储 | 知识图谱边relation_type=CO_OCCURS |
### G4.2 记忆预取管道
**设计依据**§6.7
| 项目 | 内容 |
|------|------|
| 位置 | `go/internal/storage/recall.go` 或新文件 `prefetch.go` |
| 逻辑 | recall 完成后 → 查询 CO_OCCURS 权重 > 0.6 的记忆 → 放入预取队列 |
| 预取窗口 | 14 天 |
| 当前推送 | 先实现 HTTP 回调方式WebSocket 后续推),即 recall 返回中带 prefecth 字段 |
| 验收 | recall 结果中返回 `prefetch` 字段(非空时有相关内容) |
### G4.3 WebSocket 8 种事件推送
**设计依据**§2.7 — 8 种事件类型
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/api/routes/ws.go` + `go/internal/api/routes/ws_events.go` |
| 当前 | WS 端点存在 (`/api/v1/ws/{agent_id}`),返回 400 因为无 Agent 通过 WebSocket 连接 |
| 修复-1 | 确认 WS handler 实现完整(升级、读、写、心跳) |
| 修复-2 | 实现 8 种事件的推送点和序列化 |
| 修复-3 | 实现事件队列(防止 Agent 掉线丢失事件) |
| 事件列表 | `prefetch.push`, `gap.detected`, `gap.filled`, `memory.updated`, `conflict.detected`, `conflict.resolved`, `deep.consolidation.done`, `quality.drop` |
| 验收 | 测试程序通过 WebSocket 连接 → 触发事件 → 收到推送 |
---
## Phase G5: 评估 + 金标 + V值
> 目标:评估框架可运行、金标集 12 维、V 值反向传播打通
> 当前状态:端点存在但 0 次运行
> 优先级:中(依赖数据积累 → 需要 Hermes 日常使用产生数据后才有意义)
### G5.1 评估框架跑通
**设计依据**§6.1
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/api/routes/eval.go` |
| 当前 | `POST /api/v1/eval/run` 返回 400 "at least one query required" |
| 修复-1 | 确认 `eval/run` 需要提交 query 列表才能执行 |
| 修复-2 | 先确保 `eval/generate` 能生成足够多的金标查询(当前只生成 2 条) |
| 修复-3 | 执行 eval 时:对每个金标 query 执行 recall → 检查 expected_id 是否出现在结果中 |
| 指标 | recall_at_5, precision_at_5, mean_reciprocal_rank |
| 验收 | eval/run 返回含 recall_at_5 等指标的完整报告 |
### G5.2 金标集 12 维
**设计依据**§6.1 — 4 类记忆 × 3 个难度等级 = 12 个查询
| 项目 | 内容 |
|------|------|
| 方式 | `POST /api/v1/eval/generate` → LLM 从现有记忆生成 |
| 分类 | `system_fact` / `user_pref` / `proj_context` / `tool_usage` |
| 难度 | 简单(直接关键词)/ 中等(同义改写)/ 困难(推理需要),各 1 条 |
| 当前 | 只生成 2 条(因为记忆分类不全) |
| 修复 | 增强 `eval/generate` 的 LLM prompt确保覆盖 4 类×3 级 = 12 条 |
| 验收 | generate 返回 12 条金标查询,每条含 `query` + `expected_ids` |
### G5.3 V 值反向传播
**设计依据**§6.3
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/selfoptimize/vprop.go` + `go/internal/api/routes/core.go` |
| 当前 | `POST /api/v1/trace/vprop` 返回 429 |
| 修复-1 | 确认 `vprop` 端点和 handler 实现 |
| 修复-2 | V = α·R + (1-αγ·V_{t+1},从最终结果反向传播到上游记忆 |
| 修-3 | trace 存储 + 衰减管理 |
| 验收 | 写入 trace 后调用 vprop → 上游记忆 quality_score 变化 |
---
## Phase G6: 聚类 + 蒸馏深化
> 目标DBSCAN 聚类跑通、蒸馏质量回溯、缺口分类、L2 Patterns 层
> 优先级:中(依赖足够数据,约 100+ 条记忆后可运行)
### G6.1 DBSCAN 聚类
**设计依据**§3.1 深度整合 Step 1
| 项目 | 内容 |
|------|------|
| 文件 | Rust sidecar `rust/src/cluster.rs` |
| 当前 | 代码存在但从未被调用consolidate.timer 条件不满足) |
| 修复-1 | `t_consolidation` 触发器应改为 Go 端直接触发 Rust 调用,而不是等 systemd timer |
| 修复-2 | 通过 Unix Socket IPC 发送 ConsolidateRequest 触发 Rust 执行 DBSCAN |
| 修复-3 | DBSCAN eps/ min_points 参数:默认 eps=0.5, min_points=3可调 |
| 验收 | 聚类发现至少 1 个 cluster当数据量足够时 |
### G6.2 蒸馏质量回溯
**设计依据**§3.1 深度整合 Step 4
| 项目 | 内容 |
|------|------|
| 文件 | Rust sidecar `rust/src/quality_backtrace.rs` |
| 当前 | 代码存在但从未执行 |
| 修复 | 确认 Rust 实现可独立调用,或改为 Go 端直接 LLM 调用 |
| 抽样 | 分层 20 条新鲜5/核心5/有用5/无用5 |
| 方法 | L0→LLM→L1→LLM→L0' → bge-m3 cosine(L0, L0') < 0.8 信息损失过大 |
| 验收 | 质量回溯报告生成 |
### G6.3 知识缺口分类
**设计依据**§3.2.2
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/api/routes/gaps.go` + `gap_repair.go` + `gap_full_repair.go` |
| 当前 | `GET /api/v1/gaps` 返回 0 缺口(从未触发过检测) |
| 修复-1 | 确认 `t_gap` trigger → handler 调用 `detectGaps` → 读取 recall miss 日志 |
| 修复-2 | 连续 3 次 miss → 执行分类Type A (真未知) / B (同义词) / C (召回失败) / D (碎片化) |
| 修复-3 | Type B/C 自动修复 → 验证 → 关闭 |
| 边界 | recall miss 的定义recall 返回 0 条或最高分 < 0.3 |
| 验收 | 搜索不存在的主题 3 次 → `GET /api/v1/gaps` 返回 1 条缺口 |
### G6.4 L2 Patterns 层
**设计依据**§1.2 四层模型
| 项目 | 内容 |
|------|------|
| 当前 | L2 完全不存在 |
| 实现 | 蒸馏 consolidation Step 3 "模式挖掘" 的输出 → 存为 category="pattern" 的记忆 |
| 触发 | 连续 3+ 条同类型蒸馏 → 提取 pattern → 写入 L2 |
| 验收 | 有 category="pattern" 的记忆条目 |
---
## Phase G7: Skill + 自动化流程
> 目标Skill 结晶 (Beta-Bernoulli)、5 个自动化流程端到端打通
> 优先级:低(依赖前面所有阶段完成)
### G7.1 Skill 结晶
**设计依据**§6.5
| 项目 | 内容 |
|------|------|
| 文件 | `go/internal/api/routes/skill_bayes.go` + `tuning.go` |
| 当前 | `POST /api/v1/skills/{name}/trial` 返回 400 |
| 修复-1 | 确认 skill 注册 + trial 记录 + Beta-Bernoulli 更新逻辑 |
| 修复-2 | 资格评估:证据 ≥ 3 次有用反馈 + 无冲突 |
| 修复-3 | η ≥ 0.8 → active, 0.5≤η<0.8 probation, η<0.5 retired |
| 验收 | 创建 skill → 记录 3 次 trial → η 更新 |
### G7.2 5 个自动化流程端到端
**设计依据**§3.6
| 流程 | 当前状态 | 所需组件 |
|------|---------|---------|
| 流程1: commit→图谱 | ⚠️ 半成 | 需冲突自动裁决 + PassiveValidator |
| 流程2: recall→反馈闭环 | ❌ | 需 CO_OCCURS + 预取 + Hermes 自动标记 |
| 流程3: 缺口→关闭 | ❌ | 需缺口分类引擎 |
| 流程4: 修正→级联审查 | ❌ | 需 DEPENDS_ON 追踪 + WebSocket |
| 流程5: 深度整合 | ❌ | 需 DBSCAN + 质量回溯 + 衰减校准 |
**实施策略**
每个流程单独验收,不追求一次全通。验收标准:手动触发该流程的所有步骤 → 观察各步骤按设计执行 → 仪表盘指标更新。
---
## Phase G8: 基础设施加固
> 目标:备份、监控、反向代理、数据迁移
> 优先级:中(运维安全)
### G8.1 备份自动化
**设计依据**§4.3
| 项目 | 内容 |
|------|------|
| 备份目录 | `/backup/zhiyi/`(不存在,需创建) |
| 执行 | `POST /api/v1/admin/backup` 端点存在(返回 200但从未系统调用 |
| 操作 | 创建 systemd timer `zhiyi-backup.timer` 每天 3:00 触发 |
| 保留 | 最近 7 天每天 + 最近 4 周周日 |
| 格式 | `backup-{date}.tar.gz`,含 LanceDB checkpoint + SQLite .backup + Redis BGSAVE |
| 验收 | `/backup/zhiyi/` 存在备份文件 |
### G8.2 Prometheus 告警
**设计依据**§4.3
| 项目 | 内容 |
|------|------|
| 当前 | `deploy/prometheus-alerts.yml` 存在但未加载 |
| 操作 | 检查 `deploy/prometheus.yml` 并部署 prometheus 服务 |
| 告警规则 | > 5 冲突 / p99 > 2s / LLM 调用 > 85% 日限 / consolidate 3 次连续失败 |
| 验收 | prometheus `/metrics` 端点可达,告警规则加载 |
### G8.3 Nginx 反向代理
**设计依据**§4.3
| 项目 | 内容 |
|------|------|
| 操作 | 部署 `deploy/nginx-zhiyi.conf` 到 nginx |
| 路由 | 7821 不可直接访问,通过 nginx 代理(如果牧尘需要) |
| 验收 | 通过 nginx 端口访问织忆正常工作 |
### G8.4 FAISS → LanceDB 迁移
**设计依据**Part 7 + 附录
| 项目 | 内容 |
|------|------|
| 当前 | `~/projects/zhiyi/data/` 下仍有旧 FAISS 数据sbert_index.faiss 等) |
| 操作 | `scripts/migrate_faiss_to_lance.go` 执行迁移 |
| 迁移 | 读取 FAISS 索引 + sbert_docs.jsonl → 批量写入 LanceDB |
| 验证 | 迁移后总数 vs 原 FAISS 数量一致recall 部分抽样一致性 |
| 完成 | 旧数据目录归档,不删除 |
---
## Phase G9: 集成拓展
> 优先级:低(功能完善后的增值项)
### G9.1 Hermes 自动标记 (useful/not-useful)
**设计依据**§5.1
| 项目 | 内容 |
|------|------|
| 位置 | `~/.hermes/plugins/zhiyi/__init__.py` |
| 逻辑 | 任务成功 → 标记 recall 结果为 useful任务失败 → 分析根因 → 标记 |
| 验收 | Hermes 完成任务后自动调用 feedback API |
### G9.2 Obsidian 双向同步
**设计依据**§5.2
| 项目 | 内容 |
|------|------|
| 当前 | `carriers/shared/` 目录存在但为空 |
| 实现 | Go 端 `obsidian.go` + `obsidian_carrier.go` 代码存在但未激活 |
| 操作 | 确认同步逻辑、触发条件、防止冲突覆盖 |
| 验收 | carriers 目录有内容Obsidian 能看到 |
### G9.3 Go SDK + 多实例文档
- 确认 `go/client/sdk.go` 完整
- 多实例方案留作文档,暂不部署
---
## 执行顺序与依赖
```
G1 (Recall管线) ← Hermes 日常使用数据量 → G3 (治理) ← → G4 (共现/WS)
↓ ↓
G2 (Embedding) G5 (评估/V值)
↓ ↓
G6 (聚类/蒸馏深化) ← 数据量 100+ ← G3+G5 数据反馈
G7 (Skill + 自动化流程)
G8 (基础设施) ← 可并行
G9 (集成) ← 可并行
```
**关键路径**G1 → G3 → G6 → G7功能链
**非阻塞**G2独立、G8运维、可并行、G9增值、可并行
---
## 验收标准总表
| Phase | 验收指标 | 检查方式 |
|-------|---------|---------|
| G1 | recall 结果含 MMR 去重 + 图谱扩展 + navigate 双向 BFS | API 测试 |
| G2 | 本地 vLLM BGE 响应 < 200mscosine 0.99 | 端点测试 + 一致性测试 |
| G3 | self-metrics 的 deprecated_per_day > 0, auto_resolve_rate > 0 | GET /metrics/self |
| G4 | recall 返回含 prefetch 字段WS 收到事件 | 端到端测试 |
| G5 | eval/run 返回完整 IR 报告V值可传播 | API 测试 |
| G6 | cluster 发现 ≥1质量回溯报告gap 分类 Type A/B/C/D | API + 日志 |
| G7 | skill η 更新5 个流程可手动触发走通 | 端到端测试 |
| G8 | 每日备份存在prometheus 可访问FAISS 迁移完成 | 文件检查 + curl |
| G9 | Hermes 自动反馈carriers 有内容 | 观察 |
---
## 失忆恢复锚点
如果 session 丢失,按以下顺序恢复:
1. **文件锚**: `~/projects/memoryweave/REPAIR-FULL.md`
2. **进度文件**: `~/projects/memoryweave/IMPLEMENTATION.md`(实时记录当前 Phase 和进度)
3. **记忆锚**: `memory_search("织忆 v3.8 修复计划 当前阶段")`
4. **设计锚**: `~/mc/小唯/07-Wiki/concepts/织忆(MemoryWeave)-v3.8-完整定稿.md`
5. **状态锚**: `curl http://localhost:7821/api/v1/stats`
---
*计划版本 1.0。逐条对照设计文档 v3.8 制定。*

182
REPAIR-PLAN.md Normal file
View File

@ -0,0 +1,182 @@
# 织忆 v3.8 修复计划 — 分阶段执行
> 创建: 2026-05-30 01:30
> 状态: 🔴 未开始
> 当前阶段: F1将要开始
---
## 核心矛盾
代码框架完成度很高12,000+ 行 Go+Rust但**数据流从未真正跑通**。
Hermes 插件没传 `agent_id` → commit 静默失败 → 系统里只有 3 条测试数据。
所有依赖数据的子系统(蒸馏/自优化/治理/评估/Skill/预取)全部处于空转状态。
---
## 阶段总览
| 阶段 | 名称 | 天数 | 当前状态 |
|------|------|------|---------|
| **F1** | 打通端到端管道 | 1天 | 🔴 未开始 |
| **F2** | 存储层归位 + Recall 管线 | 2天 | 🔴 未开始 |
| **F3** | 蒸馏 + 自优化激活 | 2天 | 🔴 未开始 |
| **F4** | 治理 + 图谱修复 | 1天 | 🔴 未开始 |
| **F5** | 竞争性架构激活 | 2天 | 🔴 未开始 |
| **F6** | 部署固化 + 监控告警 | 1天 | 🔴 未开始 |
---
## F1打通端到端管道
### F1.1 修复 Hermes 插件 commit 静默失败
**根因**`~/.hermes/plugins/zhiyi/__init__.py` 的 `commit()` 没传 `agent_id``namespace`
**修复**payload 追加 `"agent_id": "hermes-a06"` + `"namespace": "hermes-main"`
**验证**memory_write → /stats total_memories +1
### F1.2 修复 episodes 存储计数
**根因**commit 创建了 episode + memory但 stats 显示 episodes=0
**修复**:检查 core.go commit handler 的 episodes 表写入路径
### F1.3 插件 recall 参数对齐
**根因**:插件用 `top_k`Go 端用 `limit`
**修复**Go 端 `/api/v1/recall` 同步支持 `top_k``limit`
### F1.4 打通后冒烟测试
- [ ] memory_write 5 条 → total_memories ≥ 5
- [ ] recall "牧尘" 返回 ≥ 1 条
- [ ] /api/v1/graph/stats 节点增加
---
## F2存储层归位 + Recall 管线
### F2.1 切换存储后端到 LanceDB
**现状**`STORAGE_BACKEND=sqlite` — 设计要求 LanceDB
**修复**env 改为 lancedb验证 Rust sidecar 读写SQLite 降级为仅存图谱
### F2.2 Recall 管线逐级修复
- [ ] BGE 编码正常POST /v1/embeddings → 1024维
- [ ] ANN 搜索命中LanceDB HNSW 索引)
- [ ] Rerank 正常(模力方舟 API
- [ ] MMR 去重正常diversity=0.5
- [ ] 搜索缓存工作Redis TTL 1h
### F2.3 知识图谱 query/navigate API 修复
**根因**graph/query 参数不匹配graph/navigate 与设计文档格式不同
**修复**:兼容新旧参数格式,返回对齐设计文档 schema
### F2.4 新增 recall/debug 诊断端点(优化)
**路径**POST /api/v1/recall/debug
**产出**4 步诊断encode / ann_search / rerank / mmr各自延迟和状态
---
## F3蒸馏 + 自优化激活
### F3.1 蒸馏引擎联调
- [ ] commit → trigger distill 触发
- [ ] 硬规则过滤 → LLM 5 维评估 → distilled 写回
- [ ] 知识图谱更新 → PassiveValidator
### F3.2 Rust 蒸馏质量回溯
- [ ] Go ↔ Rust IPC 通信正常
- [ ] quality_backtrace 完成至少 1 次
### F3.3 自优化仪表盘数据填充
- [ ] 7 项指标至少有 5 项 > 0
### F3.4 新增蒸馏手动触发命令(优化)
**路径**POST /api/v1/admin/distill/force
### F3.5 遗忘策略首次运行
- [ ] 衰减扫描覆盖 LanceDB
- [ ] recency_factor 正确计算
---
## F4治理 + 图谱修复
### F4.1 冲突检测激活
- [ ] 两条冲突事实 → /conflicts ≥ 1
- [ ] 自动裁决 / ask_user
### F4.2 知识缺口检测
- [ ] 3 次 miss → /gaps ≥ 1
- [ ] Type A/B/C/D 分类
### F4.3 图谱自动扩展
- [ ] 蒸馏后 → 实体抽取 → 节点/边增长
### F4.4 PassiveValidator
- [ ] 三层匹配P1/P2/P3正常
---
## F5竞争性架构激活
### F5.1 评估框架
- [ ] /eval/generate → 12 个金标查询
- [ ] /eval/run → recall@5 > 0.7
### F5.2 V 值反向传播
- [ ] trace 记录 + V 值计算
### F5.3 Skill 结晶
- [ ] ≥ 1 条 active skill
### F5.4 记忆预取
- [ ] CO_OCCURS 共现统计
- [ ] prefetch map
### F5.5 L3 世界模型升级
- [ ] 蒸馏触发动态更新
### F5.6 触发器退避机制
- [ ] fail_count 递增 → 3 次失败自动停用
---
## F6部署固化 + 监控告警
### F6.1 本地 BGE
- [ ] bge-embed.service 运行
- [ ] recall 延迟 < 200ms
### F6.2 WebSocket Agent 注册
- [ ] Hermes WS 连接
- [ ] 事件推送验证
### F6.3 备份自动化
- [ ] systemd timer 每天 3:00
### F6.4 Prometheus 告警
- [ ] 8 条告警规则加载
### F6.5 Hermes 环境变量
- [ ] ZHIYI_URL 配置
---
## 全局验收清单
| 功能 | 标准 | 阶段 |
|------|------|------|
| memory_write | 5 条写入成功 | F1 |
| memory_search | 返回 ≥ 1 条 | F1 |
| Recall 延迟 | < 200ms | F2 |
| 存储后端 | LanceDB | F2 |
| 蒸馏 | 自动运行 | F3 |
| 自优化仪表盘 | 5/7 有数据 | F3 |
| 图谱 query | 返回节点 | F4 |
| 冲突检测 | 检测+裁决 | F4 |
| 知识缺口 | 检测+关闭 | F4 |
| 评估 | recall@5 > 0.7 | F5 |
| Skill | ≥ 1 active | F5 |
| V 值 | trace+计算 | F5 |
| 预取 | 共现统计 | F5 |
| L3 | 动态更新 | F5 |
| 本地 BGE | recall < 200ms | F6 |
| WS 推送 | Hermes 连接 | F6 |
| 备份 | 每天 3:00 | F6 |
| 告警 | 8 条规则 | F6 |

View File

@ -0,0 +1,150 @@
# 智能记忆路由讨论记录
> 日期2026-05-29
> 状态:讨论中,可能有错误方向,待验证
---
## 问题:织忆系统要智能地提供合适的信息,不是由 Agent 手动操作
### 现状问题
当前 recall 是被动 API
```
Agent 调 memory_search → 返回结果 → Agent 决定用不用
```
本质是"查工具",不是"记忆本能"。
理想态记忆自动出现在该出现的地方Agent 不需要"想起去调 API"。
---
## 第一轮讨论:理想形式
### 第一性原理
**目标不是"信息找到我",而是"知识长在我脑子里"**。
```
最优形式:
"zhiyi-sidecar 用 systemctl --user"
— 这条知识直接成为我说的话,没有"记忆感"
次优:
"根据织忆记录2026-05-29zhiyi-sidecar 的管理方式是..."
避免:
"[记忆1] xxx | [记忆2] yyy"
— 这是在说"我查了记忆"
```
### 设计原则(第一轮)
1. 不打断思维
2. 来源透明但无形
3. 精确优先于全面
4. 可纠错
---
## 第二轮:现实约束(幻觉问题)
### 现实约束
| 约束 | 含义 |
|------|------|
| LLM 幻觉 | 我基于记忆说出断言,无法区分准确还是过时 |
| 可纠错要求 | 说错之后,需要知道是哪条记忆把我带偏 |
| 不确定性边界 | 什么时候该说"不确定"而不是凭记忆推断 |
| 时效性 | 配置改了但记忆没更新,我还在用旧记忆 |
### 可行的设计
**幻觉风险高(事实性/时效性)→ 显式标注**
```
zhiyi-sidecar 用 systemctl --user织忆记录2026-05-29
```
**幻觉风险低(偏好/风格/铁律)→ 尽量无痕**
```
牧尘话少直接,结论先行
```
**模糊关联(语义相似但不精确)→ 标注置信度**
```
可能相关 — "上次修 pagerank 时也有类似报错"
```
### 重新定义"透明"
> 透明不是"让记忆消失",而是"让记忆的可信度一目了然"
---
## 第三轮Architecture 冲突审查
### 发现的冲突点
| 冲突 | 问题 | 风险 |
|------|------|------|
| Knowledge Gap Detection | 2.8 主动感知触发"不确定"→推送记忆,但 gap detection 也监听的犹豫词,形成正反馈循环 | 中 |
| 质量反馈回路 | useful/not-useful 反馈如果影响 recall 排序Agent 可能主动压制某些记忆 | 低 |
| 自进化系统的黑盒性 | muchen 自进化是隐藏基础设施,不应被 Agent 感知 | 高 |
| 两条 commit 路径 | Middleware 和自进化系统都可能在 commit谁主导 | 高 |
### 核心冲突:两条主动感知链路
**现有设计(自进化)**
```
Agent 回答出现"不确定" → 触发 knowledge gap → WebSocket 推送 → 记录为"需要学习"
```
**2.8 新设计(主动感知)**
```
Agent 回答出现"不确定" → Middleware 感知 → 推送相关记忆
```
两者都监听 Agent 输出的犹豫信号。谁优先级更高?
### 边界明确
| 职责 | 自进化系统muchen | 2.8 Middleware |
|------|---------------------|----------------|
| 定位 | 隐藏的基础设施Agent 不可见 | 消息流拦截层Agent 可见 |
| 触发 | 背景recall 连续 miss、反馈质量下降 | 前台query 进来、Agent 输出 |
| 动作 | 蒸馏优化、gap 分类、衰减调整 | 路由、织入、proactive push |
| Proactive | 无直接推送gap → 人工处理) | 直接推送给 Agent |
---
## 结论
### 三级可信度注入
| 等级 | 形式 | 示例 |
|------|------|------|
| L1 无痕融合 | 输出不带"织忆"字样,直接是答案 | 牧尘话少直接 |
| L2 轻量标注 | "zhiyi-sidecar 用...(记忆标注)" | 配置时效性标注 |
| L3 明确提示 | "织忆建议关注:...(置信度 0.6" | 模糊匹配 |
### 关键约束
- L1 要有明确准入标准(不满足就降级),不是所有记忆都无痕
- L2/L3 保证可纠错性
- 自进化系统作为底层引擎,不对 Agent 暴露黑盒存在
- Proactive push 统一到 Middlewaregap detection 只做背景计数
### 待验证
- 这个方向是否比当前"被动 API"有本质提升?
- L1 的准入标准如何量化?
- 对 Hermes Agent 的改造代价有多大?
---
## 下一步
1. 先不修改 DESIGN.md
2. 在 Hermes Agent 侧做小规模实验验证
3. 确认方向正确后再动设计文档

View File

@ -8,11 +8,13 @@ Type=simple
User=muc
WorkingDirectory=/home/muc/projects/memoryweave/go
Environment=PORT=7821
Environment=STORAGE_BACKEND=sqlite
Environment=STORAGE_BACKEND=lancedb
Environment=SQLITE_PATH=/var/lib/memoryweave/memoryweave.db
Environment=API_KEY=zhiyi-dev-key-2026
Environment=VLLM_ENDPOINT=https://ai.gitee.com/v1/embeddings
Environment=MOLIFANG_API_KEY=3TSVVXRFFECE4TISXHGE1VXDAXBIPAP6O1VPJK18
Environment=VLLM_ENDPOINT=http://localhost:8000/v1/embeddings
Environment=LLM_ENDPOINT=http://127.0.0.1:3000/v1/chat/completions
Environment=LLM_MODEL=qwen/qwen3.5-122b-a10b
Environment=LLM_API_KEY=sk-0ExNiLblJvIWBDpkS50fwOBw4MmqLyKdHJK5iQtlw9dOMWBP
Environment=RERANK_ENDPOINT=https://ai.gitee.com/v1
Environment=GRAPH_PATH=/var/lib/memoryweave/graph.db
ExecStartPre=/bin/mkdir -p /var/lib/memoryweave

View File

@ -6,6 +6,7 @@ import (
"fmt"
"math"
"net/http"
"strings"
"time"
"github.com/xiaoxue/memoryweave/internal/models"
@ -179,7 +180,9 @@ func (a *API) Recall(w http.ResponseWriter, r *http.Request) {
var req struct {
Query string `json:"query"`
Limit int `json:"limit"`
TopK int `json:"top_k"` // 兼容插件 top_k 参数
Namespace string `json:"namespace"`
AgentID string `json:"agent_id"` // 用于推导默认 namespace
Diversity float64 `json:"diversity"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -190,12 +193,16 @@ func (a *API) Recall(w http.ResponseWriter, r *http.Request) {
respondError(w, 400, "query required")
return
}
// 兼容 limit 和 top_k
if req.Limit <= 0 && req.TopK > 0 {
req.Limit = req.TopK
}
if req.Limit <= 0 {
req.Limit = 10
}
if req.Namespace == "" {
// 默认搜 agent 自己的 namespace + shared§1.3
req.Namespace = "default-main"
// 按 §1.3 推导 namespace
req.Namespace = deriveNamespace(req.AgentID)
}
results, err := a.Pipeline.Recall(
@ -207,6 +214,118 @@ func (a *API) Recall(w http.ResponseWriter, r *http.Request) {
respond(w, 200, map[string]interface{}{"results": results, "count": len(results)})
}
// POST /api/v1/recall/debug — recall 诊断端点(优化),返回各阶段耗时和状态
func (a *API) RecallDebug(w http.ResponseWriter, r *http.Request) {
var req struct {
Query string `json:"query"`
Limit int `json:"limit"`
TopK int `json:"top_k"`
Namespace string `json:"namespace"`
AgentID string `json:"agent_id"`
Diversity float64 `json:"diversity"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.Query == "" {
respondError(w, 400, "query required")
return
}
if req.Limit <= 0 && req.TopK > 0 {
req.Limit = req.TopK
}
if req.Limit <= 0 {
req.Limit = 10
}
if req.Namespace == "" {
req.Namespace = deriveNamespace(req.AgentID)
}
type stepInfo struct {
Step string `json:"step"`
Status string `json:"status"`
LatencyMs int64 `json:"latency_ms"`
Detail string `json:"detail,omitempty"`
}
var steps []stepInfo
var totalStart = time.Now()
// Step 1: 编码
t1 := time.Now()
queryVec, err := a.Embedder.EncodeSingle(req.Query)
encodeTime := time.Since(t1).Milliseconds()
if err != nil {
steps = append(steps, stepInfo{"encode", "error", encodeTime, err.Error()})
respond(w, 200, map[string]interface{}{"steps": steps, "results": []interface{}{}, "total_ms": time.Since(totalStart).Milliseconds()})
return
}
steps = append(steps, stepInfo{"encode", "ok", encodeTime, fmt.Sprintf("dim=%d", len(queryVec))})
// Step 2: ANN 搜索
t2 := time.Now()
nsList := []string{req.Namespace}
if req.Namespace != "shared" {
nsList = append(nsList, "shared")
}
var candidates []models.MemoryRecord
for _, ns := range nsList {
r, err := a.LanceDB.Search("memories", queryVec, 50, ns)
if err == nil {
candidates = append(candidates, r...)
}
}
annTime := time.Since(t2).Milliseconds()
if len(candidates) == 0 {
steps = append(steps, stepInfo{"ann_search", "ok", annTime, "0 candidates"})
respond(w, 200, map[string]interface{}{"steps": steps, "results": []interface{}{}, "total_ms": time.Since(totalStart).Milliseconds()})
return
}
steps = append(steps, stepInfo{"ann_search", "ok", annTime, fmt.Sprintf("%d candidates from %d namespaces", len(candidates), len(nsList))})
// Step 3: Rerank
t3 := time.Now()
docs := make([]string, len(candidates))
for i, c := range candidates {
docs[i] = c.Content
}
reranked, rErr := a.Reranker.Rerank(req.Query, docs, req.Limit*2)
rerankTime := time.Since(t3).Milliseconds()
if rErr != nil {
steps = append(steps, stepInfo{"rerank", "fallback", rerankTime, rErr.Error()})
} else {
steps = append(steps, stepInfo{"rerank", "ok", rerankTime, fmt.Sprintf("top_score=%.4f", reranked[0].Score)})
}
// Step 4: MMR
t4 := time.Now()
finalIndices := storage.MMRSelect(reranked, candidates, req.Limit*2, req.Diversity)
mmrTime := time.Since(t4).Milliseconds()
steps = append(steps, stepInfo{"mmr", "ok", mmrTime, fmt.Sprintf("diversity=%.1f, final=%d", req.Diversity, len(finalIndices))})
results := make([]models.RecallResult, 0, req.Limit)
for i, idx := range finalIndices {
if i >= req.Limit { break }
if idx < len(candidates) {
c := candidates[idx]
score := 0.0
for _, r := range reranked {
if r.Index == idx { score = r.Score; break }
}
results = append(results, models.RecallResult{
ID: c.ID, Content: c.Content, Category: c.Category,
Score: score, Timestamp: c.CreatedAt.Format(time.RFC3339),
})
}
}
totalTime := time.Since(totalStart).Milliseconds()
respond(w, 200, map[string]interface{}{
"query": req.Query, "steps": steps, "results": results,
"count": len(results), "total_ms": totalTime,
})
}
// GET /api/v1/bootstrap
func (a *API) Bootstrap(w http.ResponseWriter, r *http.Request) {
agentID := r.URL.Query().Get("agent_id")
@ -364,6 +483,21 @@ var sharedKeywords = []string{
"设计文档", "技术栈", "项目路径", "项目结构",
}
// deriveNamespace 按 §1.3 从 agent_id 推导 namespace
// hermes-a06 → hermes-main, openclaw → openclaw-main, 默认 → default-main
func deriveNamespace(agentID string) string {
if agentID == "" {
return "default-main"
}
// 处理 hermes-a06 / hermes-bot / openclaw 等常见 agent ID
for _, prefix := range []string{"hermes", "openclaw"} {
if strings.HasPrefix(agentID, prefix) {
return prefix + "-main"
}
}
return agentID + "-main"
}
func classifyNamespace(category, agentID, content string) string {
// 如果显式传入 namespace 且非空,信任调用方
// (此函数仅在 namespace 为空时调用)
@ -380,11 +514,8 @@ func classifyNamespace(category, agentID, content string) string {
}
}
// 规则3默认归 agent 私有 → {agent}-main
if agentID == "" {
return "default-main"
}
return agentID + "-main"
// 规则3默认归 agent 私有 → deriveNamespace 推导
return deriveNamespace(agentID)
}
func stringContains(s, substr string) bool {

View File

@ -29,16 +29,36 @@ func (ga *GraphAPI) Stats(w http.ResponseWriter, r *http.Request) {
}
// POST /api/v1/graph/query
// 设计文档 §2.5.4: match 格式兼容; 同时保留 entity+relation 简洁格式
func (ga *GraphAPI) Query(w http.ResponseWriter, r *http.Request) {
var req struct {
Entity string `json:"entity"`
Relation string `json:"relation"`
Namespace string `json:"namespace"`
// 设计文档兼容格式: {"match": {"type": "entity", "label": "..."}}
Match *struct {
Type string `json:"type"`
Label string `json:"label"`
} `json:"match"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
// 兼容设计文档 match 格式
if req.Match != nil && req.Entity == "" {
// match.type 过滤 node type, match.label 做 name 模糊搜索
var nodes []map[string]interface{}
if req.Match.Label != "" {
nodes = ga.Graph.SearchNodes(req.Match.Label, req.Namespace)
} else if req.Match.Type != "" {
nodes = ga.Graph.ListNodesByType(req.Match.Type, req.Namespace)
} else {
nodes = ga.Graph.ListNodes(req.Namespace)
}
respond(w, 200, map[string]interface{}{"nodes": nodes, "count": len(nodes)})
return
}
// 默认跨 namespace 搜索(空 = 匹配所有§2.5.6
results := ga.Graph.Query(normalizeEntity(req.Entity), req.Relation, req.Namespace)
respond(w, 200, map[string]interface{}{"results": results, "count": len(results)})

View File

@ -4,6 +4,7 @@ package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
@ -135,13 +136,20 @@ func NewServer() http.Handler {
Content: input.Content, Facts: result.Facts,
Entities: entityNames, Namespace: input.Namespace,
})
existing := make([]map[string]interface{}, 0)
for _, c := range conflictDetector.Scan(input.Content, entityNames, existing) {
// 冲突检测:加载同 namespace 已有记忆进行比较
zeroVec := make([]float32, 1024)
existingMems, _ := ldb.Search("memories", zeroVec, 100, input.Namespace)
conflictExisting := make([]map[string]interface{}, 0)
for _, m := range existingMems {
conflictExisting = append(conflictExisting, map[string]interface{}{
"content": m.Content,
"entities": entityNames, // 用当前内容提取的实体做交叉比较
})
}
for _, c := range conflictDetector.Scan(input.Content, entityNames, conflictExisting) {
if c.Strategy == "latest_wins" { conflictDetector.AutoResolve(c) }
}
// 被动验证:加载同 namespace 已有记忆进行 P1/P2/P3 匹配
zeroVec := make([]float32, 1024)
existingMems, _ := ldb.Search("memories", zeroVec, 50, input.Namespace)
// 被动验证:使用已加载的记忆进行 P1/P2/P3 匹配
validMems := make([]selfoptimize.MemoryForValidation, len(existingMems))
for i, m := range existingMems {
validMems[i] = selfoptimize.MemoryForValidation{
@ -200,6 +208,7 @@ func NewServer() http.Handler {
}()
})
mux.HandleFunc("/api/v1/recall", api.Recall)
mux.HandleFunc("/api/v1/recall/debug", api.RecallDebug)
mux.HandleFunc("/api/v1/bootstrap", api.Bootstrap)
mux.HandleFunc("/api/v1/stats", api.Stats)
mux.HandleFunc("/api/v1/batch-commit", api.BatchCommit)
@ -441,6 +450,28 @@ func NewServer() http.Handler {
"decay_rate": AgentTypeDecayOrDefault(forgetter.AgentType()),
})
})
mux.HandleFunc("/api/v1/admin/distill/force", func(w http.ResponseWriter, r *http.Request) {
if routes.DistillEngineRef == nil {
respondJSON(w, 400, map[string]string{"error": "distill engine not initialized"})
return
}
var req struct {
Content string `json:"content"`
Category string `json:"category"`
Namespace string `json:"namespace"`
AgentID string `json:"agent_id"`
}
json.NewDecoder(r.Body).Decode(&req)
if req.Content == "" {
respondJSON(w, 400, map[string]string{"error": "content required"})
return
}
if req.Namespace == "" { req.Namespace = "hermes-main" }
if req.AgentID == "" { req.AgentID = "hermes-a06" }
epID := fmt.Sprintf("ep_manual_%d", time.Now().UnixNano())
routes.AutoDistillTrigger(epID, req.Content, req.Category, req.Namespace, req.AgentID)
respondJSON(w, 200, map[string]string{"status": "queued", "episode_id": epID})
})
mux.HandleFunc("/api/v1/distilled/{id}", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "DELETE" {
adminAPI.DeleteDistilled(w, r)

View File

@ -10,6 +10,7 @@ import (
"io"
"log"
"net/http"
"strings"
"sync"
"time"
)
@ -167,23 +168,31 @@ func (e *Engine) flush() {
func (e *Engine) distillOne(input DistillInput) DistillResult {
// 如果 LLM 端点不可用,降级
if e.LLMEndpoint == "" {
log.Printf("[distill] LLMEndpoint empty, fallback for %s", input.EpisodeID)
return fallbackSingle(input)
}
// LLM 5维评估
score, err := e.callLLM5D(input.Content)
if err != nil {
log.Printf("[distill] callLLM5D err for %s: %v", input.EpisodeID, err)
return fallbackSingle(input)
}
overall := score.IS*Weights.IS +
score.SU*Weights.SU +
score.PA*Weights.PA +
score.VD*Weights.VD +
score.RU*Weights.RU
log.Printf("[distill] score for %s: overall=%.3f is=%.2f su=%.2f pa=%.2f vd=%.2f ru=%.2f",
input.EpisodeID, overall, score.IS, score.SU, score.PA, score.VD, score.RU)
if overall < 0.7 && score.VD < 0.8 {
// 达不到阈值,跳过
log.Printf("[distill] score below threshold for %s, skip LLM entity extraction", input.EpisodeID)
// 即使阈值未通过仍然用启发式提取实体§3.1 降级策略)
facts, entities := e.extractFacts(input.Content)
if len(entities) > 0 {
log.Printf("[distill] heuristic entities for %s: %d entities", input.EpisodeID, len(entities))
return DistillResult{Facts: facts, Entities: entities, Score5D: score, Overall: overall}
}
return DistillResult{}
}
@ -265,19 +274,139 @@ func (e *Engine) callLLM5D(content string) (FiveDScore, error) {
return score, nil
}
// extractFacts 从内容中提取事实和实体
// extractFacts 从内容中提取事实和实体(关键词 + 命名实体启发式)
func (e *Engine) extractFacts(content string) ([]string, []Entity) {
var facts []string
var entities []Entity
// 降级: 关键词提取
if len(content) > 20 {
facts = append(facts, truncate(content, 200))
}
// 启发式实体提取:提取大写单词、数字组合、中文命名实体
words := strings.Fields(content)
seen := make(map[string]bool)
for _, w := range words {
w = strings.Trim(w, ",.;:!?,。;:!?、\"'()[]【】")
if len(w) < 2 {
continue
}
// 大写字母开头英文命名实体Hermes, ComfyUI, Redis 等)
runes := []rune(w)
if len(runes) >= 2 && runes[0] >= 'A' && runes[0] <= 'Z' {
normalized := strings.ToLower(w)
if !seen[normalized] && !isStopWord(normalized) {
seen[normalized] = true
entities = append(entities, Entity{
Name: w, Type: "entity", Properties: []string{"extracted"},
})
}
}
// 中文实体2-20 个纯中文字符,不含标点)
cleanChinese := stripNonChinese(w)
if len(cleanChinese) >= 2 && len(cleanChinese) <= 20 {
if !seen[cleanChinese] {
seen[cleanChinese] = true
isTech := containsAny(cleanChinese, []string{"端口", "配置", "系统", "内存", "显卡", "服务", "记忆", "织忆", "飞书", "版本", "模型", "工具", "项目", "安装", "部署", "目录", "路径", "开发", "语言"})
etype := "entity"
if isTech { etype = "fact" }
entities = append(entities, Entity{
Name: cleanChinese, Type: etype, Properties: []string{"extracted"},
})
}
}
// 数字/字母组合或纯数字(如 3050, 4GB, 8188, 7821
if isTechToken(w) || isAllDigits(w) {
if !seen[w] {
seen[w] = true
entities = append(entities, Entity{
Name: w, Type: "entity", Properties: []string{"technical"},
})
}
}
}
return facts, entities
}
// isChineseSequence 判断是否为连续的中文字符串(长度在 minLen 到 maxLen 之间)
func isChineseSequence(s string, minLen, maxLen int) bool {
runes := []rune(s)
if len(runes) < minLen || len(runes) > maxLen {
return false
}
for _, r := range runes {
if r < 0x4E00 || r > 0x9FFF {
return false
}
}
return true
}
// isTechToken 判断是否为技术标记(包含数字+字母组合)
func isTechToken(s string) bool {
hasDigit := false
hasLetter := false
for _, r := range s {
if r >= '0' && r <= '9' { hasDigit = true }
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { hasLetter = true }
}
return hasDigit && hasLetter
}
// containsAny 判断字符串是否包含任意一个子串
func containsAny(s string, subs []string) bool {
for _, sub := range subs {
if strings.Contains(s, sub) {
return true
}
}
return false
}
// isStopWord 判断是否为停用词
func isStopWord(w string) bool {
stops := map[string]bool{
"the": true, "a": true, "an": true, "is": true, "are": true,
"was": true, "were": true, "be": true, "been": true, "being": true,
"have": true, "has": true, "had": true, "do": true, "does": true,
"did": true, "will": true, "would": true, "could": true, "should": true,
"may": true, "might": true, "can": true, "shall": true,
"this": true, "that": true, "these": true, "those": true,
"it": true, "its": true, "they": true, "them": true, "their": true,
"we": true, "our": true, "you": true, "your": true, "he": true, "she": true,
"his": true, "her": true, "and": true, "or": true, "but": true,
"not": true, "no": true, "if": true, "then": true, "else": true,
"for": true, "with": true, "as": true, "at": true, "by": true,
"from": true, "in": true, "of": true, "on": true, "to": true, "up": true,
}
return stops[w]
}
// stripNonChinese 只保留中文字符
func stripNonChinese(s string) string {
var buf []rune
for _, r := range s {
if r >= 0x4E00 && r <= 0x9FFF {
buf = append(buf, r)
}
}
return string(buf)
}
// isAllDigits 判断是否为纯数字
func isAllDigits(s string) bool {
if len(s) == 0 {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// checkDailyLimit 每日限额检查
func (e *Engine) checkDailyLimit() {
now := time.Now()

View File

@ -557,3 +557,45 @@ func (fg *FileGraph) EvidenceCount(entity string) int {
func (fg *FileGraph) Save() error {
return fg.save()
}
// SearchNodes 按 label 模糊搜索节点
func (fg *FileGraph) SearchNodes(label, namespace string) []map[string]interface{} {
fg.mu.RLock()
defer fg.mu.RUnlock()
var out []map[string]interface{}
for _, n := range fg.nodes {
if namespace != "" && n.Namespace != namespace {
continue
}
if searchSubstring(n.Name, label) {
out = append(out, map[string]interface{}{
"id": n.ID, "name": n.Name, "type": n.Type, "namespace": n.Namespace,
})
}
}
return out
}
// ListNodesByType 按 type 列出节点
func (fg *FileGraph) ListNodesByType(nodeType, namespace string) []map[string]interface{} {
fg.mu.RLock()
defer fg.mu.RUnlock()
var out []map[string]interface{}
for _, n := range fg.nodes {
if namespace != "" && n.Namespace != namespace {
continue
}
if nodeType != "" && n.Type != nodeType {
continue
}
out = append(out, map[string]interface{}{
"id": n.ID, "name": n.Name, "type": n.Type, "namespace": n.Namespace,
})
}
return out
}
// ListNodes 列出所有节点
func (fg *FileGraph) ListNodes(namespace string) []map[string]interface{} {
return fg.ListNodesByType("", namespace)
}

View File

@ -272,3 +272,54 @@ func (g *InMemoryGraph) GetGraph(namespace string) ([]map[string]interface{}, []
}
return nodes, edges
}
// SearchNodes 按 label 模糊搜索节点
func (g *InMemoryGraph) SearchNodes(label, namespace string) []map[string]interface{} {
g.mu.RLock()
defer g.mu.RUnlock()
var out []map[string]interface{}
for _, n := range g.nodes {
if namespace != "" && n.Namespace != namespace {
continue
}
if searchSubstring(n.Name, label) {
out = append(out, map[string]interface{}{
"id": n.ID, "name": n.Name, "type": n.Type, "namespace": n.Namespace,
})
}
}
return out
}
// ListNodesByType 按 type 列出节点
func (g *InMemoryGraph) ListNodesByType(nodeType, namespace string) []map[string]interface{} {
g.mu.RLock()
defer g.mu.RUnlock()
var out []map[string]interface{}
for _, n := range g.nodes {
if namespace != "" && n.Namespace != namespace {
continue
}
if nodeType != "" && n.Type != nodeType {
continue
}
out = append(out, map[string]interface{}{
"id": n.ID, "name": n.Name, "type": n.Type, "namespace": n.Namespace,
})
}
return out
}
// ListNodes 列出所有节点
func (g *InMemoryGraph) ListNodes(namespace string) []map[string]interface{} {
return g.ListNodesByType("", namespace)
}
func searchSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

View File

@ -262,6 +262,47 @@ func (gs *SQLiteGraphStore) Prune(minWeight float64) {
execSQL(gs.db, `DELETE FROM graph_nodes WHERE id NOT IN (SELECT DISTINCT source FROM graph_edges UNION SELECT DISTINCT target FROM graph_edges)`)
}
func (gs *SQLiteGraphStore) GetGraph(namespace string) ([]map[string]interface{}, []map[string]interface{}) {
nodes := []map[string]interface{}{}
edges := []map[string]interface{}{}
var nodeSQL string
if namespace == "" || namespace == "all" {
nodeSQL = "SELECT id, name, type, namespace, properties FROM graph_nodes"
} else {
nodeSQL = "SELECT id, name, type, namespace, properties FROM graph_nodes WHERE namespace = '" + escape(namespace) + "'"
}
rows := queryRows(gs.db, nodeSQL)
for _, n := range rows {
props := n["properties"].(string)
var properties map[string]interface{}
if props != "" {
json.Unmarshal([]byte(props), &properties)
}
nodes = append(nodes, map[string]interface{}{
"id": n["id"], "name": n["name"], "type": n["type"],
"namespace": n["namespace"], "properties": properties,
})
}
var edgeSQL string
if namespace == "" || namespace == "all" {
edgeSQL = "SELECT id, source, target, relation, weight, namespace FROM graph_edges"
} else {
edgeSQL = "SELECT id, source, target, relation, weight, namespace FROM graph_edges WHERE namespace = '" + escape(namespace) + "'"
}
edgeRows := queryRows(gs.db, edgeSQL)
for _, e := range edgeRows {
edges = append(edges, map[string]interface{}{
"id": e["id"], "source": e["source"], "target": e["target"],
"relation": e["relation"], "weight": e["weight"], "namespace": e["namespace"],
})
}
return nodes, edges
}
func (gs *SQLiteGraphStore) ExpandFromResults(results []models.RecallResult, namespace string, maxHops int) []models.RecallResult {
// 从 recall 结果提取实体,展开图谱邻居
expanded := make([]models.RecallResult, len(results))
@ -279,6 +320,47 @@ func (gs *SQLiteGraphStore) ExpandFromResults(results []models.RecallResult, nam
return expanded
}
// SearchNodes 按 label 模糊搜索节点§2.5.4 match 格式兼容)
func (gs *SQLiteGraphStore) SearchNodes(label, namespace string) []map[string]interface{} {
gs.mu.RLock()
defer gs.mu.RUnlock()
nsClause := "1=1"
if namespace != "" {
nsClause = fmt.Sprintf("namespace = '%s'", escape(namespace))
}
sql := fmt.Sprintf(
"SELECT id, name, type, namespace, pagerank FROM graph_nodes WHERE name LIKE '%%%s%%' AND %s LIMIT 100",
escape(label), nsClause)
return queryRows(gs.db, sql)
}
// ListNodesByType 按 type 列出节点
func (gs *SQLiteGraphStore) ListNodesByType(nodeType, namespace string) []map[string]interface{} {
gs.mu.RLock()
defer gs.mu.RUnlock()
nsClause := "1=1"
if namespace != "" {
nsClause = fmt.Sprintf("namespace = '%s'", escape(namespace))
}
sql := fmt.Sprintf(
"SELECT id, name, type, namespace, pagerank FROM graph_nodes WHERE type = '%s' AND %s ORDER BY pagerank DESC LIMIT 100",
escape(nodeType), nsClause)
return queryRows(gs.db, sql)
}
// ListNodes 列出所有节点
func (gs *SQLiteGraphStore) ListNodes(namespace string) []map[string]interface{} {
gs.mu.RLock()
defer gs.mu.RUnlock()
nsClause := "1=1"
if namespace != "" {
nsClause = fmt.Sprintf("namespace = '%s'", escape(namespace))
}
sql := fmt.Sprintf(
"SELECT id, name, type, namespace, pagerank FROM graph_nodes WHERE %s ORDER BY pagerank DESC LIMIT 100", nsClause)
return queryRows(gs.db, sql)
}
func (gs *SQLiteGraphStore) PageRank(damping float64, iterations int) map[string]float64 {
gs.mu.RLock()
defer gs.mu.RUnlock()
@ -439,54 +521,3 @@ func escape(s string) string {
}
return result
}
// GetGraph 返回指定 namespace 的所有节点和边(供可视化用)
func (gs *SQLiteGraphStore) GetGraph(namespace string) ([]map[string]interface{}, []map[string]interface{}) {
nodes := []map[string]interface{}{}
edges := []map[string]interface{}{}
// 根据 namespace 构建查询(使用 escape() 防注入)
var nodeSQL string
if namespace == "" || namespace == "all" {
nodeSQL = "SELECT id, name, type, namespace, properties FROM graph_nodes"
} else {
nodeSQL = "SELECT id, name, type, namespace, properties FROM graph_nodes WHERE namespace = '" + escape(namespace) + "'"
}
rows := queryRows(gs.db, nodeSQL)
for _, n := range rows {
props := n["properties"].(string)
var properties map[string]interface{}
if props != "" {
json.Unmarshal([]byte(props), &properties)
}
nodes = append(nodes, map[string]interface{}{
"id": n["id"],
"name": n["name"],
"type": n["type"],
"namespace": n["namespace"],
"properties": properties,
})
}
var edgeSQL string
if namespace == "" || namespace == "all" {
edgeSQL = "SELECT id, source, target, relation, weight, namespace FROM graph_edges"
} else {
edgeSQL = "SELECT id, source, target, relation, weight, namespace FROM graph_edges WHERE namespace = '" + escape(namespace) + "'"
}
edgeRows := queryRows(gs.db, edgeSQL)
for _, e := range edgeRows {
edges = append(edges, map[string]interface{}{
"id": e["id"],
"source": e["source"],
"target": e["target"],
"relation": e["relation"],
"weight": e["weight"],
"namespace": e["namespace"],
})
}
return nodes, edges
}

View File

@ -16,6 +16,11 @@ type GraphStore interface {
NavigateBiDir(source, target string, maxHops int, namespace string) ([]map[string]interface{}, error)
Query(entity, relation, namespace string) []map[string]interface{}
// 图节点搜索§2.5.4 match 格式兼容)
SearchNodes(label string, namespace string) []map[string]interface{}
ListNodesByType(nodeType string, namespace string) []map[string]interface{}
ListNodes(namespace string) []map[string]interface{}
// 统计与维护
Stats() (nodeCount, edgeCount int, density float64)
Prune(minWeight float64)

View File

@ -207,6 +207,10 @@ func (rc *RustLanceDBClient) Stats() (map[string]interface{}, error) {
var s map[string]interface{}
json.Unmarshal([]byte(resp.ReportJSON), &s)
s["backend"] = "lancedb (Rust IPC)"
// Rust sidecar 只跟踪 memories/tombstonesepisodes 在本地内存
_local.mu.RLock()
s["total_episodes"] = len(_local.episodes)
_local.mu.RUnlock()
return s, nil
}

View File

@ -104,7 +104,7 @@ func (p *RecallPipeline) Recall(query, namespace string, topK int, diversity flo
}
// Step 4: MMR diversity
finalIndices := mmrSelect(reranked, candidates, topK*2, diversity)
finalIndices := MMRSelect(reranked, candidates, topK*2, diversity)
results := make([]models.RecallResult, 0, topK)
for i, idx := range finalIndices {
@ -189,7 +189,8 @@ func (p *RecallPipeline) Recall(query, namespace string, topK int, diversity flo
return results, nil
}
func mmrSelect(reranked []models.RerankResult, candidates []models.MemoryRecord, k int, lambda float64) []int {
// MMRSelect 多样性去重 — 开放给 recall/debug 诊断端点
func MMRSelect(reranked []models.RerankResult, candidates []models.MemoryRecord, k int, lambda float64) []int {
if len(reranked) == 0 {
return nil
}