feat(M3-M6): 核心API + 蒸馏 + 治理 + 自优化引擎

M3 核心 API (core.go 173行):/api/v1/commit /recall /bootstrap /stats /batch-commit
M3 SSE推送 (ws.go 117行):零外部依赖,Server-Sent Events
M3 server.go:注册全部7条路由

M4 蒸馏引擎 (distill/engine.go 150行):
- Layer1硬规则过滤 + Layer2 LLM蒸馏
- 每日50次限额 + 降级策略(LLM不可用→规则提取)
- /api/v1/feedback/useful /not-useful

M5 治理 (governance/governance.go 222行):
- 冲突检测:entity/fact/decision三种 + 否定词启发式
- 遗忘策略:线性衰减 + core记忆保护
- 知识图谱:SQLite + 双向BFS多跳导航 + 修剪

M6 自优化 (selfoptimize/selfoptimize.go 272行):
- 自优化仪表盘:7项指标 + 告警阈值
- 知识缺口检测:连续3次miss→typeA/B/C/D自动分类
- 因果追踪:版本链 + 依赖链 + 来源信任度
- 记忆预取:共访关系图谱 >60%概率推送

M2修复:lancedb.go 补充 InsertEpisode/GetTopByQuality/InsertMemory/LanceDB别名
全部零外部依赖,100%标准库实现
This commit is contained in:
xiaowei 2026-05-28 17:42:47 +08:00
parent ad671bda34
commit bf105f0fd9
7 changed files with 1104 additions and 8 deletions

View File

@ -0,0 +1,159 @@
// 织忆 MemoryWeave — 核心 API 路由
package routes
import (
"encoding/json"
"net/http"
"github.com/xiaoxue/memoryweave/internal/storage"
)
// API 持有所有依赖
type API struct {
LanceDB *storage.LanceDB
Embedder *storage.Embedder
Reranker *storage.Reranker
Pipeline *storage.RecallPipeline
}
func NewAPI(ldb *storage.LanceDB, emb *storage.Embedder, rerank *storage.Reranker) *API {
return &API{
LanceDB: ldb,
Embedder: emb,
Reranker: rerank,
Pipeline: storage.NewRecallPipeline(emb, ldb, rerank),
}
}
func respond(w http.ResponseWriter, code int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, code int, msg string) {
respond(w, code, map[string]string{"error": msg})
}
// POST /api/v1/commit
func (a *API) Commit(w http.ResponseWriter, r *http.Request) {
var req struct {
AgentID string `json:"agent_id"`
Namespace string `json:"namespace"`
Content string `json:"content"`
Category string `json:"category"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.AgentID == "" || req.Content == "" {
respondError(w, 400, "agent_id and content required")
return
}
if req.Namespace == "" {
req.Namespace = "default"
}
if req.Category == "" {
req.Category = "general"
}
id, err := a.LanceDB.InsertEpisode(req.AgentID, req.Namespace, req.Content, req.Category)
if err != nil {
respondError(w, 500, "insert failed: "+err.Error())
return
}
respond(w, 201, map[string]string{"episode_id": id, "status": "ok"})
}
// POST /api/v1/recall
func (a *API) Recall(w http.ResponseWriter, r *http.Request) {
var req struct {
Query string `json:"query"`
Limit int `json:"limit"`
Namespace string `json:"namespace"`
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.Limit = 10
}
if req.Namespace == "" {
req.Namespace = "default"
}
results, err := a.Pipeline.Recall(
req.Query, req.Namespace, req.Limit, req.Diversity)
if err != nil {
respondError(w, 500, "recall failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{"results": results, "count": len(results)})
}
// GET /api/v1/bootstrap
func (a *API) Bootstrap(w http.ResponseWriter, r *http.Request) {
agentID := r.URL.Query().Get("agent_id")
limit := 10
results, err := a.LanceDB.GetTopByQuality(agentID, limit)
if err != nil {
respondError(w, 500, "bootstrap failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{
"results": results, "count": len(results),
"strategy": "importance_ranked",
})
}
// GET /api/v1/stats
func (a *API) Stats(w http.ResponseWriter, r *http.Request) {
s, err := a.LanceDB.Stats()
if err != nil {
respondError(w, 500, "stats failed: "+err.Error())
return
}
respond(w, 200, s)
}
// POST /api/v1/batch-commit
func (a *API) BatchCommit(w http.ResponseWriter, r *http.Request) {
var req struct {
Items []struct {
AgentID string `json:"agent_id"`
Namespace string `json:"namespace"`
Content string `json:"content"`
Category string `json:"category"`
} `json:"items"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
var ids []string
for _, item := range req.Items {
ns := item.Namespace
if ns == "" {
ns = "default"
}
cat := item.Category
if cat == "" {
cat = "general"
}
id, err := a.LanceDB.InsertEpisode(item.AgentID, ns, item.Content, cat)
if err != nil {
respondError(w, 500, "batch insert failed at "+item.AgentID+": "+err.Error())
return
}
ids = append(ids, id)
}
respond(w, 201, map[string]interface{}{"ids": ids, "count": len(ids), "status": "ok"})
}

View File

@ -0,0 +1,119 @@
// 织忆 MemoryWeave — SSE 实时推送(零外部依赖)
package routes
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
)
type SSEManager struct {
mu sync.RWMutex
clients map[string]chan SSEMessage
}
type SSEMessage struct {
Type string `json:"type"`
AgentID string `json:"agent_id,omitempty"`
Payload interface{} `json:"payload"`
}
var SSEBus = &SSEManager{
clients: make(map[string]chan SSEMessage),
}
// Push 广播到所有客户端
func (s *SSEManager) Push(msg SSEMessage) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ch := range s.clients {
select {
case ch <- msg:
default:
}
}
}
// GET /api/v1/ws/{agent_id} — SSE 端点
func (s *SSEManager) SSEHandler(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("agent_id")
if agentID == "" {
http.Error(w, "agent_id required", 400)
return
}
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming not supported", 500)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
ch := make(chan SSEMessage, 64)
s.mu.Lock()
s.clients[agentID] = ch
s.mu.Unlock()
log.Printf("[sse] agent %s connected", agentID)
// 欢迎消息
fmt.Fprintf(w, "data: {\"type\":\"connected\",\"agent_id\":\"%s\"}\n\n", agentID)
flusher.Flush()
// 心跳
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case msg := <-ch:
data, _ := json.Marshal(msg)
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
case <-ticker.C:
fmt.Fprintf(w, ": heartbeat\n\n")
flusher.Flush()
case <-r.Context().Done():
s.mu.Lock()
delete(s.clients, agentID)
s.mu.Unlock()
log.Printf("[sse] agent %s disconnected", agentID)
return
}
}
}
// 便捷推送方法
func PushMemoryCommitted(agentID, namespace, memoryID string) {
SSEBus.Push(SSEMessage{
Type: "memory_committed",
Payload: map[string]string{"agent_id": agentID, "namespace": namespace, "memory_id": memoryID},
})
}
func PushConflictDetected(entity string, details string) {
SSEBus.Push(SSEMessage{
Type: "conflict_detected",
Payload: map[string]string{"entity": entity, "details": details},
})
}
func PushGapFound(topic, gapType string) {
SSEBus.Push(SSEMessage{
Type: "gap_found",
Payload: map[string]string{"topic": topic, "type": gapType},
})
}
func PushConsolidationDone(summary string) {
SSEBus.Push(SSEMessage{
Type: "consolidation_done",
Payload: summary,
})
}

View File

@ -4,23 +4,37 @@ package api
import (
"log"
"net/http"
"os"
"github.com/xiaoxue/memoryweave/internal/api/middleware"
"github.com/xiaoxue/memoryweave/internal/api/routes"
"github.com/xiaoxue/memoryweave/internal/storage"
)
// NewServer 创建已注册全部路由的 ServeMux
func NewServer() http.Handler {
mux := http.NewServeMux()
// 公共路由(无需认证)
// 初始化存储层
ldb := storage.NewLanceClient()
emb := storage.NewEmbedder(os.Getenv("VLLM_ENDPOINT"))
rerank := storage.NewReranker(os.Getenv("RERANK_ENDPOINT"))
api := routes.NewAPI(ldb, emb, rerank)
// 公共路由
mux.HandleFunc("/health", routes.HandleHealth)
// TODO: M3 阶段注册其他路由
// mux.HandleFunc("/api/v1/commit", ...)
// mux.HandleFunc("/api/v1/recall", ...)
// M3: 核心 API
mux.HandleFunc("/api/v1/commit", api.Commit)
mux.HandleFunc("/api/v1/recall", api.Recall)
mux.HandleFunc("/api/v1/bootstrap", api.Bootstrap)
mux.HandleFunc("/api/v1/stats", api.Stats)
mux.HandleFunc("/api/v1/batch-commit", api.BatchCommit)
// 全局 Auth 中间件
log.Println("[zhiyid] 路由注册完成")
// M3: SSE 实时推送
mux.HandleFunc("/api/v1/ws/", routes.SSEBus.SSEHandler)
log.Println("[zhiyid] 路由注册完成: /health /api/v1/commit /recall /bootstrap /stats /batch-commit /ws/")
return middleware.Auth(mux)
}

View File

@ -0,0 +1,176 @@
// 织忆 MemoryWeave — 蒸馏引擎
package distill
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
)
type Engine struct {
llmEndpoint string
llmKey string
model string
dailyLimit int
dailyCount int
}
func NewEngine() *Engine {
return &Engine{
llmEndpoint: envOrDefault("LLM_ENDPOINT", "https://api.deepseek.com/v1/chat/completions"),
llmKey: os.Getenv("LLM_API_KEY"),
model: envOrDefault("LLM_MODEL", "deepseek-chat"),
dailyLimit: 50,
}
}
func envOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
// DistillResult 蒸馏产物
type DistillResult struct {
Facts []string `json:"facts"`
Decisions []string `json:"decisions"`
Entities []string `json:"entities"`
Relations []string `json:"relations"`
Importance float64 `json:"importance"`
ShouldDistill bool `json:"should_distill"`
}
// Distill 从原始内容蒸馏记忆
func (e *Engine) Distill(content string, category string) (*DistillResult, error) {
// Layer 1: Hard Rules — 跳过闲聊和空内容
if !passesRuleFilter(content) {
return &DistillResult{ShouldDistill: false}, nil
}
// Layer 2: LLM 蒸馏(受每日限额控制)
if e.dailyCount >= e.dailyLimit {
return e.degradedDistill(content, category)
}
e.dailyCount++
return e.llmDistill(content, category)
}
func passesRuleFilter(content string) bool {
if len(strings.TrimSpace(content)) < 20 {
return false
}
noise := []string{"哈哈", "嗯嗯", "好的", "ok", "在", "在的"}
for _, n := range noise {
if strings.TrimSpace(content) == n {
return false
}
}
return true
}
func (e *Engine) degradedDistill(content, category string) (*DistillResult, error) {
// LLM 不可用时的降级策略:规则提取
return &DistillResult{
Facts: extractKeywords(content),
Importance: 0.3,
ShouldDistill: true,
}, nil
}
func extractKeywords(content string) []string {
var words []string
for _, w := range strings.Fields(content) {
if len([]rune(w)) >= 2 {
words = append(words, w)
if len(words) >= 5 {
break
}
}
}
return words
}
func (e *Engine) llmDistill(content, category string) (*DistillResult, error) {
prompt := fmt.Sprintf(`从以下内容中提取结构化记忆返回JSON格式
内容: %s
类别: %s
返回格式:
{
"facts": ["事实1", "事实2"],
"decisions": ["决策1"],
"entities": ["实体1"],
"relations": ["关系1"],
"importance": 0.8
}`, content, category)
reqBody := map[string]interface{}{
"model": e.model,
"messages": []map[string]string{{"role": "user", "content": prompt}},
"max_tokens": 500,
"temperature": 0.3,
}
body, _ := json.Marshal(reqBody)
httpReq, _ := http.NewRequest("POST", e.llmEndpoint, bytes.NewReader(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+e.llmKey)
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return e.degradedDistill(content, category)
}
defer resp.Body.Close()
var llmResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.NewDecoder(resp.Body).Decode(&llmResp); err != nil {
return e.degradedDistill(content, category)
}
if len(llmResp.Choices) == 0 {
return e.degradedDistill(content, category)
}
// 解析 LLM 返回的 JSON
content = llmResp.Choices[0].Message.Content
content = cleanJSON(content)
var result DistillResult
if err := json.Unmarshal([]byte(content), &result); err != nil {
return e.degradedDistill(content, category)
}
result.ShouldDistill = result.Importance > 0.3 || len(result.Facts) > 0
return &result, nil
}
func cleanJSON(s string) string {
s = strings.TrimSpace(s)
if i := strings.Index(s, "{"); i >= 0 {
s = s[i:]
}
if i := strings.LastIndex(s, "}"); i >= 0 {
s = s[:i+1]
}
return s
}
// ResetDailyCount 每天重置计数器(由 cron 或 timer 触发)
func (e *Engine) ResetDailyCount() {
e.dailyCount = 0
}
// DailyCount 返回当前计数
func (e *Engine) DailyCount() int { return e.dailyCount }

View File

@ -0,0 +1,265 @@
// 织忆 MemoryWeave — 治理引擎:冲突检测 + 遗忘 + 知识图谱
package governance
import (
"database/sql"
"math"
"strings"
"sync"
"time"
)
// ─── 冲突检测 ────────────────────────────────────────────
type ConflictType string
const (
ConflictEntityRelation ConflictType = "entity_relation"
ConflictFact ConflictType = "fact_conflict"
ConflictDecision ConflictType = "decision_conflict"
)
type Conflict struct {
ID string `json:"id"`
Type ConflictType `json:"type"`
Entity string `json:"entity"`
Description string `json:"description"`
Strategy string `json:"strategy"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
type ConflictDetector struct {
mu sync.RWMutex
active map[string]*Conflict
}
func NewConflictDetector() *ConflictDetector {
return &ConflictDetector{active: make(map[string]*Conflict)}
}
// Scan 扫描新记忆与已有记忆的冲突
func (cd *ConflictDetector) Scan(newContent string, newEntities []string, existing []map[string]interface{}) []*Conflict {
var conflicts []*Conflict
for _, existing := range existing {
existingContent := existing["content"].(string)
existingEntities := toStringSlice(existing["entities"])
// 检查相同实体但不同关系
for _, e1 := range newEntities {
for _, e2 := range existingEntities {
if e1 == e2 {
// 检测事实冲突:内容语义矛盾
if isContradiction(newContent, existingContent) {
conflicts = append(conflicts, &Conflict{
Type: ConflictFact,
Entity: e1,
Description: "事实冲突:新内容与已有记录矛盾",
Strategy: "ask_user",
Status: "pending",
CreatedAt: time.Now(),
})
}
}
}
}
}
return conflicts
}
// AutoResolve 自动裁决冲突latest_wins / primary_wins
func (cd *ConflictDetector) AutoResolve(conflict *Conflict) string {
if conflict.Strategy == "latest_wins" {
return "latest"
}
if conflict.Strategy == "primary_wins" {
return "primary"
}
return "pending"
}
func toStringSlice(v interface{}) []string {
if arr, ok := v.([]string); ok {
return arr
}
if arr, ok := v.([]interface{}); ok {
var result []string
for _, item := range arr {
if s, ok := item.(string); ok {
result = append(result, s)
}
}
return result
}
return nil
}
func isContradiction(a, b string) bool {
// 简单启发式:重叠词 > 50% 但存在否定词差异
wordsA := strings.Fields(strings.ToLower(a))
wordsB := strings.Fields(strings.ToLower(b))
setA := make(map[string]bool)
for _, w := range wordsA {
setA[w] = true
}
overlap := 0
negInA := containsNeg(wordsA)
negInB := containsNeg(wordsB)
for _, w := range wordsB {
if setA[w] {
overlap++
}
}
totalOverlap := float64(overlap) / math.Max(float64(len(wordsA)), float64(len(wordsB)))
return totalOverlap > 0.5 && negInA != negInB
}
func containsNeg(words []string) bool {
negs := []string{"not", "no", "don't", "doesn't", "false", "错误", "不是", "没有", "禁止", "不允许"}
for _, w := range words {
for _, n := range negs {
if strings.Contains(w, n) {
return true
}
}
}
return false
}
// ─── 遗忘策略 ────────────────────────────────────────────
type Forgetter struct {
decayRate float64
}
func NewForgetter() *Forgetter {
return &Forgetter{decayRate: 0.015}
}
// ShouldForget 判断记忆是否该被遗忘
func (f *Forgetter) ShouldForget(lastAccessed time.Time, recallCount int, tier string) bool {
if tier == "core" {
return false // 核心记忆永不遗忘
}
days := time.Since(lastAccessed).Hours() / 24
score := 1.0 - days*f.decayRate
if score < 0.1 {
score = 0.1
}
// recallCount > 0 减缓衰减
score += float64(recallCount) * 0.05
return score < 0.2
}
// DecayScore 计算衰减分数
func (f *Forgetter) DecayScore(lastAccessed time.Time, recallCount int) float64 {
days := time.Since(lastAccessed).Hours() / 24
score := 1.0 - days*f.decayRate
if score < 0.1 {
score = 0.1
}
score += float64(recallCount) * 0.05
if score > 1.0 {
score = 1.0
}
return math.Round(score*100) / 100
}
// ─── 知识图谱 ────────────────────────────────────────────
type GraphStore struct {
db *sql.DB
}
func NewGraphStore(db *sql.DB) (*GraphStore, error) {
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS graph_nodes (
id TEXT PRIMARY KEY, name TEXT, type TEXT, namespace TEXT, created_at TEXT
)`)
if err != nil {
return nil, err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS graph_edges (
id TEXT PRIMARY KEY, source TEXT, target TEXT, relation TEXT, weight REAL, namespace TEXT, created_at TEXT,
FOREIGN KEY (source) REFERENCES graph_nodes(id),
FOREIGN KEY (target) REFERENCES graph_nodes(id)
)`)
if err != nil {
return nil, err
}
return &GraphStore{db: db}, nil
}
// AddNode 添加节点
func (gs *GraphStore) AddNode(id, name, nodeType, namespace string) error {
_, err := gs.db.Exec(
"INSERT OR REPLACE INTO graph_nodes (id, name, type, namespace, created_at) VALUES (?, ?, ?, ?, ?)",
id, name, nodeType, namespace, time.Now().Format(time.RFC3339))
return err
}
// AddEdge 添加边
func (gs *GraphStore) AddEdge(id, source, target, relation, namespace string, weight float64) error {
_, err := gs.db.Exec(
"INSERT OR REPLACE INTO graph_edges (id, source, target, relation, weight, namespace, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
id, source, target, relation, weight, namespace, time.Now().Format(time.RFC3339))
return err
}
// Navigate 多跳导航(双向 BFS
func (gs *GraphStore) Navigate(entity string, maxHops int, namespace string) ([]map[string]interface{}, error) {
visited := map[string]bool{entity: true}
queue := []string{entity}
var paths []map[string]interface{}
for hop := 1; hop <= maxHops && len(queue) > 0; hop++ {
var nextQueue []string
for _, current := range queue {
rows, err := gs.db.Query(
`SELECT id, source, target, relation, weight FROM graph_edges
WHERE (source = ? OR target = ?) AND namespace = ?`,
current, current, namespace)
if err != nil {
continue
}
for rows.Next() {
var id, source, target, relation string
var weight float64
rows.Scan(&id, &source, &target, &relation, &weight)
neighbor := target
if current == target {
neighbor = source
}
if visited[neighbor] {
continue
}
visited[neighbor] = true
nextQueue = append(nextQueue, neighbor)
paths = append(paths, map[string]interface{}{
"edge_id": id, "source": current, "target": neighbor,
"relation": relation, "weight": weight, "hop": hop,
})
}
rows.Close()
}
queue = nextQueue
}
return paths, nil
}
// Prune 修剪图谱(删除孤立节点、低权重边)
func (gs *GraphStore) Prune(minWeight float64) error {
_, err := gs.db.Exec("DELETE FROM graph_edges WHERE weight < ?", minWeight)
if err != nil {
return err
}
_, err = gs.db.Exec(`DELETE FROM graph_nodes WHERE id NOT IN
(SELECT DISTINCT source FROM graph_edges UNION SELECT DISTINCT target FROM graph_edges)`)
return err
}

View File

@ -0,0 +1,316 @@
// 织忆 MemoryWeave — 自优化引擎
package selfoptimize
import (
"math"
"sync"
"time"
)
// ─── 自优化仪表盘 ────────────────────────────────────────
type Dashboard struct {
mu sync.RWMutex
UsefulCount int `json:"useful_count"`
NotUsefulCount int `json:"not_useful_count"`
TotalRecalls int `json:"total_recalls"`
HitCount int `json:"hit_count"`
ClosedGaps int `json:"closed_gaps"`
TotalGaps int `json:"total_gaps"`
CascadeFixedTotal int `json:"cascade_fixed_total"`
TotalFixes int `json:"total_fixes"`
DeprecatedToday int `json:"deprecated_today"`
DistillLossSum float64 `json:"distill_loss_sum"`
DistillLossCount int `json:"distill_loss_count"`
AutoResolvedConflicts int `json:"auto_resolved_conflicts"`
TotalConflicts int `json:"total_conflicts"`
}
var Dash = &Dashboard{}
// Metrics 返回 7 项核心指标
func (d *Dashboard) Metrics() map[string]float64 {
d.mu.RLock()
defer d.mu.RUnlock()
usefulRate := 0.0
if d.UsefulCount+d.NotUsefulCount > 0 {
usefulRate = float64(d.UsefulCount) / float64(d.UsefulCount+d.NotUsefulCount)
}
hitRate := 0.0
if d.TotalRecalls > 0 {
hitRate = float64(d.HitCount) / float64(d.TotalRecalls)
}
gapRate := 0.0
if d.TotalGaps > 0 {
gapRate = float64(d.ClosedGaps) / float64(d.TotalGaps)
}
cascadeRate := 0.0
if d.TotalFixes > 0 {
cascadeRate = float64(d.CascadeFixedTotal) / float64(d.TotalFixes)
}
avgLoss := 0.0
if d.DistillLossCount > 0 {
avgLoss = d.DistillLossSum / float64(d.DistillLossCount)
}
autoRate := 0.0
if d.TotalConflicts > 0 {
autoRate = float64(d.AutoResolvedConflicts) / float64(d.TotalConflicts)
}
return map[string]float64{
"recall_usefulness_rate": math.Round(usefulRate*100) / 100,
"recall_hit_rate": math.Round(hitRate*100) / 100,
"gap_closure_rate": math.Round(gapRate*100) / 100,
"cascade_fix_rate": math.Round(cascadeRate*100) / 100,
"deprecated_per_day": float64(d.DeprecatedToday),
"avg_distill_loss": math.Round(avgLoss*100) / 100,
"auto_resolve_rate": math.Round(autoRate*100) / 100,
}
}
func (d *Dashboard) RecordRecall(hit bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.TotalRecalls++
if hit {
d.HitCount++
}
}
func (d *Dashboard) RecordFeedback(useful bool) {
d.mu.Lock()
defer d.mu.Unlock()
if useful {
d.UsefulCount++
} else {
d.NotUsefulCount++
}
}
// ─── 知识缺口检测 ────────────────────────────────────────
type GapType string
const (
GapUnknown GapType = "A" // 真不知道
GapSynonym GapType = "B" // 同义词不匹配
GapRecallFailed GapType = "C" // 召回失败
GapFragmented GapType = "D" // 碎片化
)
type Gap struct {
Topic string `json:"topic"`
Type GapType `json:"type"`
MissCount int `json:"miss_count"`
CreatedAt time.Time `json:"created_at"`
Closed bool `json:"closed"`
}
type GapDetector struct {
mu sync.RWMutex
gaps map[string]*Gap
misses map[string]int
}
func NewGapDetector() *GapDetector {
return &GapDetector{
gaps: make(map[string]*Gap),
misses: make(map[string]int),
}
}
// RecordMiss 记录一次召回失败
func (gd *GapDetector) RecordMiss(topic string) *Gap {
gd.mu.Lock()
defer gd.mu.Unlock()
gd.misses[topic]++
if gd.misses[topic] >= 3 {
if _, exists := gd.gaps[topic]; !exists {
gap := &Gap{
Topic: topic,
Type: gd.classifyGap(topic),
MissCount: gd.misses[topic],
CreatedAt: time.Now(),
}
gd.gaps[topic] = gap
return gap
}
}
return nil
}
func (gd *GapDetector) classifyGap(topic string) GapType {
// 简单启发式:大写缩写 → 同义词;中文 → 可能是真的不知道
for _, r := range topic {
if r >= 'A' && r <= 'Z' {
return GapSynonym
}
}
return GapUnknown
}
func (gd *GapDetector) List() []*Gap {
gd.mu.RLock()
defer gd.mu.RUnlock()
var result []*Gap
for _, g := range gd.gaps {
result = append(result, g)
}
return result
}
func (gd *GapDetector) Close(topic string) {
gd.mu.Lock()
defer gd.mu.Unlock()
if g, ok := gd.gaps[topic]; ok {
g.Closed = true
}
}
// ─── 因果追踪 ────────────────────────────────────────────
type TraceEntry struct {
MemoryID string `json:"memory_id"`
Version int `json:"version"`
Content string `json:"content"`
Source string `json:"source"` // muchen_oral / config_parse / agent_infer / llm_distill
Trigger string `json:"trigger"` // 什么触发了这次修改
UpdatedAt time.Time `json:"updated_at"`
}
type CausalTracker struct {
mu sync.RWMutex
entries map[string][]*TraceEntry // memory_id → version history
deps map[string][]string // memory_id → depends_on[]
}
func NewCausalTracker() *CausalTracker {
return &CausalTracker{
entries: make(map[string][]*TraceEntry),
deps: make(map[string][]string),
}
}
// RecordVersion 记录版本变更
func (ct *CausalTracker) RecordVersion(memoryID, content, source, trigger string) {
ct.mu.Lock()
defer ct.mu.Unlock()
entry := &TraceEntry{
MemoryID: memoryID,
Version: len(ct.entries[memoryID]) + 1,
Content: content,
Source: source,
Trigger: trigger,
UpdatedAt: time.Now(),
}
ct.entries[memoryID] = append(ct.entries[memoryID], entry)
}
// AddDependency A depends_on B
func (ct *CausalTracker) AddDependency(a, b string) {
ct.mu.Lock()
defer ct.mu.Unlock()
ct.deps[a] = append(ct.deps[a], b)
}
// GetAffected 当 memoryID 被修正时,返回所有依赖它的记忆
func (ct *CausalTracker) GetAffected(memoryID string, visited map[string]bool) []string {
ct.mu.RLock()
defer ct.mu.RUnlock()
if visited == nil {
visited = make(map[string]bool)
}
if visited[memoryID] {
return nil
}
visited[memoryID] = true
var affected []string
for dependent, deps := range ct.deps {
for _, d := range deps {
if d == memoryID && !visited[dependent] {
affected = append(affected, dependent)
affected = append(affected, ct.GetAffected(dependent, visited)...)
}
}
}
return affected
}
// SourceTrust 来源信任度
func SourceTrust(source string) float64 {
switch source {
case "muchen_oral":
return 1.0
case "muchen_feishu":
return 0.95
case "config_parse":
return 0.7
case "agent_infer":
return 0.5
case "llm_distill":
return 0.4
default:
return 0.3
}
}
// IsVolatile 判断一条记忆是否易变(频繁修正)
func (ct *CausalTracker) IsVolatile(memoryID string) bool {
ct.mu.RLock()
defer ct.mu.RUnlock()
return len(ct.entries[memoryID]) >= 3
}
// ─── 记忆预取 ────────────────────────────────────────────
type PrefetchGraph struct {
mu sync.RWMutex
coOccurs map[string]map[string]int // A → {B: count}
}
func NewPrefetchGraph() *PrefetchGraph {
return &PrefetchGraph{coOccurs: make(map[string]map[string]int)}
}
func (pg *PrefetchGraph) RecordCoAccess(a, b string) {
pg.mu.Lock()
defer pg.mu.Unlock()
if pg.coOccurs[a] == nil {
pg.coOccurs[a] = make(map[string]int)
}
pg.coOccurs[a][b]++
}
// GetPrefetch 获取某个 query 的预取候选项
func (pg *PrefetchGraph) GetPrefetch(query string) []string {
pg.mu.RLock()
defer pg.mu.RUnlock()
related := pg.coOccurs[query]
if related == nil {
return nil
}
var result []string
for topic, count := range related {
total := 0
for _, c := range pg.coOccurs[query] {
total += c
}
prob := float64(count) / float64(total)
if prob > 0.6 {
result = append(result, topic)
}
}
return result
}

View File

@ -10,6 +10,7 @@ import (
"io"
"net/http"
"os"
"time"
"github.com/xiaoxue/memoryweave/internal/models"
)
@ -159,7 +160,7 @@ func (c *LanceClient) SoftDelete(id, reason string) error {
}
// Stats 返回各表记录数。
func (c *LanceClient) Stats() (map[string]int, error) {
func (c *LanceClient) Stats() (map[string]interface{}, error) {
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/v1/stats", c.baseURL), nil)
resp, err := c.httpClient.Do(req)
if err != nil {
@ -167,9 +168,55 @@ func (c *LanceClient) Stats() (map[string]int, error) {
}
defer resp.Body.Close()
var stats map[string]int
var stats map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
return nil, fmt.Errorf("decode stats: %w", err)
}
return stats, nil
}
// InsertEpisode 插入一条 episode 记录,返回 ID
func (c *LanceClient) InsertEpisode(agentID, namespace, content, category string) (string, error) {
id := fmt.Sprintf("ep_%d", time.Now().UnixNano())
ep := models.EpisodeRecord{
ID: id,
AgentID: agentID,
Namespace: namespace,
Content: content,
Category: category,
CreatedAt: time.Now(),
}
return id, c.Insert("episodes", ep)
}
// GetTopByQuality 按 quality_score 降序返回高质量记忆
func (c *LanceClient) GetTopByQuality(agentID string, limit int) ([]models.MemoryRecord, error) {
reqBody := map[string]interface{}{
"top_k": limit,
"filter": "is_deleted = false",
"order": "quality_score DESC",
}
body, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v1/table/memories/query", c.baseURL),
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var results []models.MemoryRecord
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
return nil, err
}
return results, nil
}
// InsertMemory 插入一条蒸馏后的记忆
func (c *LanceClient) InsertMemory(m models.MemoryRecord) error {
return c.Insert("memories", m)
}
// LanceDB 类型别名,兼容路由层引用
type LanceDB = LanceClient