返回文章列表

Agent 记忆系统怎么设计:短期、长期与遗忘机制

给 Agent 加记忆,大部分人的第一反应是"把所有历史丢进上下文"。这个方案在小规模下有效,规模化后必然崩溃。这篇讲三层记忆架构和什么时候该主动遗忘。

4 分钟阅读1072

一个 Agent 跑了三天之后,你面对的第一个现实问题是:它的历史已经放不进上下文了。

然后你会面临选择。大部分人的第一反应是压缩——把旧对话总结一下塞回去。这个方案能续命,但治标不治本,因为摘要本身也在累积。

真正的问题是:你根本不应该把所有历史同等对待。

三层记忆架构

我用的分层方式:

层级 存什么 存活周期 存储方式 典型容量
工作记忆 当前任务的推理链 单次任务 上下文窗口 全部
情景记忆 发生过的具体事件 数天到数周 向量库 + 时间索引 数千条
语义记忆 提炼出的稳定事实 长期 结构化存储 数百条

关键洞察是:从情景记忆到语义记忆,需要一个主动的提炼过程,而且这个过程是有损的。

这个"有损"不是缺陷,是特性。记忆系统的价值不在于记住多少,而在于忘掉多少噪音

工作记忆:别浪费在重复上

工作记忆是稀缺资源,但它常被浪费。最典型的浪费是重复读取相同的上下文

class WorkingMemory:
    """带引用计数的工作记忆,避免同一份内容反复占用上下文。"""
 
    def __init__(self, max_tokens: int = 100_000):
        self.max_tokens = max_tokens
        self._blocks: dict[str, Block] = {}
        self._hits: dict[str, int] = {}
 
    def add(self, key: str, content: str, tokens: int) -> None:
        # 内容相同的块只存一份,靠 key 去重
        if key in self._blocks:
            self._hits[key] += 1
            return
        self._blocks[key] = Block(content, tokens)
        self._hits[key] = 1
        self._evict_if_needed()
 
    def _evict_if_needed(self) -> None:
        while self.total_tokens > self.max_tokens:
            # 淘汰优先级:命中次数少 且 加入时间早
            victim = min(
                self._blocks.keys(),
                key=lambda k: (self._hits[k], self._blocks[k].created_at),
            )
            del self._blocks[victim]
            del self._hits[victim]
 
    @property
    def total_tokens(self) -> int:
        return sum(b.tokens for b in self._blocks.values())

这个设计的核心在 _evict_if_needed淘汰策略同时考虑访问频率和时间,而不是简单的 FIFO 或 LRU。

纯 LRU 会有个问题——某个早期建立但持续被引用的关键约束,可能因为"最近没被访问"而被误删。加入命中次数作为第一排序键,能保住这类"低热度但高价值"的内容。

情景记忆:检索时要带时间权重

情景记忆存的是"发生了什么",比如"用户在 8 月 3 日要求过所有输出必须是中文"。

查询这类记忆时,纯向量相似度是不够的,因为相似的事件可能发生在完全不同的时间背景下。

def recall(self, query: str, top_k: int = 5) -> list[Episode]:
    """混合检索:语义相似度 × 时间衰减。"""
    semantic_scores = self.vector_store.search(query, top_k * 4)
 
    now = time.time()
    results = []
    for episode, sim in semantic_scores:
        age_hours = (now - episode.timestamp) / 3600
        # 半衰期设为 7 天:一周前的事件权重衰减到 0.5
        recency = 0.5 ** (age_hours / (24 * 7))
        # 0.7 语义 + 0.3 时效,这个配比在多数场景下表现稳定
        score = 0.7 * sim + 0.3 * recency
        results.append((episode, score))
 
    results.sort(key=lambda x: x[1], reverse=True)
    return [ep for ep, _ in results[:top_k]]

半衰期参数值得单独说明。设得太短(比如 1 天),Agent 会表现得"记性差";设得太长,"最近的要求"会被"很久以前的要求"淹没。

我的经验起点是 7 天,然后根据业务节奏调——高频迭代的项目缩短到 2-3 天,长期项目可以放宽到 30 天。

语义记忆:提炼要保守

语义记忆是从情景中提炼的稳定事实,比如"用户偏好中文输出""项目用 Python 3.11""部署在阿里云"。

这一层的危险在于过度提炼

EXTRACTION_PROMPT = """
从以下对话片段中提取**可以长期复用的稳定事实**。
 
严格标准:
1. 只提取在多个不同场景中反复出现或被明确强调的信息
2. 单次出现的偏好、临时决定、具体任务细节一律不提取
3. 每条事实必须能独立成立,不依赖上下文
4. 不确定是否稳定时,宁可不提取
 
输出 JSON 数组,每项含 fact、confidence(0-1)、evidence(引用的原话)。
 
对话片段:
{conversation}
"""

第 4 条是我踩坑后加的:不确定就不提取

早期版本没有这条约束,结果 Agent 把"帮我查一下今天的天气"提炼成了"用户关心天气",然后每次对话都主动汇报天气。这类噪音一旦进入语义记忆,会持续污染后续所有交互。

遗忘机制

这是最容易被忽略的部分。一个只会记不会忘的系统,最终会被自己的记忆淹死。

三个遗忘触发条件:

def should_forget(self, memory: Memory) -> bool:
    # 1. 时间衰减:长期未被引用且超过有效期
    if memory.last_accessed < now - memory.ttl:
        return True
 
    # 2. 被显式纠正:用户说过"不对"的相关记忆立即失效
    if any(memory.id in correction.targets for correction in recent_corrections):
        return True
 
    # 3. 冲突淘汰:与新提取的事实矛盾的旧事实
    if any(conflicts(memory, new_fact) for new_fact in recent_extractions):
        return True
 
    return False

第二条最重要:用户纠正必须立即生效

用户说"我从来没说过要用 TypeScript",系统还在下次对话里引用那条错误记忆——这是最伤信任的体验。宁可错删,不可错留。

一个反模式

最后提一个我见过的反模式:把所有记忆都做成向量检索。

结构化事实(用户偏好、项目配置、环境参数)用键值对查询又快又准,做成向量检索反而引入不确定性。向量检索只适合处理"模糊的、需要语义匹配的"记忆,也就是情景记忆。

用向量检索去查"用户用的是什么编程语言",就像用搜索引擎查自己昨天存在抽屉里的钥匙——能用,但没必要。

小结

记忆系统的核心矛盾是有限的上下文与无限的交互历史之间的冲突。

解法不是压缩,而是分层 + 主动遗忘。记住:一个设计良好的记忆系统,删除操作的次数应该和写入操作的次数在同一个量级。

相关阅读