记忆工程:让 agent 的记忆越用越聪明,而不是越用越臃肿

以下是正文
人们很容易把 agent 记忆想成一个数据库:把 agent 见过的一切倒进去,以后再捞出来。这个定义本身就是第一个错误。什么都存的 agent 不会变聪明,它只会变慢,并且开始捞回过期的垃圾,污染自己的推理。真正管用的记忆是反过来建的:它会遗忘、会压缩、会把事件变成教训。agent 变聪明靠的不是存得更多,而是把存下来的东西巩固(consolidate)成知识。
到 2026 年,业界收敛到了一套来自认知科学的分类法,它在 LLM 上的正式化版本是普林斯顿的 CoALA 框架。agent 记忆是四种不同的机制,不是一个数据库;把它们混为一谈,系统会坏,而且坏法是可以预料的。这篇讲的是:记忆由什么构成、遗忘怎么运作、为什么巩固才是主机制、以及记忆在哪里变成攻击面。讲代码和机理,不做厂商巡礼。
四类记忆,以及它们为什么不能混
agent 记忆不是一层,是四种机制,各有各的存活时长、各自装不同的东西、检索方式也不一样。
工作记忆(working memory)。 就是 context window,模型唯一能直接在上面推理的记忆。其它一切都必须被检索进来,才能影响输出。生命周期只有一次推理。
情景记忆(episodic memory)。 绑定在某个时刻上的具体过往事件:发生了什么、什么时候、在什么上下文里。它保留细节,是 agent 的原始经历。
语义记忆(semantic memory)。 从若干情景里抽出来的一般化知识:事实、规则、偏好,不绑定任何具体个案。这是 agent 理解到的东西,不是它遇到的东西。
程序性记忆(procedural memory)。 技能和做事方式:怎么执行一项任务,而不是关于这项任务知道些什么。在 coding agent 里,它活在 CLAUDE.md 这类文件里,工作流程就住在那儿。
四类记忆各有存活时长和检索方式;把「事件」和「教训」混在一起,是这一层最常见的塌陷。
关键的分界线在情景和语义之间。一条情景是「2 月 14 日,X 股那笔交易栽在财报上」。一条语义是「财报前波动率会上升,进场要更谨慎」。前者是事件,后者是从许多事件里抽出来的教训。只存情景的 agent,什么都记得,什么都不理解。从前者走到后者,记忆才开始起作用。
不同类型需要不同的存储后端,这是架构决策,不是细节。情景记忆适合放在向量加图的混合结构上,因为事件既要按意思找,也要按彼此之间的关联找。语义记忆适合图或键值存储,事实要被精确地放住。程序性记忆就活在文件里。给同一类记忆混搭后端,或者把所有东西塞进同一个向量索引,等于把每一层各自赖以成立的东西全丢掉。
四类记忆的后端、长处与短板。把它们全塞进一个向量索引,就等于同时接受四种短板。
巩固:情景怎么变成知识
巩固是把情景记忆变成语义记忆的机制:把相似的事件聚成簇,从簇里提炼出一条一般化的教训。按 2026 年 2 月关于情景记忆的那批工作,让 agent 具备长期推理能力的是巩固,不是存储量。
形式上是两步:把相关的情景分组,再把每组概括成一条紧凑的规则。一个天真的系统会存下一千条情景,然后一条条捞。一个会巩固的系统会把这一千条折成十几条带适用条件的规则,只留规则,把情景送进冷存档。
支撑数不够的簇不会升格成规则 —— 一个案例不是规律,是噪声。
def consolidate(episodes: list[Episode], min_cluster: int = 5) -> list[SemanticRule]:
clusters = cluster_by_similarity(episodes) # 把相似事件分组
rules = []
for cluster in clusters:
if len(cluster) < min_cluster:
continue # 单个案例不构成一般化:那是噪声
rule = summarize_to_rule(cluster) # 从这一组里提炼出规则
rule.support = len(cluster) # 它压着多少条情景
rule.condition = extract_condition(cluster) # 在什么条件下成立
rule.observed_range = date_span(cluster) # 观测时间跨度
rules.append(rule)
return rules
这里的 min_cluster 阈值是根本性的,它挡住的是巩固环节最主要的错误:拿单个案例做一般化。一次亏损不是规律,只是一条情景。从一个事件里推出来的规则,是在对噪声过拟合,和回测里那种过拟合一模一样。一条规则只有压着足够多条互相独立的情景,才配存在;而且支撑数必须和规则一起存下来,以后才好据此加权决定信它几分。
巩固不必跑在热路径上——也就是用户正盯着等结果的那条在线请求链路。更划算的模式是在空闲时段、会话之间跑后台巩固,类比大脑在睡眠中固化记忆。让一个独立的子 agent 在后台读积攒下来的情景,趁主 agent 不干活的时候更新语义层。这样热路径保持快,知识在离线时慢慢累。
遗忘是设计,不是缺陷
这里是最主要的一次心智翻转,而且反直觉:遗忘不是记忆的缺陷,是它的功能。 一个什么都永久存着的系统会线性膨胀、检索变慢,并且不断攒下过期材料——这些材料迟早会浮上来,污染推理。生物记忆是有意识地遗忘的,人造记忆也必须如此。
遗忘不该靠定时删除来做,而该靠受控的可及性衰减。策略有好几种,而且可以叠加使用。
时间衰减。 一条记录的可及性随年龄沿一条遗忘曲线下降,指数或 Weibull 形式。旧的东西不是被突然删掉,而是在检索时的权重变轻。
按重要性加权的遗忘。 遗忘概率与记录的重要性成反比。同样的年龄,关键的比日常的留得久。
间隔强化。 被访问过很多次的记录留得更久。常用的知识不该仅仅因为年头长就掉出去。
预算淘汰。 预算溢出时淘汰价值最低的,价值由新鲜度、重要性、访问频次共同决定。
定时删除是一刀切到零;受控衰减让权重慢慢降,被用到就抬回来。
def retention_weight(mem, as_of: date) -> float:
age = (as_of - mem.last_access).days
# 遗忘曲线:指数衰减,再由重要性和使用频次调制
decay = 0.5 ** (age / mem.half_life_days)
reinforcement = min(1.0, mem.access_count / 10) # 常用的记录留得更久
return mem.importance * decay * (0.5 + 0.5 * reinforcement)
def evict_to_budget(memories: list, budget_tokens: int, as_of: date) -> list:
ranked = sorted(memories, key=lambda m: retention_weight(m, as_of), reverse=True)
kept, used = [], 0
for m in ranked:
if used + m.tokens <= budget_tokens:
kept.append(m); used += m.tokens
else:
archive(m) # 不是删除,是送进冷存档
return kept
注意被淘汰的记录不是删掉,而是移进冷存档。这个区分很要紧:硬删除会永久丢数据,而可及性衰减只是把记录挪出热检索,万一哪天真需要,还能捞回来。有设计的遗忘管的是可及性,不是销毁。
实际效果是可度量的:受生物学启发的遗忘方案能明显压低存储量,量级在四成以上,同时还能提升多跳推理的准确率——因为模型不再淹没在一堆不相干的旧记录里。
冲突与陈旧:记忆为什么会随时间撒谎
事实发生变化时,会冒出另一类问题。用户换了工作,公司换了战略,市场进入了另一种行情。天真的记忆会把旧记录和新记录一起存着,检索时两条都返回,没有任何裁决机制。这叫冲突盲区(conflict blindness);没有主动的冲突消解,陈旧事实会不断堆积,开始污染推理。
冲突消解不等于自动挑更新的那条,因为更新的不一定更真。它要按来源可靠度、新鲜度、以及与其余知识的一致性来加权。相互矛盾的证据应当把权重从旧值上挪走,而不是并排放着。
def resolve_conflict(existing: Memory, incoming: Memory) -> Memory:
# 更新的不代表更真:按可靠度和一致性加权
if incoming.contradicts(existing):
if incoming.reliability > existing.reliability:
existing.confidence *= 0.5 # 旧的被降权,而不是当场抹掉
return incoming.with_provenance(supersedes=existing.id)
else:
incoming.confidence *= 0.5 # 不可靠的新记录不许覆盖可靠的旧记录
return merge(existing, incoming)
关键在于旧记录不是立刻被抹掉,而是被降权。如果新证据后来被证伪,旧的还能恢复。而且每一次取代都要留下 provenance(来源溯源:这条记录从哪来、什么时候来、经由哪次交互来)——哪条替换了哪条、依据是什么。在受监管的行业里,这不是锦上添花,是硬要求:你必须能解释 agent 为什么相信它现在相信的东西。
记忆作为攻击面
这是几乎所有讲记忆的文章都不碰、但在 2026 年变得要命的一块。一旦 agent 会存记忆、会复用记忆,这份记忆就成了攻击载体。OWASP 已经把记忆投毒收进了 agentic 系统的威胁清单,编号 ASI06。
攻击机理很简单,正因如此才危险。攻击者通过普通的交互,在 agent 的记忆里种下一条假记录,这条记录之后会影响决策。已有研究过的变体:只靠提问就完成注入,成功率很高;还有潜伏投毒(sleeper poisoning)——有害记录在这一次会话里种下,在下一次会话里才激活。后者尤其阴险,因为种下和激活之间隔着时间,来源被掩盖了。
防守要分几层来搭,任何一层单独都不够。
来源信任打分。 每条记录都带一个来源可靠度分数,来自不可靠来源的记录没有资格覆盖可靠的知识——和上面的冲突消解同理。
每条记录都要有 provenance。 记录从哪儿来、什么时候来、来自哪次交互。来源追不回去的记录,按定义就是可疑的。
推理时做共识检查。 投毒能靠分歧暴露出来:如果某条推理路径依赖的那条记录和另外好几条相矛盾,这条记录就有嫌疑。
按来源封顶影响力。 来自低信任渠道的记录不能影响高风险决策,哪怕它技术上确实进了记忆。
由此可以得出一个重要结论:纯粹靠向量库做检索,结构上既不会遗忘、也无法防守,因为它只会捞出相似的,不会评估这个相似的是从哪来的、有没有过期。记忆变成攻击面,就要求模型成为自己记忆的主动策展者,而不是检索返回什么就照单全收的被动消费者。
落到代码层面,防守是两道闸:写入闸和使用闸。来自不可靠来源的记录不能悄悄和已验证的记录躺在一起;在为重要决策做检索时,低于信任下限的记录要被切掉。
TRUST = {"verified_db": 1.0, "internal_tool": 0.8, "user_input": 0.5, "web_scrape": 0.3}
def admit(record, existing: list) -> bool:
record.trust = TRUST.get(record.source, 0.2)
# 共识检查:与多条可靠记录相矛盾的记录,有嫌疑
contradictions = [m for m in existing
if m.contradicts(record) and m.trust >= 0.8]
if contradictions and record.trust < 0.8:
quarantine(record, reason="与可靠共识相矛盾")
return False
return True
def use_for_decision(memories: list, risk: str) -> list:
# 决策风险越高,信任下限越高
floor = {"low": 0.3, "medium": 0.5, "high": 0.8}[risk]
return [m for m in memories if m.trust >= floor]
关键在于信任下限随决策风险浮动。来自用户输入的记录,拿来写一份草稿笔记没问题;但要决定动一笔钱、或者拒掉一个客户,就只准高信任来源进场。潜伏投毒恰恰断在这道闸上:一条来自低信任渠道的有害记录,物理上够不着高风险决策,不管种下和激活之间隔了多少次会话。
程序性记忆:最被低估的一层
四类里,程序性记忆是工具生态最不成熟的一层——而这恰恰是它对愿意认真设计它的人杠杆最大的原因。语义记忆让 agent 知道得更多,程序性记忆让 agent 活干得更好。
程序性记忆是 agent 怎么执行一项任务,而不是它关于这项任务知道什么。在 coding agent 里,它以声明式的形态存在,就住在 CLAUDE.md 或 AGENTS.md 这类会被自动注入 context 的文件里。这是一种轻量、可维护的程序性记忆:把工作流程、约定、验证过的动作序列写成文字,当作行动指令交给 agent。
这一层的威力在于它闭合了自我改进的回路。agent 执行了一项任务,从中提炼出「下次怎么做得更好」的教训,把这条教训写进程序性记忆,下一次再被取出来用。这就是 evolving-context(让上下文自己进化)的思路:generator 产出结果,reflector 找出错误和疏漏,curator 提炼教训并更新程序性 playbook。按相关研究,这个回路能在 agent benchmark 上带来大约一成的提升,全程不需要对模型做任何 fine-tuning,纯粹靠程序性记忆正确地积累。
def self_improve(task, playbook: list[str]) -> list[str]:
# GENERATOR:带着当前 playbook 执行任务
result = generator.run(task, context=playbook)
# REFLECTOR:评估结果,找出错误和疏漏
critique = reflector.run(task, result)
if critique.is_clean:
return playbook # 没什么可学的,playbook 原样保留
# CURATOR:把发现的错误变成一条程序性教训
lesson = curator.extract_lesson(critique)
# 去重:不要繁殖出一堆近义条目(这是防上下文侵蚀的关键)
if not any(similar(lesson, p) for p in playbook):
playbook.append(lesson) # 只追加一条,不重写全文
return playbook[-MAX_PLAYBOOK:] # 让 playbook 保持有界
有两个细节,决定了这个回路是持续变好还是持续退化。去重防止 playbook 被一堆几乎一样的教训撑爆——reflector 一次次抓到同一类错误时,它们就会这样堆起来。只追加一条、而不是重写整份 playbook,防的是上下文侵蚀本身:重写会把条件和例外洗掉。教训要作为单独一行、连同适用条件一起加进去,而不是熔进正文——熔进去,下一次压缩就把它抹了。
语义教训和程序性教训的差别很细微,但很重要。语义教训是「这个库在大数组上很慢」。程序性教训是「碰到大数组,一开始就用那个库、按那个调用方式写」。前者提供信息,后者改变动作。程序性记忆是 agent 表现真正复利的地方,也是厂商给的现成东西最少的地方——所以只能你自己动手设计。
搭建顺序
把 agent 记忆装起来的技术顺序是这样的:先把四类记忆分进不同的存储、给不同的存活时长;然后搭起从情景到语义的巩固,带上支撑数阈值;然后是有设计的遗忘,做成受控的可及性衰减而不是删除;然后是冲突消解,靠降权加 provenance;然后是投毒防御,靠来源信任和共识检查;程序性记忆则单独作为一条自我改进回路来做。
每个机制各自封掉一类问题:分类型让事件不和教训混在一起,巩固把体量变成知识,遗忘让系统保持快和干净,冲突消解让陈旧事实不再污染推理,投毒防御守住信任边界,程序性记忆积累手艺。这样装起来的记忆会随时间变聪明;装成一个什么都倒进去的垃圾场的记忆,只会随时间变臃肿、并且开始撒谎。
原文:Memory Engineering: Designing Agent Memory That Gets Smarter Instead of Bloating,作者 @zostaff。中文编译与配图本地化:Yaqin Hei(非原创)。
Subscribe for updates
Get the latest AI engineering posts delivered to your inbox.



