Files
HomeAgent/docs/zh/memory-usability-plan.md
JianFeeeee e2cb6e70e0 feat(memory): doc 层去掉 jieba 摘要 + doc_query 返回真实内容 + 三层记忆提示词
上一轮(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)
2026-10-22 19:49:25 +08:00

292 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 三层记忆可用性修复计划:统一向量化 + 提示词让 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 入库?