# 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 的记忆系统就会从“可用”变成“可信、可演进”。
