更新于 2026年8月10日

第2节 短期记忆管理及在 LangGraph 中的使用#

在经过上一篇文章「Agent 记忆机制入门详解:短期记忆、长期记忆与 LangGraph 记忆系统架构对比指南」的介绍我们已经理解了短期记忆的底层机制, 此时我们有一个工程问题不得不面对,那就是对话历史会无限增长但模型的上下文窗口是有限的,也是本节要解决的核心问题。 围绕它,LangGraph 提供了 3 种应对策略——删除、修剪、总结,下面我们逐个拆开讲。

2.1 为什么必须管短期记忆#

在一个对话型应用中,每一轮交互都会产生新的消息——用户说一句,模型回一句。你和模型一来一回,最终,在 LangGraph 中这些消息会被依次追加到全局状态的消息列表中。前几轮可能完全没事,但聊到几十、几百轮之后,消息列表就会越积越长,词元总量就便会持续膨胀,进而带来两类后果。

(1)超出上下文窗口直接报错

每个大模型都有一个上下文窗口(Context Window)的硬上限——8K、128K、200K 不等。当消息列表的总 token 数突破这个上限,模型会直接抛错,而且这种错误一般不可恢复,只能截断历史或从头开始对话。

(2)即使没超限,效果也会变差

即使模型的上下文窗口足够大,过长的上下文也未必带能带来好的效果。那些"过时但还在占位"的早期消息会分散模型的注意力,导致回答质量下滑、响应变慢——而且这种退化比"报错"更隐蔽,更容易被忽视。

所以一个核心结论是:更长的上下文 ≠ 更好的效果——超出合理长度之后,冗余的历史信息反而成了干扰。怎么办?LangGraph 给了 3 种策略来进行管理,删除、修剪、总结,分别是记忆修剪、记忆总结和记忆删除,下面依次进行介绍。

2.2 记忆删除:直接删掉旧消息#

先说最简单粗暴的一种:直接删。把不需要的历史消息按 ID 丢掉,只留你想保留的一定数量部分,思路如图2-1所示。

图2-1 记忆删除示意图
图2-1 记忆删除示意图

LangGraph 里专门提供了 RemoveMessage 类用来按 ID 删除单条或多条消息。下面是一个常见的删除函数写法:

1 def delete_messages(agent, config: RunnableConfig) -> None:
2     snapshot = agent.get_state(config)
3     messages = snapshot.values.get("messages", []) if snapshot.values else []
4     if len(messages) < 2:
5         print("当前记忆不足 2 条,跳过删除。")
6         return
7     updates = {"messages": [RemoveMessage(id=message.id) for message in messages[:2]]}
8     agent.update_state(config, updates, as_node="generate_answer")
9     print("已删除最前面的两条记忆。")

这段代码做了 3 件事:第 3 行从快照状态 checkpoint 里捞出当前所有历史消息;第 7 行挑出最先发生的两条构造删除指令;第 8 行写回状态快照。这里 as_node="generate_answer" 是关键——它告诉 LangGraph"这次状态变更模拟成 generate_answer 节点产生的"。因为 update_state() 不是简单改一行 JSON,而是按 LangGraph 的图执行模型去改状态,所以需要声明"以谁的名义改"。

as_node 填哪个节点没有强制要求——不填的话默认会用图的最后一个节点(前提是没有歧义,否则报错)。但建议显式指定一个真实存在的节点。原则是:选一个本来就返回 {"messages": ...} 的节点,因为 update_state() 更新的是 messages 字段。本例中这次"修改"最接近"对消息状态做一次人工修正",所以 generate_answer 是最自然的选择;选 rewrite_question 也能跑通,但状态快照里 next() 的指向就不太合理了。

假设当前 checkpoint 快照里有 6 条历史消息:

StateSnapshot(values={'messages': [
HumanMessage(content='郭靖和杨康是什么关系?', , id='31e1f411-8015-4e27'), 
AIMessage(content='', id='lc_run--019e2558-e520-7be1', tool_calls=[{'name': 'retrieve_context', 'args': {'query': '郭靖和杨康的关系'}, 'id': 'call_7cdad6b1a9394482846227', 'type': 'tool_call'}]), 
ToolMessage(content='杨康边哭边说,涕泪滂沱,断断续续地道:......', name='retrieve_context', id='69d27ef5-2a11-4e2a', tool_call_id='call_7cdad6b1a9394482846227', artifact=), 
AIMessage(content='郭靖和杨康是结义兄弟。两人在郭啸天灵前对拜八拜,结为兄弟,郭靖先出世一个月,为兄,杨康为弟。', id='lc_run--019e2558-f1ba-7c42', ), 
HumanMessage(content='郭靖比杨康先出生几个月?', , id='e5336c6b-d60f-44bf'), 
AIMessage(content='根据提供的文本,郭靖比杨康先出生**一个月**。', id='lc_run--019e2559-4511-7403', )]}, 
next=(), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f14f650-3615-6440'}}, metadata={'k': 20, 'step': 6, 'top_n': 3, 'source': 'loop', 'parents': {}}, created_at='2026-05-17T07:17:49.219715+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f14f650-278b-6c8a'}}, tasks=(), interrupts=())

执行 delete_messages 之后,消息列表会变成:

StateSnapshot(values={'messages': [
ToolMessage(content='杨康边哭边说,涕泪滂沱,断断续续地道:......', name='retrieve_context', id='69d27ef5-2a11-4e2a', tool_call_id='call_7cdad6b1a9394482846227', artifact=), 
AIMessage(content='郭靖和杨康是结义兄弟。两人在郭啸天灵前对拜八拜,结为兄弟,郭靖先出世一个月,为兄,杨康为弟。', id='lc_run--019e2558-f1ba-7c42', ), 
HumanMessage(content='郭靖比杨康先出生几个月?', , id='e5336c6b-d60f-44bf'), 
AIMessage(content='根据提供的文本,郭靖比杨康先出生**一个月**。', id='lc_run--019e2559-4511-7403', )]}, next=(), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f14f650-aa3c-6440'}}, metadata={'k': 20, 'step': 7, 'top_n': 3, 'source': 'update', 'parents': {}}, created_at='2026-05-17T07:18:01.399182+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f14f650-3615-6440'}}, tasks=(), interrupts=())

可以看到,最先发生的两条已经被删除。此时如果用户接着追问"基于当前保留下来的对话记忆,继续回答:郭靖比杨康先出生几个月?" 模型会答出"郭靖比杨康先出生一个月。"

再来一轮新的对话之后,checkpoint 里的状态变成:

StateSnapshot(values={'messages': [ToolMessage(content='杨康边哭边说,涕泪滂沱,断断续续地道:......', name='retrieve_context', id='69d27ef5-2a11-4e2a', tool_call_id='call_7cdad6b1a9394482846227', artifact=), AIMessage(content='郭靖和杨康是结义兄弟。两人在郭啸天灵前对拜八拜,结为兄弟,郭靖先出世一个月,为兄,杨康为弟。', id='lc_run--019e2558-f1ba-7c42', ), HumanMessage(content='郭靖比杨康先出生几个月?', , id='e5336c6b-d60f-44bf'), AIMessage(content='根据提供的文本,郭靖比杨康先出生**一个月**。', id='lc_run--019e2559-4511-7403', ), HumanMessage(content='基于当前保留下来的对话记忆,继续回答:郭靖比杨康先出生几个月?', , id='c052ef69-9484-4bba'), AIMessage(content='郭靖比杨康先出生**一个月**。', id='lc_run--019e2559-7b45-7282',  )]}, next=(), config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f14f650-bae5-6332'}}, metadata={'k': 20, 'step': 10, 'top_n': 3, 'source': 'loop', 'parents': {}}, created_at='2026-05-17T07:18:03.146080+00:00', parent_config={'configurable': {'thread_id': '5', 'checkpoint_ns': '', 'checkpoint_id': '1f14f650-ab4b-608e'}}, tasks=(), interrupts=())

以上完整示例代码可参见 Code/Chapter05/C04_short_memory_delete.py 文件。

2.3 记忆修剪:保留最近 N 条#

讲完删除,再看修剪。修剪的思路非常直接,预先设定一个数量上限(消息数或 token 数),当消息列表接近这个上限时自动裁剪多余部分,只留下需要的部分。不过按 token 截断其实不太直接——因为喂给模型的 token 远不止消息本身,还包括工具调用的 Schema、Tool ID、Tool Call 等附加信息。所以工程上更建议按消息数截断,思路如图2-2所示。

图2-2 记忆修剪示意图
图2-2 记忆修剪示意图

在图2-2 中展示的是"只保留最近 4 条,且系统消息始终保留,以及必须以用户消息开头"的策略。可以看出,修剪和删除策略有相似之处,但修剪更灵活——可以精确控制保留什么、丢弃什么。

LangGraph 里提供了 trim_messages 来做这件事。只需要传两个关键参数——“保留多少"和"保留哪一段”——就能完成一次修剪。先看代码:

 1 if __name__ == '__main__':
 2     messages = [
 3         SystemMessage(content="你是一个问答助手。"),
 4         HumanMessage(content="郭靖是谁?"),
 5         AIMessage(content="郭靖是《射雕英雄传》的主角,出生于临安府牛家村。"),
 6         HumanMessage(content="他的武功是谁教的?"),
 7         AIMessage(content="郭靖先后师从江南七怪和洪七公,后又得到马钰道长的内功传授。"),
 8         HumanMessage(content="黄蓉是谁?")]
 9     trimmed = trim_messages(messages, max_tokens=4, token_counter=len,
10         strategy="last", start_on="human", end_on=("human", "tool"),
11         include_system=True)
12     for msg in trimmed:
13         print(f"[{msg.type}]: {msg.content}")

在上述代码中,第2~8行是模拟的一个交互对话消息列表。第9行trim_messages 便是用于对历史消息进行修剪,其中 max_tokens 用于指定最大 Token 数或历史消息数,取决于参数 token_counter 的设定。例如这里 token_counter=len 时,max_tokens表示最大历史消息数。第10行 strategy="last" 表示修剪时的丢弃策略,有 firstlast 两种策略,即保留前面 max_tokens 部分还是后面 max_tokens 部分; start_onend_on 分别表示必须以什么样的消息类型开始或结束,为 None 则无此限制。第11行 include_system 表示是否一直保留系统消息,即 SystemMessage。通常来说会将 include_system 设定为 True,因为系统消息中一般会包含固定的指令。

对于上述示例来说一共需要保留4条消息记录,且保留策略为 last,同时除了系统消息外第1条必须为用户提问,因此最终输出结果为:

[system]: 你是一个问答助手。
[human]: 他的武功是谁教的?
[ai]: 郭靖先后师从江南七怪和洪七公,后又得到马钰道长的内功传授。
[human]: 黄蓉是谁?

如果把 start_on="human" 限制去掉,同时把 max_tokens 改成 2,结果就变成:

[system]: 你是一个问答助手。
[human]: 黄蓉是谁?

以上完整示例代码可参见 Code/Chapter05/C05_shrot_memory_trim.py 文件。

2.4 记忆总结:滚动总结旧消息#

记忆删除和修剪虽然直接,但有一个代价——被裁掉的消息彻底没有了。如果用户早在几轮前提过的关键信息对当前问题依然重要,模型就再也看不到了。所以需要一种更友好的替代方案,这就是记忆总结(也叫消息摘要)。

记忆总结的思路不是丢弃旧消息,而是把它压缩成一段摘要保留下来——这样既释放了上下文空间,又没完全丢失历史信息。具体做法是,当消息列表积累到一定数量时触发一次摘要操作,让模型把当前的历史对话概括成一段文字存入全局状态,然后删掉大部分旧消息,只留最近若干条。流程如图2-3所示。

图2-3 记忆总结示意图
图2-3 记忆总结示意图

具体地,需要先在全局消息状态 MessagesState 中增加一个 summary 字段,用于保存历史消息的摘要总结,示例代码如下:

1 class State(MessagesState):
2     summary: str  # 在 MessagesState 基础上新增摘要字段

进一步,专门定义一个图节点来完成记忆总结这部分内容,代码如下:

 1 def summarize_conversation(state: State):
 2     summary = state.get("summary", "")
 3     if summary:
 4         summary_message = (
 5             f"以下是目前为止对话的摘要:{summary}\n\n"
 6             "请结合上方新的对话内容,对摘要进行补充和更新:")
 7     else:
 8         summary_message = "\n请对上方的对话内容生成一段摘要:"
 9     messages = state["messages"][:-2] + [HumanMessage(content=summary_message)]
10     response = get_llm_model().invoke(messages)
11     delete_messages = [RemoveMessage(id=message.id) for message in state["messages"][:-2]]
12     return {"summary": response.content, "messages": delete_messages}

在上述代码中,第2行用于取当前节点之前的历史总结内容。第3~8行是根据不同的情况构造提示词。第9~10行是只保留最后两条消息,然后对其余消息进行总结,可以发现每次触发总结时,都会把上一次的总结作为上下文,让模型在此基础上继续扩展,而不是从头重新总结,这种"滚动总结"的设计保证了历史信息不会彻底丢失,同时消息列表的长度始终保持在可控范围内。第11行是标记已经被总结的待删除记忆内容。

接着写一个最小可跑通的测试:

 1 if __name__ == '__main__':
 2     with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
 3         ......
 4         messages = [
 5             SystemMessage(content="你是一个问答助手。"),
 6             HumanMessage(content="郭靖是谁?"),
 7             AIMessage(content="郭靖是《射雕英雄传》的主角,出生于临安府牛家村。"),
 8             HumanMessage(content="他的武功是谁教的?"),
 9             AIMessage(content="郭靖先后师从江南七怪和洪七公,后又得到马钰道长的内功传授。"),
10             HumanMessage(content="黄蓉是谁?")]
11         inputs = {"messages": messages}
12         for event in agent.stream(inputs, config=config):
13             for node, update in event.items():
14                 print(f"#### 节点 {node} 处理完毕")
15         snapshot = agent.get_state(config)
16         print(f"========== 当前摘要 ==========\n{snapshot.values.get('summary', '')}")
17         print_memory(agent, config, "========== 总结并删除后的数据库记忆 ==========")

根据 summarize_conversation 节点中的设定,上面前4条消息会被总结,然后保留最后两条消息在全局状态中。最终,上述代码运行后的出处结果类似如下:

#### 节点 summarize 处理完毕
========== 当前摘要 ==========
对话中,用户首先询问“郭靖是谁”,回答指出他是金庸武侠小说《射雕英雄传》的主角,出生于临安府牛家村;随后用户追问“他的武功是谁教的”,但该问题尚未得到回答。当前对话仅完成前两轮,信息停留在郭靖的身份介绍层面。
========== 总结并删除后的数据库记忆 ==========
snapshot: StateSnapshot(values={'messages': [AIMessage(content='郭靖先后师从江南七怪和洪七公,后又得到马钰道长的内功传授。', id='1e9a444a-ac01-4904'), HumanMessage(content='黄蓉是谁?', id='bfdaca1a-7a7a-44c6')], 'summary': '对话中,用户首先询问“郭靖是谁”...介绍层面。'})
读取到 thread_id=20260519 的短期记忆,共 2 条消息:
01. ai: 郭靖先后师从江南七怪和洪七公,后又得到马钰道长的内功传授。
02. human: 黄蓉是谁?

从上述输出结果可以看出,summarize_conversation 节点确实只对前面4条消息进行了总结,同时在全局状态中完整的保留了最近的两条消息记录,以上完整示例代码可参见 Code/Chapter05/C06_shrot_memory_summary.py 文件。

到此,对于短期记忆管理到这里就讲完了。虽然短期记忆解决了"会话内记忆不丢失"的问题,但 想要 Agent 真正变得更"懂你",还得靠跨会话也能调用的记忆——也就是长期记忆。在下一节「Agent 长期记忆管理与持久化:PostgresStore 存储策略与语义、情景、程序记忆分类详解」中我们将继续介绍长期记忆怎么分类、怎么存以及如何实现跨会话复用。

引用#

[1] https://docs.langchain.com/oss/python/langgraph/add-memory

阅读 --

第1节 MiniChatGPT 导读与 Agent 记忆机制

记忆机制是一个让 Agent 能够记住历史交互信息的系统。具体来说,我们希望它能够做到三件事情:① 记住之前的对话内容;② 从用户的反馈中学习;③ 根据用户的偏好调整自身的行为。虽然这三件事听起来好像很简单也很自然,但对于一个实用的 AI …

第3节 长期记忆的定义与持久化管理

长期记忆解决的是 Agent 跨会话认识你的问题——用户的偏好、经历、知识,不会因为会话关闭就消失。本节系统讲解长期记忆的 3 大分类(语义/情景/程序记忆)、2 种存储方式(用户画像/记忆集合)、2 类写入策略(热路径/后台)以及 …