mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
上一轮(memory-blockflow-repair-plan)修好了 L1↔L2 结构,但功能不可用。
用生产数据实测:三个互不相关查询分数全挤在 0.75~0.79,区分度≈0,
命中内容与查询无关。本次修四层。
## ① 向量输入:Summary + Content → 事件正文
denseFor 原用 `VectorizeDense(doc.Summary + " " + doc.Content)`,而 Summary
由 summarizeEntries → memory.ExtractKeywords → TokenizeContentWords → jieba.Tag()
生成。同一来源的摘要逐字同质:
来自 1 个来源的 1 条对话 (src0) 涉及: 用户, 提问
来自 1 个来源的 1 条对话 (src1) 涉及: 用户, 提问
真实 chineseclip 实测(4 篇主题完全不同的短正文):两两余弦 0.85~0.95。
这与设计矛盾 —— ARCHITECTURE 写的是「统一多模态空间(主)」,而 Summary
是主路径的输入,却由最末一级的词法分词产出。
改为 documentTextForVector(doc):按事件顺序拼原文(块里的正文)。
存量无 Events 的文档退回 Content。
★ 媒体不单独生成摘要:媒体块与文本块在同一事件内
(Doc.Events[i].Blocks),事件正文进入向量即覆盖其语义。
为媒体单独写描述正是 CAS 时代「描述式索引」的错误,v1.2.0 已拆除。
「按图搜图」是视觉路(RecallBlocks)的职责,已有独立通路。
## ② doc 层移除 jieba 语义生成
- summarizeEntries:jieba 词袋 → 取首条事件正文首行(真实内容,可读)
- 删除 extractTags / extractEntities(纯 jieba 词袋)
- 删除 Doc.Tags / Doc.Entities(前者唯一消费点是 doc_query 返回的
「标签: map, content」噪声行;后者零消费点)
- doc_commit 删除 tags 参数(无处可存,留着会骗模型)
★ TF-IDF 回退路径**保留**(Store.tfidfEmb/tfidfIdx/ensureTFIDF/Reindex):
它是「稠密空间没加载」时的兜底,按 ARCHITECTURE「统一多模态空间(主)
→ 词嵌入 → TF-IDF(回退)」必须保留。但它不能是主路径 —— 一旦稠密空间
可用,jieba 就不参与。
## ③ doc_query 返回真实内容
旧返回:「已加载 3 篇文档记忆: #1(来自 1 个来源的 1 条对话 (agent) 涉及: ok)」
——全 jieba 词袋,模型无法判断有没有用(这是它几乎不调用 doc_query 的直接原因)。
新返回:
已召回 3 篇历史记忆,原文已按各自**原始时间戳**插回对话时序(不是当前对话):
[1] 2026-06-07 08:09 · webui · 2 条事件 · 含 1 份媒体
配色方案怎么定?
结论:紫蓝红三色带按六比三比一…
共回填 N 条事件。要引用其中的细节,直接看对话时序里对应时间点即可。
每条给出「时间 · 来源 · 事件数 · 媒体数」+ 真实正文前几行。
## ④ 提示词:agent 终于能理解三层记忆
- doc_query 描述:一句话 → 覆盖「L2 是什么 / 何时用 / 调用后原文回填到时序 /
与 memory_recall 分工 / 消费式副作用」
- doc_commit 描述:说清「适合写整段过程结论,不适合写单个事实」
- 系统提示词新增【三层记忆】段(内核运行时块):L1 对话时序 / L2 历史记忆 /
L3 图记忆各自是什么、怎么取、如何选择
- 预填充文案(crossModalMarkdown):统计描述 → 时间+来源+正文开头,
并在块尾明确引导「要完整原文用 doc_query」
## ⑤ 向量算法版本(否则修复对存量无效)
实测踩到的坑:改完 denseFor 后,1055 篇存量文档分数纹丝不动(0.78/0.79/0.76,
区分度 0.003)—— 因为 DenseFP 只记「哪个向量空间」,不记「用什么文本算的」,
指纹不变 ⇒ 全部命中跳过条件 ⇒ 永不重算。
修法:DenseFP = 空间指纹 + "|" + docVectorAlgoVersion(v2)。
算法一变,存量自动重算一次。
## 判据(每条都有变异自证)
document/retrieval_quality_test.go(6 条):
- Top1HitsCorrectDoc / DistinctQueriesHitDistinctDocs / ScoreGapIsSignificant
- VectorInputIsBodyNotSummary(正文哨兵必须进向量)
- SummaryNoiseMustNotPolluteVector(主题不同的短正文,两两余弦 <0.80)
- AlgoVersionChangeForcesRecompute(换算法必须触发存量重算)
core/recalldoc_test.go(+2):返回值必须含时间/来源/真实正文/时序说明
core/prompt_memory_test.go(5 条):工具描述五项覆盖、doc_commit 范围、
三层记忆段落、tags 参数已删、预填充文案含内容与 doc_query 引导
变异全部被抓住(截断落库 / Summary+Content / 去掉算法版本 / 旧预填充文案 /
一句话工具描述 / 删三层记忆段)。
## 验证
- go build ./... 通过;go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- go test -race ./internal/memory/document/ ./internal/agent/core/:通过
- 真实数据(chineseclip 512 维):
· 旧格式存量(1055 篇,无 Events):4/5 → 仍是旧分数,等自然冲刷
· 新格式文档(走 ContextToDoc):4/5 top1 命中正确,
区分度最高 0.18(旧实现 0.003)
292 lines
12 KiB
Markdown
292 lines
12 KiB
Markdown
# 三层记忆可用性修复计划:统一向量化 + 提示词让 agent 真正理解
|
||
|
||
> 状态:**待执行**(2026-10-22)
|
||
> 前置:`docs/zh/memory-blockflow-repair-plan.md`(L1↔L2 结构同构,已实施)
|
||
> 本文修的是**上一轮没修的那一层**:结构对了,但检索质量与提示词仍然不可用。
|
||
|
||
---
|
||
|
||
## 〇、起点:承认上一轮的判断错误
|
||
|
||
上一轮我修了 L1↔L2 的结构(事件列表 + 正文进块 + 按原时序回填),
|
||
然后声称「机制已通」。**结构确实通了,但功能不可用。**
|
||
|
||
用生产数据实测(1054 篇文档、chineseclip 512 维)三个互不相关的查询:
|
||
|
||
```
|
||
查询 "doc 层召回是怎么设计的"
|
||
score=0.7834 来自 4 个来源的 61 条对话 (cold_storage, agentcli...)
|
||
score=0.7795 来自 1 个来源的 2 条对话 (agent) 涉及: map, content, dsh, 占用
|
||
score=0.7768 来自 1 个来源的 1 条对话 (agent) 涉及: 自检, OK
|
||
|
||
查询 "hmap 插件安装验签"
|
||
score=0.7922 来自 1 个来源的 1 条对话 (agent) 涉及: map, content, 是不是, pi
|
||
|
||
查询 "QQ 头像"
|
||
score=0.7601 来自 1 个来源的 1 条对话 (agent) 涉及: CQ, qq, ok
|
||
```
|
||
|
||
**分数全挤在 0.75~0.79,区分度≈0,命中内容与查询无关。**
|
||
|
||
### 判据缺失(最该记的一条)
|
||
|
||
上一轮写了 16 条判据、6 个变异,全部通过。但**没有一条验证「检索结果必须与查询相关」**。
|
||
所以「全绿」证明的只是我实现了我以为的东西 —— 不是它可用。
|
||
|
||
> ★ 教训:**结构判据不能替代质量判据**。
|
||
> 「字段非空」「条数正确」「不 panic」都可能在检索完全失效时依然全绿。
|
||
|
||
---
|
||
|
||
## 一、根因(三条,按依赖排序)
|
||
|
||
### 根因 1:向量的输入是 jieba 词袋,不是正文
|
||
|
||
`internal/memory/document/document.go` — `denseFor`:
|
||
|
||
```go
|
||
if tv, err := s.denseSpace.VectorizeDense(doc.Summary + " " + doc.Content); err == nil {
|
||
```
|
||
|
||
而 `Summary` 由 `summarizeEntries` 生成,后者调用链是:
|
||
|
||
```
|
||
summarizeEntries → memory.ExtractKeywords → TokenizeContentWords → jieba.Tag()
|
||
extractTags → 同上
|
||
extractEntities → 同上
|
||
```
|
||
|
||
`TokenizeContentWords`(`internal/memory/cut.go:256`)就是 `jieba.Tag()` 分词 + 停用词过滤,
|
||
产出形如 `涉及: map, content, 是不是, pi`。
|
||
|
||
**这是与设计直接矛盾的**。`ARCHITECTURE.md` L131 / L174 明写向量层级:
|
||
|
||
```
|
||
统一多模态空间(主,带 fingerprint)→ StaticEmbedder 词嵌入 → TF-IDF(回退)
|
||
```
|
||
|
||
`Summary` 是**主路径的输入**,却由**最末一级的词法分词**产出。
|
||
全库所有文档的 Summary 因此高度同质(都是「来自 N 个来源的 M 条对话 涉及: …」),
|
||
余弦相似度自然全挤在高位、无区分度。
|
||
|
||
### 根因 2:媒体块没有语义,且不需要单独生成
|
||
|
||
设计要求「doc 支持多模态摘要」。当前实现里:
|
||
|
||
- `Doc.Summary` 只有文本路径,**媒体块从不参与**
|
||
- `crossModalMarkdown` 里媒体只渲染成 `[image/png blk_media_xxx]`(`blockLabel`:MIME + 短 ID)
|
||
- 一篇带 20 张图的文档,摘要里没有「有图」的任何信息
|
||
|
||
**但「多模态摘要」这个提法本身是歧路**(用户 2026-10-22 指出):
|
||
|
||
> 媒体 doc 块一般是和文本紧邻的,所以对应的文本就足够了
|
||
|
||
`Doc.Events[i].Blocks` 里媒体块与文本块**在同一事件内**。媒体块的语义由
|
||
**同事件的文本**天然覆盖,不需要为每个媒体单独生成描述(那正是 CAS 时代
|
||
「描述式索引」被拆除的同一个错误)。
|
||
|
||
⇒ 正确做法不是「给媒体写摘要」,而是:**向量用事件正文算,媒体自动被覆盖**。
|
||
|
||
### 根因 3:提示词与注入文案没有任何引导
|
||
|
||
**(a)`doc_query` 的工具描述只有一句话**
|
||
|
||
```go
|
||
toolDefWith("doc_query", "查询文档记忆。输入查询内容,返回相关文档摘要。", ...)
|
||
```
|
||
|
||
逐条对照问题:
|
||
|
||
| 应有信息 | 现状 |
|
||
|---|---|
|
||
| L2 是什么 | ❌ 只说「文档记忆」,没有定义 |
|
||
| 何时该用 | ❌ 无 |
|
||
| 调用后会发生什么 | ❌ 「返回相关文档摘要」**与真实行为不符**(真实是逐条按原时序回填到对话时序) |
|
||
| 与 `memory_recall` 的分工 | ❌ 无 |
|
||
|
||
对比同文件里 `【输出规则】`(6 条)、`【工具执行顺序】`(4 条)的详细程度,
|
||
`doc_query` 是最敷衍的一个。
|
||
|
||
**(b)预填充注入文案没有引导**
|
||
|
||
`crossModalMarkdown` 输出:
|
||
|
||
```
|
||
【跨模态相关记忆】
|
||
[1] 来自 4 个来源的 61 条对话 (cold_storage, agentcli...) (来源:context_archived) 相关度:0.78(文本命中)
|
||
```
|
||
|
||
**没有一个字说「可以用 doc_query 取全文」**。模型看到这段只能忽略。
|
||
|
||
**(c)系统提示词从未解释三层记忆是什么**
|
||
|
||
`grep 记忆 internal/agent/core/tooldefs.go` 全量命中里,没有任何一段说明
|
||
Context / Document / Graph 三者的分工与使用时机。agent 只能从工具名猜。
|
||
|
||
---
|
||
|
||
## 二、修复步骤(按依赖,每步先写会红的判据)
|
||
|
||
### 步骤 1:判据先行 —— 检索质量
|
||
|
||
**先写测试,此时必须红。**
|
||
|
||
`internal/memory/document/retrieval_quality_test.go`:
|
||
|
||
- 构造真实语料(约 50 篇,内容取自生产副本的真实事件正文 + 人工标注的查询-答案对)
|
||
- 断言:给定查询,**正确文档必须排第一**(top1 准确率 100%,或至少 top3 内)
|
||
- 断言:互不相关的查询,top1 结果**必须不同**
|
||
- 断言:正确命中的分数必须**显著高于**非命中(区分度阈值)
|
||
|
||
**变异**:把 `denseFor` 改回 `Summary + Content` ⇒ 必须红。
|
||
**变异**:把正文替换成 jieba 词袋 ⇒ 必须红。
|
||
|
||
这一步是整个计划的地基。没有它,后面每一步都只是「我以为改对了」。
|
||
|
||
### 步骤 2:向量输入改为正文
|
||
|
||
`internal/memory/document/document.go` — `denseFor`:
|
||
|
||
```
|
||
现在:VectorizeDense(doc.Summary + " " + doc.Content)
|
||
改为:VectorizeDense(事件正文拼接)
|
||
有 Events → 按事件顺序取 InOut() 的 input + response(文本块内容)
|
||
无 Events(存量)→ 退回 Content
|
||
```
|
||
|
||
**媒体如何被覆盖**:媒体块与文本块在同一事件内,事件的正文进入向量即覆盖它。
|
||
不需要为媒体单独编码 —— 若将来要「按图搜图」,那是视觉路(`RecallBlocks`)的职责,
|
||
已有独立通路,与文本路不冲突。
|
||
|
||
### 步骤 3:删掉 jieba 摘要路径
|
||
|
||
`Summary` / `Tags` / `Entities` 目前全部由 `ExtractKeywords`(jieba)产出。
|
||
|
||
- `Tags`:唯一消费点是 `doc_query` 返回值里的「标签: …」(`toolcall.go:1038`),
|
||
**是词袋噪声,删除该行**
|
||
- `Entities`:`Doc.Entities` 无任何非测试读取点(跨包的 `injected.Entities`
|
||
是 `memory.Entity` 另一个类型,无关),**字段可删**
|
||
- `Summary`:改为**纯展示**(人工阅读 / 日志),由正文首句截取生成,**不参与向量**
|
||
|
||
⚠️ 需先确认:`Doc.Tags` / `Doc.Entities` 是否有 WebUI / SDK 侧读取。
|
||
(已知 `sdk/memory_impl.go` 的 `Doc` 只有 `ID/Title/Content`,无 Tags/Entities。)
|
||
|
||
### 步骤 4:预填充注入正文片段 + 引导
|
||
|
||
`crossModalMarkdown` 改为:
|
||
|
||
```
|
||
【历史记忆(按相关度)】
|
||
[1] 2026-10-12 11:09 · qq · 相关度 0.83
|
||
紫蓝红三色带的配色参考,老大要求…(正文首 2 行)
|
||
[2] …(含 2 张图:image/png, image/jpeg)
|
||
|
||
这些是**历史**片段(不是当前对话)。要看完整原文,
|
||
调用 doc_query 把对应事件按原时间戳拉回对话时序。
|
||
```
|
||
|
||
要点:
|
||
- 去掉「来自 N 个来源的 M 条对话」这类统计描述
|
||
- 给出**时间 + 来源 + 正文开头**(真实内容,让模型能判断相关性)
|
||
- 媒体只报数量与类型,不编描述
|
||
- **明确告知 doc_query 的作用**
|
||
|
||
### 步骤 5:`doc_query` 工具描述重写
|
||
|
||
按 `memory_*` 的详细度,必须覆盖:
|
||
|
||
- L2 是什么(历史事件层,不直接进当前对话)
|
||
- 何时用(记得办过但想不起细节 / 跨会话跨天 / 需要原始过程而非结论)
|
||
- 调用后发生什么(**把事件逐条按原时间戳回填到对话时序**,不是返回一段摘要)
|
||
- 与 `memory_recall` 的分工(图记忆=关系与最新值;doc=整段过程与原文)
|
||
- 副作用(从 L2 取出,取走后该文档不再在 L2)
|
||
|
||
### 步骤 6:系统提示词增加「三层记忆」段落
|
||
|
||
在 `buildSystemPrompt` 里加一段,说清三层是什么、怎么用:
|
||
|
||
```
|
||
【三层记忆】记忆分三层,你现在看到的是第一层。
|
||
- 对话时序(L1):最近的对话,已在你的上下文里。超过窗口的会被移出。
|
||
- 历史记忆(L2):移出后的事件与原文仍完整保存。自动预填充会给你相关片段;
|
||
要给看全文/完整过程,用 doc_query —— 它会把那些事件按原时间戳插回对话时序。
|
||
- 图记忆(L3):关系与事实的长期存储(谁是什么、什么等于什么、最新值是什么)。
|
||
用 memory_recall 查;用 memory_commit 记。
|
||
选择:要"原文与过程"→ doc_query;要"关系与当前值"→ memory_recall;
|
||
要"成文知识"→ knowledge_search。
|
||
```
|
||
|
||
**注意**:这段属于「内核运行时块」(提示词分层规范:内核行为由内核生成),
|
||
不是工具用法(工具用法归 `description`)。它讲的是**内核的真实执行语义**
|
||
(回填到时序、自动预填充),符合分层原则。
|
||
|
||
---
|
||
|
||
## 三、判据清单(每步都要能红)
|
||
|
||
| # | 判据 | 变异(必须红) |
|
||
|---|---|---|
|
||
| 1 | 检索质量:top1 命中 / 不相关查询 top1 不同 / 分数有区分度 | `denseFor` 改回 `Summary+Content`;或换回 jieba 词袋 |
|
||
| 2 | 向量输入是正文:断言 `denseFor` 读的是 Events 正文(可用哨兵串) | 正文里埋哨兵,改回 Summary ⇒ 哨兵不出现在向量输入 |
|
||
| 3 | 摘要不含 jieba 词袋:`Summary` 不出现「来自 N 个来源的 M 条对话」 | 恢复 `summarizeEntries` ⇒ 红 |
|
||
| 4 | 预填充文案含正文与引导:必须含 `doc_query` 字样 + 正文片段 | 改回旧渲染 ⇒ 红 |
|
||
| 5 | 工具描述覆盖五项(L2 定义/何时用/调用后行为/分工/副作用) | 删任一项 ⇒ 红 |
|
||
| 6 | 系统提示词含三层记忆段落且语义与实现一致 | 回填行为改掉而提示词不改 ⇒ 红(跨文件一致性) |
|
||
|
||
**判据 6 特别说明**:提示词说「按原时间戳插回时序」这件事,
|
||
必须与 `restoreDocEvents` 的真实行为绑在一条判据里 ——
|
||
否则就是「提示词在说谎」,那比没说更糟。
|
||
|
||
---
|
||
|
||
## 四、禁止事项
|
||
|
||
| 禁止 | 原因 |
|
||
|---|---|
|
||
| 用 jieba 词袋当向量输入或摘要内容 | 与「统一向量化」直接矛盾;全库同质,检索失效 |
|
||
| 为媒体块单独生成文本描述 | CAS 时代的「描述式索引」错误;媒体语义由同事件文本覆盖 |
|
||
| 只改结构不改提示词 | 机制改了模型不知道 = 等于没改 |
|
||
| 用「字段非空 / 条数正确 / 不 panic」当检索判据 | 检索完全失效时这些也全绿 |
|
||
| 声称「机制已通」而不给出生产数据的检索质量实测 | 上一轮的教训 |
|
||
|
||
---
|
||
|
||
## 五、验证
|
||
|
||
1. `go test ./... -count=1` 全绿(现有 58 包基线)
|
||
2. 新判据全绿,且 6 个变异各自被抓住
|
||
3. **生产副本实测**:用真实数据跑检索,给出 top1/top3 命中率与分数分布
|
||
(对比修复前的 0.75~0.79 无区分度)
|
||
4. 生产部署后用真实对话观察:agent 是否在需要时调用 `doc_query`
|
||
|
||
---
|
||
|
||
## 六、标准流程(本次严格执行)
|
||
|
||
上次直接提交 `main` + rebase + amend + 绕过 CI,不符合 `docs/git-branching.md`。
|
||
这次必须:
|
||
|
||
```
|
||
1. git checkout -b feature/memory-usability main
|
||
2. 按步骤 1→6 提交(每步独立 commit,先写判据再改)
|
||
3. git push -u origin feature/memory-usability
|
||
4. 开 PR
|
||
5. 等 CI 10 项全部通过
|
||
6. 合并 main(不绕过必需检查)
|
||
7. 部署 + 生产验证
|
||
```
|
||
|
||
⚠️ 远端仓库已改名 `JianFeeeee/HologramAgent`(origin 与 gitcode 均已更新)。
|
||
|
||
---
|
||
|
||
## 七、待确认
|
||
|
||
1. **`Doc.Tags` / `Doc.Entities` 是否可删?**
|
||
我查到非测试读取点只有 `doc_query` 返回里的「标签:」一行(可删)。
|
||
但需确认 WebUI / 外部插件是否依赖(SDK 的 `Doc` 无这两个字段)。
|
||
|
||
2. **判据 1 的语料从哪来?**
|
||
建议从生产副本抽 50 篇真实文档 + 人工标注 10~15 个查询-答案对。
|
||
是否允许我把这些真实文本(脱敏后)作为测试 fixture 入库?
|