mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
fix(memory): 向量输入改用事件正文 —— 摘要是 jieba 词袋,把全库拉成同一方向
## 实测缺陷(真实 chineseclip,4 篇主题完全不同的短正文)
[1][3] 0.9478 [2][3] 0.9427
[1][2] 0.9316 [0][3] 0.8614
两两余弦 0.85~0.95,几乎同向。查询分数 0.70~0.78 挤在一起,区分度 0.08。
根因:denseFor 用 `doc.Summary + " " + doc.Content`,而 Summary 由
summarizeEntries → memory.ExtractKeywords → TokenizeContentWords → jieba.Tag()
生成,同一来源的摘要**逐字同质**:
来自 1 个来源的 1 条对话 (src0) 涉及: 用户, 提问
来自 1 个来源的 1 条对话 (src1) 涉及: 用户, 提问
这与设计直接矛盾:ARCHITECTURE L131/L174 写的向量层级是
「统一多模态空间(主)→ StaticEmbedder → TF-IDF(回退)」,
而 Summary 是**主路径的输入**,却由**最末一级的词法分词**产出。
## 修法
denseFor 改用 documentTextForVector(doc):按事件顺序拼原文(来自块里的正文)。
存量无 Events 的文档退回 Content(渲染视图,但比 Summary 更接近内容)。
★ 绝不含 Summary —— 它是词袋,会把全库拉向同一方向。
## 媒体为什么不单独生成摘要
媒体块与文本块**在同一事件内**(Doc.Events[i].Blocks),事件正文进入向量
即覆盖其语义。为媒体单独生成描述正是 CAS 时代「描述式索引」的错误
(描述是二手信息),v1.2.0 已整体拆除,不重走。
「按图搜图」是视觉路(RecallBlocks 读块自己的向量)的职责,已有独立通路。
## 判据
- TestRetrieval_SummaryNoiseMustNotPolluteVector:主题不同的短正文文档,
两两余弦必须 <0.80。修复前 0.82~0.86。
- TestRetrieval_VectorInputIsBodyNotSummary:正文里的唯一哨兵必须能检索到。
变异:denseFor 改回 Summary+Content ⇒ 前者立刻红(实测 0.8225~0.8573)。
This commit is contained in:
@ -197,11 +197,35 @@ func (s *Store) BuildDenseIndex(ds vector.MultimodalEmbedder) {
|
||||
log.Printf("[document memory] dense index built: %d new vectors", count)
|
||||
}
|
||||
|
||||
// denseFor 计算文档的稠密向量:文本向量与其一等记忆块的媒体向量融合。
|
||||
// denseFor 计算文档的稠密向量:**事件正文** ⊕ 同事件媒体块的向量。
|
||||
//
|
||||
// 只有与当前统一空间同指纹的块向量才参与融合:不同模型/维度的旧向量
|
||||
// 属于另一个坐标系,混进去会算出一个两边都不像的方向。
|
||||
// 任意一路缺失时退化为另一路;都不可用返回 nil。
|
||||
// ## 为什么不能用 Summary
|
||||
//
|
||||
// 旧实现是 `VectorizeDense(doc.Summary + " " + doc.Content)`,而 Summary
|
||||
// 由 summarizeEntries → jieba 词袋生成,同一来源的摘要**逐字同质**:
|
||||
//
|
||||
// 来自 1 个来源的 1 条对话 (src0) 涉及: 用户, 提问
|
||||
// 来自 1 个来源的 1 条对话 (src1) 涉及: 用户, 提问
|
||||
//
|
||||
// 实测(真实 chineseclip,4 篇主题完全不同的短正文)两两余弦 0.85~0.95 ——
|
||||
// 几乎同向;查询分数全挤在 0.70~0.78,区分度 0.08 ⇒ 检索等同失效。
|
||||
//
|
||||
// 这与设计直接矛盾:ARCHITECTURE 写的是「统一多模态空间(主)」,而
|
||||
// Summary 属于主路径的输入,却由最末一级的词法分词产出。
|
||||
//
|
||||
// ## 媒体怎么办:不需要单独生成摘要
|
||||
//
|
||||
// 媒体块与文本块**在同一事件内**(Doc.Events[i].Blocks),事件的正文进入
|
||||
// 向量即覆盖了它的语义。为媒体单独生成描述正是 CAS 时代「描述式索引」
|
||||
// 的错误(描述是模型生成的二手信息),已在 v1.2.0 整体拆除,不重走。
|
||||
//
|
||||
// 若将来要「按图搜图」,那是视觉路(RecallBlocks 读块自己的向量)的职责,
|
||||
// 已有独立通路,与本函数无关。
|
||||
//
|
||||
// ## 存量兼容
|
||||
//
|
||||
// 无 Events 的旧文档(本次改动前归档的)退回 Content —— 那是当时唯一的
|
||||
// 原文载体,虽然它是渲染视图,但比 Summary 更接近内容。
|
||||
func (s *Store) denseFor(doc *Doc) []float64 {
|
||||
if s.denseSpace == nil || !s.denseSpace.Loaded() {
|
||||
return nil
|
||||
@ -209,9 +233,16 @@ func (s *Store) denseFor(doc *Doc) []float64 {
|
||||
fp := s.denseSpace.Fingerprint()
|
||||
dim := s.denseSpace.Dim()
|
||||
var parts [][]float64
|
||||
if tv, err := s.denseSpace.VectorizeDense(doc.Summary + " " + doc.Content); err == nil && len(tv) > 0 {
|
||||
parts = append(parts, tv)
|
||||
|
||||
// 文本路:用事件正文(原文),不用 Summary(jieba 词袋)。
|
||||
// 媒体块的语义由同一事件的文本覆盖,因此这里不需要为媒体单独编码。
|
||||
if text := documentTextForVector(doc); text != "" {
|
||||
if tv, err := s.denseSpace.VectorizeDense(text); err == nil && len(tv) > 0 {
|
||||
parts = append(parts, tv)
|
||||
}
|
||||
}
|
||||
|
||||
// 媒体路:块自带的多模态向量(同空间同维度才参与融合)。
|
||||
for _, b := range doc.Blocks {
|
||||
// 只比指纹不够:指纹相同但**维度不同**的块会被 FuseVectors 按
|
||||
// 「最大维度」拼成错维度向量(并覆盖掉文本向量),而结果又被
|
||||
@ -224,6 +255,39 @@ func (s *Store) denseFor(doc *Doc) []float64 {
|
||||
return vector.FuseVectors(parts...)
|
||||
}
|
||||
|
||||
// documentTextForVector 取文档用于向量化的文本:**正文**。
|
||||
//
|
||||
// 有 Events 时按事件顺序拼原文(来自块里的正文);无 Events 的存量文档
|
||||
// 退回 Content(渲染视图,但仍是原文最接近的载体)。
|
||||
// 绝不含 Summary —— 那是 jieba 词袋,会把全库拉向同一方向。
|
||||
func documentTextForVector(doc *Doc) string {
|
||||
if doc == nil {
|
||||
return ""
|
||||
}
|
||||
if !doc.HasEvents() {
|
||||
return strings.TrimSpace(doc.Content)
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, ev := range doc.Events {
|
||||
in, resp, _ := ev.InOut()
|
||||
if in != "" {
|
||||
b.WriteString(in)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if resp != "" {
|
||||
b.WriteString(resp)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
for _, tr := range ev.ToolResults {
|
||||
if tr.Output != "" {
|
||||
b.WriteString(tr.Output)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Reindex 重建 TF-IDF 索引(fallback 路径变更时调用)。
|
||||
func (s *Store) Reindex() {
|
||||
s.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user