在这里插入图片描述

别让Agent变成"记忆黑洞"!LangGraph记忆衰减实战:教你给AI装上"人脑遗忘曲线",让Agent从"背锅侠"进化为"智慧体",彻底告别Token爆炸和上下文污染!

记忆衰减与遗忘机制

要点1:Agent不是垃圾桶

主动遗忘的必要性

要点2:时间衰减算法

艾宾浩斯曲线

要点3:重要性评分

核心vs碎片

要点4:Token预算裁剪

滑动窗口

要点5:分层记忆架构

短期长期工作

要点6:LangGraph实战

代码落地

本文目录:

  • 要点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预算管理。总预算是固定的,各个模块要量入为出。

预算分配策略可以这样设计:

  1. 系统提示:固定开销,必须保留
  2. 长期记忆:按重要性排序,保留Top-K(高分记忆优先)
  3. 短期历史:如果还超预算,先做摘要,再压缩
  4. 最新对话:必须保留,这是当前任务的上下文

来看一个基于tiktoken的裁剪实现:

新消息进入

计算总Token

超出预算?

正常调用LLM

按重要性排序记忆

摘要最旧的低分记忆

丢弃低于阈值记忆

重新计算Token

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都会自动:

  1. 让旧记忆按时间衰减
  2. 清理彻底过时的信息
  3. 对消息历史做摘要和裁剪
  4. 从新的对话中提取关键事实,存入长期记忆

这样一来,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 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐