PicoBot/docs/MEMORY_SYSTEM_DESIGN.md

554 lines
15 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.

# PicoBot 记忆系统设计与迁移方案
编写日期2026-06-17
> 历史说明:本文的 `ContextCompressor`、Timeline 回填和时间戳边界描述是 1.19.0 之前的基线。当前上下文恢复由单一活动 checkpoint 和 durable `seq` 边界驱动Timeline 只是提交后的派生检索记录;以 [CONTEXT_COMPACTION_DESIGN.md](CONTEXT_COMPACTION_DESIGN.md) 和 [ARCHITECTURE.md](ARCHITECTURE.md) 为准。
## 背景
PicoBot 当前已经具备最基础的记忆能力:
- `Knowledge` 记忆:长期事实、偏好、项目知识。
- `Timeline` 记忆:由上下文压缩产生的会话摘要。
- `memory_recall` / `timeline_recall`:模型可主动检索记忆。
- 上下文压缩时会把摘要写入 `Timeline`,并在恢复会话时回填最近摘要。
这套实现已经能工作,但它更像“存储 + 召回”的初版,而不是完整的记忆系统。主要短板是:
- 记忆写入依赖显式工具调用,缺少自动抽取与整理闭环。
- 检索主要是关键词/FTS缺少排序、衰减和冲突消解。
- `Timeline``Knowledge`、运行时上下文之间的边界还不够严格。
- 记忆治理能力不足,缺少过期、失效、来源追踪、置信度等元数据。
本方案目标是把 PicoBot 的记忆从“能记住”升级为“记得准、找得到、能更新、会遗忘、可观测”。
## 现状基线
当前代码中的记忆链路大致如下:
1. `SessionManager` 在处理用户消息时,先召回 `Knowledge` 记忆并拼进运行时上下文。
2. `ContextCompressor` 在上下文过大时压缩消息历史,并把压缩结果写为 `Timeline` 记忆。
3. `timeline_recall` 工具允许模型主动检索历史摘要。
4. `memory_store` / `memory_recall` / `memory_forget` 允许模型手动管理 `Knowledge` 记忆。
现有实现的特点:
- 存储是 SQLite。
- 检索是 FTS5 + LIKE 回退。
- 记忆条目只有 `key/content/category/importance/session_id/timestamps`
- 配置里已经预留了 `recall_limit``timeline_retention_days``idle_consolidation_minutes` 等参数,但整体闭环还没有完全落地。
## 设计目标
### 必须达到
1. 自动化
- 从对话中自动抽取稳定事实、偏好、项目约束、关键决定。
- 不依赖模型每次都显式调用 `memory_store`
2. 可控
- 记忆写入要有明确来源、置信度和类别。
- 支持更新、失效、覆盖、删除。
3. 可检索
- 检索不能只依赖关键词匹配。
- 需要结合相关性、重要性、时效性、会话范围进行排序。
4. 可回填
- 会话恢复时,要能回填“最近摘要 + 相关历史 + 相关知识”,但不能把噪声无限回灌。
5. 可治理
- 需要定期清理过期 timeline。
- 低质量或冲突记忆要可降权、可 supersede、可追溯。
### 暂不做
- 不在第一阶段引入复杂的分布式记忆服务。
- 不强制接入外部向量数据库。
- 不把记忆系统做成一个独立的产品边界;它仍然属于 PicoBot runtime。
## 目标架构
建议把记忆系统拆成四层。
### 1. 运行时上下文层
用途:
- 当前轮的系统提示词。
- 运行时间、会话 ID、临时提醒、技能提示。
- 不应被长期记忆污染。
规则:
- 只属于当前 turn。
- 不入库,或仅作为可追踪的审计记录入库,不参与长期 recall。
### 2. Timeline 层
用途:
- 会话摘要。
- 上下文压缩结果。
- 历史状态回放。
规则:
- 按 session 归属。
- 可被 `timeline_recall` 查询。
- 默认保留有限时间,过期可清理。
- 适合作为“发生过什么”的记录,而不是“世界上长期成立的事实”。
### 3. Knowledge 层
用途:
- 用户稳定偏好。
- 项目事实。
- 长期决策。
- 可复用的经验和约束。
规则:
- 需要来源追踪和更新时间。
- 可以被时间衰减、冲突消解、覆盖和删除。
- 适合被 `memory_recall` 检索并注入 system/runtime context。
### 4. Archive 层
用途:
- 低价值但不该直接丢弃的历史。
- 被 supersede 的旧知识。
- 过期 timeline 的冷存档。
规则:
- 不参与默认 recall。
- 仅在排障、导出、审计或手工恢复时查看。
## 统一数据模型
建议将 `memories` 表从“扁平文本”升级为“可治理条目”。
### 推荐字段
```text
id 唯一 ID
key 语义 key稳定标识一条知识或摘要
content 正文
category knowledge / timeline / archive / scratch
session_id 归属会话
source_session_id 来源会话
source_message_id 来源消息或 turn 标识
source_type explicit_tool / auto_consolidation / context_compression / manual
importance 重要性 0.0-1.0
confidence 置信度 0.0-1.0
created_at 创建时间
updated_at 更新时间
last_accessed_at 最近召回时间
expires_at 过期时间,可空
superseded_by 被哪条记忆覆盖
status active / superseded / archived / deleted
tags 便于过滤和检索
embedding_ref 未来可选的向量引用
```
### 字段意义
- `importance`:这条记忆值不值得保留。
- `confidence`:这条记忆有多可靠。
- `source_*`:这条记忆从哪里来,方便审计和冲突处理。
- `expires_at`:是否该被自动遗忘。
- `superseded_by`:是否已经被更可信的新版本覆盖。
## 写入策略
### 1. 显式写入
仍保留 `memory_store` 工具。
适用场景:
- 用户明确要求记住。
- 模型确认了稳定事实。
- 人工/外部流程显式提供知识条目。
要求:
- 必须带稳定 `key`
- 建议附带 `importance``confidence`
- 允许覆盖同 key 的旧值,但要保留变更痕迹。
### 2. 自动知识抽取
新增一条 consolidation 流程,从 turn 中抽取结构化记忆:
- `facts`
- `preferences`
- `decisions`
- `constraints`
- `open_loops`
抽取结果应满足:
- 只写“稳定可复用”的信息。
- 不写临时情绪、不写会话噪声、不写大段原文。
- 默认先进入“候选记忆区”,通过规则或 LLM 二次确认后再升格为 active knowledge。
### 3. 会话压缩写入 Timeline
当前 `ContextCompressor` 继续承担“会话摘要”的职责,但建议改成两步:
1. 压缩当前上下文,生成可注入的短摘要。
2. 同时生成结构化 timeline entry写入 timeline store。
这样 timeline 摘要和给模型看的压缩摘要可以一致,但不必完全相同。
## 读取策略
### 默认读取顺序
每轮 user turn 建议按以下顺序构建上下文:
1. 运行时上下文
2. 当前会话最近消息
3. 当前会话最近 timeline 摘要
4. 与当前 query 相关的 Knowledge 记忆
5. 必要时再补充更旧的 Timeline 召回
### Knowledge 召回排序
建议使用混合评分:
```text
final_score =
relevance_score
+ importance_weight
+ recency_weight
- redundancy_penalty
- superseded_penalty
```
建议排序规则:
- 先按相关性筛选候选。
- 再按重要性和更新时间重排。
- 被 supersede 的条目默认不参与主召回。
- 低置信度条目只在结果不足时补位。
### Timeline 召回策略
Timeline 更适合按 session 和时间窗口召回:
- 恢复会话时优先加载最近几条摘要。
- 只有当模型显式需要回顾历史,或者当前话题明显切换,才主动拉更多 timeline。
- 跨会话回顾时先查同主题摘要,再查同 session 摘要。
## 冲突与失效
### 冲突类型
1. 同 key 冲突
- 新记忆与旧记忆 key 相同。
- 处理方式upsert旧版本保留更新历史。
2. 语义冲突
- 内容不同,但描述的是同一事实。
- 处理方式:标记旧条目 superseded保留新条目为 active。
3. 时效冲突
- 旧事实已经过期。
- 处理方式:按 `expires_at` 或规则自动归档。
### 推荐处理流程
1. 新记忆先入候选队列。
2. 对候选记忆做相似度检查。
3. 如果与已有 active knowledge 冲突:
- 保留新条目。
- 给旧条目标记 `superseded_by`
4. 如果置信度过低:
- 降权但不立即删除。
5. 如果确定失效:
- 移入 archive 或直接删除。
## 过期与清理
### Timeline 清理
Timeline 默认保留 90 天是合理起点,但建议把它变成真正的后台任务:
- 每日或定时运行。
- 清理 `category = timeline` 且过期的条目。
- 清理前先做统计和日志记录。
### Knowledge 清理
Knowledge 不建议简单按天数删。
更合理的是:
- 低重要度 + 低置信度 + 长时间未访问的记忆,先降权。
- 明确过期的条目进入 archive。
- 被 superseded 的条目保留一段审计窗口后再清理。
### 噪声过滤
禁止写入或默认不回灌的内容:
- 自动压缩摘要的中间副本。
- 运行时元信息。
- 工具回显噪声。
- 模板/框架泄漏。
- 明显的无意义重复片段。
## 与现有代码的映射
建议的模块职责演进如下:
### `src/memory/`
保留高层 API但增加
- 条目元数据
- 记忆状态机
- 统一评分接口
- 冲突处理接口
### `src/storage/memory.rs`
负责:
- SQLite CRUD
- FTS/LIKE 检索
- Timeline 清理
- 批量更新 superseded 状态
后续可扩展:
- `last_accessed_at` 更新
- 记忆状态批处理
- 按 session / namespace 的索引优化
### `src/agent/context_compressor.rs`
继续负责上下文压缩,但建议拆成两步:
- 压缩历史
- 产出 timeline 记录
并把“是否生成 timeline / 是否写入 memory”做成明确开关。
### `src/session/session.rs`
负责:
- 选择哪些记忆进入当前 turn
- 会话恢复时注入最近 timeline
- 持久化 session 级别的压缩/归档状态
不应承担记忆抽取和冲突消解的重逻辑。
### `src/tools/memory.rs`
保留工具接口,但建议:
- 增加 `confidence``expires_at``tags` 等参数。
- `memory_recall` 支持更清晰的过滤条件。
- `memory_forget` 支持软删和 hard delete 两种模式。
## 迁移方案
建议分 5 个阶段推进。
### Phase 0: 文档和协议对齐
目标:
- 先把目标讲清楚,避免边改边跑偏。
产物:
- 本文档。
- 更新 `README` 中的记忆入口。
- 补齐记忆数据模型和流程图。
验收:
- 团队能明确区分 runtime / timeline / knowledge / archive 四层。
### Phase 1: 只扩 schema不改行为
目标:
- 给后续治理能力留好数据位。
改动建议:
- `memories` 表增加 `confidence``source_type``source_message_id``source_session_id``last_accessed_at``expires_at``superseded_by``status``tags` 等字段。
- 兼容老数据:老字段缺省时回退到旧逻辑。
- `SessionMeta` 可继续保留现有压缩时间戳。
验收:
- 老数据能正常读写。
- 现有记忆工具和压缩流程不需要改调用方。
### Phase 2: 补自动 consolidation
目标:
- 从“显式写记忆”升级为“自动抽取记忆”。
改动建议:
- 在一次 turn 结束后,异步启动 consolidation。
- 从最近 turn 中抽取:
- timeline summary
- knowledge candidates
- 新增去噪逻辑:
- 不写工具噪声
- 不写运行时上下文
- 不写重复摘要
推荐落点:
- `src/session/session.rs`
- `src/agent/context_compressor.rs`
- `src/memory/`
验收:
- 用户不手动调用 `memory_store` 时,也能逐步积累稳定知识。
- timeline 记录与知识条目不再混写。
### Phase 3: 引入混合召回与排序
目标:
- 让 recall 真正“像记忆”而不是“像全文搜索”。
改动建议:
- 召回增加排序层:
- 相关性
- 重要性
- 时效性
- 状态过滤
- `Knowledge``Timeline` 使用不同召回策略。
- 恢复会话时优先加载最近 timeline再按 query 召回知识。
可选增强:
- 后续接入 embedding / rerank。
验收:
- 长会话和多主题对话的召回质量明显提升。
- 旧记忆不会总是压过新记忆。
### Phase 4: 冲突消解、失效与清理
目标:
- 让记忆系统可治理。
改动建议:
- 对知识条目做冲突检测。
- 支持 supersede。
- 对 timeline 做定期清理。
- 对低质量或长期未访问的知识降权或归档。
推荐定时任务:
- timeline retention cleanup
- stale memory decay
- archive compaction
验收:
- 过期内容不会无限膨胀。
- 旧事实能被新事实覆盖。
### Phase 5: 兼容层收口
目标:
- 把临时兼容逻辑收敛成稳定接口。
改动建议:
- 清理历史遗留的“摘要直接当知识”路径。
- 统一只从 memory service 读取记忆,不再绕过治理层直接查表。
- 为 debug/export 保留只读视图,但不参与默认注入。
验收:
- 记忆系统对外只有稳定接口。
- 内部实现可继续演进,而不影响 Session / Agent 调用方。
## 推荐实现顺序
如果只做一轮最有性价比的改造,我建议按这个顺序:
1. `schema + metadata`
2. `自动 consolidation`
3. `混合召回与重排`
4. `冲突消解与衰减`
5. `定时清理`
这个顺序的好处是:
- 先把可观察和可治理的数据补齐。
- 再补自动写入,避免“写进去但以后无法管理”。
- 最后再优化召回体验。
## 风险与回退
### 风险
- 自动抽取可能把短期上下文误判成长期知识。
- 召回排序不稳时,模型可能看到过多或过少记忆。
- 迁移 schema 时如果没有兼容逻辑,老数据会丢。
### 回退策略
- 保留现有 `memory_store` / `memory_recall` 作为兼容入口。
- 所有新字段都应可空。
- consolidation 可以通过配置关闭。
- 召回排序可退回 FTS5 + importance 的简化模式。
## 验收标准
记忆系统完成迁移后,应满足:
1. 用户不手动存记忆时,系统仍能逐步积累稳定知识。
2. `Knowledge` 的 recall 结果更准,噪声更少。
3. `Timeline` 不会无限膨胀,且能按 session 回放。
4. 旧事实可被新事实覆盖,冲突状态可追踪。
5. `SessionManager` 不再承担记忆抽取的核心业务逻辑。
6. 任意记忆条目都能回答三个问题:
- 它从哪里来?
- 它为什么还活着?
- 它什么时候该被忘掉?
## 对 PicoBot 当前实现的落地建议
最直接的落点是:
- 先在 `src/storage/memory.rs``src/memory/types.rs` 扩字段。
- 再在 `src/agent/context_compressor.rs` 增加结构化摘要输出。
- 然后在 `src/session/session.rs` 增加 consolidation hook。
- 最后把 `src/tools/memory.rs` 升级为带治理字段的工具接口。
如果只做最小闭环,至少要先实现:
- `Timeline` 的定时清理
- `Knowledge` 的自动抽取
- `Knowledge` 的冲突消解
- 召回结果的时间衰减和重排
这样 PicoBot 的记忆系统就会从“可用”变成“可信、可演进”。