mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
fix(memory): 把插件的 NoMemory 声明落到事件上
声明链此前断在最后一步:
InjectOptions.NoMemory / ChannelDef.NoMemory
→ payload["no_memory"] → stageCtx.NoMemory ✅ 已合成
→ ContextEvent.NoMemory ❌ 从未赋值
后果:QQ 那句取件样板("来自「X」的私聊消息(message_id=…) 先用
qq_get_message 取正文…")每轮都进计算层(向量化/关键词/蒸馏),
而它是指令不是内容 —— 真实对话内容在记忆里被它挤薄。
按字段**既有契约**处置(context.go 的注释写明):只影响计算层,
归档照常。理由是同一条注释里的 2026-10-22 教训 —— coldcontext_query
的 Consume 已从 L2 删除原文,回填事件必须能落回 L2,否则三层全无。
判据:nomemory_flag_test.go
(NoMemory 事件 textForVector 为空但 Input/Response 保留;普通事件不受影响)
This commit is contained in:
@ -410,6 +410,15 @@ func (c *RelevanceContext) Save() error {
|
|||||||
// (另一处 InsertByTimestamp 同样处理)。所以明文拦截放在这里,
|
// (另一处 InsertByTimestamp 同样处理)。所以明文拦截放在这里,
|
||||||
// 而不是在每个调用点 —— 逐个补必漏(实测踩到:只拦了 f.Msgs,
|
// 而不是在每个调用点 —— 逐个补必漏(实测踩到:只拦了 f.Msgs,
|
||||||
// 而 f.CleanInput / f.ToolResults / f.Response 都是独立副本)。
|
// 而 f.CleanInput / f.ToolResults / f.Response 都是独立副本)。
|
||||||
|
// snapshotEvents 返回当前事件的浅拷贝(判据/诊断用;调用方不得修改元素)。
|
||||||
|
func (c *RelevanceContext) snapshotEvents() []*ContextEvent {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
out := make([]*ContextEvent, len(c.events))
|
||||||
|
copy(out, c.events)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (c *RelevanceContext) Append(evt ContextEvent) {
|
func (c *RelevanceContext) Append(evt ContextEvent) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|||||||
61
internal/agent/core/nomemory_flag_test.go
Normal file
61
internal/agent/core/nomemory_flag_test.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
// 插件的 `NoMemory` 声明必须落到**事件**上(2026-10-27)。
|
||||||
|
//
|
||||||
|
// 声明链:InjectOptions.NoMemory / ChannelDef.NoMemory
|
||||||
|
// → payload["no_memory"] → stageCtx.NoMemory → ContextEvent.NoMemory
|
||||||
|
//
|
||||||
|
// 此前最后一步从来没发生:`ContextEvent.NoMemory` 只有在 coldcontext_query
|
||||||
|
// 回填时才被置 true,于是 QQ 那句取件样板
|
||||||
|
//("来自「X」的私聊消息(message_id=…) 先用 qq_get_message 取正文…")
|
||||||
|
// 每轮都进计算层(向量化 / 关键词 / 蒸馏)。
|
||||||
|
//
|
||||||
|
// 判据(按字段既有契约,不是新增语义):
|
||||||
|
// ① NoMemory 事件的 textForVector 为空(不进计算层);
|
||||||
|
// ② 但事件**照常归档**(Input/Response 原样保留,能从 L1 落回 L2);
|
||||||
|
// ③ 非 NoMemory 事件不受影响。
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newNoMemFlagContext() *RelevanceContext {
|
||||||
|
return NewRelevanceContext("", memory.NewStaticEmbedder(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoMemoryEventSkipsComputationButKeepsArchiveFields(t *testing.T) {
|
||||||
|
rc := newNoMemFlagContext()
|
||||||
|
ev := ContextEvent{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Source: "agent",
|
||||||
|
NoMemory: true,
|
||||||
|
Input: "来自「—/」的私聊消息(message_id=1)。先用qq_get_message取正文",
|
||||||
|
Response: "收到。",
|
||||||
|
}
|
||||||
|
rc.Append(ev)
|
||||||
|
|
||||||
|
got := rc.snapshotEvents()
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("应写入一条事件,实际 %d", len(got))
|
||||||
|
}
|
||||||
|
// ① 计算层:不进向量
|
||||||
|
if txt := textForVector(got[0], nil, nil); txt != "" {
|
||||||
|
t.Fatalf("NoMemory 事件不得进入计算层,实际 textForVector=%q", txt)
|
||||||
|
}
|
||||||
|
// ② 归档字段保留(否则 L2 回放闭环断掉)
|
||||||
|
if got[0].Input == "" || got[0].Response == "" {
|
||||||
|
t.Fatalf("归档字段必须保留,实际 Input=%q Response=%q", got[0].Input, got[0].Response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalEventStillEntersComputation(t *testing.T) {
|
||||||
|
rc := newNoMemFlagContext()
|
||||||
|
rc.Append(ContextEvent{Timestamp: time.Now(), Source: "webui", Input: "帮我看下日志"})
|
||||||
|
got := rc.snapshotEvents()
|
||||||
|
if txt := textForVector(got[0], nil, nil); txt == "" {
|
||||||
|
t.Fatal("普通事件必须进入计算层")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -558,9 +558,22 @@ func (a *Agent) finishInputTask(f *TaskFrame, out stepOutcome) {
|
|||||||
|
|
||||||
// 本轮捕获的媒体一起挂到这条事件上:用户上传的、插件注入的,以及模型调
|
// 本轮捕获的媒体一起挂到这条事件上:用户上传的、插件注入的,以及模型调
|
||||||
// multimodal_see_picture / see_video 时经 SetToolBlocks 注入的。
|
// multimodal_see_picture / see_video 时经 SetToolBlocks 注入的。
|
||||||
|
// ★ 把插件声明落到事件上(2026-10-27):`NoMemory` 只影响**计算层**
|
||||||
|
//(不向量化 / 不提关键词 / 不蒸馏)与**记忆视图**,而**归档照常** ——
|
||||||
|
// 这是 ContextEvent.NoMemory 的既有契约(见 context.go 的字段注释:
|
||||||
|
// coldcontext_query 的 Consume 已从 L2 删掉原文,回填事件必须能落回 L2,
|
||||||
|
// 否则三层都不在,内容被永久丢弃)。
|
||||||
|
//
|
||||||
|
// 此前这个位从来没被赋值:插件声明 InjectOptions.NoMemory=true(QQ 那句
|
||||||
|
// "来自「X」的私聊消息(message_id=…) 先用 qq_get_message 取正文…"),
|
||||||
|
// 经 payload.no_memory 合进 stageCtx.NoMemory 后,却只有
|
||||||
|
// emitMemoryCandidate 一处消费它 —— 于是这句样板每轮都进计算层。
|
||||||
|
//
|
||||||
|
// 声明来源:payload["no_memory"](插件注入)或 ChannelDef.NoMemory(通道)。
|
||||||
turnEvt := ContextEvent{
|
turnEvt := ContextEvent{
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Source: "agent",
|
Source: "agent",
|
||||||
|
NoMemory: f.StageCtx.NoMemory,
|
||||||
Input: f.CleanInput,
|
Input: f.CleanInput,
|
||||||
Response: f.Response,
|
Response: f.Response,
|
||||||
ToolsUsed: f.ToolsUsed,
|
ToolsUsed: f.ToolsUsed,
|
||||||
|
|||||||
Reference in New Issue
Block a user