【LangGraph实战】《LangGraph实战》_103.[第5章 记忆系统] 记忆衰减与遗忘机制:让Agent像人一样管理记忆

别让Agent变成"记忆黑洞"!LangGraph记忆衰减实战:教你给AI装上"人脑遗忘曲线",让Agent从"背锅侠"进化为"智慧体",彻底告别Token爆炸和上下文污染!
本文目录:
- 要点1:Agent不是垃圾桶——为什么必须主动遗忘
- 要点2:时间衰减算法——让记忆自然"褪色"
- 要点3:重要性评分机制——核心记忆vs闲聊碎片
- 要点4:Token预算下的记忆裁剪——当LLM的"脑容量"满了
- 要点5:分层记忆架构——像人脑一样分区管理
- 要点6:LangGraph实战——构建一个会"忘事"的Agent
嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《LangChain核心技术与LLM项目实践》
好记性不如烂笔头,但烂笔头要是从不撕页,迟早变成垃圾场。咱们程序员写代码,内存满了都知道清缓存,Redis过期了都知道设TTL,数据库满了都知道做归档,可一到做Agent,很多人突然就"失忆"了——不是忘了写代码,而是忘了给Agent设计"遗忘机制"。你是不是也这样?总觉得Agent记忆越多越好,生怕漏掉任何上下文,结果呢?Token费用像坐了火箭,模型响应越来越慢,更惨的是,Agent被一堆陈年旧信息干扰,开始胡言乱语,仿佛一个背着几百斤行李还让他跑马拉松的选手。今天咱们就来聊聊,怎么让Agent像人一样,该记的记,该忘的忘。
要点1:Agent不是垃圾桶——为什么必须主动遗忘
你有没有见过那种从不删聊天记录的手机?三年前的广告、去年的验证码、前天的快递通知全堆在一起,想找个人名得翻半天。很多新手做的Agent,State就是这部手机。
痛点分析
新手最容易犯的错,就是把State当成无限容量的硬盘。对话历史无限append,工具调用结果全量保留,观察日志一股脑塞进去。最后State像滚雪球一样膨胀,后果简直灾难现场。
第一,API费用爆表。Token是按量计费的,你每往上下文里多塞一个字,钱包就抖一下。跑了二十轮对话,Token从2k飙到30k,这谁顶得住?
第二,上下文窗口被占满。模型不是神仙,它的"注意力"是有限的。当上下文里塞满了无关信息,模型就会抓不住重点,就像你在嘈杂的菜市场里想听清朋友说话一样困难。
第三,旧信息干扰新决策。这是最致命的。三天前用户说"我喜欢吃辣",昨天用户纠正说"我感冒了,最近吃清淡点"。如果两条信息同时躺在State里,模型可能会精神分裂,给你推荐一整页麻辣火锅——这Agent不就成了"背锅侠"吗?
来看看典型的错误示范:
# 错误做法:数字囤积症
def chat_node(state):
# 只进不出,永不清理
state["messages"].append(HumanMessage(content=user_input))
response = llm.invoke(state["messages"]) # 把整个历史全丢给模型
state["messages"].append(AIMessage(content=response))
return state
这代码看着简单,跑起来就是一颗定时炸弹。二十轮之后,上下文里可能有上万Token,其中80%都是早该扔掉的老黄历。
解决方案
记住,会遗忘才是智慧的开始。我们要给Agent建立"保留策略",而不是无差别囤积。
核心思路很简单:分舱管理。最近N轮对话,保留原文,这是当前交流的"现场";超过N轮的历史,生成一段摘要,压缩成"档案";至于那些无关痛痒的信息,直接丢弃,不要心疼。
来看看修正后的思路:
def chat_node(state):
state["messages"].append(HumanMessage(content=user_input))
# 保留策略:超过10轮就摘要,把旧对话压进summary
if len(state["messages"]) > 10:
old_msgs = state["messages"][:5]
# 用LLM把旧消息压缩成摘要
state["summary"] = summarize(old_msgs)
# 只保留近期消息,释放Token压力
state["messages"] = state["messages"][5:]
# 组装上下文:摘要 + 近期消息
context = []
if state.get("summary"):
context.append(SystemMessage(content=f"历史摘要:{state['summary']}"))
context.extend(state["messages"])
response = llm.invoke(context)
state["messages"].append(AIMessage(content=response))
return state
这样做的好处立竿见影。Token消耗被控制在稳定区间,模型始终聚焦在最近几轮对话上,不会因为远古信息而分心。更重要的是,Agent开始有了"时间感",知道什么是最新的、什么是过去的。
小结
遗忘不是缺陷,而是智能的筛选器。别让Agent的State变成垃圾填埋场,学会主动清理,Agent才能轻装上阵。
要点2:时间衰减算法——让记忆自然"褪色"
人脑不是硬盘,记忆是有"半衰期"的。德国心理学家艾宾浩斯早就发现,20分钟后我们会忘记42%的内容,一天后忘记74%。Agent的记忆也该如此,不能一旦写入就永久全量生效。
痛点分析
新手给记忆往往只有0和1两种状态:要么有,要么无。没有中间态,没有"褪色"过程。这会导致什么后果?信息已经过时了,但Agent还把它当成新鲜热乎的真理来用。
举个真实的坑:用户上周一说"我在北京出差",Agent认真记下了。这周五用户已经回上海了,但那条"出差"消息在State里躺了四天,权重没有任何衰减,和新消息一样被模型看到。结果呢?Agent继续推荐北京的餐厅和会议地点,用户气得想砸键盘:“我明明都回来了!”
来看看错误的做法:
class Memory:
def __init__(self, content):
self.content = content
self.created_at = time.time()
# 完蛋!没有衰减方法,一旦创建,永恒满分
def get_weight(self):
return 1.0
这种"永生记忆"在真实世界里是灾难。昨天的"最优解",可能是今天的"毒药"。用户的喜好会变,环境会变,任务目标会变,Agent如果感知不到这种变化,就会越做越错。
解决方案
我们要给每条记忆加上时间戳和衰减函数。最常用的是指数衰减,公式很简单:
当前分数 = 初始分数 × e^(-衰减率 × 经过时间)
老照片会随着时间泛黄,记忆也会随着 time_delta 逐渐失去影响力。来看代码实现:
import math
from datetime import datetime
class DecayingMemory:
def __init__(self, content, initial_score=1.0, decay_rate=0.05):
self.content = content
self.initial_score = initial_score
self.decay_rate = decay_rate # 每小时衰减系数
self.created_at = datetime.now()
def current_score(self):
# 计算经过了多少小时
hours = (datetime.now() - self.created_at).total_seconds() / 3600
return self.initial_score * math.exp(-self.decay_rate * hours)
def is_forgotten(self, threshold=0.2):
return self.current_score() < threshold
# 在LangGraph中作为记忆管理节点
def decay_node(state):
# 让所有记忆衰减一次
for mem in state["memories"]:
# 这里只读取,实际衰减在属性计算中完成
pass
# 过滤掉已经遗忘的记忆
state["memories"] = [
m for m in state["memories"]
if not m.is_forgotten()
]
return state
这个设计的妙处在于,记忆不是突然消失的,而是慢慢淡出视野。就像你真的在忘记一件事一样,今天还能想起个大概,下周只剩模糊印象,下个月彻底想不起来。Agent对世界变化变得敏感了,旧信息自然退场,新信息才有舞台。
小结
时间是最好的策展人。给记忆加上时间的刻度,Agent才能分清"现在"和"过去",做出贴合当下的决策。
要点3:重要性评分机制——核心记忆vs闲聊碎片
你翻聊天记录就会发现,里面80%都是"嗯嗯"、“好的”、“哈哈哈哈”、“确实”。这些是噪声。真正重要的,是那20%的关键信息,比如"我对花生过敏"、“请用Python写”、“周五之前交付”。Agent必须学会区分,不能一视同仁。
痛点分析
新手不做区分,一股脑全存。结果长期记忆里塞满了"你好"、“谢谢”,真正重要的"我对青霉素过敏"反而被淹没在垃圾山里。这不仅仅是效率问题,这可能是安全问题。
想象一下,一个医疗辅助Agent,用户明确说过"我对青霉素严重过敏"。但这条信息被淹没在一堆"今天天气真好"、"吃了吗"的闲聊里。当医生Agent询问病史时,模型没看到这条关键信息,然后开出了含青霉素的处方——这锅谁背?Agent背,但你作为开发者,能睡得着吗?
错误的做法长这样:
def save_memory(state, message):
# 全部保存,不分轻重,一碗水端平
state["long_term_memories"].append(message)
另一个思维误区是:“LLM很强大,它能自己从上下文里分辨重点。” 兄弟,LLM的注意力也是有限的。你给它的上下文里如果塞满垃圾,金子也会被埋没。这就像把 needle 扔进了 haystack,还指望别人一眼找到。
解决方案
给记忆打重要性分数。让Agent学会"断舍离"。
我们可以设计一套评分规则:
- 10分:涉及安全、健康、关键指令(如过敏史、密码规则、硬性约束)
- 5分:一般性事实(如职业、兴趣、常用工具)
- 1-2分:寒暄、语气词、临时性内容(如"哈哈"、“ok”、“今天挺热的”)
实现方式有两种。一种是启发式规则,快速轻量:
def score_importance(content: str) -> int:
# 关键信息关键词
critical = ["过敏", "密码", "禁止", "必须", "偏好", "严禁", "只能"]
if any(k in content for k in critical):
return 10
# 语气词和短句通常不重要
if len(content) < 5 or content in ["好的", "嗯", "哈哈", "ok"]:
return 1
return 5
def memory_filter_node(state):
new_msgs = state.get("new_messages", [])
for msg in new_msgs:
score = score_importance(msg.content)
if score >= 8:
# 关键信息进长期记忆,永久保留(直到衰减)
state["critical_memories"].append(msg)
elif score >= 4:
# 一般信息进短期记忆
state["general_memories"].append(msg)
else:
# 闲聊?直接丢弃,不心疼!
pass
return state
另一种更精准的做法,是让一个小型LLM或分类器来给记忆打分。虽然多一次调用,但区分度更高。对于关键业务,这点成本值得花。
这样做之后,Agent终于学会了抓重点。关键时刻,它能回忆起你的过敏史;闲聊时刻,它不会把"今天天气不错"当成金科玉律。
小结
智能的本质是抓重点。给记忆分出三六九等,Agent才知道什么值得刻进脑子里,什么该随风而去。
要点4:Token预算下的记忆裁剪——当LLM的"脑容量"满了
LLM的上下文窗口就像人脑的工作记忆,容量是有限的。哪怕你用的是128k的模型,那也不是让你拿来浪费的。不做预算管理的Agent,就像一个没有财务观念的月光族,月初大手大脚,月末吃土喝风。
痛点分析
新手直接把所有历史消息塞进messages列表,直到触发ContextLengthError才傻眼。或者更可怕的是,用了最粗暴的"截断"——直接丢掉最旧的消息或最新的消息,导致信息链断裂。
我看过一个真实案例:一个Agent在总结一份长文档,State里积累了50k Token的messages。直接传给GPT-4,啪,报错。新手一慌,直接截断前25k,保留后25k。结果呢?文档前半部分是背景定义和术语解释,后半部分是结论和数据分析。没有背景,结论成了天书,模型输出完全跑偏。
错误的做法:
# 粗暴截断,不管语义完整性
messages = messages[-10:] # 只保留最后10条,不管多长
另一个误区是觉得"摘要能代替一切"。摘要确实能压缩信息,但过度摘要会丢失细节。比如代码里的参数、精确的数字、用户的原话,这些一旦摘要了,可能就失真了。
解决方案
我们要实施严格的Token预算管理。总预算是固定的,各个模块要量入为出。
预算分配策略可以这样设计:
- 系统提示:固定开销,必须保留
- 长期记忆:按重要性排序,保留Top-K(高分记忆优先)
- 短期历史:如果还超预算,先做摘要,再压缩
- 最新对话:必须保留,这是当前任务的上下文
来看一个基于tiktoken的裁剪实现:
import tiktoken
def trim_to_budget(state, model="gpt-4", max_tokens=8000):
enc = tiktoken.encoding_for_model(model)
messages = state["messages"]
# 计算当前总Token数
total = sum(len(enc.encode(m.content)) for m in messages)
# 如果超预算,从旧的、不重要的消息开始处理
while total > max_tokens and len(messages) > 2:
# 保留最新的两条(当前轮对话),其余作为候选
candidates = messages[:-2]
if not candidates:
break
# 找到最旧且重要性最低的
target = min(candidates, key=lambda m: m.metadata.get("importance", 5))
messages.remove(target)
# 重新计算
total = sum(len(enc.encode(m.content)) for m in messages)
state["messages"] = messages
return state
更高级的做法是"分层压缩":先尝试摘要最旧的低分记忆,如果还超,再丢弃;而不是一刀切。就像整理行李箱,不是扔掉衣服,而是把厚的压成真空袋,把不穿的拿出来。
小结
量入为出不仅是理财之道,也是Agent管理记忆的生存法则。在有限的Token预算里最大化信息量,这才是高手该干的事。
要点5:分层记忆架构——像人脑一样分区管理
人脑不是一个大杂烩,而是高度分区的。海马体管短期记忆,大脑皮层管长期记忆,前额叶管工作记忆。各干各的,互不干扰。Agent的State如果只有一个大列表,那就是在"一锅乱炖"。
痛点分析
所有记忆存在一个list里,是当前新手最常见的设计。后果是什么?任务上下文、用户画像、工具调用结果、历史对话,全搅在一起。上一个任务的信息干扰下一个任务,Agent仿佛得了"注意力涣散症"。
看个离谱的例子:一个Agent同时接到两个任务线索。任务A是"写一段Python爬虫代码",任务B是"帮我订一份外卖"。结果State里代码片段和菜单混在一起。当Agent执行代码任务时,上下文里突然冒出"宫保鸡丁多少钱";当它执行订餐任务时,又看到"import requests"。Agent直接精神分裂,输出的代码里混着菜品名称,订餐请求里带着HTTP状态码。
错误的做法:
state = {
"messages": [
# 代码、对话、工具结果、观察日志全在一起...
]
}
这种"大一统"的管理方式,看似简单,实则是简单到混乱。没有分层,就没有边界;没有边界,就没有清晰的读取和遗忘策略。
解决方案
我们要设计三层架构,各司其职:
1. 工作记忆(Working Memory)
只存当前任务所需的上下文。生命周期极短,可能只有一个Node或一个Task。任务完成,直接清空。就像你心算数学题时,脑子里只保留当前步骤的数字。
2. 短期记忆(Short-term Memory)
存当前会话的近期对话。采用滑动窗口 + 摘要策略。会话结束,要么清空,要么压缩后进长期记忆。就像你今天和同事讨论的需求,下班后可能只记得结论。
3. 长期记忆(Long-term Memory)
存用户画像、关键事实、跨会话学到的知识。持久化存储,但内部有衰减和压缩机制。就像你记得同事的名字和喜好,但可能忘了三年前他提过的某次会议细节。
来看代码结构:
class AgentState(TypedDict):
working_memory: List[BaseMessage] # 仅当前任务
short_term_memory: List[BaseMessage] # 本会话
long_term_memory: List[ScoredMemory] # 跨会话,带分数
summary: str # 压缩后的历史摘要
不同层有不同的遗忘策略:
- 工作记忆:任务结束即焚,或覆盖。
- 短期记忆:超窗裁剪,会话结束摘要。
- 长期记忆:时间衰减 + 重要性评分,定期压缩。
当任务切换时,工作记忆被清空重建,只从长期记忆里加载相关背景。Agent终于能像人一样,一边写代码一边不用惦记昨天聊了什么天气。
小结
分区管理,各司其职,是Agent从单细胞生物进化到多细胞智能体的关键一步。大脑不乱,决策才不乱。
要点6:LangGraph实战——构建一个会"忘事"的Agent
理论千万条,落地第一条。看了这么多道理,现在咱们在LangGraph里把这些遗忘机制串起来,写一版真正能跑的代码。
痛点分析
很多小伙伴看完理论后,最大的困惑是:我到底该在哪个Node里做遗忘?在Entry节点里清理?还没处理输入,上下文先没了。在Exit节点里清理?中间节点已经因为Token超限崩了。在LLM调用里直接改全局State?LangGraph的State是函数式的,乱改可能触发不了检查点,还会污染逻辑。
错误的做法:
# 错的!在chat_node里直接粗暴截断
def chat_node(state):
# 这会丢失关键上下文,且只处理了messages,没管其他记忆
state["messages"] = state["messages"][-5:]
response = llm.invoke(state["messages"])
return {"messages": [AIMessage(content=response)]}
这种做法就像为了省电直接拔电源——确实省Token了,但Agent也"死机"了。遗忘机制不是事后补救,而是架构中的核心组件。
解决方案
在LangGraph中,正确的姿势是建立一个独立的记忆管家节点(memory_manager),通过条件边在每一轮对话结束后执行遗忘逻辑。这样,对话节点只管对话,记忆节点只管整理,职责分离,干净漂亮。
来看完整的架构实现:
from typing import TypedDict, List, Annotated
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage, AIMessage
import operator
import math
from datetime import datetime
class ScoredMemory:
"""带分数和衰减能力的记忆"""
def __init__(self, content, score=1.0):
self.content = content
self.score = score
self.created_at = datetime.now()
def decay(self):
"""执行时间衰减"""
hours = (datetime.now() - self.created_at).total_seconds() / 3600
self.score *= math.exp(-0.05 * hours)
return self
class AgentState(TypedDict):
messages: Annotated[List[BaseMessage], operator.add]
long_term_memories: List[ScoredMemory]
summary: str
def chat_node(state: AgentState):
"""主对话节点:负责组装上下文和生成回复"""
context = []
# 1. 加载历史摘要
if state.get("summary"):
context.append(SystemMessage(content=f"历史摘要:{state['summary']}"))
# 2. 加载高分长期记忆(只取分数>0.5的)
relevant_mems = [m for m in state["long_term_memories"] if m.score > 0.5]
for mem in relevant_mems:
context.append(SystemMessage(content=f"关键记忆:{mem.content}"))
# 3. 加载近期对话(最近3轮,6条消息)
context.extend(state["messages"][-6:])
# 4. 调用LLM
response = llm.invoke(context)
return {"messages": [AIMessage(content=response)]}
def memory_manager_node(state: AgentState):
"""记忆管家节点:负责衰减、裁剪、摘要"""
# 1. 长期记忆时间衰减
for mem in state["long_term_memories"]:
mem.decay()
# 2. 遗忘低分记忆(低于0.3的彻底删除)
state["long_term_memories"] = [
m for m in state["long_term_memories"] if m.score > 0.3
]
# 3. 消息历史摘要与裁剪
msgs = state["messages"]
if len(msgs) > 12:
# 把最旧的6条消息生成摘要
old_msgs = msgs[:-12]
new_summary = summarize_messages(old_msgs)
state["summary"] = (state["summary"] or "") + "\n" + new_summary
# 只保留最近12条
state["messages"] = msgs[-12:]
# 4. 从AI回复中提取新记忆(简单规则示例)
if msgs:
last_ai = msgs[-1].content
if any(k in last_ai for k in ["喜欢", "过敏", "讨厌", "偏好"]):
state["long_term_memories"].append(
ScoredMemory(content=last_ai, score=0.9)
)
return state
def should_continue(state):
"""简单路由:消息数没爆炸就继续"""
return "continue" if len(state["messages"]) < 100 else "end"
# 构建图
builder = StateGraph(AgentState)
builder.add_node("chat", chat_node)
builder.add_node("memory_manager", memory_manager_node)
# 对话后必须整理记忆
builder.add_edge("chat", "memory_manager")
# 记忆整理完成后,决定是继续对话还是结束
builder.add_conditional_edges(
"memory_manager",
should_continue,
{"continue": "chat", "end": END}
)
builder.set_entry_point("chat")
graph = builder.compile()
这段代码的精髓在于,遗忘被架构化了。它不是在某个角落偷偷摸摸地删数据,而是作为一个正式节点(memory_manager)存在于流程图中。每个回合结束,Agent都会自动:
- 让旧记忆按时间衰减
- 清理彻底过时的信息
- 对消息历史做摘要和裁剪
- 从新的对话中提取关键事实,存入长期记忆
这样一来,State始终保持在一个健康的大小,Agent既有"记性"又有"忘性"。
小结
代码跑起来,Agent才能真正学会"忘记"。遗忘机制不是补丁,而是LangGraph架构中的核心组件。
写在最后
咱们今天聊了很多,从为什么要遗忘,到怎么让记忆随时间衰减,怎么给记忆打分,怎么在Token预算里做取舍,怎么给大脑分区,再到最后落地成LangGraph代码。其实归根结底,就一句话:Agent的智能,不仅取决于它记住了什么,更取决于它忘记了什么。
新手做Agent,总是害怕遗忘,生怕漏掉一丝信息。但你看那些成熟的系统,Redis有TTL,数据库有归档,操作系统有虚拟内存换页。遗忘和清理,本来就是工程世界的基本法则。Agent不是神,它不需要全知全能,它需要的是在当下的语境里,抓住最关键的信息,做出最合理的决策。
编程之路不易,但每一步成长都算数。你可能会踩过State爆炸的坑,可能会为Token账单肉疼过,但没关系,这些都是你成为高手的勋章。保持好奇,持续学习,敢于给Agent做"减法",你也能成为代码高手。记住,好代码不是堆出来的,是删出来的。Agent的记忆,也是如此。
关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如:
《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》
《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》
《课程:2026 年 AGI 大模型系统课 23 期》
《课程:2026 年 AGI 大模型系统课 21 期》
《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》
《课程:AI 大模型系统实战课三期》
《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》
《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》
《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》
《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》
《课程:LLM 多模态视觉大模型系统课》
《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》
《课程:大模型智能体线上速成班 V2.0》
《课程:Java+AI 大模型智能应用开发全阶课》
《课程:Python+AI 大模型实战视频教程》
《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》
《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》
《课程:AI 大模型零基础到商业实战全栈课第五期》
《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》
《课程:AI 大模型实战训练营 从入门到实战轻松上手》
《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》
更多推荐


所有评论(0)