智能体三层记忆加异步遗忘机制:跑完千轮还能精准记住你的底细


你给AI喂了一整年的聊天记录,结果它把你上周说的话和两年前的话混在一起回答你。这叫记忆吗?这叫垃圾堆。

AI的记忆不是存得多就叫好。存得越多,错得越离谱。一个Agent如果记住了你所有的废话和矛盾,它反而什么真话都说不出来了。真正的记忆工程,不是让AI记住一切,而是让它知道该忘掉什么。

人类靠忘事才能正常活着,AI的记忆系统也一样!

传统做AI记忆的人,思路特别简单粗暴——把所有对话往向量数据库里一扔,然后祈祷检索的时候能捞出有用的。这种做法在100条消息的时候还能蒙混过关,到了1000条就开始露馅,到了10000条就彻底完蛋。数据库里堆满了互相矛盾的陈年旧账,AI每次回答都像是从垃圾堆里翻东西,什么都翻得到就等于什么都翻不到。

记忆工程这个领域专门解决这个问题,它不管存多少,只管怎么存、怎么更新、怎么忘。

今天这篇东西会把记忆拆成三个层次,讲一条原始对话怎么变成一条可用的记忆,再告诉你为什么没有遗忘机制的记忆系统,用久了比没有记忆还可怕。

工作记忆、情景记忆、语义记忆:脑子不是一块硬盘

认知科学里有个分类法,出自普林斯顿和CMU合作的CoALA框架,后来Letta、Mem0、LangChain这些AI框架都照着这个来。AI的记忆不是一整块,而是三个层次,每个层次的存活时间和功能完全不一样。

工作记忆就是当下正在处理的这段上下文。它只活几秒钟,每次运行结束就清空,大小被模型的窗口长度死死卡住。模型真正能拿来推理的,只有这一小块。这就跟你脑子里正在算账的时候那几串数字一样,算完就扔。

情景记忆是一串按时间顺序发生的事。它记录的是具体场景里的具体事件,比如“用户要了一份报告,我生成了,他让我重写开头”。这条记录保留了顺序和上下文,不提炼、不概括,就是流水账。它的用处是让你在需要回顾某个具体事件的时候能找回来。

语义记忆是从具体事件里抽象出来的事实,跟发生的时间和场景彻底脱钩。不是说“周三那天用户说他不吃肉”,而是直接记住“用户不吃肉”。这是Agent关于这个世界和这个用户的一般性认知,持久、稳定、概括。

三个层次里最关键的分界线在于:模型只能在工作记忆里推理。情景和语义都是存着的,只有被拉进工作窗口的那一刻模型才能用到。记忆不是“把一切都塞进窗口”,记忆是“需要的时候能把对的拉进来”。有些人非要在三个层次之外再加一个程序性记忆,那是存技能和固定流程的,但这三个才是真正扛活的,所以只聊这三个。

一条记忆的命:形成、演化、检索三步,每一步都能搞砸

记忆不是一个静态的存储动作,而是一个完整的过程。每一条记录都要经历三个阶段,每个阶段都有自己独特的翻车方式。

形成阶段要干的事叫提取。从原始的对话流里把值得记住的东西抽出来,转成结构化的记录。不能把对话逐字逐句存下来,得提取事实。这个阶段最常见的死法是存了一堆噪音,什么鸡毛蒜皮都往里塞,整个记忆池子变成了一锅粥。

演化阶段负责整合和遗忘。记录不是一成不变的,相似的会合并,过时的要扔掉,矛盾的必须解决。“用户喜欢Python”和后来“用户换Rust了”不能同时存在,后者应该更新前者。这个阶段最可怕的失败是虚假记忆,矛盾没解决,Agent同时记住了互斥的两件事,回答的时候左右互搏。

检索阶段就是在正确的时刻把相关的记录拽进工作上下文。翻车的方式是拽错了或者什么都没拽到,搜出来的东西完全不相关,或者该出来的没出来。

三个阶段里,演化是扩展性的命门。它决定什么能活过时间的冲刷,它的质量直接决定了记忆系统是越用越聪明还是越用越像个垃圾堆。所以单独给它开一节。

什么该记什么该忘,以及为什么要异步搞

让记忆系统真正能用的两个工程思路。

第一个思路:整合必须异步执行,不能跟主推理走同一条路。很多人喜欢让模型自己管记忆,像MemGPT那样,模型自己调用函数来存和取。这条路走不远。模型花在管理记忆上的推理开销太大了,干正事的算力被挤占,而且经常出现一种尴尬情况——上下文窗口快爆了,模型还没来得及保存关键信息就被清空了。更好的做法是:面向用户的模型只管接收记忆作为输入,永远不需要思考记忆操作,而整合交给后台另一个模型,在对话结束后跑。记忆管理和主推理彻底解耦。

第二个思路:按重复触发来整合,不是每条都整合。这是直接照搬人类记忆的机制,RecMem这类方案就这么干的。一条孤立的、只出现一次的事件先扔在廉价缓冲区里。昂贵的LLM整合调用只在一种情况下触发——新进来的事件在缓冲区里找到了足够多的语义相似兄弟,也就是说,形成了一个簇。没簇就不调用昂贵的模型。

这个逻辑同时算了两笔账。经济账:LLM整合是记忆系统里最贵的操作,每条消息都触发一次成本扛不住。质量账:只说了一次的东西大概率是噪音,反复出现的模式才是值得长期记住的信号。重复触发这个门禁同时砍了成本和噪音,一刀两断。

python
import numpy as np
from dataclasses import dataclass, field

@dataclass
class MemoryBuffer:
    """原始事件的廉价缓冲区,存文本和嵌入向量,不调LLM。"""
    events: list = field(default_factory=list)     # (文本, 嵌入向量)

    def add(self, text: str, embedding: np.ndarray):
        self.events.append((text, embedding))

    def similar(self, embedding: np.ndarray, threshold: float = 0.82):
        """找出与新事件形成语义簇的那些已有事件。"""
        hits = []
        for text, emb in self.events:
            sim = float(np.dot(embedding, emb) /
                        (np.linalg.norm(embedding) * np.linalg.norm(emb)))
            if sim >= threshold:
                hits.append(text)
        return hits

def maybe_consolidate(buffer, new_text, new_emb, consolidate_fn,
                      cluster_min: int = 3):
    """只有新事件形成一个簇的时候,才调用昂贵的LLM整合。"""
    cluster = buffer.similar(new_emb)
    buffer.add(new_text, new_emb)
    if len(cluster) + 1 >= cluster_min:
        # 簇形成了,现在值得花LLM的算力了
        return consolidate_fn([new_text] + cluster)
    return None      # 孤立事件,留在缓冲区里,LLM碰都不碰

簇的门槛是个调节杆。cluster_min设低了,整合就激进,LLM调用多,记录多,噪音也多。设高了,整合就保守,省钱,但可能漏掉一个缓慢重复出现的模式。调这个值取决于你的consolidate_fn有多贵,以及输入流有多脏。

从簇里榨出两种记忆:情景摘要和语义事实

当一个簇攒够了,整合器要从这个簇里提取两种完全不同的东西——一段情景摘要和一堆语义事实。这是从情景层向语义层的跃迁。

python
CONSOLIDATE_SYSTEM = """你是一个记忆整合器。给你一组来自Agent历史记录的相似事件。
请分开提取两种东西。

1. 情景摘要:一段概括发生了什么的话,保留顺序和上下文。
2. 语义事实:一组现在普遍成立的持久事实,跟具体事件脱钩。
   只留下能活过本轮会话的东西。

如果这些事件跟之前的知识矛盾,在contradicts字段里明确标出来,
让演化层能去解决冲突。

严格按JSON格式回复:
{"episodic": "...", "semantic": ["...", "..."], "contradicts": ["..."]}"""

def consolidate(cluster_texts, ask):
    joined = "\n".join(f"- {t}" for t in cluster_texts)
    import json
    raw = ask(CONSOLIDATE_SYSTEM, f"事件簇:\n{joined}")
    s, e = raw.find("{"), raw.rfind("}") + 1
    return json.loads(raw[s:e])

在输出的时候把情景和语义分开,这不是走形式。情景回答的是“发生了什么”,语义回答的是“什么是对的”。它们在完全不同的场景下被检索——需要回顾过去的交互背景时用情景,需要知道关于用户或世界的一般性知识时用语义。把两者混在同一条记录里就丢失了这个差异,该取事实的时候给你吐出来一段流水账,该取流水账的时候给你一句干巴巴的结论。

contradicts字段是通向下一个问题的桥梁——新知识跟旧认知冲突的时候怎么办。

遗忘不是损失,不清除记忆才叫脑瘫

最被低估的一个环节。直觉告诉你记忆应该保留一切,因为丢掉有用的东西太可怕了。但从不忘事的记忆系统很快会变成废物,理由如下。

第一,矛盾堆积。用户说“我超爱Python”,六个月后说“我现在写Rust了”。两条事实如果永远共存,检索的时候全拽出来,Agent就会拿着过时的那条去做决策。这里的遗忘不是损失,是冲突解决——新的更新旧的。

第二,噪音会累积。就算有重复触发的门禁,总有一些记录回头看根本没屁用。没有衰减机制,它们就会稀释检索质量,就像额外的文本块稀释上下文窗口一样——完全一样的稀释效应,只不过发生在存储端。

机制就是“不用则衰,用则强化”。每条记录都有一个权重,随时间衰减,每当被检索到并且确实有用的时候就涨回去。权重掉到阈值以下的记录直接扔掉。

python
import time, math

@dataclass
class MemoryItem:
    text: str
    kind: str                       # "情景" 或 "语义"
    weight: float = 1.0
    last_used: float = field(default_factory=time.time)
    created: float = field(default_factory=time.time)

    def decayed_weight(self, half_life_days: float = 30.0) -> float:
        """根据上次使用以来的时间做指数衰减。"""
        age_days = (time.time() - self.last_used) / 86400
        return self.weight * math.exp(-age_days * math.log(2) / half_life_days)

    def reinforce(self, amount: float = 0.5):
        """被检索到并且有用:涨权重,刷新时间戳。"""
        self.weight = min(self.weight + amount, 5.0)
        self.last_used = time.time()

def forget_pass(items, min_weight: float = 0.15):
    """扔掉衰减后权重低于阈值的记录。语义事实的衰减比情景慢。"""
    survivors = []
    for it in items:
        hl = 90.0 if it.kind == "语义" else 21.0
        if it.decayed_weight(half_life_days=hl) >= min_weight:
            survivors.append(it)
    return survivors

注意不同层级的半衰期不一样。语义事实比如“用户吃素”应该活得久,半衰期90天。情景事件比如“上周我们修了bug X”过时得快,21天。这反映了两层的本质差异——概括性知识比具体事件更稳定。遗忘不是一视同仁的,它要尊重记忆的类型。

强化把闭环给接上了:一个事实如果频繁被检索并且确实有用,权重就涨,就不衰减。记忆自己打扫自己,留着真正在用的,扔掉占着茅坑不拉屎的。

检索不光看相似度,还得看新鲜度和死活

最后一步。把对的记录拽进工作上下文,不只是“余弦相似度取前K个”那么简单。好的检索要权衡三样东西:跟查询的语义接近程度、记录的衰减后权重(新鲜度加有用性)、以及当前任务需要什么类型的记忆。

python
def retrieve(items, query_emb, embed_of, k: int = 5,
             w_sim: float = 1.0, w_weight: float = 0.4):
    """按相似度和衰减后权重的组合来排序。"""
    scored = []
    for it in items:
        emb = embed_of(it)
        sim = float(np.dot(query_emb, emb) /
                    (np.linalg.norm(query_emb) * np.linalg.norm(emb)))
        score = w_sim * sim + w_weight * it.decayed_weight()
        scored.append((score, it))
    scored.sort(key=lambda x: x[0], reverse=True)
    top = [it for _, it in scored[:k]]
    for it in top:
        it.reinforce()      # 被拽出来了,强化它
    return top

两个细节。排序用组合分数而不是只看相似度——一条记录可能在语义上很接近,但太老了而且从来没被用过,衰减后的权重就会把它拉下来。另一个是访问即强化——记录被检索到这一事实本身就让它权重上涨,实现了“有用就强化”的回路。检索和遗忘不是两个独立的系统,而是一体两面——一个往外拉,一个往里清,两个都喂同一个权重。

这里跟上下文工程也有直接关联——少拽几条,k设成5,因为拽太多进上下文就是在稀释它,窗口中间的盲区会把多余的吞掉。记忆是给上下文喂料的,上下文是稀缺资源,检索必须贪婪地抢相关性最高的。

总结

别把记忆做成向量垃圾堆。

三层的结构配上生命周期管理才是正路。

工作记忆管当下,情景记忆管发生过什么,语义记忆管一般性事实,模型只在第一层上推理,后两层随用随取。

一条记录要经过形成、演化和检索,整合是瓶颈——异步搞,按重复触发搞,别每条消息都搞。遗忘不是损失,而是可信的前提——不用则衰、用则强化,让记忆自己保持干净,情景和语义用不同的半衰期。

天真的记忆什么都存,等于什么都没存。工程化的记忆存得少但存得对,而且知道怎么忘掉过期的。
两者之间的差距就是:一个Agent在一千条消息之后淹死在自己的历史里,另一个在一千条消息之后还记得关于你的一切要紧事。
你选哪个?

原文期刊 / X(原Twitter)@h100envy / 2026年8月2日 / Memory Engineering: Giving an Agent a Memory You Can Trust / 独立研究者