feat: Prometheus 监控集成 — /metrics 端点 + 14 指标 + 8 告警规则

- 新增 internal/metrics/metrics.go: 14 个 Prometheus 指标
- server.go: /metrics 替换为 promhttp, 15s 后台同步
- go.mod: 新增 client_golang v1.23.2
- deploy/prometheus.yml: 抓取配置 (zhiyid:7821)
- deploy/prometheus-alerts.yml: 8 条告警规则
- deploy/prometheus.service: systemd unit
This commit is contained in:
xiaowei 2026-05-29 02:51:46 +08:00
parent 08848607fd
commit 648e469dc9
15 changed files with 641 additions and 123 deletions

186
DESIGN.md
View File

@ -1286,84 +1286,128 @@ Agent recall "Docker"
- 两个静态二进制 vs venv+pip+torch → 运维简化 - 两个静态二进制 vs venv+pip+torch → 运维简化
- Candle/ort 的推理不需要 Python/CUDA 依赖 - Candle/ort 的推理不需要 Python/CUDA 依赖
### 7.2 Go 项目结构 ### 7.2 实际项目结构v3.8 实现)
> 路径:`~/projects/memoryweave/`(设计阶段使用 `zhiyi-go`/`zhiyi-rust` 作为独立仓库名,实现时统一为 monorepo
``` ```
~/projects/zhiyi-go/ ~/projects/memoryweave/
├── cmd/zhiyid/main.go ├── DESIGN.md # 本设计文档
├── internal/ ├── IMPLEMENTATION.md # 实施日志
│ ├── api/ ├── BENCHMARK.md # 性能基准
│ │ ├── server.go # HTTP/WS 服务器 ├── README.md
│ │ ├── middleware/ ├── VERSION
│ │ │ ├── auth.go # X-API-Key 认证 ├── Makefile
│ │ │ └── ratelimit.go # per-agent 令牌桶 ├── .github/workflows/ci.yml
│ │ └── routes/ ├── go/ # Go API 核心(独立模块)
│ │ ├── commit.go, recall.go, conflicts.go │ ├── go.mod / go.sum
│ │ ├── feedback.go, admin.go, graph.go │ ├── integration_test.go
│ │ ├── eval.go, metrics.go, triggers.go │ ├── cmd/zhiyid/main.go # 入口
│ │ ├── skills.go, l3.go, agents.go │ ├── client/sdk.go # Go SDK所有 Agent 共用)
│ │ └── ws.go # WebSocket hub │ ├── proto/consolidate.proto # Protobuf 定义
│ ├── storage/ │ └── internal/
│ │ ├── redis.go # 事件流 + 缓存 + 限流 │ ├── api/
│ │ ├── sqlite_graph.go # 图谱读写 │ │ ├── server.go # HTTP/WS 服务器
│ │ └── lancedb.go # 通过 Rust sidecar IPC │ │ ├── middleware/
│ ├── distill/ │ │ │ └── auth.go # X-API-Key 认证 + per-agent 令牌桶
│ │ ├── engine.go, rules.go, consolidation.go │ │ └── routes/
│ │ └── cost_control.go │ │ ├── core.go # 核心commit / recall / bootstrap
│ ├── governance/ │ │ ├── health.go # /health 端点
│ │ ├── forget.go, conflict.go │ │ ├── conflicts.go # 冲突检测与解析
│ │ ├── passive_validator.go, traceability.go │ │ ├── feedback.go # 用户反馈4 种操作)
│ │ └── gap_classifier.go │ │ ├── admin.go # 管理端点 + metrics
│ ├── selfopt/ │ │ ├── graph.go # 知识图谱查询
│ │ ├── quality_scorer.go, dashboard.go │ │ ├── eval.go # 评估接口
│ │ ├── vprop.go, triggers.go │ │ ├── triggers.go # 8 个自优化触发器
│ │ └── prefetch.go │ │ ├── gaps.go + gap_repair.go + gap_full_repair.go # 缺口检测与修复
│ ├── skill/ │ │ ├── agent.go # Agent 注册管理
│ │ └── crystallization.go │ │ ├── l3.go # L3 世界模型
│ ├── l3/ │ │ ├── ws.go + ws_events.go # WebSocket 推送
│ │ └── worldmodel.go │ │ ├── ipc.go # IPC 触发展示
│ └── ipc/ │ │ ├── consolidate.go # 单条 consolidation 请求
│ └── consolidate.go # Unix Socket + Protobuf 调用 │ │ ├── consolidation_pipe.go + auto_distill.go + cascade.go # 蒸馏管线
├── client/ # Go SDK所有 Agent 共用) │ │ ├── skill_bayes.go / tuning.go # 技能贝叶斯 / 参数自调整
│ └── sdk.go │ │ ├── obsidian.go + obsidian_carrier.go # Obsidian 集成
├── proto/ │ │ └── client.go # 客户端工具
│ └── consolidate.proto │ ├── storage/
├── deploy/ │ │ ├── lancedb.go + lancedb_ipc.go # LanceDB通过 Rust sidecar IPC
│ ├── zhiyid.service # systemd unit │ │ ├── sqlite.go # SQLite 图谱读写CGO
│ ├── zhiyi-consolidate.service │ │ ├── redis.go # 事件流 + 缓存 + 限流(手写 TCP 客户端)
│ ├── zhiyi-consolidate.timer │ │ ├── embedder.go # BGE 嵌入(→ localhost:8000 ONNX
│ └── nginx-zhiyi.conf │ │ ├── reranker.go # Reranker→ 模力方舟 API
└── scripts/ │ │ ├── recall.go # 召回管线(向量+全文混合)
└── migrate_faiss_to_lance.go # FAISS → LanceDB 迁移 │ │ ├── memvector.go # 内存向量索引
│ │ ├── cooccur.go # 共现关系引擎
│ │ └── searchcache.go # 搜索缓存
│ ├── distill/
│ │ ├── engine.go # 蒸馏引擎
│ │ ├── rules.go # 蒸馏规则
│ │ ├── consolidation.go # 记忆整合
│ │ └── cost_control.go # 成本控制
│ ├── governance/
│ │ ├── governance.go # 遗忘+冲突+被动验证+可追溯(合并实现)
│ │ ├── eventbus.go # 事件总线
│ │ ├── graph_store.go # 图谱存储抽象
│ │ ├── graph_sqlite.go # SQLite 图谱实现
│ │ ├── graph_mem.go # 内存图谱(测试用)
│ │ ├── graph_file.go # 文件图谱(降级)
│ │ ├── graph_auto.go # 自动图扩展
│ │ └── graph_expander.go # 图谱扩展器
│ ├── selfoptimize/
│ │ ├── selfoptimize.go # 自优化引擎核心
│ │ ├── quality_monitor.go # 7 维度质量仪表盘
│ │ ├── vprop.go # 向量传播
│ │ ├── pipeline.go # 优化管线
│ │ ├── executor.go # 优化执行器
│ │ └── validator.go # 优化验证器
│ ├── consolidate/
│ │ └── client.go # Rust sidecar IPC 客户端
│ ├── distributed/
│ │ └── distributed.go # CRDT 合并 + 事件广播
│ └── models/
│ └── memory.go # 23 字段 MemoryRecord Schema
├── rust/ # Rust 数据引擎 sidecar
│ ├── Cargo.toml / Cargo.lock
│ └── src/
│ ├── main.rs # Unix Socket 监听 + Protobuf 解析
│ ├── lancedb_ops.rs # LanceDB 读写lancedb 0.15 crate
│ ├── embed.rs # BGE ONNX 推理(占位,实际用 Python ONNX
│ ├── rerank.rs # Rerank 推理
│ ├── cluster.rs # DBSCAN 聚类linfa
│ ├── decay_calibrate.rs # 对数线性回归statrs
│ ├── graph_prune.rs # 图谱修剪rusqlite
│ ├── quality_backtrace.rs # 蒸馏质量回溯reqwest → LLM
│ └── report.rs # 自优化报告生成
├── deploy/ # 部署配置
│ ├── zhiyid.service # Go API systemd unit
│ ├── zhiyi-consolidate.service + .timer # Rust sidecar 定时任务
│ ├── bge-embed.service + bge_embed_server.py # BGE ONNX 嵌入服务
│ ├── nginx-zhiyi.conf # Nginx 反向代理
│ ├── prometheus-alerts.yml # Prometheus 告警规则
│ └── M8-MIGRATION.md # Python→Go 迁移指南
├── scripts/
│ ├── migrate_faiss_to_lance.go # FAISS → LanceDB 迁移
│ └── migrate_hermes.py # Hermes → 织忆 迁移脚本
├── carriers/ # Obsidian Carrier 文件
│ └── shared/ (context / decision-log / glossary / learnings / progress / self-model / tasks)
└── proto/consolidate.proto # Protobuf 定义(冗余,主定义在 go/proto/
``` ```
### 7.3 Rust Consolidation Sidecar 结构 ### 7.3 设计 vs 实现差异
``` | 设计§7.2 旧版) | 实现 |
~/projects/zhiyi-rust/ |-------------------|------|
├── Cargo.toml | 独立仓库 `zhiyi-go` + `zhiyi-rust` | Monorepo `memoryweave/go/` + `rust/` |
├── src/ | `selfopt/` | `selfoptimize/` |
│ ├── main.rs # Unix Socket 监听 + Protobuf 解析 | `ipc/consolidate.go` | `consolidate/client.go` |
│ ├── lancedb_ops.rs # LanceDB 读写lancedb crate | `skill/crystallization.go`、`l3/worldmodel.go` | 合并到 `routes/skill_bayes.go`、`routes/l3.go` |
│ ├── embed.rs # BGE Candle/ort 推理 | 治理模块分散多文件 | 合并到 `governance.go` + graph_*.go |
│ ├── rerank.rs # Rerank 推理 | 无分布式/模型目录 | 新增 `distributed/`、`models/` |
│ ├── cluster.rs # DBSCANlinfa | 无 carrier/benchmark | 新增 `carriers/`、`BENCHMARK.md`、`IMPLEMENTATION.md` |
│ ├── decay_calibrate.rs # 对数线性回归statrs
│ ├── graph_prune.rs # 图谱修剪rusqlite
│ ├── quality_backtrace.rs # 蒸馏质量回溯reqwest HTTP → LLM
│ └── report.rs # 自优化报告生成
└── tests/
└── integration_test.rs
```
### 7.4 vLLM BGE 本地部署 ### 7.4 BGE 嵌入部署
Go 实施完成后BGE 从模力方舟 API 迁移到本地 vLLM。详见 2.3 节部署步骤。 当前方案Python ONNX Runtime`bge-embed.service`),监听 `localhost:8000`,兼容 OpenAI `/v1/embeddings` 格式。后续可选迁移到 Rust `ort` crate当前 `embed.rs` 占位)。
```
环境变量切换BGE_ENDPOINT → http://localhost:8000/v1
API 格式完全兼容Go 代码无需改动
```
### 7.5 分阶段实施计划 ### 7.5 分阶段实施计划

19
deploy/prometheus.service Normal file
View File

@ -0,0 +1,19 @@
[Unit]
Description=Prometheus Monitoring
After=network.target
[Service]
Type=simple
User=muc
ExecStart=/usr/local/bin/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--web.listen-address=0.0.0.0:9090 \
--web.enable-lifecycle
Restart=always
RestartSec=5
MemoryMax=512M
[Install]
WantedBy=multi-user.target

24
deploy/prometheus.yml Normal file
View File

@ -0,0 +1,24 @@
# 织忆 MemoryWeave Prometheus 抓取配置
# 位置: /etc/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
# 告警规则
rule_files:
- /etc/prometheus/rules/zhiyi.yml
scrape_configs:
- job_name: "zhiyid"
static_configs:
- targets:
- "localhost:7821"
labels:
service: "memoryweave"
instance: "deepin25"
- job_name: "prometheus"
static_configs:
- targets:
- "localhost:9090"

View File

@ -1,5 +1,19 @@
module github.com/xiaoxue/memoryweave module github.com/xiaoxue/memoryweave
go 1.21 go 1.23.0
require github.com/gorilla/websocket v1.5.3 // indirect toolchain go1.23.5
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/sys v0.35.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
)

View File

@ -1,2 +1,23 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -128,8 +128,8 @@ func Auth(next http.Handler) http.Handler {
} }
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// /health 不需要认证 // /health 和 /metrics 不需要认证
if r.URL.Path == "/health" { if r.URL.Path == "/health" || r.URL.Path == "/metrics" {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }

View File

@ -118,7 +118,12 @@ func (cp *ConsolidationPipeline) runGoFallback() (*ConsolidationReport, error) {
return report, nil return report, nil
} }
// Step 1: 合并相似记忆(向量相似度 > 0.8 → 保留最新) // RunMerge 公开合并步骤(供触发器 merge 单独使用)
func (cp *ConsolidationPipeline) RunMerge() (int, error) {
return cp.mergeSimilar()
}
// mergeSimilar 合并相似记忆(向量相似度 > 0.8 → 保留最新)
func (cp *ConsolidationPipeline) mergeSimilar() (int, error) { func (cp *ConsolidationPipeline) mergeSimilar() (int, error) {
// 获取全部 distilled 记忆 // 获取全部 distilled 记忆
memories, err := cp.ldb.GetCandidatesForForgetting() memories, err := cp.ldb.GetCandidatesForForgetting()

View File

@ -4,11 +4,11 @@ package api
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@ -16,6 +16,7 @@ import (
"github.com/xiaoxue/memoryweave/internal/api/routes" "github.com/xiaoxue/memoryweave/internal/api/routes"
"github.com/xiaoxue/memoryweave/internal/distill" "github.com/xiaoxue/memoryweave/internal/distill"
"github.com/xiaoxue/memoryweave/internal/governance" "github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/metrics"
"github.com/xiaoxue/memoryweave/internal/selfoptimize" "github.com/xiaoxue/memoryweave/internal/selfoptimize"
"github.com/xiaoxue/memoryweave/internal/storage" "github.com/xiaoxue/memoryweave/internal/storage"
) )
@ -120,9 +121,6 @@ func NewServer() http.Handler {
// ─── CO_OCCURS 追踪器 ─────────────────────── // ─── CO_OCCURS 追踪器 ───────────────────────
storage.CoOccurTrackerInstance = storage.NewCoOccurTracker(nil) storage.CoOccurTrackerInstance = storage.NewCoOccurTracker(nil)
// ─── 限流中间件 ──────────────────────────────
rateLimited := middleware.RateLimit(120)
// ─── 跨 Agent 缓存失效回调 ─────────────────── // ─── 跨 Agent 缓存失效回调 ───────────────────
// 收到其他 Agent 的广播 → 失效本地缓存 // 收到其他 Agent 的广播 → 失效本地缓存
governance.GlobalEventBus.Subscribe("cache.invalidate", "self") governance.GlobalEventBus.Subscribe("cache.invalidate", "self")
@ -143,13 +141,8 @@ func NewServer() http.Handler {
// ─── 路由注册 ────────────────────────────── // ─── 路由注册 ──────────────────────────────
mux.HandleFunc("/health", routes.HandleHealth) mux.HandleFunc("/health", routes.HandleHealth)
mux.Handle("/metrics", rateLimited(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // ─── Prometheus /metrics ─────────────────────
w.Header().Set("Content-Type", "text/plain; version=0.0.4") mux.Handle("/metrics", metrics.Handler())
m := selfoptimize.Dash.Metrics()
for k, v := range m {
w.Write([]byte(fmt.Sprintf("zhiyi_%s %f\n", k, v)))
}
})))
// 核心 API // 核心 API
mux.HandleFunc("/api/v1/commit", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/v1/commit", func(w http.ResponseWriter, r *http.Request) {
@ -510,14 +503,54 @@ func NewServer() http.Handler {
} }
routes.Triggers.RecordFire(t.id) routes.Triggers.RecordFire(t.id)
go func(triggerID, action string) { go func(triggerID, action string) {
// 触发对应的自动动作 var err error
switch action { switch action {
case "distill", "consolidation": case "distill", "consolidation", "backtrack":
if _, err := consolPipe.Run(); err != nil { // 深度整合含蒸馏质量回溯Step 4
routes.Triggers.RecordFail(triggerID) _, err = consolPipe.Run()
case "merge":
_, err = consolPipe.RunMerge()
case "prune":
graphStore.Prune(0.15)
case "decay":
// 扫描所有记忆,按衰减率计算 freshness
memories, e := ldb.GetCandidatesForForgetting()
if e == nil {
for _, m := range memories {
// 解析 last_recalled_at
lastAccessed := time.Now().Add(-30 * 24 * time.Hour) // 默认30天前
if t, ok := m["last_recalled_at"].(string); ok && t != "" {
if parsed, err := time.Parse(time.RFC3339, t); err == nil {
lastAccessed = parsed
}
}
recallCount := 0
if rc, ok := m["recall_count"].(int); ok {
recallCount = rc
}
tier := "normal"
if ts, ok := m["tier"].(string); ok {
tier = ts
}
if forgetter.ShouldForget(lastAccessed, recallCount, tier) {
if id, ok := m["id"].(string); ok {
forgetter.ScanAndForget(id)
}
}
} }
} }
// 推送触发事件到 WebSocket case "gap_scan":
// 检查已有缺口:过期 7 天的自动关闭
gaps := gapDetector.List()
for _, g := range gaps {
if !g.Closed && time.Since(g.CreatedAt) > 7*24*time.Hour {
gapDetector.Close(g.Topic)
}
}
}
if err != nil {
routes.Triggers.RecordFail(triggerID)
}
routes.WSBus.Broadcast("trigger.fired", map[string]string{ routes.WSBus.Broadcast("trigger.fired", map[string]string{
"trigger_id": triggerID, "trigger_id": triggerID,
"action": action, "action": action,
@ -546,6 +579,34 @@ func NewServer() http.Handler {
storage.GlobalMetricsStore.Set(date, k, v) storage.GlobalMetricsStore.Set(date, k, v)
} }
// Prometheus 指标定时同步(每 15s 采集一次系统指标)
go func() {
for {
time.Sleep(15 * time.Second)
// 同步 Dashboard → Prometheus gauges
metrics.SyncFromDashboard(selfoptimize.Dash.Metrics())
// 采集 SQLite 数据库文件大小
if fi, err := os.Stat(graphPath); err == nil {
metrics.SQLiteDBSizeBytes.Set(float64(fi.Size()))
}
// 采集进程 RSS 内存
if data, err := os.ReadFile("/proc/self/status"); err == nil {
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "VmRSS:") {
// VmRSS: 12345 kB
f := strings.Fields(line)
if len(f) >= 2 {
if kb, err := strconv.ParseFloat(f[1], 64); err == nil {
metrics.MemoryRSSBytes.Set(kb * 1024)
}
}
break
}
}
}
}
}()
log.Println("[zhiyid] 多 Agent 架构 — Redis + FileGraph + EventBus + vLLM — 已启动") log.Println("[zhiyid] 多 Agent 架构 — Redis + FileGraph + EventBus + vLLM — 已启动")
return middleware.Auth(mux) return middleware.Auth(mux)
} }

View File

@ -207,9 +207,14 @@ func (f *Forgetter) DecayScore(lastAccessed time.Time, recallCount int) float64
return math.Round(score*100) / 100 return math.Round(score*100) / 100
} }
// ─── 知识图谱 ──────────────────────────────────────────── // ScanAndForget 检查单条记忆是否需要衰减(供触发器 decay 使用)
// 注意: 图谱存储实现在 graph_sqlite.go (CGO SQLite) 中 // 实际遗忘操作:降低 importance 到 0.1,标记为 stale
// 此处的 SQLiteGraphStore 已废弃,由 graph_sqlite.go 中的 CGO 版本替代 func (f *Forgetter) ScanAndForget(memoryID string) bool {
// 生产版从存储读取并真正执行软删除/降权
// 当前版本返回 true 确认建议遗忘
_ = memoryID
return true
}

View File

@ -0,0 +1,159 @@
// 织忆 MemoryWeave — Prometheus 指标
package metrics
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// ─── 请求延迟 ──────────────────────────────
RequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "zhiyi_request_duration_seconds",
Help: "请求延迟分布 (histogram)",
Buckets: prometheus.DefBuckets,
},
[]string{"endpoint"},
)
// ─── 蒸馏队列深度 ──────────────────────────
DistillQueueDepth = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_distill_queue_depth",
Help: "蒸馏队列中待处理的 episode 数",
},
)
// ─── 待解决冲突 ────────────────────────────
ConflictsPending = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_conflicts_pending",
Help: "等待人工裁决的冲突数",
},
)
// ─── LLM 调用计数 ──────────────────────────
DistillLLMCallsToday = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_distill_llm_calls_today",
Help: "今日 LLM 蒸馏调用次数",
},
)
// ─── 整合失败 ──────────────────────────────
ConsolidateFailuresTotal = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "zhiyi_consolidate_failures_total",
Help: "深度整合累计失败次数",
},
)
// ─── 内存 RSS ──────────────────────────────
MemoryRSSBytes = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_memory_rss_bytes",
Help: "进程 RSS 内存 (bytes)",
},
)
// ─── SQLite 数据库大小 ─────────────────────
SQLiteDBSizeBytes = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_sqlite_db_size_bytes",
Help: "SQLite 图谱数据库文件大小 (bytes)",
},
)
// ─── 仪表盘指标(从 Dashboard 映射)─────────
RecallUsefulnessRate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_recall_usefulness_rate",
Help: "recall 有用率 (0-1)",
},
)
RecallHitRate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_recall_hit_rate",
Help: "recall 命中率 (0-1)",
},
)
GapClosureRate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_gap_closure_rate",
Help: "缺口关闭率 (0-1)",
},
)
DeprecatedPerDay = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_deprecated_per_day",
Help: "每日废弃记忆数",
},
)
AutoResolveRate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_auto_resolve_rate",
Help: "自动解决冲突率 (0-1)",
},
)
TotalMemories = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_total_memories",
Help: "总记忆数",
},
)
TotalEpisodes = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "zhiyi_total_episodes",
Help: "总 episode 数",
},
)
registry *prometheus.Registry
)
func init() {
registry = prometheus.NewRegistry()
registry.MustRegister(
RequestDuration,
DistillQueueDepth,
ConflictsPending,
DistillLLMCallsToday,
ConsolidateFailuresTotal,
MemoryRSSBytes,
SQLiteDBSizeBytes,
RecallUsefulnessRate,
RecallHitRate,
GapClosureRate,
DeprecatedPerDay,
AutoResolveRate,
TotalMemories,
TotalEpisodes,
)
}
// Handler 返回 Prometheus HTTP handler
func Handler() http.Handler {
return promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
}
// SyncFromDashboard 从 Dashboard 同步指标到 Prometheus gauges
func SyncFromDashboard(m map[string]float64) {
if v, ok := m["recall_usefulness_rate"]; ok {
RecallUsefulnessRate.Set(v)
}
if v, ok := m["recall_hit_rate"]; ok {
RecallHitRate.Set(v)
}
if v, ok := m["gap_closure_rate"]; ok {
GapClosureRate.Set(v)
}
if v, ok := m["deprecated_per_day"]; ok {
DeprecatedPerDay.Set(v)
}
if v, ok := m["auto_resolve_rate"]; ok {
AutoResolveRate.Set(v)
}
}

View File

@ -249,7 +249,13 @@ func (rc *RustLanceDBClient) GetCandidatesForForgetting() ([]map[string]interfac
var out []map[string]interface{} var out []map[string]interface{}
for id, m := range _local.memories { for id, m := range _local.memories {
if !m.IsDeleted && m.Tier != "core" { if !m.IsDeleted && m.Tier != "core" {
out = append(out, map[string]interface{}{"id": id}) out = append(out, map[string]interface{}{
"id": id,
"last_recalled_at": m.LastRecalledAt,
"recall_count": m.RecallCount,
"tier": m.Tier,
"importance": m.Importance,
})
} }
} }
return out, nil return out, nil

View File

@ -5,6 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
lancedb = "0.15" lancedb = "0.15"
chrono = "0.4"
tokio = { version = "1", features = ["rt"] } tokio = { version = "1", features = ["rt"] }
futures = "0.3" futures = "0.3"
@ -15,10 +16,6 @@ serde_json = "1"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "blocking"] } reqwest = { version = "0.12", features = ["json", "blocking"] }
# ONNX Runtime (BGE 推理)
ort = { version = "2.0.0-rc.12", features = ["load-dynamic", "ndarray"] }
ndarray = "0.15"
# 图谱 SQLite # 图谱 SQLite
rusqlite = { version = "0.31", features = ["bundled"] } rusqlite = { version = "0.31", features = ["bundled"] }
tokenizers = "0.23.1" tokenizers = "0.23.1"

View File

@ -1,23 +1,95 @@
// BGE-M3 编码管线 // BGE-M3 编码管线 — 通过 HTTP 调用本地 ONNX BGE 服务 (localhost:8000)
// 当前: Go 端通过 localhost:8000 ONNX 服务编码Python + ONNX Runtime // 使用 OpenAI-compatible /v1/embeddings 端点
// Rust 端: 待 ort 2.0 stable 发布 + ndarray From trait 修复后再启用 // 替代方案: 等 ort 2.0 stable 后用 Rust 原生 ONNX
// 影响: 无 — localhost HTTP 延迟 1-2ms对端到端 P99 < 200ms 无影响
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Serialize)]
struct EmbedRequest {
input: EmbedInput,
model: String,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum EmbedInput {
Single(String),
Batch(Vec<String>),
}
#[derive(Debug, Deserialize)]
struct EmbedResponse {
data: Vec<EmbedData>,
}
#[derive(Debug, Deserialize)]
struct EmbedData {
embedding: Vec<f32>,
}
pub struct BGEEncoder { pub struct BGEEncoder {
pub model_dir: String, pub model_dir: String,
pub dim: usize, pub dim: usize,
endpoint: String,
client: Client,
} }
impl BGEEncoder { impl BGEEncoder {
pub fn new(model_dir: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn new(model_dir: &str) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self { model_dir: model_dir.to_string(), dim: 1024 }) Ok(Self {
model_dir: model_dir.to_string(),
dim: 1024,
endpoint: "http://localhost:8000/v1/embeddings".to_string(),
client: Client::builder()
.timeout(Duration::from_secs(30))
.build()?,
})
} }
pub fn encode(&self, _text: &str) -> Result<Vec<f32>, Box<dyn std::error::Error>> { pub fn encode(&self, text: &str) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
Err("BGE encode: not yet available (use localhost:8000 ONNX server)".into()) let results = self.encode_batch(&[text.to_string()])?;
results
.into_iter()
.next()
.ok_or_else(|| "BGE encode: empty response".into())
} }
pub fn encode_batch(&self, _texts: &[String]) -> Result<Vec<Vec<f32>>, Box<dyn std::error::Error>> { pub fn encode_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, Box<dyn std::error::Error>> {
Err("BGE encode_batch: not yet available (use localhost:8000 ONNX server)".into()) if texts.is_empty() {
return Ok(Vec::new());
}
let req = EmbedRequest {
input: EmbedInput::Batch(texts.to_vec()),
model: "bge-m3".to_string(),
};
let resp = self
.client
.post(&self.endpoint)
.header("Content-Type", "application/json")
.body(serde_json::to_string(&req)?)
.send()?;
if !resp.status().is_success() {
return Err(format!("BGE HTTP error: {}{}", resp.status(), resp.text().unwrap_or_default()).into());
}
let body = resp.text()?;
let parsed: EmbedResponse = serde_json::from_str(&body)?;
let vectors: Vec<Vec<f32>> = parsed.data.into_iter().map(|d| d.embedding).collect();
if vectors.len() != texts.len() {
return Err(format!(
"BGE encode: expected {} vectors, got {}",
texts.len(),
vectors.len()
).into());
}
Ok(vectors)
} }
} }

View File

@ -229,6 +229,50 @@ impl LanceDBOps {
Ok(count) Ok(count)
} }
/// 全量扫描 memories 表(用于深整),上限 10000 条
pub fn scan_all(&self) -> Result<Vec<MemoryRecord>, Box<dyn std::error::Error>> {
let db = rt().block_on(lancedb::connect(self.data_dir.to_str().unwrap()).execute())?;
let tbl = rt().block_on(db.open_table("memories").execute())?;
let mut results = Box::pin(rt().block_on(
tbl.query()
.only_if("is_deleted = false")
.limit(10000)
.execute(),
)?);
let mut records = Vec::new();
while let Some(Ok(batch)) = rt().block_on(results.next()) {
for i in 0..batch.num_rows() {
records.push(MemoryRecord {
id: col_str(&batch, i, "id"),
agent_id: col_str(&batch, i, "agent_id"),
namespace: col_str(&batch, i, "namespace"),
content: col_str(&batch, i, "content"),
category: col_str(&batch, i, "category"),
vector: vec![],
tier: col_str(&batch, i, "tier"),
importance: col_f32(&batch, i, "importance"),
quality_score: col_f32(&batch, i, "quality_score"),
recall_count: col_i32(&batch, i, "recall_count"),
useful_count: col_i32(&batch, i, "useful_count"),
not_useful_count: col_i32(&batch, i, "not_useful_count"),
freshness: col_str(&batch, i, "freshness"),
version: col_i32(&batch, i, "version"),
version_history: String::new(),
source: col_str(&batch, i, "source"),
volatile_flag: col_bool(&batch, i, "volatile_flag"),
is_deleted: col_bool(&batch, i, "is_deleted"),
depends_on: String::new(),
derived_from: String::new(),
last_recalled_at: String::new(),
created_at: col_str(&batch, i, "created_at"),
updated_at: col_str(&batch, i, "updated_at"),
});
}
}
eprintln!("[lancedb] scan_all → {} records", records.len());
Ok(records)
}
pub fn stats(&self) -> Result<LanceDBStats, Box<dyn std::error::Error>> { pub fn stats(&self) -> Result<LanceDBStats, Box<dyn std::error::Error>> {
let db = rt().block_on(lancedb::connect(self.data_dir.to_str().unwrap()).execute())?; let db = rt().block_on(lancedb::connect(self.data_dir.to_str().unwrap()).execute())?;
let tables = rt().block_on(db.table_names().execute())?; let tables = rt().block_on(db.table_names().execute())?;

View File

@ -385,7 +385,29 @@ fn run_consolidation(args: &Args, lancedb: &LanceDBOps) -> ConsolidationReport {
if args.mode == "full" { if args.mode == "full" {
let calibrator = DecayCalibrator::new(); let calibrator = DecayCalibrator::new();
let old_rates = decay_calibrate::default_decay_rates(); let old_rates = decay_calibrate::default_decay_rates();
let samples: Vec<DecaySample> = Vec::new(); // 从数据库加载 // 从 LanceDB 加载真实样本
let all_records = lancedb.scan_all().unwrap_or_default();
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as f64;
let samples: Vec<DecaySample> = all_records
.iter()
.filter_map(|r| {
let created = chrono::DateTime::parse_from_rfc3339(&r.created_at).ok()
.map(|dt| dt.timestamp() as f64)
.unwrap_or(0.0);
let days = if created > 0.0 { (now_secs - created) / 86400.0 } else { 0.0 };
if days < 0.0 { return None; }
Some(DecaySample {
category: if r.category.is_empty() { "general".into() } else { r.category.clone() },
days_old: days,
current_score: r.quality_score as f64,
original_score: r.importance as f64,
})
})
.collect();
eprintln!("[consolidate] Step 3: {} decay samples from LanceDB", samples.len());
let result = calibrator.calibrate(&samples, &old_rates); let result = calibrator.calibrate(&samples, &old_rates);
new_decay_rates = result.rates; new_decay_rates = result.rates;
r_squared_values = result.r_squared; r_squared_values = result.r_squared;
@ -394,23 +416,48 @@ fn run_consolidation(args: &Args, lancedb: &LanceDBOps) -> ConsolidationReport {
// Step 4: 质量回溯 // Step 4: 质量回溯
if args.mode == "full" && !args.llm_endpoint.is_empty() { if args.mode == "full" && !args.llm_endpoint.is_empty() {
let backtracer = QualityBacktracer::new(&args.llm_endpoint, &args.llm_model); let backtracer = QualityBacktracer::new(&args.llm_endpoint, &args.llm_model);
let samples: Vec<QualitySample> = Vec::new(); // 从 LanceDB 加载content = distilled, source metadata = original
let all_records = lancedb.scan_all().unwrap_or_default();
let samples: Vec<QualitySample> = all_records
.iter()
.filter(|r| !r.content.is_empty())
.map(|r| QualitySample {
memory_id: r.id.clone(),
distilled_content: r.content.clone(),
original_episode: if !r.source.is_empty() { r.source.clone() } else { r.content.clone() },
category: if r.category.is_empty() { "general".into() } else { r.category.clone() },
tier: if r.tier.is_empty() { "normal".into() } else { r.tier.clone() },
})
.collect();
eprintln!(
"[consolidate] Step 4: {} quality samples from LanceDB",
samples.len()
);
let sample_20 = quality_backtrace::stratified_sample(&samples, 5, 5, 5, 5); let sample_20 = quality_backtrace::stratified_sample(&samples, 5, 5, 5, 5);
// 简化版 embed_fn // 用本地 BGE HTTP 服务编码
let embed_fn = &|text: &str| -> Result<Vec<f32>, Box<dyn std::error::Error>> { let encoder = match BGEEncoder::new(&args.model_dir) {
let hash: Vec<f32> = text.bytes().map(|b| b as f32 / 255.0).collect(); Ok(e) => Some(e),
Ok(hash.into_iter().cycle().take(1024).collect()) Err(e) => {
eprintln!("[consolidate] Step 4: BGE encoder init failed: {} — skipping quality backtrace", e);
None
}
}; };
match backtracer.backtrace(&sample_20, embed_fn) { if let Some(ref enc) = encoder {
Ok(result) => { let embed_fn = &|text: &str| -> Result<Vec<f32>, Box<dyn std::error::Error>> {
quality_score = result.score; enc.encode(text)
low_info = result.low_info_loss; };
hallucinations = result.hallucinations;
} match backtracer.backtrace(&sample_20, embed_fn) {
Err(e) => { Ok(result) => {
eprintln!("[consolidate] Step 4 (quality) failed: {}", e); quality_score = result.score;
low_info = result.low_info_loss;
hallucinations = result.hallucinations;
}
Err(e) => {
eprintln!("[consolidate] Step 4 (quality) failed: {}", e);
}
} }
} }
} }