智能体面试准备(六):Agent 记忆系统设计------短期、长期与向量记忆的架构与实现
"你的 Agent 怎么记住用户上周说过的话?"------这个问题看似简单,实际是 Agent 面试里区分度最高的题目之一。因为它考的不是单点知识,而是架构设计能力:上下文窗口管理、信息压缩、检索召回、遗忘策略,每一层都有取舍。LLM 本身是无状态的,所谓"记忆"全部是工程构造出来的。这篇把 Agent 记忆系统的分层设计、每层的实现方案与代码、以及面试高频追问一次讲清。
一、为什么记忆是 Agent 的核心组件
LLM 的两个先天限制决定了记忆系统的必要性:
- 无状态:每次调用都是独立的,模型不会"记得"上一次对话,所有历史必须显式塞进 prompt;
- 上下文窗口有限且注意力会稀释:即使是 128k 窗口,塞满后不仅贵,还会出现 lost-in-the-middle(中间信息被忽略)------窗口大小不等于有效记忆容量。
所以记忆系统要解决的核心矛盾是:信息无限增长 vs 上下文有限且昂贵。解法就是分层:热数据放上下文(短期记忆),温数据压缩摘要,冷数据外部存储按需检索(长期记忆)。这个思路与操作系统的内存分层(寄存器→内存→磁盘)完全同构,面试时用这个类比会非常出彩------MemGPT 论文正是把 OS 虚拟内存思想搬进了 Agent 记忆管理。
二、记忆分类:一张表说清
| 记忆类型 | 存储位置 | 生命周期 | 典型内容 | 实现手段 |
|---|---|---|---|---|
| 短期/工作记忆 | 上下文窗口内 | 单次会话 | 当前对话、工具结果 | 消息列表 + 滑动窗口 |
| 摘要记忆 | 上下文内(压缩态) | 单次会话 | 溢出历史的摘要 | LLM 递归摘要 |
| 长期-情景记忆 | 外部存储 | 跨会话 | 历史对话事件 | 向量库 + 检索 |
| 长期-语义记忆 | 外部存储 | 跨会话 | 用户偏好、事实画像 | 结构化 KV/知识图谱 |
| 长期-程序记忆 | 外部存储 | 跨会话 | 工作流、技能、经验教训 | 文档/代码片段库 |
这套分类借用了认知科学的术语(episodic/semantic/procedural),面试报出这三个词并各给一个 Agent 场景的例子(情景=上周聊过的需求细节;语义=用户偏好 Python 且讨厌啰嗦;程序=部署项目的标准操作流程),立刻建立专业印象。
高频追问:语义记忆为什么不用向量库而用结构化存储? 因为用户画像类信息要求精确、可更新、可覆盖------"用户从北京搬到上海"必须覆盖旧值,向量检索可能同时召回新旧两条产生矛盾;结构化 KV 天然支持 upsert。向量库适合"模糊语义匹配"的情景记忆,两者是互补关系。
三、短期记忆管理:窗口溢出的三种策略
滑动窗口:只保留最近 N 轮。实现最简单,但会突然"失忆",丢掉早期关键信息(比如任务最初的目标)。
递归摘要:历史超过阈值时,把最老的一段压缩成摘要插到头部,反复进行。信息有损但保留主干,LangChain 的 ConversationSummaryBufferMemory 就是这个方案。工程细节:摘要要用独立的小模型调用做,避免打断主流程;摘要 prompt 要求保留"实体、数字、承诺、未完成事项"。
重要性分级保留:给每条消息打重要性标签(用户目标 > 关键结论 > 工具原始输出),溢出时先丢低级别内容。Claude Code、Manus 这类长程 Agent 普遍采用"工具输出可丢弃/可截断,用户指令永不丢"的策略。
实践中三者叠加:滑动窗口保最近、摘要保主干、分级保关键。
四、可运行代码:三层记忆系统的完整实现
下面用纯 Python(无第三方依赖)实现一个可运行的记忆系统:短期滑动窗口 + 溢出摘要 + 基于 TF-IDF 余弦相似度的长期向量检索(真实项目换成 embedding 模型 + 向量库,接口不变)。
python
import math
import re
import time
from collections import Counter
# ---------- 长期记忆:向量化存储 + 余弦检索 ----------
class VectorMemory:
"""用 TF-IDF 模拟 embedding 检索,真实场景替换为 embedding API + 向量库。"""
def __init__(self):
self.items = [] # [(text, meta, term_freq)]
def _tokenize(self, text):
return re.findall(r'[\u4e00-\u9fff]|[a-zA-Z]+', text.lower())
def add(self, text, meta=None):
tf = Counter(self._tokenize(text))
self.items.append((text, meta or {}, tf))
def _cosine(self, tf1, tf2):
common = set(tf1) & set(tf2)
dot = sum(tf1[t] * tf2[t] for t in common)
n1 = math.sqrt(sum(v*v for v in tf1.values()))
n2 = math.sqrt(sum(v*v for v in tf2.values()))
return dot / (n1 * n2) if n1 and n2 else 0.0
def search(self, query, top_k=2, min_score=0.05):
q_tf = Counter(self._tokenize(query))
scored = [(self._cosine(q_tf, tf), text, meta)
for text, meta, tf in self.items]
scored.sort(reverse=True)
return [(s, t, m) for s, t, m in scored[:top_k] if s >= min_score]
# ---------- 记忆管理器:短期窗口 + 溢出摘要 + 长期沉淀 ----------
class MemoryManager:
def __init__(self, window_size=4):
self.window_size = window_size
self.short_term = [] # 最近消息
self.summary = "" # 递归摘要(模拟)
self.long_term = VectorMemory() # 跨会话记忆
self.profile = {} # 语义记忆:结构化用户画像
def add_message(self, role, content):
self.short_term.append({"role": role, "content": content,
"ts": time.time()})
if len(self.short_term) > self.window_size:
evicted = self.short_term.pop(0)
# 溢出消息:进摘要(模拟 LLM 摘要)+ 沉淀到长期记忆
self.summary += f"[{evicted['role']}]{evicted['content'][:30]}; "
self.long_term.add(evicted["content"], {"role": evicted["role"]})
def update_profile(self, key, value):
"""语义记忆用 upsert 覆盖,保证精确性。"""
self.profile[key] = value
def build_context(self, query):
"""组装最终 prompt 上下文:画像 + 摘要 + 长期检索 + 短期窗口。"""
recalled = self.long_term.search(query)
parts = []
if self.profile:
parts.append(f"## 用户画像\n{self.profile}")
if self.summary:
parts.append(f"## 历史摘要\n{self.summary}")
if recalled:
lines = [f"- (相关度{s:.2f}) {t}" for s, t, _ in recalled]
parts.append("## 相关历史记忆\n" + "\n".join(lines))
recent = "\n".join(f"{m['role']}: {m['content']}" for m in self.short_term)
parts.append(f"## 当前对话\n{recent}")
return "\n\n".join(parts)
if __name__ == "__main__":
mm = MemoryManager(window_size=3)
mm.update_profile("语言偏好", "Python")
mm.add_message("user", "帮我调研 vLLM 的部署方案,重点看显存占用")
mm.add_message("assistant", "vLLM 用 PagedAttention 管理显存,建议预留 20% 余量")
mm.add_message("user", "我们的显卡是 4090,24G 显存")
mm.add_message("user", "另外记得数据库连接串要用环境变量")
mm.add_message("assistant", "好的,已记录环境变量规范")
mm.add_message("user", "对了之前说的显卡是什么型号来着?")
print(mm.build_context("显卡 显存 型号"))
运行后可以看到:窗口只留最近 3 条,早期的"4090、24G 显存"已被挤出窗口,但通过长期记忆检索被召回进上下文------这正是"Agent 记住了窗口外信息"的完整机制。这段代码覆盖手撕记忆模块的所有得分点:窗口驱逐、摘要沉淀、向量召回、结构化画像、上下文组装。
五、长期记忆的三个工程难题
写什么(记忆生成):不能把所有对话原文都存进去,噪声会淹没检索。主流做法是"反思式写入":会话结束或阶段完成时,让 LLM 提取值得长期保留的事实/偏好/教训再写入(Generative Agents 的 reflection 机制、Mem0 的事实提取都是这个思路)。追问点:什么时候写?------事件驱动(用户明确说"记住")+ 周期驱动(会话结束批量反思)结合。
怎么取(检索策略) :单纯向量相似度不够。Generative Agents 的经典打分公式是 相关性 + 时近性 + 重要性 三分量加权:相关性用余弦相似度,时近性用指数衰减(decay^Δt),重要性在写入时由 LLM 打 1~10 分。这条公式是面试的高频得分点,务必能默写并解释每个分量的作用。
怎么忘(遗忘与冲突):记忆会过时和矛盾。手段包括:TTL 过期、访问频率淘汰(LRU 思想)、写入时冲突检测(新记忆与已有记忆语义冲突时触发合并或覆盖------Mem0 的 ADD/UPDATE/DELETE 决策)。"记忆冲突怎么办"是压轴追问,答"写入时让 LLM 判断新旧关系并选择增/改/删"即可。
六、面试答题框架与高频题
"设计一个有跨会话记忆的个人助理 Agent",推荐结构:
- 分层:短期(窗口+摘要)、语义(结构化画像,upsert)、情景(向量库)、程序(技能文档);
- 写路径:会话结束触发反思式提取,LLM 判断增/改/删;
- 读路径:每轮用当前 query 检索 top-k,按相关性+时近性+重要性排序,注入 system prompt;
- 治理:PII 脱敏、用户可查看/删除自己的记忆(合规卖点)、记忆条数上限与淘汰。
其他高频题速答:
- "长上下文模型(1M token)会让记忆系统过时吗?"------不会。成本与延迟随上下文线性增长、注意力稀释依旧存在、跨会话持久化仍需外部存储;长窗口只是把"短期记忆"变大了。
- "RAG 和记忆系统什么关系?"------机制同源(外部存储+检索注入),对象不同:RAG 检索的是外部知识库,记忆检索的是 Agent 自身经历;记忆还多了写入、更新、遗忘的生命周期管理。
- "多 Agent 场景记忆怎么共享?"------公共黑板(共享记忆池)+ 私有记忆隔离,写入公共池需要格式约定与权限控制。
自检清单:能报出情景/语义/程序三分类并各举一例吗?能默写"相关性+时近性+重要性"检索公式吗?能说清语义记忆为什么用结构化存储吗?能手写一个带驱逐和召回的记忆管理器吗?四问皆通,记忆环节稳拿高分。
下一篇讲多智能体协作:角色分工、辩论机制与流水线编排的实现。