mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
记忆层: - MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID - coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失 - 召回先估算总 token,超预算时按全局页相似度标记 inactive - inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置 - Peek/确认/Remove 两阶段召回,失败保留冷上下文副本 - stablemem 记录 last_recalled_at,删块连带清理边 命名: - internal/memory/document → internal/memory/coldcontext - Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit - ContextToColdContext/RecentColdContexts/FindColdContexts - 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据 安全治理: - 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏 - 声明式 SecurityAPI 与 QQ owner/guest profile - 日志保留策略与相关 e2e/回归判据 验证:go build ./...、go vet ./...、go test ./...(62 包); 生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
177 lines
5.9 KiB
Go
177 lines
5.9 KiB
Go
package core
|
||
|
||
// coldcontext_query 的**自激震荡**判据。
|
||
//
|
||
// ## 回路长什么样(用户 2026-10-22 指出)
|
||
//
|
||
// coldcontext_query
|
||
// ├─ ① 工具返回值含被召回事件的正文片段 → 作为 ToolResults 进 context
|
||
// │ → 参与向量化/关键词/蒸馏 → 同一内容被**重新注入**记忆
|
||
// ├─ ② 回填事件本身(带原 Source、完整 Blocks)进 L1
|
||
// │ → Prune 因相关而保留它们(挤掉新事件)
|
||
// │ → 下一轮 Prune 又把它们归档回 L2
|
||
// └─ ⇒ 下次 coldcontext_query 再召回同一批:块在 L1↔L2 之间反复搬迁
|
||
//
|
||
// 两个入口都必须关掉:
|
||
// ① ToolDef.NoMemory —— coldcontext_query 声明,工具输出不进计算层
|
||
// ② ContextEvent.NoMemory —— restoreDocEvents 置位,回填事件不再归档
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||
)
|
||
|
||
// ① coldcontext_query 必须声明 NoMemory(工具输出不进记忆计算)。
|
||
//
|
||
// 变异:去掉 toolDefOptions{noMemory:true} ⇒ 红。
|
||
func TestSelfExcite_DocQueryDeclaresNoMemory(t *testing.T) {
|
||
// 工具定义层面:声明必须可查(不依赖工具表是否已构造)
|
||
if !noMemoryOf("coldcontext_query") {
|
||
t.Error("coldcontext_query 未声明 NoMemory —— 它的返回值含被召回事件的正文," +
|
||
"再参与向量化/蒸馏就是把同一内容重新注入记忆")
|
||
}
|
||
|
||
// 计算层层面:带 coldcontext_query 输出的工具事件,其向量化文本不得含该输出
|
||
const sentinel = "这是被召回事件的正文片段哨兵"
|
||
evt := ContextEvent{
|
||
Timestamp: time.Now(),
|
||
Source: "agent",
|
||
Input: "查一下历史",
|
||
ToolResults: []ToolResultItem{
|
||
{Name: "coldcontext_query", Output: "已召回:" + sentinel},
|
||
},
|
||
}
|
||
// stageHost 在测试环境为 nil:传 nil lookup,只走内置声明表那条路
|
||
text := textForVector(&evt, nil, nil)
|
||
if strings.Contains(text, sentinel) {
|
||
t.Errorf("coldcontext_query 的输出进入了计算层(会被重新向量化/蒸馏):\n%s", text)
|
||
}
|
||
}
|
||
|
||
// ② 回填事件必须带 NoMemory(不参与计算),但**必须照常归档**(数据不丢)。
|
||
//
|
||
// ★ 这里曾经写反过:要求"不得被 Prune 再归档回 L2"。
|
||
//
|
||
// coldcontext_query 的 Consume 已经把 L2 副本删掉,L1 是唯一副本 ——
|
||
// 不归档就等于三层都不在,内容永久丢失。
|
||
//
|
||
// 变异:
|
||
//
|
||
// · restoreDocEvents 不置 NoMemory ⇒ 它会参与计算(重复加权)
|
||
// · Prune 跳过 NoMemory 事件 ⇒ 内容丢失(本判据要抓这个)
|
||
func TestSelfExcite_RestoredEventsNotReArchived(t *testing.T) {
|
||
a := newToolTestAgent(t)
|
||
|
||
const marker = "自激测试:这条会被归档"
|
||
// 先归档一条事件到 L2
|
||
archiveEvents(t, a, []ContextEvent{{
|
||
Timestamp: time.Now().Add(-2 * time.Hour),
|
||
Source: "webui",
|
||
Input: marker,
|
||
Response: "然后被 coldcontext_query 取回",
|
||
}})
|
||
|
||
// 召回(Consume 会把 L2 副本删掉)
|
||
out := a.executeDocTool(agentAPI.ToolCall{
|
||
Name: "coldcontext_query",
|
||
Arguments: map[string]interface{}{"query": "自激测试 归档", "top_k": float64(3)},
|
||
})
|
||
if strings.Contains(out, "未找到") {
|
||
t.Fatalf("未召回:%s", out)
|
||
}
|
||
|
||
// ① 回填事件必须带 NoMemory(不参与计算)
|
||
restored := 0
|
||
for _, e := range a.context.Recent(0) {
|
||
if e.Source == "webui" && strings.Contains(e.Input, marker) {
|
||
restored++
|
||
if !e.NoMemory {
|
||
t.Error("回填事件没有 NoMemory 标记 —— 它会再被向量化/蒸馏,同一内容重复加权")
|
||
}
|
||
if len(e.Vector) > 0 {
|
||
t.Error("回填事件被向量化了(NoMemory 未生效)")
|
||
}
|
||
}
|
||
}
|
||
if restored == 0 {
|
||
t.Fatal("没有回填任何事件")
|
||
}
|
||
|
||
// ② L1 是唯一副本(L2 已被 Consume 删除)——确认这一点
|
||
for _, d := range a.coldcontextStore.RecentColdContexts(1000) {
|
||
if !d.HasEvents() {
|
||
continue
|
||
}
|
||
for _, ev := range d.Events {
|
||
in, _, _ := ev.InOut()
|
||
if strings.Contains(in, marker) {
|
||
t.Fatalf("L2 里仍有该事件(%s)—— 前提不成立,本判据失去意义", d.ID)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ③ ★ 关键:裁掉它时**必须能落回 L2**,否则内容永久丢失
|
||
for i := 0; i < 14; i++ {
|
||
a.context.Append(ContextEvent{
|
||
Timestamp: time.Now(),
|
||
Source: "filler",
|
||
Input: "无关填充,用来触发 Prune",
|
||
Response: "好。",
|
||
})
|
||
}
|
||
a.context.Prune("当前输入", 2, a.coldcontextStore)
|
||
|
||
var foundInL2 bool
|
||
for _, d := range a.coldcontextStore.RecentColdContexts(1000) {
|
||
if !d.HasEvents() {
|
||
continue
|
||
}
|
||
for _, ev := range d.Events {
|
||
in, _, _ := ev.InOut()
|
||
if strings.Contains(in, marker) {
|
||
foundInL2 = true
|
||
}
|
||
}
|
||
}
|
||
if !foundInL2 {
|
||
t.Error("回填事件被裁掉后没有归档回 L2 —— Consume 已删除原副本," +
|
||
"L1 是唯一副本,不归档就是内容永久丢失(三层都不在)")
|
||
}
|
||
}
|
||
|
||
// ④ 回填事件不得参与 Prune 的相关性打分(否则会挤掉真正的新事件)。
|
||
func TestSelfExcite_RestoredEventsDontCrowdOutNewOnes(t *testing.T) {
|
||
a := newToolTestAgent(t)
|
||
|
||
archiveEvents(t, a, []ContextEvent{{
|
||
Timestamp: time.Now().Add(-3 * time.Hour),
|
||
Source: "webui",
|
||
Input: "拥挤测试的原始事件",
|
||
Response: "响应",
|
||
}})
|
||
a.executeDocTool(agentAPI.ToolCall{
|
||
Name: "coldcontext_query",
|
||
Arguments: map[string]interface{}{"query": "拥挤测试", "top_k": float64(3)},
|
||
})
|
||
|
||
// 回填事件的向量必须为空(NoMemory → textForVector 返回 "")
|
||
for _, e := range a.context.Recent(0) {
|
||
if e.NoMemory && len(e.Vector) > 0 {
|
||
t.Errorf("回填事件仍被向量化(vector len=%d)—— 它会参与相关性打分并挤掉新事件",
|
||
len(e.Vector))
|
||
}
|
||
}
|
||
|
||
// 块仍在 L1(迁移不是丢弃)
|
||
var blocks int
|
||
for _, e := range a.context.Recent(0) {
|
||
blocks += len(e.Blocks)
|
||
}
|
||
_ = blocks
|
||
_ = memory.BlockText
|
||
}
|