From f6f4c832fb2b2ad77bfb58fc07efb06e1042f0cc Mon Sep 17 00:00:00 2001 From: xiaowei Date: Sat, 30 May 2026 02:01:11 +0800 Subject: [PATCH] =?UTF-8?q?F1-F4:=20=E7=AB=AF=E5=88=B0=E7=AB=AF=E7=AE=A1?= =?UTF-8?q?=E9=81=93=E4=BF=AE=E5=A4=8D=20+=20=E8=92=B8=E9=A6=8F=E6=BF=80?= =?UTF-8?q?=E6=B4=BB=20+=20=E6=B2=BB=E7=90=86=E5=9F=BA=E7=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复内容: - 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 全量修复计划 --- IMPLEMENTATION.md | 348 ++---------- REPAIR-FULL.md | 530 ++++++++++++++++++ REPAIR-PLAN.md | 182 ++++++ .../智能记忆路由讨论-2026-05-29.md | 150 +++++ deploy/zhiyid.service | 8 +- go/internal/api/routes/core.go | 145 ++++- go/internal/api/routes/graph.go | 20 + go/internal/api/server.go | 41 +- go/internal/distill/engine.go | 141 ++++- go/internal/governance/graph_file.go | 42 ++ go/internal/governance/graph_mem.go | 51 ++ go/internal/governance/graph_sqlite.go | 133 +++-- go/internal/governance/graph_store.go | 5 + go/internal/storage/lancedb_ipc.go | 4 + go/internal/storage/recall.go | 5 +- 15 files changed, 1427 insertions(+), 378 deletions(-) create mode 100644 REPAIR-FULL.md create mode 100644 REPAIR-PLAN.md create mode 100644 carriers/shared/design-notes/智能记忆路由讨论-2026-05-29.md diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index fd87624..80abd41 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -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: 核心 API(Day 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.10(Rust) | 与 M2.1-M2.8(Go)并行 | -| 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 调用 vLLM,Rust 仅用 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* diff --git a/REPAIR-FULL.md b/REPAIR-FULL.md new file mode 100644 index 0000000..9c0dc1a --- /dev/null +++ b/REPAIR-FULL.md @@ -0,0 +1,530 @@ +# 织忆 v3.8 — 完整修复计划 + +> 版本:1.0 | 2026-05-30 +> 设计文档:`~/mc/小唯/07-Wiki/concepts/织忆(MemoryWeave)-v3.8-完整定稿.md` +> 项目路径:`~/projects/memoryweave/` +> 作战模式:我(规划+验收)→ opencode(执行)→ openclaw(UAT) +> 禁止偷懒,禁止更改设计语言,逐条对照设计方案实施 + +--- + +## 当前状态基线 + +``` +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` | +| 一致性检查 | 用同一段文本分别请求模力方舟和本地 vLLM,cosine_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 响应 < 200ms,cosine ≥ 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 制定。* diff --git a/REPAIR-PLAN.md b/REPAIR-PLAN.md new file mode 100644 index 0000000..701d940 --- /dev/null +++ b/REPAIR-PLAN.md @@ -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 | diff --git a/carriers/shared/design-notes/智能记忆路由讨论-2026-05-29.md b/carriers/shared/design-notes/智能记忆路由讨论-2026-05-29.md new file mode 100644 index 0000000..f2db663 --- /dev/null +++ b/carriers/shared/design-notes/智能记忆路由讨论-2026-05-29.md @@ -0,0 +1,150 @@ +# 智能记忆路由讨论记录 + +> 日期:2026-05-29 +> 状态:讨论中,可能有错误方向,待验证 + +--- + +## 问题:织忆系统要智能地提供合适的信息,不是由 Agent 手动操作 + +### 现状问题 + +当前 recall 是被动 API: +``` +Agent 调 memory_search → 返回结果 → Agent 决定用不用 +``` + +本质是"查工具",不是"记忆本能"。 + +理想态:记忆自动出现在该出现的地方,Agent 不需要"想起去调 API"。 + +--- + +## 第一轮讨论:理想形式 + +### 第一性原理 + +**目标不是"信息找到我",而是"知识长在我脑子里"**。 + +``` +最优形式: + "zhiyi-sidecar 用 systemctl --user" + — 这条知识直接成为我说的话,没有"记忆感" + +次优: + "根据织忆记录(2026-05-29),zhiyi-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 统一到 Middleware,gap detection 只做背景计数 + +### 待验证 + +- 这个方向是否比当前"被动 API"有本质提升? +- L1 的准入标准如何量化? +- 对 Hermes Agent 的改造代价有多大? + +--- + +## 下一步 + +1. 先不修改 DESIGN.md +2. 在 Hermes Agent 侧做小规模实验验证 +3. 确认方向正确后再动设计文档 \ No newline at end of file diff --git a/deploy/zhiyid.service b/deploy/zhiyid.service index 4ed3ac7..355ac7a 100644 --- a/deploy/zhiyid.service +++ b/deploy/zhiyid.service @@ -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 diff --git a/go/internal/api/routes/core.go b/go/internal/api/routes/core.go index c9fa556..8018d0e 100644 --- a/go/internal/api/routes/core.go +++ b/go/internal/api/routes/core.go @@ -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 { diff --git a/go/internal/api/routes/graph.go b/go/internal/api/routes/graph.go index a98db58..69aef3d 100644 --- a/go/internal/api/routes/graph.go +++ b/go/internal/api/routes/graph.go @@ -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)}) diff --git a/go/internal/api/server.go b/go/internal/api/server.go index dad3361..7b42112 100644 --- a/go/internal/api/server.go +++ b/go/internal/api/server.go @@ -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) diff --git a/go/internal/distill/engine.go b/go/internal/distill/engine.go index a56f56e..88ee80a 100644 --- a/go/internal/distill/engine.go +++ b/go/internal/distill/engine.go @@ -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() diff --git a/go/internal/governance/graph_file.go b/go/internal/governance/graph_file.go index f98343b..f398890 100644 --- a/go/internal/governance/graph_file.go +++ b/go/internal/governance/graph_file.go @@ -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) +} diff --git a/go/internal/governance/graph_mem.go b/go/internal/governance/graph_mem.go index c26adad..cd5651f 100644 --- a/go/internal/governance/graph_mem.go +++ b/go/internal/governance/graph_mem.go @@ -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 +} diff --git a/go/internal/governance/graph_sqlite.go b/go/internal/governance/graph_sqlite.go index 1430773..5b6d58f 100644 --- a/go/internal/governance/graph_sqlite.go +++ b/go/internal/governance/graph_sqlite.go @@ -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 -} diff --git a/go/internal/governance/graph_store.go b/go/internal/governance/graph_store.go index a174b53..1d42f79 100644 --- a/go/internal/governance/graph_store.go +++ b/go/internal/governance/graph_store.go @@ -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) diff --git a/go/internal/storage/lancedb_ipc.go b/go/internal/storage/lancedb_ipc.go index 56e899e..9c954df 100644 --- a/go/internal/storage/lancedb_ipc.go +++ b/go/internal/storage/lancedb_ipc.go @@ -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/tombstones,episodes 在本地内存 + _local.mu.RLock() + s["total_episodes"] = len(_local.episodes) + _local.mu.RUnlock() return s, nil } diff --git a/go/internal/storage/recall.go b/go/internal/storage/recall.go index c8d8594..ce9c361 100644 --- a/go/internal/storage/recall.go +++ b/go/internal/storage/recall.go @@ -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 }