- P0-3 持久向量索引**不再空壳**:Store.Add 落库后真做 Embed+Add(失败只记日志、绝不让记忆写入失败); NewRetriever 拿到 WithVectorIndex 时**自动接上写入路径**(无需改组合根);索引无命中 → WARN + **回退内存余弦**; 新增每进程一次的**回填**(索引确实为空时把已落库条目向量化,分批 64、上限 512) - P0-4 工作记忆**按会话接线**:回合落定时写入(owner 会话 + 带会话 id 的窗口,LRU 上限 64), 组装上下文时按会话读取 ⇒ 前导块真的出现;原单窗口 API 语义不变 - P0-17 L4 学习回环**真回写**:新增 capacity.Judge(只跑判定、不施人格覆盖/不发事件/不改状态); loop 在回合落定"只写 owner 会话 + 跳过子代理分支",写 ScopeUser(L4 的读域)+ 结构化溯源 - 顺带红线加固:意图记录是主人**原话**的机器可读形态 ⇒ **绝不注入群聊/陌生人块**(design/03 §5) - 复核:三条各自的包 build/vet/test 绿;「真生效」断言含生产构造路径(vector.New + 真实 SQLite 后端 + n-gram embedder)、跑完一轮窗口非空、L4 回写后能唤醒一个自身无强信号的表达; **5 条变异反证**(其中两条专门打「受限会话零写入」与「群聊不泄露主人原话」)均已还原 - 未做(登记):Index.Search 无 ID 过滤(客户端按候选集过滤,极端情况走回退);每条记忆一次 Embed
108 lines
4.0 KiB
Go
108 lines
4.0 KiB
Go
package memory
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// TestWorkingMemory_RecordSession_Sliding_滑窗 证明会话窗口与默认窗口一样
|
|
// 是「最近 N 条」的滑窗,且只保留窗口内的最后 max 条。
|
|
func TestWorkingMemory_RecordSession_Sliding_滑窗(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorkingMemory(3)
|
|
msgs := make([]model.Message, 0, 5)
|
|
for i := 0; i < 5; i++ {
|
|
msgs = append(msgs, model.TextMessage(model.RoleUser, string(rune('a'+i))))
|
|
}
|
|
w.RecordSession("sess-1", msgs)
|
|
|
|
win := w.SessionWindow("sess-1")
|
|
if len(win) != 3 {
|
|
t.Fatalf("会话窗口长度 = %d, want 3(滑窗)", len(win))
|
|
}
|
|
for i, want := range []string{"c", "d", "e"} {
|
|
if got := win[i].Content[0].Text; got != want {
|
|
t.Errorf("窗口[%d] = %q, want %q", i, got, want)
|
|
}
|
|
}
|
|
// 摘要由该会话窗口推导,因此非空且带消息条数。
|
|
summary := w.SummarizeSession("sess-1")
|
|
if !strings.Contains(summary, "3 条消息") {
|
|
t.Fatalf("SummarizeSession = %q, want 含 3 条消息", summary)
|
|
}
|
|
if w.Summarize() != "" {
|
|
t.Fatalf("默认窗口被会话写入污染: %q", w.Summarize())
|
|
}
|
|
|
|
// 返回的切片与内部状态脱离。
|
|
win[0].Content[0].Text = "mutated"
|
|
if again := w.SessionWindow("sess-1"); again[0].Content[0].Text != "c" {
|
|
t.Fatalf("SessionWindow 返回了共享切片: %q", again[0].Content[0].Text)
|
|
}
|
|
}
|
|
|
|
// TestWorkingMemory_SessionWindow_IsolatedPerSession_会话隔离 证明窗口
|
|
// 按会话 id 隔离 —— 这是群聊/陌生人的工作记忆不混进主人会话的前提。
|
|
func TestWorkingMemory_SessionWindow_IsolatedPerSession_会话隔离(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorkingMemory(10)
|
|
w.RecordSession("web:owner", []model.Message{model.TextMessage(model.RoleUser, "主人的话")})
|
|
w.RecordSession("sess-group", []model.Message{model.TextMessage(model.RoleUser, "群里的话")})
|
|
|
|
owner := w.SessionWindow("web:owner")
|
|
if len(owner) != 1 || owner[0].Content[0].Text != "主人的话" {
|
|
t.Fatalf("owner 窗口 = %+v, want 只有主人的话(会话隔离)", owner)
|
|
}
|
|
group := w.SessionWindow("sess-group")
|
|
if len(group) != 1 || group[0].Content[0].Text != "群里的话" {
|
|
t.Fatalf("group 窗口 = %+v, want 只有群里的话", group)
|
|
}
|
|
if s := w.SummarizeSession("web:owner"); strings.Contains(s, "群里的话") {
|
|
t.Fatalf("owner 摘要混入了别的会话内容: %q", s)
|
|
}
|
|
if got := w.SessionWindow("never-seen"); len(got) != 0 {
|
|
t.Fatalf("未知会话窗口 = %+v, want 空", got)
|
|
}
|
|
if got := w.SummarizeSession("never-seen"); got != "" {
|
|
t.Fatalf("未知会话摘要 = %q, want 空", got)
|
|
}
|
|
|
|
// 空会话 id 是空操作:无键的写入无从隔离,绝不落到默认窗口。
|
|
w.RecordSession("", []model.Message{model.TextMessage(model.RoleUser, "无键写入")})
|
|
if got := w.Summarize(); got != "" {
|
|
t.Fatalf("空会话 id 的写入污染了默认窗口: %q", got)
|
|
}
|
|
}
|
|
|
|
// TestWorkingMemory_Sessions_Bounded_会话窗口有界 证明保留的会话窗口数
|
|
// 有上限:会话数随渠道消息无界增长,最久未用的窗口被淘汰,内存不失控。
|
|
func TestWorkingMemory_Sessions_Bounded_会话窗口有界(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorkingMemory(10)
|
|
for i := 0; i < maxWorkingSessions; i++ {
|
|
w.RecordSession(sessionName(i), []model.Message{model.TextMessage(model.RoleUser, "x")})
|
|
}
|
|
// 超出上限后再写入一个新会话:最久未用的第一个会话被淘汰。
|
|
w.RecordSession("overflow", []model.Message{model.TextMessage(model.RoleUser, "y")})
|
|
|
|
if got := len(w.SessionWindow(sessionName(0))); got != 0 {
|
|
t.Fatalf("最久未用的会话窗口仍在(%d 条): 会话窗口数没有上限", got)
|
|
}
|
|
if got := len(w.SessionWindow("overflow")); got != 1 {
|
|
t.Fatalf("新会话窗口 = %d 条, want 1", got)
|
|
}
|
|
if got := len(w.SessionWindow(sessionName(maxWorkingSessions - 1))); got != 1 {
|
|
t.Fatalf("最近使用的会话窗口被误淘汰: %d 条", got)
|
|
}
|
|
}
|
|
|
|
// sessionName 生成第 i 个会话 id。
|
|
func sessionName(i int) string {
|
|
return "sess-" + string(rune('A'+i%26)) + string(rune('a'+i/26))
|
|
}
|