From 685ed7e4a037b3277e894b51750a112591c356e8 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 12 Sep 2026 08:30:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(knowledge):=20=E8=A6=86=E7=9B=96=E5=90=8C?= =?UTF-8?q?=E5=90=8D=E6=9D=A1=E7=9B=AE=E6=97=B6=E6=91=98=E6=8E=89=E6=97=A7?= =?UTF-8?q?=E5=90=91=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从一次真实的知识库更新里发现:在线实例更新一个已有条目之后, knowledge_count=32 而 vector_count=33——多出来的那一条是上一版的副本。 成因:vector.Store.Insert 是**追加**语义(s.docs = append + index.Add),不按 id 去重; 而 Store.Add 走的是「写 content.md + 覆盖 items[id] + Insert 向量」。 文件与内存条目都被正确替换了,只有向量索引多留了一份。 危害不在于多占内存:**检索可能命中已被替换掉的旧内容**,而且完全静默—— 条目数看起来是对的,只有向量数比条目数多。 修法:Insert 之前先 s.vec.Remove(id)(Remove 已按 id 过滤 docs 与倒排索引)。 回归测试 TestAddOverwriteReplacesVector 钉住 knowledge_count / vector_count / content.md 三者都必须只剩新版。 注:该文件在 origin/main 上本就有 32 行 gofmt 差异(结构体字段注释对齐), 不属本次改动,按纪律不做整体重排。 --- internal/knowledge/knowledge.go | 7 +++++ internal/knowledge/knowledge_test.go | 45 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/internal/knowledge/knowledge.go b/internal/knowledge/knowledge.go index c550f7d..4fd7319 100644 --- a/internal/knowledge/knowledge.go +++ b/internal/knowledge/knowledge.go @@ -201,6 +201,13 @@ func (s *Store) Add(name, content string) error { } s.items[id] = k + // 覆盖同名条目时必须先摘掉旧向量。 + // + // vector.Store.Insert 是**追加**语义(s.docs = append + index.Add),不按 id + // 去重。少了这一步,更新一条知识会在向量索引里留下上一版的副本:条目数看起来 + // 是对的,只有向量数比条目数多——而检索可能因此命中已被替换掉的旧内容。 + s.vec.Remove(id) + vec := s.vectorize(name + " " + content) s.vec.Insert(id, name+": "+content, vec, map[string]string{ "name": name, "path": path, diff --git a/internal/knowledge/knowledge_test.go b/internal/knowledge/knowledge_test.go index 73b010b..64de126 100644 --- a/internal/knowledge/knowledge_test.go +++ b/internal/knowledge/knowledge_test.go @@ -44,6 +44,51 @@ func TestAddAndSearch(t *testing.T) { } } +// 覆盖同名条目必须把旧向量摘掉,而不是再插一份。 +// +// 这条是从一次真实的知识库更新里发现的:在线上实例更新一个已有条目后, +// knowledge_count=32 但 vector_count=33 ——多出来的那一条是上一版的副本。 +// 成因是 vector.Store.Insert 为追加语义(s.docs = append + index.Add),不按 id 去重。 +// 危害不在于多占一份内存:检索可能命中**已被替换掉的旧内容**。 +func TestAddOverwriteReplacesVector(t *testing.T) { + dir, err := os.MkdirTemp("", "know_overwrite_*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + s := NewStore(dir) + s.Start() + defer s.Stop() + + if err := s.Add("recent", "第一版内容:旧的多模态描述式索引"); err != nil { + t.Fatal(err) + } + if got := s.Stats()["vector_count"].(int); got != 1 { + t.Fatalf("首次写入后 vector_count 应为 1,实为 %d", got) + } + + if err := s.Add("recent", "第二版内容:媒体已成为图记忆的一等节点"); err != nil { + t.Fatal(err) + } + + if n := s.Stats()["knowledge_count"].(int); n != 1 { + t.Fatalf("同名覆盖后 knowledge_count 应为 1,实为 %d", n) + } + if n := s.Stats()["vector_count"].(int); n != 1 { + t.Fatalf("同名覆盖后 vector_count 应为 1(多了就是旧版没被摘掉),实为 %d", n) + } + + // 目录里也只应有一份内容,且是新的那份 + b, err := os.ReadFile(dir + "/recent/content.md") + if err != nil { + t.Fatal(err) + } + if string(b) != "第二版内容:媒体已成为图记忆的一等节点" { + t.Fatalf("content.md 未被新内容覆盖,实为 %q", string(b)) + } +} + func TestList(t *testing.T) { dir, err := os.MkdirTemp("", "know_list_*") if err != nil {