mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-26 20:33:15 +00:00
fix(knowledge): 名称规范化/路径安全 + IDF 增量维护 + 多模态稠密路 + 派生数据落盘
一次知识库子系统的集中加固,四类缺陷各有实测复现:
1. 名称与路径(数据安全,最严重)
- sanitize 不过滤 .. ⇒ Remove("..") 直接 RemoveAll 掉整个数据目录
(实测把 <data> 整棵删掉,含 memory/documents/media),且返回 nil,
工具层回报"已删除";Add("../../x") 写到知识根外,重启扫不回来
⇒ 幽灵条目。
- Add 整串 sanitize 而建目录逐段 sanitize,内存键与盘上目录从**第一次
落盘起**就不一致;重启后 name 漂移,knowledge_delete 静默删不掉
(RemoveAll 删空目录返 nil)。同一个根因。
- 修法:新增 normalizeName 作为唯一入口(逐段 + 拒绝空段/点段/隐藏段);
Remove 改为取条目自记的 Path(不再用名字重拼)+ 返回 ErrNotFound。
- resolve 三层退让(原样 → 规范名 → 叶名大小写不敏感,唯一命中才接受):
scanDir 按盘上目录原样建键,遗留大写目录若只查规范名会变成
"List 看得到、Remove 报不存在"。删的路径仍取自 Path,退让无风险。
2. IDF 与索引不同步(功能缺陷,非优化)
- TFIDF Vectorize 跳过 df<=0 的特征,而 Add 只往只增不减的 summaries
追文本、从不更新 DF ⇒ 库满(≥3篇) + 新词时,新知识**当场搜不到**,
重启才恢复(实测 Search("量子纠缠") == [])。
- 修法:vector.Store 新增 AddDoc/RemoveDoc(文档级去重口径与 Train 一致,
totalDocs 下界守卫,零频 DF 删除防表膨胀);knowledge 删掉 summaries,
改 index/unindex/retrain 三件套,覆盖写先 RemoveDoc 旧文本。
3. 多模态稠密路(此前知识库端到端纯文本)
- 新增 SetDenseSpace/SetMediaGetter/ReindexDense/DenseStats 与
AddWithMedia/AttachMedia,媒体成为一等节点参与跨模态召回。
- 维度与指纹双守卫:维度不符的向量会被 FuseVectors 按最大维度拼成错维度
结果且被当成"已对齐"永久错下去(docStore 踩过);模态不支持(音频)时
静默跳过该媒体、退化为纯文本向量,绝不拿别的模型的向量顶替。
- 未注入多模态空间时行为与此前逐字一致(退化为 0.5/0.5 两路融合)。
4. 派生数据落盘 + 分层参与召回
- 媒体引用是**作者数据**(丢失即丢信息)→ 条目目录内 .media.json;
稠密向量是**派生数据**(可重算)→ 全局 .dense.json,tmp+rename 原子。
混存会让派生数据损坏连带作者数据一起丢。
- 新增 SearchIn(query, category, topK):分类前缀匹配子树,让分层真正
参与召回(此前检索全库平铺,分层只是存储布局)。归一化取作用域内
最大值,否则范围外的强命中会把域内分数压没。
- 删除 SearchTree/SearchCategories 死代码(零调用方,且停留在 Search
修复前的单路口径:无词法融合、0.05 阈值)。
顺带修掉 Add 的 O(N)/写:buildTreeLocked 逐条重算向量(s.vec 里已有)
改为一次建表复用;.index.json 改为标脏 + Flush/Stop 收口。实测单条 Add
2.1ms@50 → 13.7ms@400 压平到 ~400µs(34×)。
存量目录改名迁移落在 migrate_names.go,只报告不改名(os.Rename 不可逆),
冲突整批拒绝以免半迁移。
This commit is contained in:
@ -254,22 +254,87 @@ func (v *TFIDFVectorizer) Train(docs []string) {
|
||||
defer v.mu.Unlock()
|
||||
|
||||
v.docFreq = make(map[string]float64)
|
||||
v.totalDocs = len(docs)
|
||||
v.totalDocs = 0
|
||||
|
||||
seen := make(map[string]map[string]bool)
|
||||
for _, doc := range docs {
|
||||
features := v.tokenizer(doc)
|
||||
key := doc
|
||||
if seen[key] == nil {
|
||||
seen[key] = make(map[string]bool)
|
||||
}
|
||||
for _, f := range features {
|
||||
if !seen[key][f] {
|
||||
seen[key][f] = true
|
||||
v.docFreq[f]++
|
||||
v.addDocLocked(doc, seen)
|
||||
}
|
||||
}
|
||||
|
||||
// AddDoc 把一篇新文档计入 DF 统计(增量)。
|
||||
//
|
||||
// 存在的理由:Train 是全量重训,而知识库的 Add 是逐条发生的。此前 Add 只把
|
||||
// 文本追进一个 summaries 切片、不更新 DF,于是**新引入的词 df=0**,
|
||||
// 而 Vectorize 会跳过 df<=0 的特征 —— 运行时新增的知识当场搜不到,
|
||||
// 重启(重新 Train)后才恢复。这不是优化项,是功能缺陷。
|
||||
//
|
||||
// 注意:document 是**文档级去重**的(同一词在同篇里多次出现只记 1 次 df),
|
||||
// 与 Train 里那份 seen 表的语义必须一致,否则 IDF 会随写入路径不同而漂移。
|
||||
func (v *TFIDFVectorizer) AddDoc(doc string) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
v.addDocLocked(doc, nil)
|
||||
}
|
||||
|
||||
// RemoveDoc 把一篇文档从 DF 统计中移出(AddDoc 的逆操作)。
|
||||
//
|
||||
// totalDocs 可能减到 0;此后 Vectorize 会走 totalDocs < 3 的退化分支
|
||||
// (直接给 tf,不乘 IDF),这是可接受的行为 —— 库里都没东西了,
|
||||
// IDF 本来也无从谈起。采用**下界守卫**:减到 0 后即使 RemoveDoc 被多调
|
||||
// 一次,也不会变成负数。
|
||||
func (v *TFIDFVectorizer) RemoveDoc(doc string) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
for f := range v.seenFeatures(doc) {
|
||||
if v.docFreq[f] > 0 {
|
||||
v.docFreq[f]--
|
||||
if v.docFreq[f] == 0 {
|
||||
// 删掉零频条目:否则 DF 表会被"曾经出现过一次"的词永久撑大,
|
||||
// 而这正是 summaries 只增不减之外的第二处泄漏。
|
||||
delete(v.docFreq, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.totalDocs > 0 {
|
||||
v.totalDocs--
|
||||
}
|
||||
}
|
||||
|
||||
// addDocLocked 是 AddDoc/Train 共用的记账内核。seen 非 nil 时复用调用方的表
|
||||
// (Train 的整轮去重),nil 时本函数内使用自己的表。
|
||||
//
|
||||
// 调用方必须已持写锁。
|
||||
func (v *TFIDFVectorizer) addDocLocked(doc string, seen map[string]map[string]bool) {
|
||||
var local map[string]bool
|
||||
if seen != nil {
|
||||
if seen[doc] == nil {
|
||||
seen[doc] = make(map[string]bool)
|
||||
}
|
||||
local = seen[doc]
|
||||
} else {
|
||||
local = make(map[string]bool)
|
||||
}
|
||||
|
||||
for _, f := range v.tokenizer(doc) {
|
||||
if local[f] {
|
||||
continue
|
||||
}
|
||||
local[f] = true
|
||||
v.docFreq[f]++
|
||||
}
|
||||
v.totalDocs++
|
||||
}
|
||||
|
||||
// seenFeatures 返回一篇文档的**去重**特征集(与 addDocLocked 的口径一致)。
|
||||
// 调用方必须已持写锁。
|
||||
func (v *TFIDFVectorizer) seenFeatures(doc string) map[string]bool {
|
||||
out := make(map[string]bool)
|
||||
for _, f := range v.tokenizer(doc) {
|
||||
out[f] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (v *TFIDFVectorizer) Vectorize(text string) Vector {
|
||||
|
||||
Reference in New Issue
Block a user