揭秘AI记忆系统:短期与长期记忆如何协同工作

📅 2026/7/15 21:38:51 👁️ 阅读次数 📝 编程学习
揭秘AI记忆系统:短期与长期记忆如何协同工作

AI Agent记忆系统技术深度解析:短期记忆、长期记忆与RAG的协作机制

为什么Claude Code能记住你10分钟前改过的文件名?为什么Agent运行1小时后开始遗忘之前的决策?为什么同样一段代码,有时Agent能回忆起来,有时却像第一次看到?记忆系统是Agent从玩具走向工具的关键基础设施。本文分析Agent记忆系统的四种核心类型、它们的实现原理和协作机制。

一、Agent为什么需要记忆系统

1.1 没有记忆的Agent是什么样

当前大多数Agent的运行模式是"会话式"的——每次对话是一个独立的上下文窗口,Agent只看到当前会话中的信息。

用户:帮我重构UserService.java这个文件 Agent:好的,我来看一下... (读取UserService.java) ​ 用户:现在重构LoginService.java Agent:好的,我来看一下... (读取LoginService.java,完全忘记刚才的UserService) ​ 用户:UserService里也做同样的重构 Agent:什么UserService?我要重新读一遍...

这不是模型能力的问题,是记忆机制的问题。每次对话开始时,Agent的上下文是空的,它不记得任何之前发生过的事。

1.2 记忆系统的分类

Agent记忆系统通常分为四种类型,每种的特性不同:

记忆类型存储位置生命周期容量检索方式典型应用
短期记忆(上下文窗口)内存(对话上下文)一个会话8K-1M token顺序访问当前任务上下文
工作记忆(Agent运行时)内存(临时存储)一个子任务有限结构化引用中间计算结果
长期记忆(持久存储)文件/向量数据库跨会话几乎无限语义检索用户偏好、项目知识
情景记忆(会话记录)数据库跨会话时间+语义历史决策回溯

二、短期记忆:上下文窗口的管理

2.1 上下文窗口的物理限制

短期记忆的本质是LLM的上下文窗口。当前主流的上下文窗口大小:

模型上下文窗口可容纳的对话轮次(估算)可容纳的代码量
GPT-4o128K~300-500轮~50,000行
Claude Opus 4.8200K~500-800轮~80,000行
DeepSeek V41M~2500-4000轮~400,000行
GLM-5.21M~2500-4000轮~400,000行

但物理容量和有效容量是两回事。研究发现,在长上下文中,模型对中间位置的信息检索准确率显著下降——称为"lost in the middle"效应(Liu et al., 2024)。在一个1M token的上下文中,模型对开头和结尾的信息几乎可以准确回忆,但对中间的信息回忆准确率可能不到50%。

2.2 上下文压缩

为了让有限的窗口容纳更多有效信息,需要进行上下文压缩。

消息摘要:将早期的对话轮次压缩为摘要。不是每轮对话都保留完整内容,而是每经过N轮或上下文达到某个阈值时,对前面的内容做一次总结。

class ContextManager: def __init__(self, max_tokens=128000): self.max_tokens = max_tokens self.summary = "" # 压缩后的摘要 self.active_messages = [] # 最近的消息(完整保留) ​ def add_message(self, message): self.active_messages.append(message) if self._total_tokens() > self.max_tokens * 0.7: self._compress() ​ def _compress(self): # 将最早的一半消息压缩为摘要 old_messages = self.active_messages[:len(self.active_messages)//2] new_summary = self._summarize_messages(old_messages) ​ # 合并摘要 self.summary = self._merge_summaries(self.summary, new_summary) ​ # 移除已压缩的消息 self.active_messages = self.active_messages[len(self.active_messages)//2:]

一个关键问题:摘要本身也会消耗token。摘要的摘要的摘要...最终摘要会变得过于抽象,丢失细节。比较好的做法是将摘要限制在不超过总上下文的10%(即128K窗口中摘要不超过12K token)。

选择性保留:不是所有消息都同等重要。工具调用的结果(可能很大)和用户的原始输入,重要性完全不同。

def select_messages_for_retention(messages, importance_fn): """根据消息的重要性选择性保留,而非全部压缩""" scored = [(msg, importance_fn(msg)) for msg in messages] scored.sort(key=lambda x: x[1], reverse=True) return [msg for msg, score in scored[:KEEP_COUNT]]

什么消息更重要?经验规则:

  • 用户明确要求的修改("把这个函数的参数改为可选")→ 高重要性

  • Agent做出的决策和选择("我选择使用二分查找")→ 中高重要性

  • Agent执行的结果总结("重构完成,通过全部测试")→ 中重要性

  • 错误的工具调用和重试 → 低重要性

  • 完整的工具输出(大量中间数据)→ 低重要性(可被摘要替代)

2.3 窗口滑动策略

滑动窗口策略决定了当上下文满时,哪些内容被丢弃,哪些被保留。

FIFO策略(先进先出):丢弃最早的消息。简单但不聪明——如果最早的消息包含了关键决策信息,丢弃它会导致Agent失去上下文。

重要性策略:根据消息的重要性分数丢弃。比FIFO好,但需要额外计算重要性分数。每轮对话都计算,会增加约5-10%的推理成本。

混合策略:保留开头(系统提示、用户需求)和结尾(当前正在做的),丢弃中间。这是实践中最常用的策略,因为它利用了"lost in the middle"的特点——即使完整保留中间部分,模型也回忆不准。不如直接丢弃,用摘要替代。

三、工作记忆:运行时数据的结构化管理

3.1 工作记忆的定位

短期记忆存储"发生了什么",工作记忆存储"当前正在处理什么"。两者的区别:

短期记忆:用户要求重构UserService.java → 我读取了文件 → 我开始修改第42行 → 修改完成 ​ 工作记忆(Agent运行时状态): { "current_task": "重构UserService.java", "file_path": "/src/UserService.java", "current_line": 42, "modification_type": "rename_method", "dependencies": ["UserRepository.java"], "validation_results": {"syntax": "pass", "tests": "pending"} }

工作记忆是结构化数据,不是自然语言。它保存在Agent的运行环境中,每次工具调用前后都会被读取和更新。

3.2 结构化工作记忆的实现

工作记忆通常是一个JSON对象,Agent可以读取和修改它:

# Agent工作记忆的结构 working_memory = { "task_id": "task_001", "task_description": "给项目的用户模型添加邮箱验证字段", "current_step": 3, "total_steps": 5, "active_files": ["/src/models/user.py", "/src/migrations/001_add_email.py"], "decisions": [ { "step": 2, "decision": "使用Django的EmailField而非自定义验证", "reason": "Django内置验证已经满足需求,不需要重复造轮子" } ], "pending_decisions": [ { "question": "验证邮件发送方式", "options": ["SMTP直发", "使用SendGrid API", "使用阿里云邮件"], "status": "waiting" } ], "errors": [], "timestamps": { "started_at": "2026-06-15T10:00:00Z", "last_activity": "2026-06-15T10:15:30Z" } }

工作记忆的关键设计:它需要被编码到上下文中,但又不能占用太多token。一种做法是只保留工作记忆中最关键的部分到上下文中,完整的工作记忆存储在文件系统中。

3.3 工作记忆的持久化

当Agent运行时间超过1小时,工作记忆需要定期保存到持久化存储。保存频率:

def should_persist_working_memory(wm): """判断是否需要持久化当前工作记忆""" reasons = [] elapsed = time.time() - wm["timestamps"]["last_persist"] if elapsed > 300: # 5分钟没有持久化 reasons.append("超过5分钟未持久化") if len(wm.get("decisions", [])) != last_persisted_decisions: reasons.append("有新的决策记录") if wm.get("current_step") != last_persisted_step: reasons.append("步骤有更新") return reasons

持久化的不是整个对话上下文,只有工作记忆JSON。恢复时,Agent读取工作记忆JSON,不需要重放所有对话记录就能知道当前任务的状态。

四、长期记忆:跨会话的知识保持

4.1 长期记忆存储什么

长期记忆是Agent在不同会话之间共享的知识。和短期记忆不同,长期记忆是持久化的,不随会话结束而消失。

长期记忆存储的数据类型:

数据类型示例存储方式更新频率
用户偏好"用户喜欢清晰的代码注释"键值存储低(用户主动设置)
编码规范"项目使用4空格缩进"文件低(项目初始化时)
工具使用模式"用户常用search_files搜日志"向量数据库中(每次使用更新频率)
错误解决记录"上次数据库连接失败是因为密码过期"向量数据库中(每次解决更新)
项目领域知识"这个项目的用户模型有email字段"向量数据库高(每次代码变更更新)

4.2 向量数据库的检索策略

长期记忆通过语义检索获取相关记忆。向量数据库的检索策略直接决定了长期记忆的质量。

Top-K检索:返回相似度最高的K条记忆。简单但问题很多——如果K太小,可能漏掉重要信息。如果K太大,检索到的无关信息会稀释有效信息。

MMR(最大边际相关性)检索:在相关性和多样性之间取平衡。返回的记忆不仅和查询相似,而且彼此之间不重复。

def mmr_search(query_embedding, memory_store, top_k=5, lambda_param=0.5): """ MMR: lambda_param=1时完全是相关性,0时完全是多样性 """ candidates = memory_store.get_all_embeddings() selected = [] remaining = list(range(len(candidates))) ​ for _ in range(top_k): if not remaining: break scores = [] for idx in remaining: # 相关性:和查询的余弦相似度 relevance = cosine_sim(query_embedding, candidates[idx]["embedding"]) # 多样性:和已选记忆的最大相似度 if selected: diversity = max(cosine_sim(candidates[idx]["embedding"], candidates[s]["embedding"]) for s in selected) else: diversity = 0 # MMR分数 mmr_score = lambda_param * relevance - (1 - lambda_param) * diversity scores.append((idx, mmr_score)) # 选择MMR分数最高的 best_idx = max(scores, key=lambda x: x[1])[0] selected.append(best_idx) remaining.remove(best_idx) ​ return [memory_store[idx] for idx in selected]

MMR的lambda_param控制相关性和多样性的权重。对于Agent记忆检索,建议lambda_param设为0.6-0.7(相关性为主,多样性为辅)。因为记忆检索的目标是找到有用的信息,不是最大化信息覆盖。

时间加权检索:在语义相似度的基础上叠加时间衰减。越近的记忆权重越高。

def time_weighted_search(query_embedding, memory_store, top_k=5, time_decay=0.01): """ time_decay:时间衰减系数。越大表示越看重近期记忆。 """ candidates = memory_store.get_all_embeddings() scored = [] now = time.time() for candidate in candidates: relevance = cosine_sim(query_embedding, candidate["embedding"]) age_hours = (now - candidate["timestamp"]) / 3600 time_weight = math.exp(-time_decay * age_hours) combined_score = relevance * time_weight scored.append((candidate, combined_score)) scored.sort(key=lambda x: x[1], reverse=True) return [c for c, _ in scored[:top_k]]

时间加权检索的一个重要参数是time_decay。0.01的衰减意味着24小时后记忆权重衰减到约79%,168小时(7天)后衰减到约20%。适用于编码规范、项目知识等不需要长期保留的记忆。对于用户偏好等长期不变的记忆,time_decay应该设得更小(如0.001)。

4.3 记忆的选择性写入

不是所有信息都值得写入长期记忆。写入太多无关信息会让检索时噪声过大。

选择性写入的判断规则:

def should_write_to_long_term(context, memory_store): """ 判断一个信息是否值得写入长期记忆 """ # 规则1:信息是否和已有记忆高度重复 similar = memory_store.search(context["key"], top_k=1) if similar and similar[0]["similarity"] > 0.95: return False # 几乎相同的信息,不写 ​ # 规则2:信息是否可被重新发现 if context.get("rediscoverable", True): # 如果这个信息可以通过代码分析再次获得(如函数签名),不写 return False ​ # 规则3:信息是否具有长期价值 if context.get("temporary", False): # 临时性信息(如"当前在第42行"),不写 return False ​ # 规则4:信息的置信度 if context.get("confidence", 0) < 0.7: # 低于70%置信度的信息,不写 return False ​ return True

记忆写入的门控是长期记忆质量的关键。写入太松导致噪声,写入太紧导致信息缺失。一个实用的策略:宁可宽松写入,但要给每条记忆附加置信度分数。检索时可以根据置信度过滤。

4.4 记忆的更新和冲突解决

当新知识和已有记忆冲突时,不是简单覆盖,而是需要解决冲突。

def resolve_memory_conflict(new_fact, old_fact): """ 解决新知识和已有记忆的冲突 """ ​ # 情况1: 新知识更稳定(来自用户明确声明) if new_fact["source"] == "user_explicit": replace(old_fact, new_fact) return "replaced" ​ # 情况2: 新知识来自最近的观察 if new_fact["timestamp"] > old_fact["timestamp"] + 86400: # 新知识的时效性显著更新,但保留旧知识作为备选 add_as_alternative(old_fact, new_fact) return "added_alternative" ​ # 情况3: 两个知识都有可靠来源 if new_fact["confidence"] > 0.9 and old_fact["confidence"] > 0.9: # 两个都保留,标记为冲突,下次检索时让Agent决定 mark_as_conflict(new_fact, old_fact) return "marked_conflict" ​ # 情况4: 默认保留置信度更高的 if new_fact["confidence"] > old_fact["confidence"]: replace(old_fact, new_fact) return "replaced_by_confidence" else: return "kept_old"

冲突标记后,检索时返回两个互相矛盾的知识,Agent在推理时决定信哪个。这种方式比硬性覆盖更安全——如果Agent发现矛盾,可以主动向用户确认。用户看到Agent问"您之前说用PostgreSQL,现在又提到MySQL,应该用哪个?"会觉得Agent很聪明,而不是觉得它记忆混乱。

五、情景记忆:决策回溯和错误学习

5.1 情景记忆vs长期记忆

维度长期记忆情景记忆
存储内容提取后的知识(去除了时间上下文)完整的事件记录(包含时间、前因后果)
检索方式语义相似度时间+语义
用途当前任务的知识支持回溯决策、错误分析
容量大(可以存储大量知识碎片)大(但需要压缩)
精度高(提取后更精确)低(原始记录包含噪声)

一个例子:

长期记忆存储:项目使用PostgreSQL 15,数据库名agent_db 情景记忆存储:2026-06-15 10:30,用户说"数据库用PostgreSQL 15,数据库名叫agent_db,密码在.env文件里"

长期记忆提取了知识,丢失了上下文。情景记忆保留了原始过程,让Agent可以回溯"用户当时为什么选PostgreSQL"。

5.2 会话摘要(Episode Summary)

情景记忆的核心是会话摘要。每个Agent会话结束后,生成一个结构化的摘要:

episode_summary = { "session_id": "ses_20260615_001", "user_goal": "添加用户登录功能", "completed_tasks": [ "创建User模型", "实现注册接口", "实现登录接口", "添加JWT验证" ], "key_decisions": [ { "context": "选择密码加密方案", "decision": "使用bcrypt而非SHA256", "reason": "bcrypt自带salt,更安全" } ], "errors": [ { "error": "数据库连接失败", "cause": "密码中含有特殊字符未转义", "solution": "使用URL编码处理密码" } ], "unfinished": [ "忘记添加密码重置功能" ], "files_changed": ["/src/models/user.py", "/src/auth/login.py"], "tools_used": ["read_file", "write_file", "terminal", "web_search"] }

这个摘要的每个字段都有不同的用途:

  • user_goal:用于搜索相关情景

  • completed_tasks:快速了解已做的工作

  • key_decisions:在Agent遇到类似决策时参考

  • errors:错误学习的重要来源

  • unfinished:提醒还有未完成的工作

  • files_changed:用于Agent判断哪些文件有修改历史

5.3 错误学习

情景记忆的一个重要功能是从错误中学习。当Agent犯了一个错误(如选错了工具、产生了错误的代码、忘记了依赖关系),错误记录会被保存。

下次Agent遇到类似场景时,系统会自动检索相关的错误记录,附带到上下文中:

当前上下文: [系统] 以下是过去相似的场景中发生过的问题: - 错误记录(2026-06-10):使用write_file覆盖了已有的配置文件,导致数据丢失。解决方法:先read_file确认内容,再决定是否覆盖。 - 错误记录(2026-06-12):忘记安装依赖包就运行测试。解决方法:运行测试前先检查requirements.txt。 ​ 用户:把这个配置文件更新一下 Agent:(读取了错误记录)让我先看一下当前文件的内容,再决定怎么更新...

错误学习的两个关键设计:

错误特征的提取:不是保存整个错误场景(太大了),而是提取特征——当时Agent在执行什么任务、遇到了什么错误、正确的做法是什么。特征的粒度是"足够类似场景触发即可",不需要完全匹配。

相似度阈值:当前场景和错误记录的相似度超过某个阈值时才触发。阈值设得太低(如0.7),会被大量错误记录淹没,增加上下文负担。阈值设得太高(如0.95),类似错误会再次发生。推荐阈值约0.85。

六、四种记忆的协作机制:一个完整的Agent任务示例

6.1 多轮对话中的记忆流转

以一个"重构用户管理模块"的任务为例,展示四种记忆的协作:

第1轮:用户说"帮我重构UserService.java" 工作记忆:写入当前任务(重构UserService.java) 长期记忆:检索这个项目的编码规范(4空格缩进、TypeScript、JSDoc) 情景记忆:检索这个文件的历史修改记录 短期记忆:用户的输入写入上下文 ​ 第10轮:重构完成,开始写测试 短期记忆:已有10轮对话,接近上下文窗口上限 工作记忆:更新为"写UserService的测试" 长期记忆:检索项目的测试框架配置(Jest、覆盖率要求) ​ 第15轮:上下文即将溢出 短期记忆:触发压缩,前10轮的消息压缩为摘要 工作记忆:重新保存到持久化存储 情景记忆:如果发生了值得记录的错误或决策,写入情景记忆 ​ 第20轮:用户关闭会话 情景记忆:生成完整的会话摘要 长期记忆:提取有价值的持久信息 工作记忆:保存到长期记忆(作为"未完成任务"的特殊标记) ​ 第2天(新会话):用户说"继续昨天的UserService重构" 情景记忆:检索昨日会话,找到"UserService重构"的未完成任务 长期记忆:检索项目编码规范 工作记忆:从昨日的工作记忆中恢复任务状态 短期记忆:将昨日摘要和新输入加载到上下文

6.2 信息流转图

用户输入 │ ▼ 短期记忆(上下文窗口) │ ├── 到达容量阈值 → 压缩 → 摘要加入上下文,原始消息丢弃 │ ├── 需要当前状态 → 读取工作记忆 → 加入上下文 │ ├── 需要领域知识 → 检索长期记忆 → 向量搜索 → 加入上下文 │ └── 需要历史参考 → 检索情景记忆 → 时间+语义搜索 → 加入上下文 │ ▼ Agent生成回复 │ ▼ 执行工具 → 结果反馈 → 加入上下文 │ ▼ 更新工作记忆 → 如果达到持久化条件,写入持久化存储 │ ▼ 如果值得长期记住 → 写入长期记忆(向量数据库) │ ▼ 如果值得情景记录 → 写入情景记忆(会话摘要更新)

这个流转每轮对话都在发生。用户看到的是Agent在正常对话,背后是四种记忆系统的持续读写。一个运行1小时的Agent任务,工作记忆可能被更新50次,长期记忆检索10次,情景记忆检索2-3次,短期记忆压缩2-4次。

七、2026年的技术现状与局限

7.1 各项目的记忆系统现状

项目短期记忆工作记忆长期记忆情景记忆错误学习
Claude Code上下文管理文件系统(项目记忆)会话记录
Codex CLI基础上下文会话记录
OpenAI o3长上下文用户设置
MemGPT/Letta上下文压缩有(结构化)向量数据库会话摘要有限
AutoGPT 2.0上下文管理有限向量数据库会话记录有限
Hermes Agent上下文管理记忆系统会话索引通过技能
OpenHands上下文管理有限文件系统

MemGPT(Letta)在长期记忆和情景记忆上做得最好——它专门为Agent-用户对话设计了记忆系统。Claude Code没有显式的长期记忆系统,但通过文件系统间接实现了跨会话的知识保持。Codex CLI在记忆系统上几乎没有投入——每次会话是独立的。

7.2 当前局限

记忆检索的准确率:长期记忆的检索准确率受embedding模型限制。在代码相关的检索上,通用embedding模型(如text-embedding-3-small)的效果不如代码专用模型(如code-embedding)。但代码专用模型的生态还不够成熟。语义检索在代码场景下容易出现"语义接近但逻辑不相关"的误匹配。

记忆冲突的自动化处理:当长期记忆和情景记忆中的信息冲突时,目前的Agent无法自动解决。少数项目会标记冲突并让用户决定,多数项目简单用新覆盖旧。

记忆的遗忘机制:目前所有项目的记忆系统都是"只增不删"的。长期记忆随着使用时间线性增长,没有主动遗忘机制。一个运行半年的Agent,长期记忆可能有数百万条记录,检索噪声显著增加。主动遗忘(删除过时、错误、不再相关的记忆)是2026年尚未解决的问题。

八、总结

  • Agent记忆系统分为四类:短期记忆(上下文窗口)、工作记忆(运行时状态)、长期记忆(持久知识)、情景记忆(历史记录)

  • 短期记忆的有效容量远小于物理容量(lost in the middle效应),需要上下文压缩

  • 消息摘要保留不超过总上下文的10%,选择性保留按重要性排序

  • 工作记忆是结构化JSON,每5分钟或无变化5步后持久化

  • 长期记忆通过向量数据库检索,MMR检索(lambda=0.6-0.7)优于简单Top-K

  • 时间加权检索的衰减系数:短期知识0.01,长期知识0.001

  • 选择性写入四规则:不重复、不可重新发现、非临时、置信度>0.7

  • 记忆冲突用标记替代覆盖,让Agent在推理时决定

  • 情景记忆存储完整事件上下文(含时间、前因后果),错误学习是重要应用

  • 错误学习的触发阈值推荐0.85,特征提取比完整场景更实用

  • 四种记忆每轮对话都在流转,运行1小时的任务约60次记忆读写

  • 2026年主要局限:代码场景检索准确率不足、冲突无自动解决、无主动遗忘机制