LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 2 · State & Memory
1 2-1 State Schema
2 2-2 Reducers
3 2-3 Multi Schemas
4 2-4 Trim & Filter
5 2-5 Chatbot Summary
6 2-6 Ext Memory
SUM Module Summary
HomeLangGraphModule 2 · State & Memory2-5 Chatbot Summary
📓 Notebook 📖 Explained
LANGGRAPH MODULE 2 · LESSON 5

带摘要记忆的 Chatbot
对话压缩 · 渐进式摘要 · MemorySaver

长对话会撑爆上下文窗口。学会用 LLM 自动生成"滚动摘要",
让 Chatbot 既记得全程对话,又不消耗大量 Token。

1 问题背景:长对话为何会失忆?

在前几节课里,我们已经学会用 MessagesState 维护对话历史,也尝试过用 trim_messagesfilter_messages 来控制上下文长度。但这些方法有一个共同的缺陷:它们是丢弃式的——旧消息被直接删掉,那部分对话内容就永久消失了。

真实 LLM 对话面临的挑战是:

上下文占用示意(假设模型上限 8K Token) 7,800 / 8,000

上图展示了一场长对话接近上下文上限的危险状态。直接 trim 的结果是让 LLM 忘掉对话开头的关键信息;而本节课的方案——对话摘要(Conversation Summarization)——则是用 LLM 把旧消息压缩成一段摘要文字保留下来,而不是直接删除。

核心权衡

摘要方案的本质是用信息压缩换取 Token 节省。摘要必然有信息损失,但它保留了对话的语义要点,而 trim 则是硬性丢弃原始文字。对于需要长期追踪用户意图的 Chatbot,摘要方案远优于简单截断。

方法 旧消息的命运 信息保留程度 实现复杂度
trim_messages 直接从 State 中删除 低(完全丢失)
filter_messages 按类型/ID 过滤掉 中(保留特定类型)
摘要(本节课) LLM 提炼为摘要字符串 高(保留语义要点)

2 解决方案:用 LLM 生成滚动摘要

本节课实现的摘要策略叫做渐进式滚动摘要(Progressive Rolling Summary)。它的工作方式如下:

摘要策略的状态演变示意

对话初期(6 条以内)
messages: [
"Hi, I'm Lance"
"Hello Lance!"
"I like 49ers"
"Great team!"
]
summary: ""
无摘要,直接对话
超过阈值后(触发摘要)
messages: [
"Nick Bosa..."
"Yes, $170M"
]
summary: "Lance is a 49ers
fan, likes Bosa..."
摘要取代旧消息

这套机制实现了真正的无损长期记忆:消息列表始终保持短小,而摘要字段随对话进展不断累积语义信息,成为 LLM 的"长期记忆载体"。

3 State 扩展:在 MessagesState 中添加 summary 字段

实现摘要机制的第一步,是扩展 State 的数据结构。LangGraph 提供了一个内置的 MessagesState,它已经包含了 messages 字段(带 add_messages Reducer,支持追加语义)。我们只需要继承它,并额外添加一个 summary 字符串字段:

from langgraph.graph import MessagesState

class State(MessagesState):
    # MessagesState 已内置:
    #   messages: Annotated[list[AnyMessage], add_messages]
    # 我们新增一个 summary 字段,用于存储对话摘要
    summary: str

这里有几个关键点值得理解:

messages: [...]
summary: ""
初始 State(summary 为空)
messages: [最近2条]
summary: "Lance 是 49ers 球迷..."
摘要触发后的 State
设计巧妙之处

summary 设计为覆盖语义(而不是追加语义)是故意的:每次摘要节点都会生成一个包含了历史信息的完整新摘要,因此旧摘要可以安全覆盖。messages 使用追加语义,保证每轮 AI 回复都正确追加到列表末尾,而不会覆盖之前的消息。两者配合,构成完整的状态管理方案。

4 call_model 节点:将摘要注入对话上下文

call_model 是负责调用 LLM 的核心节点。它的特别之处在于:在调用 LLM 之前,它会检查 State 中是否存在摘要,如果有,就把摘要包装成 SystemMessage 插入到消息列表最前面,让 LLM 能"看到"之前的对话背景。

from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage

def call_model(state: State):

    # ① 尝试获取现有摘要,不存在则默认为空字符串
    summary = state.get("summary", "")

    # ② 如果有摘要,把它包装成 SystemMessage 前置到消息列表
    if summary:
        system_message = f"Summary of conversation earlier: {summary}"
        # SystemMessage 放最前面,后面接最近的几条真实消息
        messages = [SystemMessage(content=system_message)] + state["messages"]
    else:
        # 没有摘要时直接使用原始消息列表
        messages = state["messages"]

    # ③ 调用 LLM,传入(可能已注入摘要的)消息列表
    response = model.invoke(messages)

    # ④ 返回 AI 的回复(add_messages Reducer 会自动追加)
    return {"messages": response}

让我们通过一个对话演示来直观理解 LLM 实际接收到的内容:

实际发送给 LLM 的消息序列(摘要触发后的第 5 轮对话)

SYS
Summary of conversation earlier: Lance introduced himself and mentioned that he is a fan of the San Francisco 49ers, specifically highlighting his admiration for Nick Bosa. The conversation noted that as of September 2023, Nick Bosa became the highest-paid defensive player in NFL history with a five-year, $170 million contract extension with the 49ers.
人类
Do you think they'll win the Super Bowl this year?
AI
(AI 可以回忆起 Lance 是 49ers 球迷,并基于摘要给出有针对性的回答)
注意:SystemMessage 不存入 State

call_model 函数在本地构建了包含 SystemMessage 的消息列表,但只把 LLM 的 response 返回给 State。SystemMessage 只是调用 LLM 时的"上下文补丁",不会永久存入 State,因此不会占用额外的 messages 空间。

为什么用 SystemMessage 而不是 HumanMessage?

在 LangChain 的消息体系中,SystemMessage 代表系统级指令,是告诉 LLM"你现在的背景/角色/规则"的最合适方式。摘要作为对话背景,以 SystemMessage 传入,语义上比用 HumanMessage 更准确,也更符合大多数 LLM 的训练惯例。

5 summarize_conversation 节点:生成并更新摘要

这是本课最核心的节点,它完成三件事:

  1. 生成摘要:调用 LLM,把当前所有消息压缩成一段摘要文字
  2. 滚动更新:如果已存在旧摘要,则要求 LLM 把旧摘要和新消息合并,而非从头重写
  3. 清理旧消息:用 RemoveMessage 删除除最近 2 条以外的全部旧消息
def summarize_conversation(state: State):

    # ① 获取已有摘要(可能为空字符串)
    summary = state.get("summary", "")

    # ② 根据是否已有摘要,构造不同的提示词
    if summary:
        # 已有摘要 → 要求"在现有摘要基础上延伸",而不是从头写
        # 这就是"滚动摘要"的关键:避免重复计算旧内容
        summary_message = (
            f"This is summary of the conversation to date: {summary}\n\n"
            "Extend the summary by taking into account the new messages above:"
        )
    else:
        # 第一次生成摘要
        summary_message = "Create a summary of the conversation above:"

    # ③ 构造给 LLM 的完整消息列表(历史消息 + 摘要请求)
    messages = state["messages"] + [HumanMessage(content=summary_message)]
    response = model.invoke(messages)

    # ④ 生成 RemoveMessage 对象列表,标记需要删除的旧消息
    # state["messages"][:-2] = 除最后2条外的所有消息
    delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]

    # ⑤ 同时返回:新摘要(覆盖旧摘要) + 删除指令(清理旧消息)
    return {"summary": response.content, "messages": delete_messages}

RemoveMessage 的工作原理

RemoveMessage 是 LangChain Core 提供的一个特殊消息类型。当它被放入 messages 列表返回时,add_messages Reducer 会识别它,并从 State 中删除具有对应 id 的消息,而不是把 RemoveMessage 本身追加进去。这是一种优雅的"消息删除信号"机制。

RemoveMessage 的执行效果

摘要前(messages 列表)
msg[0]: "Hi, I'm Lance" ← 删除
msg[1]: "Hello Lance!" ← 删除
msg[2]: "I like 49ers" ← 删除
msg[3]: "Great team!" ← 删除
msg[4]: "Nick Bosa..." ← 删除
msg[5]: "Yes, $170M" ← 保留
msg[6]: "Amazing deal" ← 保留
摘要后(messages 列表)
msg[5]: "Yes, $170M"
msg[6]: "Amazing deal"
summary 字段
"Lance 是 49ers 球迷,提到 Nick Bosa 以 1.7亿美元成为 NFL 防守最高薪..."

为什么保留最近 2 条而不是 0 条?

保留最近 2 条消息(通常是最后一轮的 Human + AI 消息对)有两个目的:

6 should_continue:控制何时触发摘要的条件边

条件边的路由函数 should_continue 决定了图在每轮对话结束后的走向:是直接结束(END),还是先去做摘要再结束。

from langgraph.graph import END
from typing_extensions import Literal

def should_continue(state: State) -> Literal["summarize_conversation", END]:

    """返回下一个要执行的节点名称。"""

    messages = state["messages"]

    # 关键阈值:超过 6 条消息就触发摘要
    if len(messages) > 6:
        return "summarize_conversation"

    # 消息数在阈值内,直接结束(等下一轮继续积累)
    return END

条件边的分支逻辑

conversation
call_model 节点
should_continue() 条件路由
len(messages) > 6
summarize_conversation
摘要节点
END
len(messages) <= 6
END

阈值 6 的含义与可调整性

阈值设为 6 是一个示例值。在实际项目中,你可以根据以下因素调整:

考量因素 建议策略
模型上下文窗口大小 窗口越小,阈值应越低;GPT-4o(128K)可以设较高阈值
每条消息的平均长度 如果消息都很长(比如代码块),应降低阈值
摘要的调用成本 摘要本身也消耗 Token,过于频繁会得不偿失
对话连贯性要求 高连贯性场景(如客服)阈值调高,保留更多原始消息
更精细的触发策略

除了按消息条数触发,你也可以改为按估算 Token 数触发(用 model.get_num_tokens()),或者按时间间隔触发(比如对话超过 10 分钟就摘要一次)。本节课的实现是最简单的条数方案,适合入门理解。

7 装配完整图:MemorySaver、Thread、Checkpointer

现在把所有零件组装起来,构建一个带持久化记忆的完整 Chatbot 图。

from IPython.display import Image, display
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START

# ① 创建图的蓝图(使用自定义的 State)
workflow = StateGraph(State)

# ② 注册两个节点
workflow.add_node("conversation", call_model)       # LLM 对话节点
workflow.add_node(summarize_conversation)           # 摘要节点(函数名即节点名)

# ③ 设置入口:START → conversation
workflow.add_edge(START, "conversation")

# ④ 设置条件边:conversation 完成后,由 should_continue 决定走向
workflow.add_conditional_edges("conversation", should_continue)

# ⑤ 摘要节点完成后直接结束
workflow.add_edge("summarize_conversation", END)

# ⑥ 编译时注入 MemorySaver 作为 checkpointer
# MemorySaver 是内存中的 KV 存储,生产环境可换成 SqliteSaver / PostgresSaver
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)

# 可视化图结构
display(Image(graph.get_graph().draw_mermaid_png()))

完整图的结构

__start__
START
conversation
call_model(LLM 调用)
should_continue() 路由判断
消息数 > 6
summarize_conversation
生成摘要 + 删除旧消息
__end__
消息数 <= 6
__end__

MemorySaver 与 Thread 的作用

compile() 传入 checkpointer=memory 后,图就具备了持久化能力:每一步执行完毕,LangGraph 会自动把当前 State 快照保存到 MemorySaver 中。下次调用同一个 thread_id 时,图会从上次保存的状态继续,而不是从头开始。

# 创建一个对话线程(Thread),通过 thread_id 标识
config = {"configurable": {"thread_id": "1"}}

# 第一轮对话
input_message = HumanMessage(content="hi! I'm Lance")
output = graph.invoke({"messages": [input_message]}, config)

# 第二轮对话(同一 thread_id,图从上次状态继续)
input_message = HumanMessage(content="what's my name?")
output = graph.invoke({"messages": [input_message]}, config)

# 读取当前 State 的摘要字段
current_summary = graph.get_state(config).values.get("summary", "")

Thread(线程)的类比:可以把 Thread 理解为 Slack 的不同频道——每个 thread_id 是一个独立的对话频道,互不干扰。你可以同时跟用户 A 和用户 B 各维护一个 Thread,它们的 State(包括摘要)完全独立。

MemorySaver 的局限性

MemorySaver进程内存中的存储,程序重启后所有 State 都会消失。它适合开发和测试阶段。生产环境中,应使用 SqliteSaver(文件持久化)或 PostgresSaver(数据库持久化)替代,无需修改图的任何其他代码,只需替换 checkpointer 的类型即可。

8 完整运行演示与关键技术总结

Notebook 中的完整对话流程

以下是 Notebook 中的完整运行演示,展示了从普通对话到触发摘要的完整过程:

Thread "1" 的完整对话历史

人类
hi! I'm Lance
AI
Hello Lance! How can I assist you today?
人类
what's my name?
AI
You mentioned that your name is Lance. How can I help you today?
人类
i like the 49ers!
AI
That's great! The San Francisco 49ers have a rich history and a passionate fan base. Do you have a favorite player or a memorable game that you particularly enjoyed?
— 此时消息数 = 6,尚未触发摘要 —
人类
i like Nick Bosa, isn't he the highest paid defensive player?
AI
Yes, as of September 2023, Nick Bosa became the highest-paid defensive player in NFL history. He signed a five-year contract extension with the San Francisco 49ers worth $170 million, with $122.5 million guaranteed...
— 此轮结束后消息数 = 8,触发 summarize_conversation —

第 4 轮对话结束后,should_continue 检测到 len(messages) == 8 > 6,路由到 summarize_conversation 节点。LLM 生成摘要,旧的 6 条消息被删除,只保留最新的 2 条。

# 查看摘要结果
graph.get_state(config).values.get("summary", "")

# 输出:
# 'Lance introduced himself and mentioned that he is a fan of the
#  San Francisco 49ers, specifically highlighting his admiration for
#  Nick Bosa. The conversation noted that as of September 2023, Nick
#  Bosa became the highest-paid defensive player in NFL history with
#  a five-year, $170 million contract extension with the 49ers.'

关键技术要点总结

State 设计
  • • 继承 MessagesState
  • • 追加 summary: str 字段
  • • messages:追加语义(Reducer)
  • • summary:覆盖语义(默认)
消息管理
  • SystemMessage 注入摘要
  • RemoveMessage 清理旧消息
  • • 保留最近 2 条原始消息
  • • 摘要不存入 messages 列表
流程控制
  • • 条件边路由摘要触发时机
  • • 阈值 6 条可按需调整
  • • 摘要后直接 END,下轮继续
  • • 滚动摘要避免重复 LLM 调用
持久化记忆
  • MemorySaver 作为 checkpointer
  • • thread_id 隔离不同对话
  • • get_state() 随时查看状态
  • • 生产可换 SqliteSaver 等
与前几节课的对比

Module 2 的前几节课讲了 trim_messages(滑动窗口截断)和 filter_messages(按条件过滤)。本节课的摘要方案是这个系列的最终形态:它用 LLM 的语言理解能力把信息"压缩"而非"丢弃",是生产级长期对话 Chatbot 的标准配置。三种方案各有适用场景,按需选择即可。

下一步:Module 3 Human-in-the-Loop

掌握了摘要记忆之后,你的 Chatbot 已经能够处理无限长的对话而不失忆。Module 3 会在这个基础上引入人工介入机制(Breakpoint):在图执行的特定节点暂停,等待人类确认或修改 State,再继续执行。这是构建安全、可控 Agent 系统的关键技术。

Module 2-5(本节) 滚动摘要 + MemorySaver,实现长期记忆
Module 3 Breakpoint 打断图执行,等待人工审批
Module 4 并行节点、子图,构建多 Agent 研究系统
Module 5 Memory Store,实现跨对话的长期用户记忆