mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 12:53:35 +00:00
上一版场景是声明/派生的:调用方写 scene="chan:qq",或由通道机械派生。 那不是涌现,是贴标签——标签谁定、怎么定全靠人。按「像人一样:干了什么事, 后续类似场面自动唤起对应记忆」的要求重做。 机制(全部取自运行时可观察量,无需模型配合、无需人工标注): - **场面指纹 Situation**:每轮采集 `chan:xx / peer:xx / peer_group:xx / tool:xx / topic:xx / part:xx`。权重按种类:通道与对象最强(1.0), 工具次之(0.8),话题是软信号(0.4),时段最弱(0.2)。 - **归属判定用加权 Jaccard**(不是字符串相等):共享特征权重和 / 并集权重和。 加权是必须的——`chan:qq` 与 `topic:排班` 的证据力差 2.5 倍,不加权会让 一次偶然的话题重合把两个不同场面并成一个。 - **涌现**:同类指纹重复到 minSceneEvidence=2 次才长出场景 (首次只登记 situation_evidence 足迹)。一次性的交互不是「场面」, 给它建场景会让库被一次性事件撑满、之后每次路过都召回一堆只发生过一次的事。 - **强化**:场景每次重现 strength+1、并入新特征。 - **唤起**:RecallBySituation 按**相似度**取回(阈值 0.35,比归属阈值 0.5 低 ——想不起来是损失,多想起一条只是多几行上下文),与措辞无关。 - **遗忘**:DecaySceneRefs 按半衰期让久未重现的关联淡出,低于 floor 直接删; 已接进 archive 心跳(半衰期 30 天,比「这个月没做过这类事」更久)。 三个必须讲清的边界: 1. 一轮只解析一次场景(TaskFrame 缓存)——多解析一次就多记一次强度, 「工具调得多」会被误读成「这个场面更常出现」。 2. 声明与涌现**并存**:声明是「我知道这是哪个场面」(插件注入点最清楚), 涌现是「这轮看起来像哪个场面」。两者都进召回。 3. 记忆挂载全自动:memory_commit 没写 scene 时落到本轮涌现场景, 模型不需要知道场景这回事。 验证:go build/vet 干净,go test -count=1 ./... 全绿。 新增用例(核心证据): - TestSceneEmergesFromRepetition:首次不建场景 → 第 2 次同类场面长出场景 → 同场面**不同话题**仍并入同一场景 → 换通道的场面自己长出独立场景(共 2 个)→ 强度随重现增长、特征多条。全程没有任何人声明过场景键。 - TestSceneRecallsBySituationNotWording:场面里写下的规则,换措辞后仍被 自动唤起(含原句),无关场面不唤起。 - TestSceneRefDecay:一个半衰期权重减半、第二个半衰期低于 floor 被清掉, 仍在重现的场景不受影响。 - TestSituationFeaturesFor:指纹维度齐全、归一化、数值 group_id 转换、nil 安全。
124 lines
4.3 KiB
Go
124 lines
4.3 KiB
Go
// memgc 清理图记忆里已存在的「噪音实体」「孤立实体」及其关系。
|
||
//
|
||
// 为什么需要这个命令:噪音闸门(internal/memory.IsNoiseEntity)只能拦住
|
||
// **新写入**的噪音。旧库里那批(常用词 / 归档内部标记 / 模板摘要回声)是
|
||
// 闸门上线前攒下的存量,没人清就一直在——热实体被它们占着,召回预算被
|
||
// 同构垃圾边挤满。清理是一次性动作,但需要可重复执行、可先看不做。
|
||
//
|
||
// 两件事分开开关:-orphans 处理的是「零关系的空节点」(清理噪音后另一端
|
||
// 留下的壳),它们的名字本身可能没问题,但已经不在图里了。
|
||
//
|
||
// 用法(默认 dry-run,只列不删):
|
||
//
|
||
// memgc -db /home/newqqagent/memory/graph.db
|
||
// memgc -db /home/newqqagent/memory/graph.db -orphans -apply
|
||
//
|
||
// 清理生产库前请先备份:sqlite3 graph.db ".backup 'graph.db.bak-<ts>'"
|
||
// 不要用 cp —— WAL 模式下会复制出主库与 -wal 不一致的快照。
|
||
package main
|
||
|
||
import (
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||
)
|
||
|
||
func main() {
|
||
path := flag.String("db", "", "graph.db 路径(必填)")
|
||
apply := flag.Bool("apply", false, "真正删除;不加则只 dry-run 打印")
|
||
orphans := flag.Bool("orphans", false, "同时处理「零关系孤立实体」(先被清理的噪音在另一端留下的空节点)")
|
||
tagScene := flag.String("tag-scene", "", "存量引导:把实体名匹配 -entity-glob 的活跃关系标进该场景键(如 chan:qq)")
|
||
entityGlob := flag.String("entity-glob", "", "配合 -tag-scene 的 GLOB 模式(如 *QQ*)。GLOB 区分大小写,避免把 /home/newqqagent 这类路径卷进场景")
|
||
sceneStats := flag.Bool("scene-stats", false, "只打印场景规模摘要")
|
||
flag.Parse()
|
||
|
||
if *path == "" {
|
||
flag.Usage()
|
||
log.Fatal("memgc: 必须指定 -db")
|
||
}
|
||
|
||
g, err := memory.NewGraphDB(*path)
|
||
if err != nil {
|
||
log.Fatalf("memgc: open %s: %v", *path, err)
|
||
}
|
||
defer g.Close()
|
||
|
||
if *sceneStats {
|
||
stats, err := g.SceneStats()
|
||
if err != nil {
|
||
log.Fatalf("memgc: scene stats: %v", err)
|
||
}
|
||
fmt.Printf("场景 %d 个:\n", len(stats))
|
||
for _, st := range stats {
|
||
fmt.Printf(" %-44s refs=%-5d rel=%-5d ent=%-4d strength=%-4d features=%-3d updated=%s\n",
|
||
st.Key, st.Refs, st.Relations, st.Entities, st.Strength, st.Features,
|
||
st.UpdatedAt.Format("2006-01-02 15:04"))
|
||
}
|
||
return
|
||
}
|
||
|
||
// 存量引导:场景是后引入的维度,老库里的规则(那批 QQ 规则就是典型)
|
||
// 没有任何场景引用,不补挂就永远吃不到场景召回。
|
||
if *tagScene != "" {
|
||
if *entityGlob == "" {
|
||
log.Fatal("memgc: -tag-scene 需要配套 -entity-glob(如 '*QQ*');不做自动猜测")
|
||
}
|
||
n, err := g.TagSceneByEntityGlob(*tagScene, *entityGlob, !*apply)
|
||
if err != nil {
|
||
log.Fatalf("memgc: tag scene: %v", err)
|
||
}
|
||
if *apply {
|
||
fmt.Printf("[APPLIED] 已把 %d 条关系标进场景 %q\n", n, *tagScene)
|
||
} else {
|
||
fmt.Printf("[DRY-RUN] 将把 %d 条关系标进场景 %q(未写库)\n", n, *tagScene)
|
||
}
|
||
return
|
||
}
|
||
|
||
junk, err := g.NoiseEntities()
|
||
if err != nil {
|
||
log.Fatalf("memgc: scan: %v", err)
|
||
}
|
||
|
||
fmt.Printf("噪音实体 %d 个:\n", len(junk))
|
||
for _, e := range junk {
|
||
fmt.Printf(" %-64s type=%-8s mentions=%d\n", e.Name, e.Type, e.MentionCount)
|
||
}
|
||
|
||
de, dr, err := g.PurgeNoise(!*apply)
|
||
if err != nil {
|
||
log.Fatalf("memgc: purge: %v", err)
|
||
}
|
||
if *apply {
|
||
fmt.Printf("[APPLIED] 噪音:已删除 实体=%d 关系=%d\n", de, dr)
|
||
} else {
|
||
fmt.Printf("[DRY-RUN] 噪音:将删除 实体=%d 关系=%d(未写库,加 -apply 才落地)\n", de, dr)
|
||
}
|
||
|
||
if *orphans {
|
||
list, err := g.OrphanEntities()
|
||
if err != nil {
|
||
log.Fatalf("memgc: orphans: %v", err)
|
||
}
|
||
fmt.Printf("孤立实体(零关系)%d 个:\n", len(list))
|
||
for _, e := range list {
|
||
fmt.Printf(" %-64s type=%-8s mentions=%d\n", e.Name, e.Type, e.MentionCount)
|
||
}
|
||
n, err := g.PurgeOrphans(!*apply)
|
||
if err != nil {
|
||
log.Fatalf("memgc: purge orphans: %v", err)
|
||
}
|
||
if *apply {
|
||
fmt.Printf("[APPLIED] 孤立实体:已删除 %d 个\n", n)
|
||
} else {
|
||
fmt.Printf("[DRY-RUN] 孤立实体:将删除 %d 个\n", n)
|
||
}
|
||
}
|
||
|
||
if *apply {
|
||
fmt.Println("提示:运行中的进程会在下一个 archive 心跳(Indexer.Sync)重建实体名向量索引,无需重启。")
|
||
}
|
||
}
|