mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-02 15:23:57 +00:00
feat(memory): 场景双通道——主动声明与被动涌现并存,且互不吞噬
按「声明式的也要支持,相当于主动被动两条路」落实。此前两者只是恰好并存,
没有边界,实测会互相吃掉(下面的坑就是)。
- Triple.Scenes []string(多值):一轮写下的记忆**两条路都挂**。
只挂一条会丢东西——只挂声明则细粒度唤起丢失,只挂涌现则首次交互
(场景还没长出来)没有兜底。单值 Scene 保留兼容。
- TurnScene:Primary 用于写(优先涌现场景,首次退到声明场景兜底),
Keys 是两条路的并集,用于召回(声明+涌动的场景一起进 RecallByScene)。
- EnterSceneWithHint:主动路 EnsureScene(声明即建场景,不等第二次),
被动路 EnterScene(指纹聚类)。写侧由 executeToolCall 把本轮场景集合
传给 memory_commit,模型不需要知道"场景"这回事。
踩到并修掉的坑(两条路互相吞噬):
最初让声明场景也吸收**整轮指纹**,于是 chan:qq 的相似度永远是 1.0,
把后续所有同类轮次全部吃掉 → 被动路再也长不出更细的场面,
实测 turn2.Emergent=true 但 Primary 仍是 chan:qq、没有 auto: 场景。
修法:给场景加 origin(declared/emergent):
- 被动聚类只认 origin='emergent' 的场景(声明场景不进相似度空间);
- 声明场景的特征**只从键自身解析**(chan:qq/peer:group_1 → {chan:qq, peer:group_1}),
白名单 kind(chan/peer/peer_group/tool/topic/part),不猜——
「老大2026-09-04_12:27_qq私聊图片」里的 12:27 也是 kind:value 形态,
放进特征空间就是往相似度里灌垃圾(有测试钉住)。
- 声明路的泛化靠**层级键前缀**(chan:qq 覆盖 chan:qq/peer:x),机制各归各。
- memgc -scene-stats 增加 [declared|emergent] 与 strength/features 两栏,
可直接观察两条路各自在长什么。
新增/改写用例:
- TestDeclaredAndEmergentBothLearn:首次交互兜底到声明场景 → 第 2 轮长出
细粒度涌现场景且**优先用于写入** → 声明场景不进相似度空间(防止压死被动路)
但仍走声明键取回 → 两条路都进召回集合 → 声明键特征解析与白名单。
- TestEffectiveScenes:多值+单值合并去重保序。
go build/vet 干净,go test -count=1 ./... 全绿。
This commit is contained in:
@ -30,6 +30,7 @@ import (
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
@ -115,6 +116,7 @@ type TaskFrame struct {
|
||||
// sceneDone 标记是否已解析过——一轮只解析一次:多解析一次就多给场景
|
||||
// 加一次强度,「工具调得多」会被误当成「这个场面更常出现」。
|
||||
Scene string
|
||||
turnScene memory.TurnScene
|
||||
sceneDone bool
|
||||
|
||||
// 游标与终态
|
||||
@ -722,7 +724,10 @@ func denialResultText(ctx *sdk.StageContext, toolName string) string {
|
||||
|
||||
// stepToolExec 执行工具。**临界区**:见设计文档 §4.3。
|
||||
func (a *Agent) stepToolExec(f *TaskFrame) stepOutcome {
|
||||
result := a.executeToolCall(f.CurTool, f.OutputChannel, f.Scene)
|
||||
// 执行工具前先解析本轮场景:写侧要用它给记忆自动挂场景(主动+被动两条路),
|
||||
// 而工具步不一定走到下面的召回分支,所以不能等那里再解析。
|
||||
turn := a.resolveTurnScenes(f, f.CurTool.Name)
|
||||
result := a.executeToolCall(f.CurTool, f.OutputChannel, turn.Keys...)
|
||||
f.CurResult = result
|
||||
f.ToolResults = append(f.ToolResults, ToolResultItem{Name: f.CurTool.Name, Output: result})
|
||||
log.Printf("[agent] tool %s result: %s", f.CurTool.Name, truncateStr(result, 100))
|
||||
@ -762,9 +767,11 @@ func (a *Agent) stepToolAfter(f *TaskFrame) stepOutcome {
|
||||
// 工具路召回的场景有两个来源:本轮输入的场面(如 chan:qq)
|
||||
// 与这一步工具本身(如 tool:qq_get_message)。带上工具场景,
|
||||
// 才能让「凡是要回 QQ 消息」这类规则在该步被取回。
|
||||
// 召回用两条路的并集:声明场景(注入点/通道/工具)+ 涌现场景
|
||||
scenes := sceneKeysFor(f.Evt, tc.Name)
|
||||
if s := a.resolveTurnScene(f, tc.Name); s != "" {
|
||||
scenes = append(scenes, s)
|
||||
turn := a.resolveTurnScenes(f, tc.Name)
|
||||
for _, k := range turn.Keys {
|
||||
scenes = append(scenes, k)
|
||||
}
|
||||
recallText = a.memoryPass(query, "tool:"+tc.Name, needPrune, needRecall, scenes).RecallText
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user