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