diff --git a/go/internal/api/routes/core.go b/go/internal/api/routes/core.go index 270741f..9fe46d6 100644 --- a/go/internal/api/routes/core.go +++ b/go/internal/api/routes/core.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/xiaoxue/memoryweave/internal/governance" "github.com/xiaoxue/memoryweave/internal/metrics" "github.com/xiaoxue/memoryweave/internal/models" "github.com/xiaoxue/memoryweave/internal/selfoptimize" @@ -17,20 +18,22 @@ import ( // API 持有所有依赖 type API struct { - LanceDB storage.LanceDB - Embedder *storage.Embedder - Reranker *storage.Reranker - Pipeline *storage.RecallPipeline + LanceDB storage.LanceDB + Embedder *storage.Embedder + Reranker *storage.Reranker + Pipeline *storage.RecallPipeline + ConflictDetector *governance.ConflictDetector } -func NewAPI(ldb storage.LanceDB, emb *storage.Embedder, rerank *storage.Reranker) *API { +func NewAPI(ldb storage.LanceDB, emb *storage.Embedder, rerank *storage.Reranker, cd *governance.ConflictDetector) *API { pipeline := storage.NewRecallPipeline(emb, ldb, rerank) pipeline.SetPrefetchPusher(&WSPrefetchAdapter{}) return &API{ - LanceDB: ldb, - Embedder: emb, - Reranker: rerank, - Pipeline: pipeline, + LanceDB: ldb, + Embedder: emb, + Reranker: rerank, + Pipeline: pipeline, + ConflictDetector: cd, } } @@ -112,6 +115,16 @@ func (a *API) Commit(w http.ResponseWriter, r *http.Request) { } } + // 3c. 矛盾检测:对所有相似记忆检查内容矛盾 + // (3a/3b 已处理 exact/near-dup,3c 检查语义矛盾) + var conflictSources []string + for _, existing := range similar { + if existing.Content != req.Content { + conflictSources = append(conflictSources, existing.Content) + } + } + conflicts := a.ConflictDetector.DetectContradiction(req.Content, conflictSources) + // 4. 新增 memID := fmt.Sprintf("mem_%d", time.Now().UnixNano()) mem := models.MemoryRecord{ @@ -156,9 +169,13 @@ func (a *API) Commit(w http.ResponseWriter, r *http.Request) { } }() - respond(w, 201, map[string]string{ + respData := map[string]interface{}{ "episode_id": epID, "memory_id": memID, "status": "ok", - }) + } + if len(conflicts) > 0 { + respData["conflicts"] = conflicts + } + respond(w, 201, respData) go AutoDistillTrigger(epID, req.Content, req.Category, req.Namespace, req.AgentID) } @@ -237,6 +254,33 @@ func (a *API) Recall(w http.ResponseWriter, r *http.Request) { respondError(w, 500, "recall failed: "+err.Error()) return } + + // E4.2: 跨 agent 知识共享 — recall 结果 < 3 时,补充搜索 "shared" namespace + if len(results) < 3 && req.Namespace != "shared" { + vec, encErr := a.Embedder.EncodeSingle(req.Query) + if encErr == nil { + shared, _ := a.LanceDB.Search("memories", vec, 5, "shared") + for _, m := range shared { + // 去重:跳过已在 own namespace 结果中的记忆 + alreadyHave := false + for _, r := range results { + if r.ID == m.ID { + alreadyHave = true + break + } + } + if !alreadyHave { + results = append(results, models.RecallResult{ + ID: m.ID, + Content: m.Content, + Category: m.Category, + Score: 0.5, // shared 结果降权,使用默认分 + }) + } + } + } + } + // Record recall hit rate (has results = hit, empty = miss) selfoptimize.Dash.RecordRecall(len(results) > 0) // VProp 自动记录:命中 → success,空结果 → failure diff --git a/go/internal/api/server.go b/go/internal/api/server.go index 9291e56..fe79b4b 100644 --- a/go/internal/api/server.go +++ b/go/internal/api/server.go @@ -75,7 +75,9 @@ func NewServer() http.Handler { ldb = storage.NewMemLanceClient(emb) log.Printf("[zhiyid] 存储后端: 内存(零依赖)") } - api := routes.NewAPI(ldb, emb, rerank) + // 冲突检测器(E4.1: Commit 时矛盾检测依赖此实例) + conflictDetector := governance.NewConflictDetector() + api := routes.NewAPI(ldb, emb, rerank, conflictDetector) // 启动时初始化 Prometheus 指标 go func() { @@ -115,7 +117,6 @@ func NewServer() http.Handler { routes.SetGraphUpdater(graphUpdater) // 冲突 - conflictDetector := governance.NewConflictDetector() conflictAPI := routes.NewConflictAPI(conflictDetector) // ─── 缺口 diff --git a/go/internal/governance/governance.go b/go/internal/governance/governance.go index 0cfbbbe..0fc2c8a 100644 --- a/go/internal/governance/governance.go +++ b/go/internal/governance/governance.go @@ -50,7 +50,7 @@ func (cd *ConflictDetector) Scan(newContent string, newEntities []string, existi for _, e2 := range existingEntities { if e1 == e2 { // 检测事实冲突:内容语义矛盾 - if isContradiction(newContent, existingContent) { + if IsContradiction(newContent, existingContent) { conflicts = append(conflicts, &Conflict{ Type: ConflictFact, Entity: e1, @@ -95,7 +95,7 @@ func toStringSlice(v interface{}) []string { return nil } -func isContradiction(a, b string) bool { +func IsContradiction(a, b string) bool { // 简单启发式:重叠词 > 50% 但存在否定词差异 wordsA := strings.Fields(strings.ToLower(a)) wordsB := strings.Fields(strings.ToLower(b)) @@ -118,6 +118,18 @@ func isContradiction(a, b string) bool { return totalOverlap > 0.5 && negInA != negInB } +// DetectContradiction 检查 newContent 是否与 existingContents 中任意一条矛盾 +// 返回矛盾的记忆内容列表 +func (cd *ConflictDetector) DetectContradiction(newContent string, existingContents []string) []string { + var conflicting []string + for _, ec := range existingContents { + if IsContradiction(newContent, ec) { + conflicting = append(conflicting, ec) + } + } + return conflicting +} + func containsNeg(words []string) bool { negs := []string{"not", "no", "don't", "doesn't", "false", "错误", "不是", "没有", "禁止", "不允许"} for _, w := range words { @@ -179,7 +191,8 @@ func (f *Forgetter) AgentType() string { } // ShouldForget 判断记忆是否该被遗忘 -func (f *Forgetter) ShouldForget(lastAccessed time.Time, recallCount int, tier string) bool { +// graphDegree: 该记忆关联实体的图谱节点度(连接数),度越高越优先保留 +func (f *Forgetter) ShouldForget(lastAccessed time.Time, recallCount int, tier string, graphDegree ...int) bool { if tier == "core" { return false // 核心记忆永不遗忘 } @@ -190,6 +203,10 @@ func (f *Forgetter) ShouldForget(lastAccessed time.Time, recallCount int, tier s } // recallCount > 0 减缓衰减 score += float64(recallCount) * 0.05 + // 图谱节点度 > 5 时,每超过 1 度 + 0.03 保留分(E4.3: 图谱推理参与遗忘决策) + if len(graphDegree) > 0 && graphDegree[0] > 5 { + score += float64(graphDegree[0]-5) * 0.03 + } return score < 0.2 }