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 with summary memory
Conversation compression · Progressive summary · MemorySaver

Long conversations can overwhelm the context window. Learn to use LLM to automatically generate "rolling summaries",
allowing Chatbot to remember the entire conversation without consuming a large amount of Tokens.

1Problem background: Why do long conversations cause amnesia?

In previous lessons, we have learned to useMessagesStateMaintain conversation history, also tried usingtrim_messagesandfilter_messagesto control the context length. But these methods have a common flaw: they aredisposable——Old messages are deleted directly, and that part of the conversation is permanently lost.

The challenges of real LLM conversations are:

Context occupancy diagram (assuming the model upper limit is 8K Tokens) 7,800 / 8,000

The image above shows how a long conversation is dangerously close to the context limit.trim directlyThe result is to make LLM forget the key information at the beginning of the conversation; and the plan for this lesson——Conversation Summarization——Use LLM to convert old messagesCompressed into a summary textKeep it instead of deleting it directly.

Core trade-offs

The essence of the abstract plan is to useInformation compression in exchange for Token savings. There is necessarily a loss of information in the summary, but it retains the dialogueSemantic points, while trim simply discards the original text. For Chatbots that need to track user intent over time, summarization schemes are far superior to simple truncation.

method The fate of old news information retention Implementation complexity
trim_messages Delete directly from State Low (completely lost) Low
filter_messages Filter out by type/ID Medium (retain specific types) Low
Summary (for this lesson) LLM distilled into summary string High (preserves semantic points) middle

2Solution: Generate rolling summaries with LLM

The summary strategy implemented in this lesson is calledProgressive Rolling Summary. Here's how it works:

Summary policy state evolution diagram

Early stage of conversation (within 6 items)
messages: [
"Hi, I'm Lance"
"Hello Lance!"
"I like 49ers"
"Great team!"
]
summary: ""
No summary, direct conversation
After threshold is exceeded (summary triggered)
messages: [
"Nick Bosa..."
"Yes, $170M"
]
summary: "Lance is a 49ers
fan, likes Bosa..."
Summary replaces old messages

This mechanism achieves realLossless long-term memory: The message list is always kept short, and the summary field continues to accumulate semantic information as the conversation progresses, becoming the "long-term memory carrier" of LLM.

3State extension: add summary field in MessagesState

The first step to implement the summary mechanism is to extend the State data structure. LangGraph provides a built-inMessagesState, which already containsmessagesfields (withadd_messagesreducer, supports append semantics). We just need to inherit it and add an additionalsummaryString field:

from langgraph.graph import MessagesState

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

There are a few key points worth understanding here:

messages: [...]
summary: ""
Initial State (summary is empty)
messages: [Last 2 items]
summary: "Lance is a 49ers fan..."
State after summary triggering
clever design

WillsummaryDesigned asCoverage semantics(rather than append semantics) is intentional: each digest node generates aFull new summary, so old digests can be safely overwritten.messagesUse append semantics to ensure that each round of AI replies is correctly appended to the end of the list without overwriting previous messages. The two work together to form a complete status management solution.

4call_model node: Inject summary into conversation context

call_modelIt is the core node responsible for calling LLM. What's special about it is that before calling LLM, it willCheck if summary exists in State, if so, wrap the summary intoSystemMessageInsert it at the front of the message list so that LLM can "see" the background of the previous conversation.

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}

Let’s walk through a conversational demonstration to intuitively understand what LLM actually receives:

Actual sequence of messages sent to LLM (conversation round 5 after digest trigger)

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.
human
Do you think they'll win the Super Bowl this year?
AI
(AI can recall that Lance is a 49ers fan and give tailored answers based on the summary)
Note: SystemMessage is not stored in State

call_modelThe function builds a message list containing SystemMessage locally, butOnly put LLM'sresponseReturn to State. SystemMessage is just a "context patch" when calling LLM and will not be permanently stored in State, so it will not occupy additional messages space.

Why use SystemMessage instead of HumanMessage?

In LangChain’s message system,SystemMessageRepresents system-level directives and is the most appropriate way to tell LLM "your current background/role/rules". The summary serves as a context for the conversation, with theSystemMessagePass in, semantically speaking, useHumanMessageMore accurate and more consistent with most LLM training conventions.

5summarize_conversation node: generate and update summaries

This is the core node of this lesson. It accomplishes three things:

  1. Generate summary: Call LLM to compress all current messages into a summary text
  2. rolling update: If an old digest already exists, LLM is required to merge the old digest with the new message instead of rewriting it from scratch.
  3. Clean up old messages:useRemoveMessageDelete all old messages except the last 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}

How RemoveMessage works

RemoveMessageIt is a special message type provided by LangChain Core. when it is put inmessagesWhen the list returns,add_messagesThe reducer will recognize it andRemove from State the correspondingidnews, instead of appending RemoveMessage itself. This is an elegant "message deletion signal" mechanism.

The execution effect of RemoveMessage

Before summary (messages list)
msg[0]: "Hi, I'm Lance" ← Delete
msg[1]: "Hello Lance!" ← Delete
msg[2]: "I like 49ers" ← Delete
msg[3]: "Great team!" ← Delete
msg[4]: "Nick Bosa..." ← Delete
msg[5]: "Yes, $170M" ← Reserved
msg[6]: "Amazing deal" ← Reserved
After summary (messages list)
msg[5]: "Yes, $170M"
msg[6]: "Amazing deal"
summary field
"Lance is a 49ers fan and mentioned that Nick Bosa is the highest paid defenseman in the NFL at $170 million..."

Why keep the most recent 2 items instead of 0?

Keeping the last 2 messages (usually the last round of Human + AI message pairs) serves two purposes:

6should_continue: conditional edge that controls when the summary is triggered

Conditional edge routing functionshould_continueDetermines the direction of the graph after each round of dialogue: it ends directly (END), it is better to do the summary first and then end it.

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

Branch logic of conditional edges

conversation
call_model node
should_continue() conditional routing
len(messages) > 6
summarize_conversation
summary node
END
len(messages) <= 6
END

Meaning and adjustability of threshold 6

A threshold of 6 is an example value. In actual projects, you can adjust based on the following factors:

Considerations Suggested strategies
Model context window size The smaller the window, the lower the threshold should be; GPT-4o (128K) can set a higher threshold
average length of each message If the messages are very long (such as code blocks), the threshold should be lowered
summary call cost The summary itself also consumes Token. If it is too frequent, the gain will outweigh the loss.
Conversation continuity requirements Increase the threshold in high-continuity scenarios (such as customer service) to retain more original messages
More granular triggering strategies

except pressingNumber of messagestrigger, you can also pressEstimate the number of Tokenstrigger (usingmodel.get_num_tokens()), or presstime intervalTrigger (e.g. summary once conversation exceeds 10 minutes). The implementation of this lesson is the simplest number of items scheme, suitable for entry-level understanding.

7Complete assembly diagram: MemorySaver, Thread, checkpointer

Now put all the pieces together to build a complete Chatbot graph with persistent memory.

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()))

The structure of the complete graph

__start__
START
conversation
call_model (LLM call)
should_continue() routing judgment
Number of messages > 6
summarize_conversation
Generate summary + delete old messages
__end__
Number of messages <= 6
__end__

The role of MemorySaver and Thread

Givecompile()incomingcheckpointer=memoryAfter that, the picture hasPersistence: After each step is executed, LangGraph will automatically save the current State snapshot to MemorySaver. The next time the same thread_id is called, the graph continues from the last saved state rather than starting from the beginning.

# 创建一个对话线程(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 analogy: Thread can be understood as different channels of Slack - eachthread_idIt is an independent dialogue channel and does not interfere with each other. You can maintain a Thread for User A and User B at the same time, and their States (including summaries) are completely independent.

Limitations of MemorySaver

MemorySaveryesin process memoryStorage, all State will disappear after the program is restarted. It is suitable for development and testing phases. In a production environment, you should useSqliteSaver(file persistence) orPostgresSaver(database persistence) replacement without modifying any other code in the graph, just replacing the type of checkpointer.

8Complete operation demonstration and summary of key technologies

Complete conversation flow in Notebook

Here's a complete running demo in Notebook, showing the complete process from a normal conversation to a triggered summary:

Complete conversation history for Thread "1"

human
hi! I'm Lance
AI
Hello Lance! How can I assist you today?
human
what's my name?
AI
You mentioned that your name is Lance. How can I help you today?
human
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?
— Number of messages = 6 at this time, digest has not been triggered yet —
human
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...
— After the end of this round, the number of messages = 8, trigger summarize_conversation —

After the fourth round of dialogue,should_continuedetectedlen(messages) == 8 > 6, route tosummarize_conversationnode. LLM generates a summary, the old 6 messages are deleted and only the latest 2 are retained.

# 查看摘要结果
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.'

Summary of key technical points

State design
  • • InheritanceMessagesState
  • • Appendsummary: strField
  • • messages: append semantics (reducer)
  • • summary: override semantics (default)
Message management
  • SystemMessageInject summary
  • RemoveMessageClean up old messages
  • • Keep the last 2 original messages
  • • Digest is not saved in messages list
process control
  • • Conditional edge routing summary trigger timing
  • • 6 thresholds can be adjusted as needed
  • • END directly after the summary and continue with the next round
  • • Rolling summary avoids duplicate LLM calls
persistent memory
  • MemorySaveras checkpointer
  • • thread_id isolates different conversations
  • • get_state() Check status at any time
  • • Produce replaceable SqliteSaver etc.
Comparison with previous lessons

The first few lessons of Module 2 were taughttrim_messages(sliding window truncation) andfilter_messages(Filter by criteria). The abstract solution for this lesson is the final form of this series: it uses LLM's language understanding capabilities to "compress" information instead of "discard" it, and is the standard configuration of production-level long-term conversation Chatbots. Each of the three options has applicable scenarios, so you can choose according to your needs.

Next step: Module 3 Human-in-the-Loop

After mastering summary memory, your Chatbot has been able to handle infinitely long conversations without losing its memory. Module 3 will introduce on this basisManual intervention mechanism (Breakpoint): Pause at a specific node of graph execution and wait for human confirmation or modification of the State before continuing. This is a key technology for building a safe and controllable Agent system.

Module 2-5 (this section) Scroll summary + MemorySaver for long-term memory
Module 3 Breakpoint interrupts graph execution and waits for manual approval
Module 4 Parallel nodes and subgraphs to build a multi-Agent research system
Module 5 Memory Store, enabling long-term user memory across conversations