如何设计一个高效的对话记忆管理系统?
设计对话记忆管理系统,最容易犯的错误是把“记住一切”当成目标。真正高效的记忆,是让 Agent 在正确的时刻,用最小的代价,拿到最有价值的上下文。拆解下来,核心就三件事:存得聪明、忘得果断、取得精准。
💡 核心设计原则:分层 + 索引分离 + 主动遗忘¶

-
热记忆:当前对话轮次,直接喂给 LLM,保证毫秒级响应。
-
温记忆:近期对话的摘要/压缩版,通过 KV 高速读取,向量辅助检索。
-
冷记忆:完整历史,走向量检索后从冷存储(DB / 对象存储)按需加载。
这三层不是互相替代,而是热度衰减下的接力。下面看具体怎么落地。
🧱 关键机制详解¶
① 写入:先过“价值过滤器”¶
并不是每句话都值得记。我会在写入前加一层轻量判断:
-
事实性信息(用户说“我女儿叫小美”)→ 走语义记忆更新。
-
决策/偏好(“我不喜欢太啰嗦的风格”)→ 打上高权重标签。
-
纯寒暄(“好的”、“收到”)→ 直接丢弃。
# 伪代码:写入前的价值评估
def should_store(message):
if len(message) < 4 and not any(kw in message for kw in ["记住", "设置", "偏好"]):
return False
if is_pure_acknowledgment(message):
return False
return True
② 压缩:窗口满了就生成摘要¶
短期窗口设一个 token 上限(比如 2000),满了就调用一次廉价的 LLM 把前 N 轮压缩成一段 200 字的摘要,存入温记忆层,然后清掉窗口里的旧消息。这样上下文永远不超限,且关键信息不丢失。
def compress_window(self):
# 取出旧对话
old_msgs = self.messages[:self.window_keep]
prompt = f"将以下对话浓缩为一段200字以内的摘要,保留所有事实、决定和用户偏好:\n{old_msgs}"
summary = self.llm.invoke(prompt)
# 摘要存入温记忆
self.warm_memory.store(summary, metadata={"type": "summary"})
# 截断窗口
self.messages = self.messages[self.window_keep:]
③ 检索:时间衰减 + 语义召回¶
当需要补充上下文时,不是一股脑把历史全喂进去。我会先从温记忆里按时间取最近的 3 条摘要,再并行去冷记忆做语义检索(取 top 3)。最后用一个简单的加权公式排序:
时间越久远,衰减越多;但高权重的“偏好型记忆”可以抗衰减。
④ 遗忘:硬策略 + 软策略¶
-
硬策略:超过 90 天未被检索过的记忆,物理删除(从向量库和 KV 都删掉)。
-
软策略:如果一个记忆被频繁修正(用户反复说“不,那是以前的,现在改成...”),自动降低其权重,并在向量库中标记为“过时”,检索时自动过滤。
💻 系统骨架代码¶
下面是一个最精简但五脏俱全的实现,整合了短期窗口、摘要压缩、向量检索和 KV 存储。
import chromadb
import redis
import json
import uuid
from datetime import datetime
from sentence_transformers import SentenceTransformer
from collections import deque
class ConversationMemoryManager:
def __init__(self, llm, window_size=2000):
self.llm = llm # 用于生成摘要的LLM
self.window = deque(maxlen=window_size) # 热记忆:消息窗口
self.encoder = SentenceTransformer('all-MiniLM-L6-v2')
# 温/冷记忆存储:向量库(索引) + KV(正本)
self.chroma = chromadb.Client()
self.collection = self.chroma.create_collection("conversations")
self.kv = redis.Redis(decode_responses=True)
# ========== 公共方法 ==========
def add_message(self, role, content):
"""写入一条消息,自动触发压缩与遗忘检查"""
if not self._should_store(content):
return
self.window.append({"role": role, "content": content, "ts": datetime.now()})
# 窗口满了?压缩后存入温记忆
if len(self.window) >= self.window.maxlen * 0.8:
self._compress_and_promote()
def retrieve_context(self, current_query, top_k=3):
"""检索最有价值的记忆,返回拼接好的上下文字符串"""
# 1. 从温记忆取最近的摘要
recent_summaries = self._get_recent_summaries(limit=2)
# 2. 从冷记忆做语义检索
cold_mems = self._semantic_search(current_query, top_k)
# 3. 合并、去重、按时间+权重排序
combined = recent_summaries + cold_mems
combined.sort(key=lambda x: x.get("weight", 0) * self._time_decay(x["ts"]), reverse=True)
# 4. 格式化为上下文
context_parts = [item["content"] for item in combined[:5]]
return "\n".join(context_parts)
# ========== 内部机制 ==========
def _should_store(self, content):
"""价值过滤器:纯确认、短句忽略"""
if len(content) < 4:
return False
if content.strip() in ["好的","收到","ok"]:
return False
return True
def _compress_and_promote(self):
"""将窗口前半部分压缩为摘要,存入温记忆"""
old_msgs = list(self.window)[:len(self.window)//2]
text_block = "\n".join([f"{m['role']}: {m['content']}" for m in old_msgs])
prompt = f"将以下对话浓缩为一段200字以内的摘要,保留事实、决定、偏好:\n{text_block}"
summary = self.llm(prompt) # 调用LLM
mem_id = str(uuid.uuid4())
mem_obj = {
"content": summary,
"type": "summary",
"weight": 0.8, # 摘要默认权重略高
"ts": datetime.now().isoformat()
}
# KV存完整对象
self.kv.set(mem_id, json.dumps(mem_obj))
# 向量库只存索引
emb = self.encoder.encode(summary).tolist()
self.collection.add(embeddings=[emb], documents=[summary],
metadatas=[{"mem_id": mem_id, "weight": 0.8}], ids=[mem_id])
# 清掉旧消息
for _ in range(len(old_msgs)):
self.window.popleft()
def _semantic_search(self, query, k):
"""冷记忆语义检索,带时间衰减"""
q_emb = self.encoder.encode(query).tolist()
results = self.collection.query(query_embeddings=[q_emb], n_results=k)
mem_ids = [meta["mem_id"] for meta in results["metadatas"][0] if meta]
# 从KV批量拉取
pipe = self.kv.pipeline()
for mid in mem_ids:
pipe.get(mid)
objects = [json.loads(obj) for obj in pipe.execute() if obj]
return objects
def _get_recent_summaries(self, limit):
"""通过KV的时间索引快速获取最新摘要(实际可用Sorted Set实现)"""
# 简化:扫描KV并过滤,生产环境用ZSET
keys = self.kv.keys("*")
items = []
for k in keys[:20]: # 粗略限制
obj = json.loads(self.kv.get(k))
if obj.get("type") == "summary":
items.append(obj)
items.sort(key=lambda x: x["ts"], reverse=True)
return items[:limit]
def _time_decay(self, ts_str):
"""时间衰减函数,记忆越旧权重越低"""
ts = datetime.fromisoformat(ts_str)
days = (datetime.now() - ts).days
return max(0.1, 1.0 - days/90) # 90天后衰减至0.1
🚀 效率优化的几个细节¶
-
异步写入:存储记忆时不阻塞当前对话,用后台任务把向量化和入库异步完成,用户感知不到延迟。
-
分层缓存:温记忆的最近几条摘要直接缓存在内存中,避免每次检索都访问 Redis。
-
元数据过滤:在向量查询时带上
where={"type": "summary"}之类的条件,缩小搜索空间,比纯全局检索快一个数量级。 -
遗忘守护进程:每天凌晨跑一个定时任务,对超过 N 天未使用或权重低于阈值的记忆进行清理,保持库的“精瘦”。
🧭 一个容易被忽视的视角¶
大部分讨论都聚焦在“如何记住”,但真正让系统高效的,往往是“选择性遗忘”的设计。我把遗忘策略看成和检索同等重要的一线模块——没有它,再漂亮的存储架构最终也会被时间腐化成噪声沼泽。在面试时能主动聊到这一点,通常会拿到非常好的反馈。