更多 AI 文章见《智周万物(AI集萃)》专栏
记忆系统(Memory System)是决定 Agent 能否进行复杂多轮对话、长任务编排以及个性化服务的核心组件。它解决了大模型上下文窗口有限、无法跨会话保留信息、不能积累经验的根本问题。
记忆系统
Agent 的核心痛点在于大语言模型(LLM)的无状态性和上下文窗口的局限性。
- 无状态性:每次 API 调用都是独立的,模型天然会"失忆";多轮对话的实现,是把历史记录打包回传,这不是真正的记忆。
- 上下文窗口有限:即便是 100K 乃至 1M Token 的超长窗口,也无法承载数年、无限轮次的对话历史。且窗口过长会引入噪音、增加成本和延迟。
- 信息组织与检索:真正的记忆不只是全量存储,而是像人脑一样,有结构、可联想、会遗忘。
因此,一个完整的记忆系统,本质上是一个信息全生命周期管理系统,负责记忆的获取、存储、检索、更新与遗忘。为 Agent 提供了类似人类的记忆能力,使其能够:
- 跨会话记住用户偏好和历史交互
- 积累领域知识和执行经验
- 执行长期任务并跟踪进度
- 基于过往经验做出更好的决策
分层架构
记忆系统的本质是解决大模型有限的 Context Window(上下文窗口)与海量业务历史数据之间的矛盾。
参考计算机存储架构:
plain
[ 计算机存储架构 ] [ Agent 记忆架构 ]
+-------------------------+ +-------------------------+
| CPU 寄存器 | ==> | 会话记忆 (Session) | -> 原始多轮对话,直接参与当前推理
+-------------------------+ +-------------------------+
| L1 / L2 缓存 (RAM) | ==> | 短期记忆 (Summary) | -> LLM压缩后的摘要,常驻Prompt
+-------------------------+ +-------------------------+
| 外部硬盘 (SSD/DB) | ==> | 长期记忆 (Vector DB) | -> 亿级海量历史,需要时才去模糊检索
+-------------------------+ +-------------------------+
1. 会话记忆 (Session Memory)
- 工程本质: 内存(或 Redis)中的一个严格保序的
List[Message]队列。 - 核心职责: 维持当前对话的连贯性。解决多轮对话中的代词指代(如:"把它删了",AI 需要通过会话记忆知道"它"是指上一轮讨论的那个文件)。
- 生命周期: 仅存在于当前会话周期,且只保留最近的 N 轮(通常 3-5 轮)。
- 工程约束:绝对不能被 LLM 修改或概括,必须保留原始的
user/assistant文本内容。
2. 短期记忆 (Short-term Memory)
- 工程本质: 由 LLM 异步提炼出来的结构化 Profile 标签或一小段文本(Summary)。
- 核心职责: 当会话变长、老消息被挤出会话记忆(Session Memory)时,短期记忆负责承接这些被挤出消息的核心含金量,作为"前情提要"常驻在 System Prompt 中。
- 生命周期: 随着当前 Session 的进行动态更新,会话结束时可选择性归档。
3. 长期记忆 (Long-term Memory)
- 工程本质: 向量数据库(Vector DB)与传统关系型数据库(SQL)的混合体。
- 核心职责: 实现跨会话(Cross-Session)的数据持久化。记住用户几个月前设定的偏好、公司规章制度、或者过去所有历史任务的最终结局。
- 生命周期: 永久存储,直到用户显式触发擦除命令(符合 GDPR 隐私法规)。
权衡与取舍
在架构落地时,必须注意:
- 会话记忆的"窗口抖动"问题:在长上下文(如代码)环境下,若
SessionMemory的限制设得太小(比如 2 轮),会因被强行塞入ShortTermSummary中,而丢失对细节的感知;随着Context 成本大幅下降,SessionMemory可增大(如,最近 8-10 轮),以给予充足的原始现场缓冲。 - 长期记忆的"垃圾数据污染":避免把无意义向量扔进长期向量库,引入记忆噪音;
- 引入 Gatekeeper(看门狗)机制:只有通过了轻量级敏感度/价值评估的对话(例如包含实体、错误码、用户明确偏好的句子),才投递给后台任务写入长期向量库。
- 数据一致性与分布式锁:连续新消息的到来时,异步短期记忆压缩(Summary Chain)可能还在运行(未完成)。此时会读取到旧的 Summary,导致大模型出现了"记忆错乱"或回复重复。
- 在 Redis 中针对
session_id加分布式锁,或者将会话的读写流变为基于时间戳的单线程队列化处理(Queue-based Processing),确保记忆的序列化线性一致性。
- 在 Redis 中针对
核心流程
Agent 的记忆不是单一模块,而是一条包含"写入"和"读取"的完整管道:
- 写入管道------从对话到记忆:把原始对话转化为可检索的记忆;需经过,感知与提取、转换与摘要(生成摘要、提取实体,或直接生成假设性问题和答案)、向量化与索引、结构化存储(事实性信息,主动更新到结构化存储或知识图谱中)。
- 读取管道------按需检索:Agent 不会将全部记忆放入 Prompt,而是在需要时触发检索。
记忆无限增长会引入噪音,并推高检索成本。因此,记忆管理必须包含整理和遗忘机制。
- 摘要与合并:
- 定期对工作记忆中的旧对话进行递归摘要。
- 将多条相关的原子记忆整合为一条高度概括的记忆。
- 反思与提炼:定期对近期情景记忆进行高层次总结,提炼出新的洞察和用户画像,作为更高级别的长期记忆。
- 主动遗忘:
- 基于时间:如艾宾浩斯遗忘曲线,降低非活跃记忆的权重。
- 基于重要程度:由 LLM 对记忆打分,清理低分记忆。
- 基于一致性:当出现冲突信息,主动更新或标记旧记忆,保留更新后的版本。
整的 Agent 请求闭环中,三层记忆系统的调用与协作流程如下
样例代码
采用外观模式 (Facade Pattern),统一封装的三层记忆调度逻辑,与异步落盘解耦的伪实现。
python
import logging
import time
from typing import List, Dict, Any, Optional
from pydantic import BaseModel, Field
# 初始化生产级日志
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("IndustrialMemorySystem")
class ChatMessage(BaseModel):
role: str = Field(..., pattern="^(user|assistant|system)$")
content: str = Field(..., min_length=1)
timestamp: float = Field(default_factory=time.time)
class SessionMemory:
"""1. 会话记忆:严格保序的 FIFO 队列,常驻 Redis 内存"""
def __init__(self, max_len: int = 4):
self.max_len = max_len
self.queue: List[ChatMessage] = []
def append(self, message: ChatMessage):
self.queue.append(message)
# 严格控制大小,超出部分将被移出,交给短期记忆去压缩
if len(self.queue) > self.max_len:
evicted = self.queue.pop(0)
logger.debug(f"[Session Memory] Evicted oldest raw message: {evicted.content[:15]}...")
def get_raw_history(self) -> List[ChatMessage]:
return self.queue.copy()
class ShortTermSummaryMemory:
"""2. 短期记忆:存储由 LLM 异步压缩后的滚动摘要(前情提要)"""
def __init__(self):
self.summary: str = ""
def update_summary(self, expired_message: ChatMessage, current_summary: str) -> None:
"""
生产实践中,此处会异步调用一个低成本的小模型(如 GPT-4o-mini / DeepSeek-V3)
将老的消息融合进现有的摘要中。
"""
logger.info("[Short-Term Memory] Triggering async LLM summary consolidation...")
# 模拟 LLM 压缩
mock_llm_output = f"{current_summary} [已提炼: 用户曾提及过 {expired_message.content[:20]}]"
self.summary = mock_llm_output
class LongTermVectorMemory:
"""3. 长期记忆:连接底层的向量数据库,跨越会话持久化"""
def __init__(self, vector_db_endpoint: str):
self.endpoint = vector_db_endpoint
self._mock_db: List[Dict[str, Any]] = [] # 模拟生产中的 Qdrant / Milvus / PgVector
def semantic_search(self, query: str, user_id: str, top_k: int = 1) -> List[str]:
"""具备防御性设计的向量检索"""
try:
if not query.strip():
return []
logger.info(f"[Long-Term Memory] Executing Vector Search for User {user_id}...")
# 模拟向量相似度匹配
matched = [
item["content"] for item in self._mock_db
if item["user_id"] == user_id and any(w in item["content"] for w in query)
]
return matched[:top_k]
except Exception as e:
# 防御性设计:向量数据库属于外部依赖,若挂掉绝不能卡死 Agent 核心响应链路
logger.error(f"[Long-Term Memory] Vector DB query failed: {str(e)}", exc_info=True)
return []
def persist_async(self, user_id: str, content: str):
"""模拟将高价值数据异步写入消息队列,最终落盘向量库"""
try:
self._mock_db.append({"user_id": user_id, "content": content, "ts": time.time()})
logger.info(f"[Long-Term Memory] Async persisted high-value info to Vector DB.")
except Exception as e:
logger.error(f"[Long-Term Memory] Failed to persist data: {str(e)}")
class ProductionMemoryManager:
"""统一记忆管理门面 (Facade)"""
def __init__(self, session_id: str, user_id: str, db_url: str):
self.session_id = session_id
self.user_id = user_id
# 初始化三层记忆
self.session_mem = SessionMemory(max_len=4)
self.short_term_mem = ShortTermSummaryMemory()
self.long_term_mem = LongTermVectorMemory(vector_db_endpoint=db_url)
def get_context_for_llm(self, user_input: str) -> Dict[str, Any]:
"""【同步阶段】为 LLM 组装当前请求所需的全部上下文"""
if not user_input.strip():
raise ValueError("User input cannot be empty.")
# 1. 并行/同步检索长期记忆
ltm_context = self.long_term_mem.semantic_search(query=user_input, user_id=self.user_id)
# 2. 获取短期记忆摘要与会话记忆
return {
"long_term_context": ltm_context,
"short_term_summary": self.short_term_mem.summary,
"session_history": self.session_mem.get_raw_history()
}
def update_pipeline_async(self, user_input: str, agent_response: str):
"""【异步阶段】对话完成后,异步更新三层记忆,解耦主线程"""
logger.info("[Pipeline] Starting asynchronous memory updates...")
user_msg = ChatMessage(role="user", content=user_input)
agent_msg = ChatMessage(role="assistant", content=agent_response)
# 1. 写入会话记忆。如果触发挤出,顺便更新短期摘要
# 生产环境中,此处的溢出判断可以做成 Hook 挂载到消息队列
if len(self.session_mem.queue) >= self.session_mem.max_len:
oldest = self.session_mem.queue[0]
self.short_term_mem.update_summary(oldest, self.short_term_mem.summary)
self.session_mem.append(user_msg)
self.session_mem.append(agent_msg)
# 2. 评估是否需要提取长期记忆(关键词触发或 LLM 意图判断)
if "修好了" in agent_response or "Bug" in user_input:
self.long_term_mem.persist_async(
user_id=self.user_id,
content=f"历史记录: 用户反馈过关于 Redis 的 Bug。最终状态: {agent_response}"
)
# ==========================================
# 生产级验证流
# ==========================================
if __name__ == "__main__":
manager = ProductionMemoryManager("sess_001", "usr_100", "milvus://localhost:19530")
# 预埋一个几个周前的长期记忆
manager.long_term_mem._mock_db.append({"user_id": "usr_100", "content": "上周历史记录: Redis 发生 1024 内存溢出报错", "ts": 0.0})
print("\n--- 第一轮对话:同步检索阶段 ---")
context = manager.get_context_for_llm("我上周提的那个 Redis 报错修好了吗?")
print(f"【LLM 看到的长期背景】: {context['long_term_context']}")
print(f"【LLM 看到的短期摘要】: {context['short_term_summary']}")
print(f"【LLM 看到的当前会话】: {context['session_history']}")
# 模拟 LLM 给出了解答,触发异步更新
manager.update_pipeline_async(
user_input="我上周提的那个 Redis 报错修好了吗?",
agent_response="修好了,我们在代码里加上了边界校验,已经合并入主分支。"
)