PicoBot/docs/MEMORY_SYSTEM_DESIGN.md

15 KiB
Raw Permalink Blame History

PicoBot 记忆系统设计与迁移方案

编写日期2026-06-17

历史说明:本文的 ContextCompressor、Timeline 回填和时间戳边界描述是 1.19.0 之前的基线。当前上下文恢复由单一活动 checkpoint 和 durable seq 边界驱动Timeline 只是提交后的派生检索记录;以 CONTEXT_COMPACTION_DESIGN.mdARCHITECTURE.md 为准。

背景

PicoBot 当前已经具备最基础的记忆能力:

  • Knowledge 记忆:长期事实、偏好、项目知识。
  • Timeline 记忆:由上下文压缩产生的会话摘要。
  • memory_recall / timeline_recall:模型可主动检索记忆。
  • 上下文压缩时会把摘要写入 Timeline,并在恢复会话时回填最近摘要。

这套实现已经能工作,但它更像“存储 + 召回”的初版,而不是完整的记忆系统。主要短板是:

  • 记忆写入依赖显式工具调用,缺少自动抽取与整理闭环。
  • 检索主要是关键词/FTS缺少排序、衰减和冲突消解。
  • TimelineKnowledge、运行时上下文之间的边界还不够严格。
  • 记忆治理能力不足,缺少过期、失效、来源追踪、置信度等元数据。

本方案目标是把 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_limittimeline_retention_daysidle_consolidation_minutes 等参数,但整体闭环还没有完全落地。

设计目标

必须达到

  1. 自动化
  • 从对话中自动抽取稳定事实、偏好、项目约束、关键决定。
  • 不依赖模型每次都显式调用 memory_store
  1. 可控
  • 记忆写入要有明确来源、置信度和类别。
  • 支持更新、失效、覆盖、删除。
  1. 可检索
  • 检索不能只依赖关键词匹配。
  • 需要结合相关性、重要性、时效性、会话范围进行排序。
  1. 可回填
  • 会话恢复时,要能回填“最近摘要 + 相关历史 + 相关知识”,但不能把噪声无限回灌。
  1. 可治理
  • 需要定期清理过期 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 表从“扁平文本”升级为“可治理条目”。

推荐字段

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
  • 建议附带 importanceconfidence
  • 允许覆盖同 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 召回排序

建议使用混合评分:

final_score =
  relevance_score
  + importance_weight
  + recency_weight
  - redundancy_penalty
  - superseded_penalty

建议排序规则:

  • 先按相关性筛选候选。
  • 再按重要性和更新时间重排。
  • 被 supersede 的条目默认不参与主召回。
  • 低置信度条目只在结果不足时补位。

Timeline 召回策略

Timeline 更适合按 session 和时间窗口召回:

  • 恢复会话时优先加载最近几条摘要。
  • 只有当模型显式需要回顾历史,或者当前话题明显切换,才主动拉更多 timeline。
  • 跨会话回顾时先查同主题摘要,再查同 session 摘要。

冲突与失效

冲突类型

  1. 同 key 冲突
  • 新记忆与旧记忆 key 相同。
  • 处理方式upsert旧版本保留更新历史。
  1. 语义冲突
  • 内容不同,但描述的是同一事实。
  • 处理方式:标记旧条目 superseded保留新条目为 active。
  1. 时效冲突
  • 旧事实已经过期。
  • 处理方式:按 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

保留工具接口,但建议:

  • 增加 confidenceexpires_attags 等参数。
  • memory_recall 支持更清晰的过滤条件。
  • memory_forget 支持软删和 hard delete 两种模式。

迁移方案

建议分 5 个阶段推进。

Phase 0: 文档和协议对齐

目标:

  • 先把目标讲清楚,避免边改边跑偏。

产物:

  • 本文档。
  • 更新 README 中的记忆入口。
  • 补齐记忆数据模型和流程图。

验收:

  • 团队能明确区分 runtime / timeline / knowledge / archive 四层。

Phase 1: 只扩 schema不改行为

目标:

  • 给后续治理能力留好数据位。

改动建议:

  • memories 表增加 confidencesource_typesource_message_idsource_session_idlast_accessed_atexpires_atsuperseded_bystatustags 等字段。
  • 兼容老数据:老字段缺省时回退到旧逻辑。
  • 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 真正“像记忆”而不是“像全文搜索”。

改动建议:

  • 召回增加排序层:
    • 相关性
    • 重要性
    • 时效性
    • 状态过滤
  • KnowledgeTimeline 使用不同召回策略。
  • 恢复会话时优先加载最近 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.rssrc/memory/types.rs 扩字段。
  • 再在 src/agent/context_compressor.rs 增加结构化摘要输出。
  • 然后在 src/session/session.rs 增加 consolidation hook。
  • 最后把 src/tools/memory.rs 升级为带治理字段的工具接口。

如果只做最小闭环,至少要先实现:

  • Timeline 的定时清理
  • Knowledge 的自动抽取
  • Knowledge 的冲突消解
  • 召回结果的时间衰减和重排

这样 PicoBot 的记忆系统就会从“可用”变成“可信、可演进”。