memoryweave/go/internal/governance/governance.go

348 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 织忆 MemoryWeave — 治理引擎:冲突检测 + 遗忘 + 知识图谱
package governance
import (
"math"
"strings"
"sync"
"time"
"unicode"
)
// ─── 冲突检测 ────────────────────────────────────────────
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
}
// containsNegCN 检查文本中是否含中文否定词或单字否定
func containsNegCN(text string) bool {
negPhrases := []string{"不是", "没有", "不存在", "禁止", "不允许", "无", "非"}
for _, n := range negPhrases {
if strings.Contains(text, n) {
return true
}
}
for _, r := range text {
if r == '不' || r == '没' || r == '莫' || r == '别' {
return true
}
}
return false
}
// splitWordsCN 中文按字符级切分(过滤标点),英文按空格分词
func splitWordsCN(text string) []string {
if len(text) == 0 {
return nil
}
hasCN := false
for _, r := range text {
if unicode.Is(unicode.Han, r) {
hasCN = true
break
}
}
if hasCN {
var result []string
for _, r := range text {
if unicode.Is(unicode.Han, r) || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
result = append(result, strings.ToLower(string(r)))
}
}
return result
}
return strings.Fields(strings.ToLower(text))
}
// IsContradiction 检查两条内容是否语义矛盾
// 中文/混合文本:字符级重叠 + 否定词差异
// 英文/空格文本:单词级重叠(原有逻辑)
func IsContradiction(a, b string) bool {
wordsA := splitWordsCN(a)
wordsB := splitWordsCN(b)
setA := make(map[string]bool)
for _, w := range wordsA {
setA[w] = true
}
overlap := 0
negInA := containsNegCN(a)
negInB := containsNegCN(b)
negInWordsA := containsNeg(wordsA) // 英文否定
negInWordsB := containsNeg(wordsB)
if negInA != negInB || negInWordsA != negInWordsB {
// 有否定词差异,再检查重叠度
} else {
// 无否定词差异,直接返回 false
return false
}
for _, w := range wordsB {
if setA[w] {
overlap++
}
}
maxLen := len(wordsA)
if len(wordsB) > maxLen {
maxLen = len(wordsB)
}
if maxLen == 0 {
return false
}
// 阈值 0.3中文字符级粒度细0.5 过高)
return float64(overlap)/float64(maxLen) > 0.3
}
// 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 {
for _, n := range negs {
if strings.Contains(w, n) {
return true
}
}
}
return false
}
// ─── 遗忘策略 ────────────────────────────────────────────
// AgentTypeDecay 不同 Agent 类型的衰减率
// 短期 Agent如一次性的任务 agent衰减快长期 Agent如主 agent衰减慢
var AgentTypeDecay = map[string]float64{
"default": 0.015, // 通用
"longterm": 0.005, // 长期记忆型
"shortterm": 0.050, // 短期任务型
"ephemeral": 0.200, // 会话级别
"hermes": 0.008, // Hermes 主 agent
"researcher": 0.030, // 研究型 agent
"executor": 0.040, // 执行型 agent
"watcher": 0.025, // 监控型 agent
}
type Forgetter struct {
agentType string
decayRate float64
}
func NewForgetter() *Forgetter {
// 2026-09-07 方案A: 0.015→0.03 曾致误删(36天库龄下 0-recall 长记忆全被清, 405字CNB经验被删),
// 回滚 0.015。碎片清除由碎片快速道(server.go <30字&>20天)负责, 普通记忆 53 天老化合理。
return &Forgetter{agentType: "default", decayRate: 0.015}
}
// NewForgetterWithType 按 Agent 类型创建遗忘器
func NewForgetterWithType(agentType string) *Forgetter {
rate, ok := AgentTypeDecay[agentType]
if !ok {
rate = 0.015
}
return &Forgetter{agentType: agentType, decayRate: rate}
}
// SetAgentType 动态调整遗忘器类型
func (f *Forgetter) SetAgentType(agentType string) {
rate, ok := AgentTypeDecay[agentType]
if !ok {
rate = 0.015
}
f.agentType = agentType
f.decayRate = rate
}
// AgentType 返回当前类型
func (f *Forgetter) AgentType() string {
return f.agentType
}
// ShouldForget 判断记忆是否该被遗忘
// graphDegree: 该记忆关联实体的图谱节点度(连接数),度越高越优先保留
func (f *Forgetter) ShouldForget(lastAccessed time.Time, recallCount int, tier string, graphDegree ...int) 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 减缓衰减
// 2026-09-07 方案A: 0.05→0.005。原 0.05/次 + 无 cap → recall_count 457-540 时 +25 分,
// 永不遗忘 (日志大量 recall_count 500+ = 每次搜索命中都 ++, 虚高保命)。
// 现 0.005/次, cap 30 次后贡献 ≤0.15 分 ≈ 5 天保护, 合理。
score += float64(recallCount) * 0.005
// 图谱节点度 > 5 时,每超过 1 度 + 0.03 保留分E4.3: 图谱推理参与遗忘决策)
if len(graphDegree) > 0 && graphDegree[0] > 5 {
score += float64(graphDegree[0]-5) * 0.03
}
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
}
// ScanAndForget 检查单条记忆是否需要衰减(供触发器 decay 使用)
// 实际遗忘操作:降低 importance 到 0.1,标记为 stale
func (f *Forgetter) ScanAndForget(memoryID string) bool {
// 标记为遗忘候选项 — 实际软删除由 trigger loop 中的 ldb.SoftDelete 执行
_ = memoryID
return true
}
// ScheduleForget 执行实际遗忘操作(由 trigger loop 调用)
func (f *Forgetter) ScheduleForget(id string, ldb LanceDBSoftDeleter) {
if id == "" {
return
}
_ = ldb.SoftDelete(id, "auto_forget_"+f.agentType)
}
// LanceDBSoftDeleter SoftDelete 接口(防止循环依赖)
type LanceDBSoftDeleter interface {
SoftDelete(id, reason string) error
}
// ListActive 返回所有活跃冲突
func (cd *ConflictDetector) ListActive() []*Conflict {
cd.mu.RLock()
defer cd.mu.RUnlock()
var list []*Conflict
for _, c := range cd.active {
if c.Status == "pending" {
list = append(list, c)
}
}
return list
}
// PendingCount 返回待处理冲突数
func (cd *ConflictDetector) PendingCount() int {
cd.mu.RLock()
defer cd.mu.RUnlock()
n := 0
for _, c := range cd.active {
if c.Status == "pending" {
n++
}
}
return n
}
// Resolve 解决冲突
func (cd *ConflictDetector) Resolve(id, resolution, winner string) error {
cd.mu.Lock()
defer cd.mu.Unlock()
if c, ok := cd.active[id]; ok {
c.Status = "resolved"
c.Strategy = resolution
}
return nil
}