Files
HomeAgent/docs/zh/memory-usability-plan.md
JianFeeeee 1d52ba85d5 docs: 术语与架构措辞对齐(coldcontext / stablemem / security+device 模块)
1. 术语统一:
   - doc_query / doc_commit → coldcontext_query / coldcontext_commit
   - 「图记忆 / Graph 层」→「稳定记忆 / Stablemem 层(L3)」
   - 分层命名补上 L1/L2/L3 语义:Context(热) / Cold-context(冷) / Stablemem(持久)
   涉及 README.md、README_EN.md、assets/docs/{zh,en}/{ARCHITECTURE,OVERVIEW}.md

2. 补上本次最大的结构变化(文档里此前完全没有):
   - 安全防火墙作为**独立内核模块**:凭据闸门、工具黑白名单+沙箱、
     敏感词与提示词注入预警、按主体授权与插件命名限式;显式代理而非 monkey patch
   - device 数据面作为**独立内核模块**:工具注册与路由、inputch/outputch、设备登记;
     路由表在 internal/toolroute 以避免 core ↔ kernelboot 成环

3. 历史计划文档(memory-*.md / context-overflow-l4-design / entities-retirement /
   git-branching)加**归档说明**:明确术语已过时、现行口径以 ARCHITECTURE.md 为准。
   刻意不改写历史正文 —— 那是决策记录,改写等于伪造。

剩余旧词只出现在归档文档与 scripts/capability-bench/PLAN.md(评测历史记录)。
2026-10-27 09:58:14 +08:00

298 lines
13 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-27)**:本文是历史计划,**术语已过时** ——
> `doc_query` / `doc_commit` 已改名为 `coldcontext_query` / `coldcontext_commit`,
> 图记忆(Graph)在对外语义上改称 **stablemem(稳定记忆)**。
> 现行架构与术语以 `assets/docs/zh/ARCHITECTURE.md` 为准;本文保留作为决策记录。
> 状态:**待执行**(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 入库?