Nemori:让人工智能真正“记住”过去的对话——一份写给开发者的情景记忆系统指南

Nature-Inspired Episodic Memory for Large Language Models


开场三问

  • 🍄

    为什么 AI 总是“转头就忘”?
    传统做法把对话切成片段,再喂给模型,但人类回忆的是「周五下午 Alice 跟我聊的那次旅行计划」,而不是「第 312 条消息」。Nemori 把聊天数据重新组织成与人类记忆粒度一致的「情景片段」,让模型在关键时刻能精准回溯。

  • 🍄

    它到底做了什么?
    把原始对话 → 探测自然边界 → 生成叙事式摘要 → 建立 BM25 索引 → 一问即得。整个过程只用两条提示词,无需额外的大模型调用。

  • 🍄

    我能拿来做什么?
    个人助理、客服机器人、教育 Agent、长期陪伴型聊天应用……任何需要「记得用户」的场景都能直接嵌入。


1. 情景记忆是什么?

人类记忆 传统 AI 记忆 Nemori 情景记忆
“上周三我和 Alice 讨论去京都” 第 312 条消息:user: … 情景标题:《Alice 与我的京都旅行计划》
摘要:2024-01-15 10:30,Alice 提议 4 月赏樱,已确认航班
关键词:京都、Alice、4 月 关键词:消息、用户 ID、时间戳 关键词:京都、旅行、Alice、赏樱、航班

Nemori 把对话重写成第三人称的小故事,保留了因果和时间,却去掉了冗余口水话。

效果:当用户问“上次 Alice 说樱花季是几月?”时,模型直接定位到《京都旅行计划》情景,而不是在几百条消息里翻找。


2. 系统全景图

用一张图先看清数据流向,再拆每一步。

graph TD
    A[原始数据<br>对话/活动/位置] -->|1. 探测边界| B[情景片段]
    B -->|2. 叙事摘要| C[情景记忆对象]
    C -->|3. 索引| D[BM25 倒排索引]
    D -->|4. 检索| E[用户问题]
    E -->|5. 生成答案| F[回复]

3. 动手实践:30 分钟跑通 LoCoMo 基准

3.1 环境准备

# 1. 克隆仓库
git clone https://github.com/nemori-ai/nemori.git && cd nemori

# 2. 安装依赖(使用 uv,一条命令)
uv sync

# 3. 配置大模型(任选其一)
export OPENAI_API_KEY="sk-xxx"          # 推荐 gpt-4o-mini
# 或 export ANTHROPIC_API_KEY="sk-ant-xxx"

3.2 两条提示词的秘密

步骤 提示词示例 输出
边界探测 Detect episode boundaries along natural topic shifts 起止消息 ID 列表
情景生成 Summarize each segment into an episodic memory 标题 + 摘要 + 关键词

代码片段:

from nemori.llm.providers import OpenAIProvider
from nemori.builders.conversation_builder import ConversationEpisodeBuilder

llm = OpenAIProvider.from_env()
builder = ConversationEpisodeBuilder(llm_provider=llm)

episode = builder.build_episode(raw_conversation, for_owner="alice")
print(episode.title)  # 《Alice 与我的京都旅行计划》

3.3 建立 BM25 索引——零额外调用

from nemori.core.retrieval import RetrievalService, RetrievalStrategy

service = RetrievalService(episode_repo)
service.register_provider(RetrievalStrategy.BM25, config={})
service.initialize()
service.add_episode_to_all_providers(episode)   # 自动分词、去停用词、建索引

3.4 一问即得

results = service.search(
    RetrievalQuery(text="樱花季几月", owner_id="alice", limit=5)
)
print(results.episodes[0].summary)
# 输出:Alice 提议 4 月赏樱,已确认航班 JAL123

4. 架构拆解:四层各司其职

层级 职责 核心类 关键设计
数据摄入层 统一原始数据格式 RawEventData 支持 8 种数据类型:对话、活动、位置、媒体…
记忆处理层 把原始数据变情景 EpisodeBuilder 每个数据类型一个专属构建器,可插拔
情景记忆层 封装“人类尺度”事件 Episode 四级层次:原子 → 复合 → 主题 → 档案
存储与检索层 存得下、找得快 RetrievalService + BM25RetrievalProvider 用户级隔离、增量索引、降级评分

5. 为什么 BM25 就够了?

在 LoCoMo 这个对话记忆基准上,Nemori 只用经典 BM25 就拿到高分,原因有三:

  1. 叙事摘要减少了噪声
    原始对话包含大量“嗯、啊、表情符号”,重写成叙事文本后,关键词密度更纯粹。

  2. 多字段加权
    标题 ×3、摘要 ×2、实体 ×2、内容 ×1 的权重组合,让标题里的“京都”比正文里的“京都”更值钱。

  3. 用户级索引
    每个用户独占索引,避免跨用户污染,召回更准确。

如果想进一步提升,可无缝接入向量检索或混合策略,接口已预留。


6. 常见问题 FAQ

6.1 情景记忆会不会太长,占 token?

目前 top-20 情景约 1.5k tokens,与直接带原始 20 条消息相当;把 top-k 降到 10 时 token 减半,性能几乎不变。

6.2 原文细节丢失了怎么办?

情景记忆 + 原文分离存储:情景负责快速定位,原文仓库保留完整消息。后续计划开源“语义记忆补全”模块,自动把关键实体(人名、地点)回填到情景。

6.3 可以处理图片或音频吗?

架构已预留 DataType.MEDIA。实现思路:用视觉/语音模型先转成文本摘要,再走同样的情景流程。

6.4 如何扩展到百万级用户?

  • 🍄
    存储层:把 DuckDB 换成 PostgreSQL 或分布式 KV 即可。
  • 🍄
    检索层:BM25 索引按用户分片,水平扩展无锁竞争。
  • 🍄
    计算层:情景构建任务可异步队列化,削峰填谷。

7. 三步集成到现有聊天机器人

步骤 操作 代码位置
① 引入依赖 uv add nemori pyproject.toml
② 注入对话 每次用户发消息 → EpisodeManager.process_raw_data() main.py
③ 增强回答 机器人回答问题前 → EpisodeManager.search_episodes() chatbot.py

示例片段:

# 每次收到用户消息
await manager.process_raw_data(
    RawEventData(
        data_type=DataType.CONVERSATION,
        content=[{"user": "alice", "text": "明天去图书馆吗?"}],
        source="telegram"
    ),
    owner_id="alice"
)

# 机器人准备回答“明天干什么?”
episodes = await manager.search_episodes("明天计划", owner_id="alice")
context = "\n".join(ep.summary for ep in episodes.episodes)
answer = await llm.generate(f"参考记忆:{context}\n用户问:明天干什么?")

8. 未来路线图(来自官方文档)

时间点 功能 说明
已计划 语义记忆补全 把丢失的专有名词、时间、地点补回情景
已计划 情景聚合 相似情景自动合并为长期主题
探索中 多模态记忆 图像、音频、传感器数据统一情景化
探索中 联邦记忆 用户本地加密存储,云端只存索引

9. 一分钟速读总结

  • 🍄
    核心思想:像人类一样“回忆事件”,而不是“搜索消息”。
  • 🍄
    技术方案:2 条提示词 + BM25 索引 = 高召回 + 低延迟。
  • 🍄
    接入成本:Python 3.12 环境,一条 uv sync,十分钟跑通。
  • 🍄
    适用场景:任何需要长期记忆的 AI 对话系统。

把 Nemori 当成“记忆乐高”:底层已帮你处理数据、索引、隔离,上层只需关心业务逻辑。剩下的,交给人类记忆的本能。