diff --git a/docs/MEMORY_SYSTEM_DESIGN.md b/docs/MEMORY_SYSTEM_DESIGN.md new file mode 100644 index 0000000..36354ba --- /dev/null +++ b/docs/MEMORY_SYSTEM_DESIGN.md @@ -0,0 +1,551 @@ +# PicoBot 记忆系统设计与迁移方案 + +编写日期:2026-06-17 + +## 背景 + +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 的记忆系统就会从“可用”变成“可信、可演进”。