feat(memory): 记忆 P2(项目索引物化 / decay 回写 / 作用域写规则 / 静默吞点 + loop 级只读红线测试)
This commit is contained in:
1 parent
9b903a89bc
commit
509dc412eb
12 files changed
+660
-55
No files matched your search
@@ -91,27 +91,46 @@ func (k *knowledgeTool) Execute(ctx context.Context, args any) (any, error) {
|
||||
if v, ok := argInt(m, "top_k"); ok && v > 0 {
|
||||
topK = v
|
||||
}
|
||||
results, err := k.retriever.Retrieve(ctx, persona, query, topK)
|
||||
// 群聊/陌生人(受限环境)是只读的(design/03 §5「能查不能写」):检索
|
||||
// 时关掉衰减退化的物化回写(memory.WithoutDecayWriteback),并在返回前
|
||||
// 复用循环注入路径上的同一隔离判据(loop.FilterSharedVisible),绝不另造
|
||||
// 第二套 —— 否则一旦管理面板把本工具放行给群聊/陌生人,就会返回角色日常
|
||||
// (ScopePersona,主人私有)与 L4 意图记录(主人逐句原话),触碰 design/03
|
||||
// §5「不泄露主人敏感信息」(审计 2026-10-05,P0 红线)。
|
||||
restricted := loop.SessionEnvFrom(ctx).Restricted()
|
||||
rctx := ctx
|
||||
if restricted {
|
||||
rctx = memory.WithoutDecayWriteback(ctx)
|
||||
}
|
||||
results, err := k.retriever.Retrieve(rctx, persona, query, topK)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("io: knowledge: retrieve: %w", err)
|
||||
}
|
||||
// 群聊/陌生人(受限环境)只读非敏感的共享域记忆:复用循环注入路径上的
|
||||
// 同一隔离判据(loop.FilterSharedVisible),绝不另造第二套 —— 否则一旦
|
||||
// 管理面板把本工具放行给群聊/陌生人,就会返回角色日常(ScopePersona,
|
||||
// 主人私有)与 L4 意图记录(主人逐句原话),触碰 design/03 §5
|
||||
// 「不泄露主人敏感信息」(审计 2026-10-05,P0 红线)。
|
||||
if loop.SessionEnvFrom(ctx).Restricted() {
|
||||
if restricted {
|
||||
results = loop.FilterSharedVisible(results)
|
||||
}
|
||||
out := make([]map[string]any, 0, len(results))
|
||||
for _, r := range results {
|
||||
out = append(out, map[string]any{
|
||||
entry := map[string]any{
|
||||
"id": r.Item.ID,
|
||||
"kind": string(r.Item.Kind),
|
||||
"scope": string(r.Item.Scope),
|
||||
"content": r.Item.Content,
|
||||
"score": r.Score,
|
||||
})
|
||||
}
|
||||
// 项目作用域记忆链接到项目索引层级(design/03 §3:项目索引 →
|
||||
// 项目描述 → 章节内容)。此前工具丢掉了 Result.Project,模型因此
|
||||
// 只看得到一条句子,够不着它所属的项目(审计 2026-10-05 §4「模型
|
||||
// 不可达章节」)。这里把链接的项目条目带出(id/title/desc),使模型
|
||||
// 能沿「项目索引 → 项目描述」继续深入,而不是盲取一条悬空记忆。
|
||||
if r.Project != nil {
|
||||
entry["project"] = map[string]any{
|
||||
"id": r.Project.ID,
|
||||
"title": r.Project.Title,
|
||||
"desc": r.Project.Desc,
|
||||
}
|
||||
}
|
||||
out = append(out, entry)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return map[string]any{
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/projectindex"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
|
||||
)
|
||||
|
||||
@@ -251,6 +252,42 @@ func TestKnowledge_RestrictedEnv_OnlySharedVisible(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestKnowledge_SurfacesLinkedProject 钉住审计 2026-10-05 §4:knowledge 工具此前
|
||||
// 丢掉了 Result.Project,模型因此够不着「项目索引 → 项目描述」层级。修复后项目
|
||||
// 作用域命中必须带出链接的项目条目(id/title/desc),非项目命中则不带该键。
|
||||
func TestKnowledge_SurfacesLinkedProject(t *testing.T) {
|
||||
t.Parallel()
|
||||
sr := &stubRetriever{results: []memory.Result{
|
||||
{
|
||||
Item: memory.MemoryItem{ID: "m1", Content: "项目记忆:Cyrene 用 Go 编写", Kind: memory.KindWorldEvent, Scope: memory.ScopeProject},
|
||||
Score: 0.7,
|
||||
Project: &projectindex.Project{ID: "proj.1", Title: "Cyrene", Desc: "陪伴型 AI 助手"},
|
||||
},
|
||||
{
|
||||
Item: memory.MemoryItem{ID: "m2", Content: "用户习惯:喜欢冰美式", Kind: memory.KindRelationship, Scope: memory.ScopeUser},
|
||||
Score: 0.4,
|
||||
},
|
||||
}}
|
||||
kt := knowledgeToolForTest(t, WithKnowledgeRetriever(sr))
|
||||
|
||||
ctx := loop.WithSessionEnv(tool.WithTurnPersona(context.Background(), "persona-a"), loop.OwnerEnv())
|
||||
got, err := kt.Execute(ctx, map[string]any{"query": "Cyrene"})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
results := got.(map[string]any)["results"].([]map[string]any)
|
||||
proj, ok := results[0]["project"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("项目作用域命中未带出链接项目:%+v", results[0])
|
||||
}
|
||||
if proj["id"] != "proj.1" || proj["title"] != "Cyrene" || proj["desc"] != "陪伴型 AI 助手" {
|
||||
t.Fatalf("project = %+v, want proj.1/Cyrene/陪伴型 AI 助手", proj)
|
||||
}
|
||||
if _, present := results[1]["project"]; present {
|
||||
t.Fatalf("非项目命中不该带 project 键:%+v", results[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestKnowledge_OwnerEnv_AllScopes 钉住反向边界:owner(全权限)环境不做隔离过滤,
|
||||
// 四类命中都照常返回 —— 修复只收窄群聊/陌生人,绝不误伤主人自己的完整读取。
|
||||
func TestKnowledge_OwnerEnv_AllScopes(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user