mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 18:38:11 +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 安全。
105 lines
3.2 KiB
Go
105 lines
3.2 KiB
Go
package core
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||
)
|
||
|
||
// TestSceneKeysFor 钉住当前场景的推导优先级:
|
||
// 注入点显式声明 > 通道 > 工具;并列命中且去重。
|
||
func TestSceneKeysFor(t *testing.T) {
|
||
// 通道 + 工具:两个都能独立成立的触发条件,都要带上
|
||
got := sceneKeysFor(&agentIO.InputEvent{Source: "qq"}, "qq_get_message")
|
||
want := []string{"chan:qq", "tool:qq_get_message"}
|
||
if len(got) != len(want) {
|
||
t.Fatalf("sceneKeysFor = %v, want %v", got, want)
|
||
}
|
||
for i := range want {
|
||
if got[i] != want[i] {
|
||
t.Errorf("sceneKeysFor[%d] = %q, want %q", i, got[i], want[i])
|
||
}
|
||
}
|
||
|
||
// 注入点显式声明排最前;归一化生效;重复声明去重
|
||
evt := &agentIO.InputEvent{
|
||
Source: "QQ",
|
||
Payload: map[string]interface{}{"scene": " chan:qq/peer:group_1 "},
|
||
}
|
||
got = sceneKeysFor(evt, "")
|
||
if len(got) != 2 || got[0] != "chan:qq/peer:group_1" || got[1] != "chan:qq" {
|
||
t.Errorf("显式声明应排最前且通道场景归一: %v", got)
|
||
}
|
||
|
||
// 数组形式声明
|
||
evt = &agentIO.InputEvent{
|
||
Source: "webui",
|
||
Payload: map[string]interface{}{"scene": []interface{}{"chan:qq", "task:reminder"}},
|
||
}
|
||
got = sceneKeysFor(evt, "")
|
||
if len(got) != 3 || got[0] != "chan:qq" || got[1] != "task:reminder" || got[2] != "chan:webui" {
|
||
t.Errorf("数组声明未生效: %v", got)
|
||
}
|
||
|
||
// nil 事件不 panic
|
||
if got := sceneKeysFor(nil, ""); len(got) != 0 {
|
||
t.Errorf("nil 事件应无场景: %v", got)
|
||
}
|
||
// 未声明的 payload 键不影响
|
||
evt = &agentIO.InputEvent{Source: "cli", Payload: map[string]interface{}{"recall_policy": "none"}}
|
||
if got := sceneKeysFor(evt, ""); len(got) != 1 || got[0] != "chan:cli" {
|
||
t.Errorf("无 scene 声明时应只有通道场景: %v", got)
|
||
}
|
||
}
|
||
|
||
// TestSituationFeaturesFor 钉住指纹来源:全部是运行时可观察量,
|
||
// 不需要模型配合也不需要人工标注。
|
||
func TestSituationFeaturesFor(t *testing.T) {
|
||
evt := &agentIO.InputEvent{
|
||
Source: "QQ",
|
||
Payload: map[string]interface{}{"group_id": float64(1027993713)},
|
||
}
|
||
feats := situationFeaturesFor(evt, "帮我看看排班表", "qq_get_message")
|
||
kinds := map[string]int{}
|
||
for _, f := range feats {
|
||
kinds[f.Kind]++
|
||
}
|
||
if kinds["chan"] != 1 || kinds["peer_group"] != 1 || kinds["tool"] != 1 || kinds["part"] != 1 {
|
||
t.Fatalf("必备维度缺失: %+v", feats)
|
||
}
|
||
if kinds["topic"] == 0 {
|
||
t.Errorf("话题软特征缺失: %+v", feats)
|
||
}
|
||
if kinds["topic"] > 3 {
|
||
t.Errorf("话题最多 3 个,得到 %d", kinds["topic"])
|
||
}
|
||
|
||
sig := memory.NewSituation(feats...)
|
||
keys := sig.Keys()
|
||
// 归一化 + 数值 id 的转换
|
||
found := false
|
||
for _, k := range keys {
|
||
if k == "chan:qq" {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("通道特征未归一化: %v", keys)
|
||
}
|
||
for _, k := range keys {
|
||
if strings.Contains(k, "peer_group:1027993713") {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("数值型 group_id 未转成特征: %v", keys)
|
||
}
|
||
|
||
// 无事件时不 panic,且只有工具特征时也成立
|
||
if feats := situationFeaturesFor(nil, "", "memory_recall"); len(feats) != 1 {
|
||
t.Errorf("仅工具场景应有 1 个特征: %+v", feats)
|
||
}
|
||
}
|