light-memory-system.md 11 KB

LIGHT 长期记忆系统说明

文档目标

本文档将论文中的 LIGHT 系统整理成可落地的工程方案,重点说明三层记忆结构、读写链路、核心数据结构,以及在产品化实现时需要重点处理的问题。

一句话定义

LIGHT 不是单纯把上下文窗口做大,而是把长期记忆拆成三层:

  • 情节记忆 episodic memory:负责从历史对话中检索证据
  • 工作记忆 working memory:负责保留最近上下文
  • 草稿板 scratchpad:负责沉淀长期稳定的高价值事实

回答时由统一的记忆编排器组合这三层记忆,再交给生成模型。

设计目标

  • 支持超长会话下的稳定问答
  • 降低“上下文很长但还是忘事”的问题
  • 兼顾近期上下文、远期事实和长期偏好
  • 为信息更新、偏好跟随、跨轮总结提供结构化基础

总体架构

┌────────────────────────────────────────────────────┐
│                    用户对话流                      │
└────────────────────────────────────────────────────┘
                         │
                         ▼
┌────────────────────────────────────────────────────┐
│                  回合后处理器                      │
├────────────────────────────────────────────────────┤
│ 1. 保存原始 turn 到会话存储                        │
│ 2. 提取 key-value 和摘要,写入情节记忆             │
│ 3. 提炼显著事实,增量更新 scratchpad              │
└────────────────────────────────────────────────────┘
                         │
                         ▼
┌────────────────────────────────────────────────────┐
│                  用户新问题到达                    │
└────────────────────────────────────────────────────┘
                         │
                         ▼
┌────────────────────────────────────────────────────┐
│                  记忆编排器                        │
├────────────────────────────────────────────────────┤
│ 1. 检索 episodic top-k                             │
│ 2. 读取最近 N 轮 working memory                    │
│ 3. 对 scratchpad 分块并过滤相关内容                │
│ 4. 组装 prompt,交给回答模型                       │
└────────────────────────────────────────────────────┘

三层记忆拆解

1. 情节记忆 episodic memory

作用:

  • 存储历史对话中的离散事件与事实证据
  • 在远距离问题中召回“当时具体说过什么”
  • 为答案提供可追溯的历史片段

写入方式:

  • 每轮 user-assistant turn pair 结束后执行一次
  • 提取结构化 key-value pairs
  • 生成该轮摘要 summary
  • 对结构化结果或摘要做向量化
  • 将原始对话片段与索引共同存入向量库

建议字段:

{
  "session_id": "session-001",
  "turn_id": 1842,
  "timestamp": "2026-03-19T18:00:00+08:00",
  "entities": [
    {"key": "居住城市", "value": "苏州"},
    {"key": "宠物名字", "value": "团子"}
  ],
  "summary": "用户提到已搬到苏州,并养了一只叫团子的猫。",
  "raw_span": "原始对话片段",
  "embedding_text": "居住城市=苏州;宠物名字=团子;用户提到已搬到苏州,并养了一只叫团子的猫。"
}

工程建议:

  • 不要只存全文向量,优先存“摘要 + 结构化提取”的索引文本
  • 检索后返回原始片段,避免摘要失真带来的二次误导
  • top-k 不要盲目调大,过多召回会引入噪声

2. 工作记忆 working memory

作用:

  • 保留最近若干轮原始对话
  • 维持局部连贯性、代词解析、刚更新的信息
  • 防止系统只看远期记忆,忽略当前上下文

实现方式:

  • 直接从会话存储读取最近 N
  • 或按 token budget 截断,而不是写死固定轮数

工程建议:

  • 优先按 token 上限裁剪,而不是按回合数裁剪
  • 最近轮的优先级通常高于远期记忆
  • 回答冲突时,应优先检查工作记忆中是否存在更近更新

3. 草稿板 scratchpad

作用:

  • 沉淀对长期任务真正重要的高层事实
  • 存储偏好、长期约束、时间线、未完成事项等
  • 避免只依赖相似度检索导致关键信息漏召回

写入方式:

  • 每轮结束后由 LLM 提炼“显著信息”
  • 将新信息合并到现有 scratchpad
  • 超过阈值后执行压缩

推荐分槽位组织,而不是一整段自由文本:

{
  "profile": [
    "用户当前住在苏州"
  ],
  "preferences": [
    "默认使用简体中文",
    "倾向简洁回答"
  ],
  "instructions": [
    "代码注释使用中文",
    "避免未验证结论"
  ],
  "timeline": [
    "2025-12: 从杭州搬到苏州",
    "2026-04: 计划去东京出差"
  ],
  "open_loops": [
    "简历修改尚未完成"
  ]
}

工程建议:

  • profile / preferences / instructions / timeline / open_loops 分开维护
  • 超过长度阈值后做压缩,但要保留高优先级槽位
  • 回答前不要整块全部注入,应先按问题相关性过滤

读写链路

写入链路

每轮对话结束后:

  1. 保存原始 turn 到会话存储
  2. 用提取模型生成 key-value + summary
  3. 写入情节记忆索引
  4. 更新 scratchpad
  5. 超过阈值时压缩 scratchpad

读取链路

收到新问题后:

  1. 对问题做向量化
  2. 从情节记忆中召回 top-k
  3. 从会话存储取最近若干轮 working memory
  4. 将 scratchpad 分块
  5. 对每块做相关性过滤
  6. 将三层记忆拼装为回答上下文
  7. 调用生成模型作答

关键数据存储

建议至少拆成四类存储:

  • conversation_store:保存原始对话 turn
  • episodic_index:保存摘要、结构化提取、embedding、原始片段引用
  • scratchpad_store:保存长期摘要与槽位化事实
  • memory_metadata:保存版本号、状态、更新时间、冲突标记

可选状态字段:

  • active:当前有效
  • outdated:已过期
  • contradicted:被冲突信息覆盖
  • tentative:不确定信息,需后续确认

记忆编排器职责

记忆编排器是系统核心,职责包括:

  • 控制每类记忆的 token 预算
  • 决定检索数量与排序策略
  • 在冲突信息出现时做优先级裁决
  • 过滤与问题无关的 scratchpad 块
  • 统一输出给回答模型的上下文格式

一个简单的优先级顺序可以是:

  1. 当前轮明确指令
  2. working memory 中的最近事实
  3. scratchpad 中的长期约束与偏好
  4. episodic memory 中召回的历史证据

最小可用实现(MVP)

最小版本只需要五个组件:

  • conversation_store
  • episodic_indexer
  • scratchpad_manager
  • memory_orchestrator
  • response_generator

伪代码:

def on_turn_end(session_id, user_msg, assistant_msg):
    save_raw_turn(session_id, user_msg, assistant_msg)

    memory_item = extract_kv_and_summary(user_msg, assistant_msg)
    vector_store.upsert(session_id, memory_item)

    scratchpad = load_scratchpad(session_id)
    scratchpad = update_scratchpad(scratchpad, user_msg, assistant_msg)
    if token_len(scratchpad) > 30000:
        scratchpad = compress_scratchpad(scratchpad, target_tokens=15000)
    save_scratchpad(session_id, scratchpad)


def answer(session_id, question):
    episodic_docs = retrieve_topk(session_id, question, k=15)
    recent_turns = load_recent_turns(session_id, token_budget=8000)
    scratchpad = load_scratchpad(session_id)
    scratch_chunks = semantic_chunk(scratchpad)
    filtered_scratch = filter_relevant_chunks(question, scratch_chunks)

    prompt = build_prompt(
        question=question,
        episodic_memory=episodic_docs,
        working_memory=recent_turns,
        scratchpad=filtered_scratch,
    )
    return llm_generate(prompt)

与普通 RAG 的区别

普通 RAG 常见流程是:

  • 文本切块
  • 向量检索
  • 拼接上下文

LIGHT 的差异在于:

  • 有独立的 working memory,保证近期连续性
  • 有 scratchpad,维护高层抽象事实
  • 有持续写入机制,而不是只在回答前做一次检索
  • 更接近“记忆系统”,而不是单次检索增强

落地时最难的四个问题

1. 信息更新

同一事实会变化,例如“住在杭州”后来变成“搬到苏州”。系统不能只追加,不做状态管理。

2. 矛盾消解

不同时间的记忆可能互相冲突。系统需要判断:

  • 哪条是最新的
  • 哪条是已过期的
  • 哪条只是暂时猜测

3. Scratchpad 膨胀

如果长期只追加不整理,scratchpad 会退化成另一份噪声上下文,导致检索和回答质量持续下降。

4. 写入污染

每轮写入依赖 LLM 提取。如果提取错误,错误会长期保留并反复影响后续回答。

产品化增强建议

如果从论文原型继续往前做,建议增加以下能力:

  • 混合召回:结合 dense、keyword、时间排序和 rerank
  • 冲突检查:回答前先做 memory conflict check
  • 偏好单独建表:用户偏好和长期指令不要完全混在 scratchpad 中
  • 版本化更新:对关键事实保留变更历史和生效时间
  • 可观察性:记录每次回答用了哪些记忆,便于审计和调试

适用场景

LIGHT 适合以下系统:

  • 长对话助手
  • 需要记住用户偏好的智能体
  • 多轮写作/策划协作系统
  • 长期任务跟踪与提醒系统
  • 小说创作助手中的角色、设定、剧情记忆系统

对本项目的启发

如果把 LIGHT 思路引入当前小说写作系统,可以这样映射:

  • 情节记忆:存章节事件、角色状态变化、伏笔和回收线索
  • 工作记忆:存当前章节任务书、最近章节摘要、当前写作约束
  • 草稿板:存角色设定、长期世界观规则、主线目标、未回收伏笔

这样可以把“长篇连载的一致性问题”从单次检索,升级成持续维护的记忆闭环。

总结

LIGHT 的核心价值不在于更大的上下文窗口,而在于把长期记忆拆成:

  • 可检索的历史证据层
  • 保持局部连贯的短期层
  • 稳定沉淀长期事实的抽象层

工程上最值得复用的是这套分层记忆与统一编排思路,而不是某个固定模型或具体实现细节。