348 lines
9.4 KiB
Go
348 lines
9.4 KiB
Go
// 织忆 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
|
||
}
|