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:
parent
34329ddabb
commit
f6f4c832fb
|
|
@ -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*
|
||||
|
|
|
|||
|
|
@ -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 制定。*
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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. 确认方向正确后再动设计文档
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue