
同一个 Agent,单轮对话对答如流,第二天却"失忆";任务一长就开始胡说八道;记忆越攒越多,最后新旧事实打架。这些往往不是模型不够强,而是你没给它的"记忆系统"分层。本文用可复现的 Python 代码,讲透短期记忆的上下文管理、长期记忆的读写链路、向量记忆与 Markdown 记忆的取舍,以及让记忆持续进化的"代谢机制"。
前言
在 AI Agent 的开发中,有一个非常普遍的痛点:Agent 在单次对话中表现出色,但一旦任务变长、或跨越多个会话,它就会"健忘"或"逻辑断层"。
- 你昨天告诉它"我用的是 Java 21,偏好关注性能而非代码规范",今天它又开始给你挑命名规范、还默认你在写 Java 8;
- 一个需要多步推进的长任务,跑到中段它就忘了最初的目标,开始绕圈子;
- 聊得越久,它越容易把几小时前的工具调用结果和现在的问题混为一谈,给出自相矛盾的答案。
这些现象的根源,往往不在模型,而在 记忆(Memory)。
业内有个被广泛引用的 Agent 核心公式:
Agent = LLM + Planning + Memory + Tools
- LLM 是推理引擎(大脑算力);
- Planning 决定它如何拆解任务、规划步骤;
- Tools 决定它能调用什么能力;
- Memory 决定它记得什么、学到了什么、此刻在处理什么。
很多人把精力都花在换更强的模型和调 Prompt 上,却忽略了:记忆系统直接决定了 Agent 能否从"聊天机器人"进化为真正的"数字员工"。 一个会遗忘、会混淆、不会积累经验的 Agent,能力再强也只能活在"这一句话"里。
读完本文你将收获:
- 理解短期记忆与长期记忆的双层架构(物理与逻辑解耦);
- 掌握短期记忆的上下文管理手段:滑动窗口、摘要压缩、重型结果卸载;
- 学会长期记忆的读写链路:对话后语义提纯写入、会话前向量检索注入;
- 厘清向量记忆 vs Markdown 记忆的适用边界;
- 拿到一套让记忆持续进化的**“代谢机制”**(反思、合并、遗忘)与 L1/L2/L3 进阶路径,全部配可复现代码。
背景或问题:Agent 为什么会"健忘"?
先抛三个几乎每个 Agent 开发者都踩过的坑:
- 跨会话失忆:用户关掉对话再打开,Agent 完全不记得你是谁、你的偏好是什么,每次都从零开始。
- 长对话后期"变笨":聊到几十轮以后,Agent 开始忘掉前面说过的话、格式混乱、甚至"幻觉式"编造已经不存在的上下文。
- 记忆越攒越乱:好不容易给 Agent 加了记忆,结果它把"用户技术栈是 Java 8"和后来"升级到 Java 21"两条都记着,下次回答时新旧事实打架,输出更乱了。
这三个问题的共同点是:它们都不是模型能力问题,而是记忆工程问题。 解决它们,需要先把记忆"分层"。
核心思路:记忆的双层架构——物理与逻辑的彻底解耦
一个成熟的 Agent 记忆系统,必须在物理和逻辑上划分为两层:短期记忆与长期记忆。

1. 短期记忆(Short-term Memory):Session 级的"工作空间"
短期记忆是 Agent 在当前单次会话中持有的暂存信息,主要依托于 LLM 自身的上下文窗口(Context Window)。它负责记录这一轮任务里必须保留的信息:用户刚提的问题、Planning 的中间计划、工具调用的入参与返回(Observations)等。
工程约束:尽管主流模型窗口已经达到 1M Token,但有两个硬约束不能忽视:
- 推理成本随长度线性增长:上下文越长,单次推理越贵、越慢;
- "中间丢失(Lost in the Middle)"现象:模型对上下文首尾的注意力强,对中段的信息容易"视而不见"——窗口再大,不代表它真的都用得上。
管理手段(按轻重排列):
- 滑动窗口(Sliding Window):只保留最近 N 轮对话,丢弃最早的消息,简单粗暴但会丢上下文;
- 摘要压缩(Summarization):把被丢弃的旧消息用 LLM 压缩成一段摘要,用信息损耗换取空间;
- 重型结果卸载(Offloading):把大文件、长返回结果存到外部存储(数据库 / 文件),Prompt 里只保留一个引用(如"结果见 result_8842.json"),需要时再取回。
2. 长期记忆(Long-term Memory):跨 Session 的"经验沉淀"
长期记忆是活在会话之外的持久化知识库,负责沉淀用户偏好、历史决策和过往教训。
核心特征——高度个性化:这一点经常被混淆。长期记忆 ≠ RAG。 RAG 挂载的通常是"共享知识源"(如公司规章、产品文档、公开资料),对所有用户一致;而长期记忆管理的是 Agent 与特定用户交互中动态沉淀的私有背景(“这个用户更在意性能”“这个项目禁止直连生产库”)。一个是"公共图书馆",一个是"私人笔记本"。
读写链路:
- 写入:通常发生在对话结束后,通过 LLM 对短期记忆进行"语义提纯"——从一堆闲聊和工具结果里,提取出值得长期记住的"事实/偏好/教训";
- 读取:通常发生在新会话开始时,通过向量检索把与当前问题相关的历史经验,注入到上下文里。
一句话总结:短期记忆是"工作台",长期记忆是"笔记本"。 工作台要随时清空保持清爽,笔记本要持续沉淀且能被检索召回。两者物理隔离、逻辑解耦,Agent 才能既专注当下,又不忘过去。
实现步骤
下面把这套记忆系统拆成可落地的工程模块,每个都配可复现的代码。为方便演示,向量与 embedding 部分用纯 Python + NumPy 做了轻量实现(实际工程替换为真实 embedding 模型和向量库即可),所有示例都能直接跑起来。
步骤一:短期记忆——给上下文窗口装一个"自动整理器"
短期记忆的关键,是在 token 预算内保住最相关的信息。我们实现一个 ShortTermMemory:它维护一个消息列表,当累计 token 超过预算时,自动触发摘要压缩——把较早的消息压成一段摘要,只保留最近几轮原文。
# short_term_memory.py
# 短期记忆:滑动窗口 + token 预算 + 自动摘要压缩
from dataclasses import dataclass
@dataclass
class Message:
role: str # "user" / "assistant" / "tool"
content: str
def _approx_tokens(text: str) -> int:
"""近似 token 计数(中文约 1 字 ≈ 1 token)。
生产环境建议用 tiktoken 等精确分词器。"""
return max(1, len(text))
class ShortTermMemory:
def __init__(self, token_budget: int = 6000, keep_recent: int = 4):
self.token_budget = token_budget # 上下文 token 预算(生产环境通常数千)
self.keep_recent = keep_recent # 压缩时保留最近几条原文
self.messages: list[Message] = []
self.summary: str = "" # 历史摘要(被压缩掉的旧消息)
def add(self, role: str, content: str) -> None:
self.messages.append(Message(role, content))
self._maybe_compress()
def _used_tokens(self) -> int:
"""当前上下文已用 token(摘要 + 全部消息)。"""
total = _approx_tokens(self.summary)
for m in self.messages:
total += _approx_tokens(m.content)
return total
def _summarize(self, old_messages: list[Message]) -> str:
"""模拟 LLM 的有损摘要:每条旧消息只保留要点。
生产环境应调用大模型,输出比原文更短的总结。"""
bits = [f"[{m.role}]{m.content[:6]}" for m in old_messages]
return "; ".join(bits)
def _maybe_compress(self) -> None:
"""超过预算时,把较早的消息压缩进摘要。
用 while 循环:一次压缩可能仍超预算,需反复压缩直到达标或仅剩最近几条。"""
guard = 0 # 防御性兜底,避免极端情况下死循环
while (self._used_tokens() > self.token_budget
and len(self.messages) > self.keep_recent
and guard < 20):
guard += 1
# 要压缩的 = 全部消息 - 需要保留的最近几条
n_to_compress = max(1, len(self.messages) - self.keep_recent)
to_compress = self.messages[:n_to_compress]
self.messages = self.messages[n_to_compress:]
new_summary = self._summarize(to_compress)
self.summary = ((self.summary + " | " + new_summary).strip(" |")
if self.summary else new_summary)
print(f"[短期记忆] 触发摘要压缩,{n_to_compress} 条旧消息已并入摘要,"
f"当前上下文 {self._used_tokens()} tokens")
def to_prompt(self) -> str:
"""拼装成可塞进 Prompt 的上下文。"""
parts = []
if self.summary:
parts.append(f"[历史摘要] {self.summary}")
for m in self.messages:
parts.append(f"[{m.role}] {m.content}")
return "\n".join(parts)
if __name__ == "__main__":
# 小预算方便演示触发压缩(生产环境 token_budget 通常为数千)
mem = ShortTermMemory(token_budget=80, keep_recent=3)
mem.add("user", "我是后端工程师,主力语言是 Java 21")
mem.add("assistant", "好的,已记录你的技术栈。")
mem.add("user", "帮我审查这段代码的性能问题,关注 OOM 风险")
mem.add("tool", "内存报告已卸载:见 result_8842.json") # 重型结果卸载,只留引用
mem.add("user", "那这部分为什么会有内存泄漏?")
print("\n=== 最终进入 Prompt 的上下文 ===")
print(mem.to_prompt())
运行结果:
[短期记忆] 触发摘要压缩,1 条旧消息已并入摘要,当前上下文 73 tokens
[短期记忆] 触发摘要压缩,1 条旧消息已并入摘要,当前上下文 95 tokens
=== 最终进入 Prompt 的上下文 ===
[历史摘要] [user]我是后端工程 | [assistant]好的,已记录
[user] 帮我审查这段代码的性能问题,关注 OOM 风险
[tool] 内存报告已卸载:见 result_8842.json
[user] 那这部分为什么会有内存泄漏?
可以看到:较早的闲聊被有损压进了 [历史摘要](每条只留要点),最近的关键对话保留原文;而那条 2000 行的内存报告走了重型结果卸载——Prompt 里只留一个引用 result_8842.json,需要时再取回。整体 token 始终被压在预算内。_maybe_compress 用 while 循环而非单次 if,是因为一次压缩后如果仍超预算,要继续压缩更早的消息。
避坑提示:摘要压缩是有损的。关键约束(如"禁止直连生产库")建议从短期记忆中卸载到长期记忆或
AGENTS.md,而不是依赖随时可能被压缩掉的对话历史。能用"引用 + 按需取回"解决的(如大文件),就别让它一直占着上下文。
步骤二:长期记忆——打通"写入"与"读取"两条链路
短期记忆解决的是"单次会话内的专注",长期记忆解决的是"跨会话的连续性"。它有两条核心链路:写入(语义提纯) 和 读取(检索注入)。

下面是一个可运行的长期记忆实现(向量部分用 NumPy 简化,便于理解链路;生产环境把 fake_embed 换成真实 embedding、把内存列表换成向量库即可):
# long_term_memory.py
# 长期记忆:写入(语义提纯) + 读取(向量检索注入)
import hashlib
import time
from dataclasses import dataclass, field
import numpy as np
def fake_embed(text: str, dim: int = 64) -> np.ndarray:
"""文本哈希生成确定性伪向量,仅供跑通链路。
生产环境替换为真实 embedding 模型(如 text-embedding-3-small)。"""
h = hashlib.md5(text.encode("utf-8")).digest()
vec = np.frombuffer((h * ((dim // 16) + 1))[: dim * 4], dtype=np.uint8)
return (vec.astype(np.float32) - 128) / 128.0
def cosine(a: np.ndarray, b: np.ndarray) -> float:
return float(np.dot(a, b) / ((np.linalg.norm(a) * np.linalg.norm(b)) + 1e-8))
@dataclass
class MemoryItem:
content: str # 提纯后的事实/偏好/教训
category: str # fact / preference / lesson
embedding: np.ndarray
created_at: float = field(default_factory=time.time)
class LongTermMemory:
def __init__(self):
self.store: list[MemoryItem] = []
# ---------- 写入链路:语义提纯 ----------
def write_from_session(self, short_term_text: str) -> None:
"""会话结束后调用:从短期记忆里'语义提纯'出值得长期记住的内容。
实际工程用 LLM 抽取,这里给一段可直接复用的提取 Prompt 模板。"""
extract_prompt = f"""请从下面这段对话记录中,提取出值得长期记住的条目。
只提取稳定的事实、用户偏好或可复用的教训,忽略闲聊和一次性信息。
每条输出一行,格式:分类|内容。分类只能是 fact/preference/lesson。
对话记录:
{short_term_text}
示例输出:
preference|该用户更关注性能而非代码规范
fact|该用户主力技术栈是 Java 21
lesson|审查该用户代码时应优先关注 OOM 风险
"""
# 演示:用写死的提取结果代替 LLM 调用
extracted = [
("preference", "该用户更关注性能而非代码规范"),
("fact", "该用户主力技术栈是 Java 21"),
("lesson", "审查该用户代码时应优先关注 OOM 风险"),
]
for category, content in extracted:
self.store.append(
MemoryItem(content=content, category=category,
embedding=fake_embed(content))
)
print(f"[长期记忆·写入] 语义提纯出 {len(extracted)} 条经验并持久化")
# ---------- 读取链路:向量检索注入 ----------
def retrieve(self, query: str, top_k: int = 3) -> list[MemoryItem]:
"""新会话开始时调用:按当前问题检索最相关的历史经验。"""
if not self.store:
return []
q_emb = fake_embed(query)
ranked = sorted(self.store, key=lambda m: cosine(q_emb, m.embedding),
reverse=True)
return ranked[:top_k]
def build_context(self, query: str) -> str:
"""把检索到的长期记忆拼成一段,注入 Agent 上下文。"""
hits = self.retrieve(query)
if not hits:
return ""
lines = [f"- [{m.category}] {m.content}" for m in hits]
return "[相关长期记忆]\n" + "\n".join(lines)
if __name__ == "__main__":
ltm = LongTermMemory()
# 场景一:上一轮会话结束后,把短期记忆提纯写入长期记忆
print("=== 会话1结束:写入长期记忆 ===")
ltm.write_from_session("用户说他用 Java 21,审查代码更在意性能 ...")
# 场景二:新一轮会话开始,带着用户新问题检索注入
print("\n=== 会话2开始:检索注入 ===")
ctx = ltm.build_context("帮我看看这段代码有没有性能问题")
print(ctx)
运行结果:
=== 会话1结束:写入长期记忆 ===
[长期记忆·写入] 语义提纯出 3 条经验并持久化
=== 会话2开始:检索注入 ===
[相关长期记忆]
- [lesson] 审查该用户代码时应优先关注 OOM 风险
- [fact] 该用户主力技术栈是 Java 21
- [preference] 该用户更关注性能而非代码规范
这就是 Agent"第二天还记得你"的底层链路:会话结束做语义提纯写入,会话开始做检索注入。 注意 write_from_session 里给出的提取 Prompt 模板是可直接复用的——生产中把它接到你的 LLM 调用上即可。
步骤三:向量记忆 vs Markdown 记忆——别选错工具
实现长期记忆时,开发者常在"重型向量库"和"轻量 Markdown"之间犹豫。其实它们不是二选一,而是适用边界不同。
| 维度 | 向量记忆(Vector Memory) | Markdown 记忆(如 CLAUDE.md / AGENTS.md) |
|---|---|---|
| 本质 | 海量非结构化文本的语义检索 | 可审计、可版本控制的"入职手册" |
| 适合存 | 多样化历史记录、长周期行为偏好 | 项目规范、固定汇报偏好、团队协作约定 |
| 优势 | 能从数十万条历史里找相似片段 | 透明可审计(随时看到 Agent 记了什么)、原生 Git 版本控制、零运维成本 |
| 代价 | 运维复杂,易产生"虚假关联"(语义相近但业务无关) | 不擅长语义模糊检索,条目多了需要人工维护 |
| 更新方式 | 自动提纯写入 + 向量召回 | 人工或 Agent 按规则追加,错误驱动进化 |
1. 向量记忆:海量非结构化文本的语义检索
当需要从数十万条交互历史中寻找相似片段时,向量检索不可替代。典型场景:跨数月分析某个用户的行为偏好、从大量历史工单里找相似案例。
代价是运维复杂度高,且有个隐蔽的坑——“虚假关联”:两段话语义相近,但业务上毫无关系,向量检索却把它们排到前面,反而误导 Agent。所以向量记忆一定要配合重排(rerank) 和业务过滤一起用。
2. Markdown 记忆:可审计的"入职手册"
这是一种极其务实的方案:把规则、约定和踩坑记录写进仓库里的 Markdown 文件(如 CLAUDE.md、AGENTS.md)。它的优势是 透明可审计(随时能看到 Agent 到底"记"了什么)、版本控制(原生集成 Git,记忆变更可追溯)、零运维成本。
实践建议遵循 "错误驱动进化"原则:只有当 Agent 真的犯过某个错,才值得在 AGENTS.md 里增加一条防错规则——而不是一开始就写一份几千字的"完美手册"。下面是一个错误驱动进化的极简管理器:
# markdown_memory.py
# Markdown 记忆:错误驱动进化,按"踩坑"增量沉淀防错规则
from pathlib import Path
class MarkdownMemory:
"""维护一个 AGENTS.md 风格的硬性约束文件。
核心原则:只在 Agent 真的犯错后,才追加一条防错规则。"""
def __init__(self, path: str = "AGENTS.md"):
self.path = Path(path)
def learn_from_mistake(self, mistake: str, rule: str) -> None:
"""Agent 犯错后调用:把教训沉淀为一条可机械化检查的规则。"""
section = "\n## 防错规则(错误驱动进化)\n" if "防错规则" not in self._read() else ""
entry = f"\n- [触发场景] {mistake}\n [约束规则] {rule}\n"
with self.path.open("a", encoding="utf-8") as f:
f.write(section + entry)
print(f"[Markdown记忆] 已从错误中学习并沉淀规则:{rule}")
def _read(self) -> str:
return self.path.read_text(encoding="utf-8") if self.path.exists() else ""
def to_prompt(self) -> str:
"""把硬性约束注入 Agent 上下文(通常放在 System Prompt 里)。"""
return self._read()
if __name__ == "__main__":
mm = MarkdownMemory(path="AGENTS.md")
# Agent 刚才在 handler 里裸写了 SQL,被结构测试拦下了
mm.learn_from_mistake(
mistake="Agent 在 src/handlers/user.py 中直接调用了 cursor.execute",
rule="handler 层禁止裸写 SQL,数据库访问必须走 repository 层",
)
运行后,AGENTS.md 会增量长出一条防错规则,下次 Agent 启动时把它注入 System Prompt,就不会再犯同样的错。
选型心法:共享的、硬性的、需要全团队对齐的约束 → Markdown 记忆;私有的、动态的、海量的用户历史 → 向量记忆。 两者通常并存:
AGENTS.md管"规矩",向量库管"经验"。
步骤四:记忆的"代谢机制"——反思、合并与遗忘
这是本文最关键的部分。如果系统只是无脑追加(Append) 信息,长期记忆很快会变成一堆高噪声的流水账——重复条目堆积、过时信息干扰、新旧事实打架。生产级系统必须有一套"代谢机制"。

1. 自我反思(Self-Reflection)
任务完成后,Agent 启动一个异步任务复盘成败,把教训提取为更高层的 Meta-Knowledge(元知识)。
例如:“在处理该用户的 Java 代码审查时,他更在意性能而非规范,未来应优先关注 OOM 风险。” 这种从具体事件中抽象出来的经验,比原始对话记录有用得多。
2. 记忆聚类与合并(Consolidation)
当出现大量碎片化、重复记录时(比如用户在 10 次对话里都提到同一个项目),系统自动触发合并任务,把它们整理成一条完整的"实体百科"。这能减少向量库冗余、提升检索一致性,避免同一个事实被召回 5 个几乎一样的版本。
3. 权重衰减与遗忘(Forgetting)
记忆不是越多越好,过时信息会严重干扰判断。
- 时间衰减:模仿人类遗忘曲线,为每条记忆维护一个分数,随时间推移降低权重——太久没被用上的记忆,逐渐"淡出";
- 冲突解决:当新旧事实冲突时(如用户技术栈从 Java 8 升级到 Java 21),旧记忆应被标记为废弃或软删除,而不是和新记忆并存打架。
下面把"代谢机制"实现成可运行的代码:
# memory_metabolism.py
# 记忆代谢机制:自我反思 + 聚类合并 + 权重衰减与遗忘
import math
import time
from dataclasses import dataclass
@dataclass
class MemRecord:
content: str
score: float = 1.0 # 当前权重/新鲜度得分
created_at: float = 0.0 # 创建时间戳
last_used: float = 0.0 # 最近一次被检索使用的时间
status: str = "active" # active / deprecated(软删除)
class MemoryMetabolism:
def __init__(self, half_life_days: float = 30.0):
self.half_life_seconds = half_life_days * 24 * 3600
self.records: list[MemRecord] = []
# ---------- 1. 自我反思:从一次任务中提取元知识 ----------
def self_reflect(self, task_summary: str) -> str:
"""任务完成后异步调用:把具体事件抽象为可复用的元知识。
实际工程用 LLM 生成,这里给 Prompt 模板 + 演示结果。"""
prompt = f"""请基于以下任务复盘,提炼一条可复用的'元知识':
要求抽象到'下次遇到类似情况该怎么办'的层面,而不是复述流水账。
任务复盘:{task_summary}
输出格式:一条元知识句子。
"""
# 演示结果(生产环境替换为 LLM 调用)
meta = "该用户在代码审查中更在意性能而非规范,未来优先关注 OOM 与性能风险"
self.records.append(MemRecord(
content=meta, created_at=time.time(), last_used=time.time()
))
print(f"[代谢·反思] 提取元知识:{meta}")
return meta
# ---------- 2. 聚类合并:碎片化重复记录整理成一条 ----------
def consolidate(self, similarity_fn) -> None:
"""把内容高度相似的碎片记录合并成一条。
similarity_fn(a, b) -> bool:由业务定义什么叫'同一件事'。"""
merged_any = False
i = 0
while i < len(self.records):
group = [self.records[i]]
rest = []
for r in self.records[i + 1:]:
if r.status == "active" and similarity_fn(group[0].content, r.content):
group.append(r)
else:
rest.append(r)
if len(group) > 1:
# 合并:取并集语义,旧的软删除
consolidated = MemRecord(
content=" | ".join(g.content for g in group),
score=max(g.score for g in group),
created_at=min(g.created_at for g in group),
last_used=max(g.last_used for g in group),
)
for g in group:
g.status = "deprecated" # 软删除碎片
self.records = self.records[:i] + [consolidated] + rest
merged_any = True
print(f"[代谢·合并] 合并 {len(group)} 条碎片为 1 条")
i += 1
if not merged_any:
print("[代谢·合并] 暂无需合并的碎片")
# ---------- 3. 权重衰减与遗忘 ----------
def decay_and_forget(self, now: float, conflict_fn=None) -> None:
"""时间衰减降权 + 新旧冲突时软删除旧记忆。"""
for r in self.records:
if r.status != "active":
continue
age = now - r.last_used
# 半衰期模型:每过一个 half_life,得分减半
r.score = math.pow(0.5, age / self.half_life_seconds)
# 得分低于阈值 = 长期未被使用,软删除
if r.score < 0.1:
r.status = "deprecated"
print(f"[代谢·遗忘] 长期未使用,软删除:{r.content[:24]}...")
# 冲突解决:新旧事实打架时,废弃旧的
if conflict_fn:
active = [r for r in self.records if r.status == "active"]
for i, a in enumerate(active):
for b in active[i + 1:]:
if conflict_fn(a.content, b.content):
older, newer = (a, b) if a.created_at <= b.created_at else (b, a)
older.status = "deprecated"
print(f"[代谢·冲突] 检测到冲突,废弃旧记忆:{older.content[:24]}...")
def active_records(self) -> list[MemRecord]:
return [r for r in self.records if r.status == "active"]
def _same_entity(a: str, b: str) -> bool:
"""演示用:两条记忆都提到'订单系统'才算同一实体的重复记录。
生产中用聚类 / LLM 判断,注意不要把'Java 8'和'Java 21'这种冲突事实误判为重复。"""
return "订单系统" in a and "订单系统" in b
def _conflict(a: str, b: str) -> bool:
"""演示用:新旧技术栈冲突。生产中用 LLM 判断事实是否矛盾。"""
return ("Java 8" in a and "Java 21" in b) or ("Java 8" in b and "Java 21" in a)
if __name__ == "__main__":
now = time.time()
meta = MemoryMetabolism(half_life_days=30)
# 1) 自我反思:沉淀元知识(刚发生,最"新鲜")
meta.self_reflect("用户要求审查 Java 代码,重点关注 OOM")
# 模拟一些历史记录,注意 created_at / last_used 各不相同
meta.records += [
MemRecord(content="用户在做的项目是订单系统", created_at=now - 86400 * 10),
MemRecord(content="用户的项目里有个订单系统模块", created_at=now - 86400 * 5),
MemRecord(content="用户技术栈是 Java 8", created_at=now - 86400 * 90),
MemRecord(content="用户技术栈升级为 Java 21", created_at=now - 86400 * 2),
]
for r in meta.records:
r.last_used = r.created_at
# 2) 聚类合并:把"订单系统"两条碎片合成一条
print()
meta.consolidate(_same_entity)
# 3) 权重衰减与遗忘(now 取当前时刻,演示半衰期降权 + 新旧冲突解决)
print()
meta.decay_and_forget(now=now, conflict_fn=_conflict)
print("\n=== 最终存活的有效记忆 ===")
for r in meta.active_records():
print(f" [score={r.score:.2f}] {r.content}")
运行结果:
[代谢·反思] 提取元知识:该用户在代码审查中更在意性能而非规范,未来优先关注 OOM 与性能风险
[代谢·合并] 合并 2 条碎片为 1 条
[代谢·冲突] 检测到冲突,废弃旧记忆:用户技术栈是 Java 8...
=== 最终存活的有效记忆 ===
[score=1.00] 该用户在代码审查中更在意性能而非规范,未来优先关注 OOM 与性能风险
[score=0.89] 用户在做的项目是订单系统 | 用户的项目里有个订单系统模块
[score=0.95] 用户技术栈升级为 Java 21
可以看到代谢机制的威力:碎片化的"订单系统"被合并成一条;过时且冲突的"Java 8"被软删除,只留下最新的"Java 21";而刚提取的元知识因为最"新鲜",拿到了接近满分的权重——记忆库从"高噪声流水账"变成了"干净的事实表"。注意 _same_entity 故意只匹配"订单系统",没有把"Java 8 / Java 21"误判为重复——否则两条矛盾的事实会被错误合并。
关键提醒:
decay_and_forget里的半衰期(默认 30 天)和遗忘阈值(0.1)需要按业务调。对"用户偏好"这类稳定事实,半衰期应设长一些;对"临时任务状态"这类易过期的,应设短一些。冲突解决务必保留"新"事实——除非有证据表明新事实是错的。
完整方案:L1/L2/L3 记忆系统进阶路径
打造 Agent 的"大脑"不能一蹴而就,建议按下面三个层级循序渐进。每一层都是上一层的前提,跳级往往事倍功半。
| 层级 | 名称 | 做什么 | 解决的问题 | 本文对应 |
|---|---|---|---|---|
| L1 | 基础 | 管好 Session 内的 Context,做摘要压缩和 Token 预算 | 长对话后期变笨、Lost in the Middle | ShortTermMemory |
| L2 | 规范 | 建立项目级 CLAUDE.md / AGENTS.md,沉淀硬性约束和 SOP | Agent 反复犯同类错误、团队不对齐 | MarkdownMemory |
| L3 | 经验 | 引入向量库存储个性化偏好,建立离线"反思-合并-遗忘"闭环 | 跨会话失忆、记忆越攒越乱 | LongTermMemory + MemoryMetabolism |
落地建议:
- 先 L1,把上下文管住:在没做好单会话上下文管理前,谈长期记忆都是空中楼阁。先用
ShortTermMemory守住 token 预算; - 再 L2,把规矩焊死:每犯一个错,就往
AGENTS.md增量沉淀一条防错规则,让 Agent"入职"越来越熟练; - 最后 L3,让经验流动:当 L1/L2 稳定后,再上向量库 + 代谢机制,让 Agent 真正"记得用户、积累经验、自动遗忘过时信息"。
常见问题与避坑
Q1:模型上下文窗口都有 1M Token 了,还需要长期记忆吗?
需要。窗口再大也解决不了两个问题:一是跨会话(关掉就没了),二是成本与 Lost in the Middle(塞满 1M 既贵又容易"中间丢失")。长期记忆的本质不是"装更多",而是"在合适的时机只召回最相关的那一小撮"。
Q2:向量记忆召回不准,总召回一些"语义相近但业务无关"的内容,怎么办?
这是典型的"虚假关联"。三个对策:①召回后加一层 rerank(重排) 模型;②检索时叠加 业务过滤条件(如按 category、时间范围、用户 id 过滤);③对召回结果做一次 LLM 相关性判定,过滤掉无关项再注入。
Q3:代谢机制(反思/合并/遗忘)多久跑一次?
建议异步、离线跑,别卡在用户对话的关键路径上。反思可以在每次会话结束后触发;合并和遗忘属于"大扫除",按数据量每天或每周跑一次批处理即可。
Q4:记忆是不是越多越好?
不是。过时、冲突、重复的记忆会严重干扰 Agent 判断。宁可保持一个"小而精"的记忆库,也不要一个"大而乱"的流水账。遗忘和合并的重要性,不亚于写入。
Q5:AGENTS.md 会不会越长越拖慢 Agent?
会。这也是它要坚持"错误驱动进化"的原因——只沉淀被验证过的防错规则,而不是堆砌一份理想化的长文档。过长的 System Prompt 本身就会把 Agent 推向"上下文焦虑"。定期回顾:哪些规则是因为"模型以前做不好"才加的,现在是否可以精简。
总结
回到开篇的三个痛点:跨会话失忆、长对话变笨、记忆越攒越乱——它们的解法都藏在"分层"二字里。
短期记忆(工作台) 长期记忆(笔记本)
├─ 滑动窗口 ├─ 写入:会话后语义提纯
├─ 摘要压缩 ├─ 读取:会话前向量检索注入
└─ 重型结果卸载 ├─ 向量记忆(海量私有经验)
├─ Markdown 记忆(硬性规矩,错误驱动进化)
└─ 代谢机制(反思 → 合并 → 遗忘)
进阶路径:L1 管上下文 → L2 沉淀规矩 → L3 经验闭环
记住记忆系统的终极目标,是让 Agent 清楚三件事:
- 它知道什么(事实)——靠长期记忆沉淀;
- 它学到了什么(经验)——靠自我反思提取元知识;
- 此刻正在处理什么(状态)——靠短期记忆守住当前任务上下文。
下次再遇到 Agent"健忘"或"逻辑断层",先别急着换模型——先问问自己:它的"记忆系统",分层了吗?代谢了吗?
1437

被折叠的 条评论
为什么被折叠?



