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 & MemoryModule Summary
LANGGRAPH MODULE 2 · SUMMARY

State & Memory
Module Summary

From Schema definition to external persistence, Module 2 builds a complete
LangGraph state management and dialogue memory system.

6 sub-course
2 core themes
3 Schema type
4 reducer strategy
Module learning path
Schema definition
L1 · TypedDict
Dataclass · Pydantic
reducer mechanism
L2 · Overwrite/Append
Parallel conflict resolution
Multiple Schemas
L3 · Private state
Input/Output filtering
Message management
L4 · filter / trim
Token control
summary memory
L5 · Rolling summary
MemorySaver
external persistence
L6 · SQLite
Cross-session memory

📌Two main lines of modules

State (state definition)
  • L1Three Schema definitions: TypedDict / Dataclass / Pydantic
  • L2reducer controls field update strategy
  • L3Multiple Schema to achieve interface isolation
  • L4Message pruning avoids context explosion
Memory (memory management)
  • L5Scrolling summary compresses conversation history
  • L6External database for cross-session persistence
  • • In-memory → SQLite → PostgreSQL
  • • Thread ID isolates multi-user sessions
Detailed explanation of 6 sub-courses
1

State Schema

TypedDict · Dataclass · Pydantic BaseModel

State Schema is the "data contract" of the graph, which defines the data structure shared by all nodes. LangGraph supports three Python data types as Schema.

  • TypedDict: Dictionary stylestate["key"], the most lightweight, no runtime verification, official example default usage
  • Dataclass: attribute stylestate.key, native supportfield(default_factory=...)Default value
  • Pydantic BaseModel: Strong verification during instantiation,@field_validatorCustom rules, preferred for production environment
  • common rules: The node always returns a dictionary; the default update strategy for fields is Overwrite.
Core insights:State is the only communication medium between nodes - Node A writes, Node B reads. Schema is the "format specification" of this shared memory.
In-depth explanation →
2

State Reducers

operator.add · add_messages · RemoveMessage

reducer defines the update rules for fields:reducer(旧值, 新值) → 最终值. It is the core mechanism for resolving write conflicts on parallel nodes.

  • Default override: No annotation, the new value directly replaces the old value (applicable to single node writing)
  • operator.addAnnotated[list[int], add], list appending, resolving parallel conflicts
  • Custom reducer: Handle edge cases such as None and merge logic arbitrarily
  • add_messages: Designed specifically for message lists, supporting appending, ID overwriting, and RemoveMessage deletion
Parallel traps:If parallel nodes in the same step write non-reducer fields at the same time, it will triggerInvalidUpdateError, the reducer must be specified with Annotated annotation.
In-depth explanation →
3

Multiple Schemas

Private state · Input Schema · Output Schema

When all nodes share the same Schema, internal intermediate data can pollute the public interface. Multiple Schema mechanisms give you precise control over data boundaries.

  • Private State: Internal intermediate data is passed between nodes and is not exposed to the caller.
  • Input Schema: Filter the fields passed in by the caller. The graph only accepts declared input.
  • Output Schema: Filter the output fields of the graph, the caller only sees the final result
  • OverallState: Complete Schema inside the graph, including all intermediate state fields
Design principles:Input/Output Schema implements "separation of interface and implementation" - the caller only sees questions and answers, and implementation details such as internal reasoning drafts and search keywords are completely hidden.
In-depth explanation →
4

Trim & Filter Messages

filter_messages · trim_messages · Token control

The LLM context window is capped and the conversation history grows indefinitely. Each of the three options has its own trade-offs, controlling the scope of messages sent to the model.

  • RemoveMessage: Physically delete messages at the State layer, and the history is truly reduced.
  • filter_messages: Filter within the node, filter by type/name, State remains intact
  • trim_messages: Exactly crop according to the number of Tokens, retaining the latest N token messages
  • Strategy selection: The latter two only affect messages sent to LLM and do not modify the State history record.
Token economy:Sending all historical messages to the model = soaring fees + soaring delays + reporting errors when the token upper limit is exceeded. Message management is an essential skill for producing Chatbots.
In-depth explanation →
5

Chatbot Summarization

Scroll Summary · MemorySaver · Conditional Edge Triggering

Use LLM to automatically generate a "rolling summary" of the conversation, allowing the Chatbot to remember the entire conversation without consuming a large amount of Tokens.

  • summary field: Extension in MessagesState to store LLM-generated conversation summaries
  • call_model node: Inject the existing summary into SystemMessage and send it to LLM together with the latest message
  • summarize_conversation node: Triggered when threshold is exceeded, new digest is generated and old messages are deleted
  • MemorySaver: Memory checkpoint, maintain state of cross-turn conversations within the same process
How it works:Number of messages > 6 → trigger summary node → LLM compression history → only keep the last 2 original messages, and compress the rest into summary. The summary is injected as context during the next round of dialogue.
In-depth explanation →
6

Chatbot External Memory

SQLite · External checkpoints · Cross-session persistence

MemorySaver stores the state in memory and disappears after the process is restarted. External database checkpoints make conversation memory truly persistent across sessions.

  • Limitations of MemorySaver: Cleared when the process exits, not suitable for production environment
  • SqliteSaver: Write the checkpoint to the local SQLite file, which can be restored after the process is restarted.
  • AsyncSqliteSaver: Asynchronous version, suitable for high concurrency scenarios
  • Thread ID: Assign unique thread_id to each user session, isolate multi-user status
Upgrade path:MemorySaver is used in the development stage → SqliteSaver is used in local testing → PostgresSaver or Redis is used in the production environment. To switch, just replacecheckpointerParameters, figure code zero modification.
In-depth explanation →
Horizontal comparison of core concepts
Topic Core API/Syntax Problem solved Applicable scenarios
TypedDict Schema class S(TypedDict): x: str Dictionary style access, zero installation cost Prototype, teaching, simple Agent
Pydantic Schema class S(BaseModel): @field_validator Strong data legality verification at runtime Production Agent, data comes from outside
operator.add Reducer Annotated[list[int], add] Parallel nodes write the same field without reporting an error Map-Reduce, parallel branch collection results
add_messages Reducer Annotated[list[AnyMessage], add_messages] Message append + overwrite + delete All dialogue agents (most commonly used)
Private State Passing individual Schema classes between nodes Internal intermediate data does not pollute the public interface Intermediate result delivery for complex multi-node graphs
Input/Output Schema StateGraph(Overall, input=In, output=Out) Exactly filter the input and output fields of the graph Provide external API diagrams and interface design
trim_messages trim_messages(msgs, max_tokens=100, ...) Control the amount of messages sent to LLM according to the number of Tokens Long conversations save tokens and prevent upper limit errors.
Chatbot Summarization summaryField + conditional edge triggers summary node Compress history, retain semantics, and not lose information Dialogue Agent that requires long-term memory
External Memory SqliteSaver / AsyncPostgresSaver The conversation state can still be restored after the process is restarted. Production environment, real user system
Module core harvest
🗂️

State is the basis of everything

The State Schema is the "data contract" of the graph through which all nodes communicate. Choosing the right Schema type (TypedDict / Dataclass / Pydantic) determines the balance between development efficiency and operational safety.

⚙️

reducer controls update behavior

passAnnotated[T, fn]Specifying a reducer for a field is the only official way to resolve parallel node conflicts.add_messagesIt is the standard reducer of dialogue agent.

🔒

Multiple Schema to achieve interface isolation

Private State isolates internal implementation details, and Input/Output Schema accurately defines the public interface of the graph, allowing complex graphs to maintain a clean boundary design.

✂️

Message management is a must-have skill

The production Chatbot must control the volume of messages sent to the LLM.filter_messagesfilter by type,trim_messagesCropped by Token, neither modifies the State history.

🧠

Scroll summary extends memory limit

Let LLM automatically compress the conversation history into a summary to achieve the effect of "remembering the entire conversation but only issuing a small number of Tokens". Summary injects SystemMessage as background context.

💾

External persistence is necessary for production

MemorySaver is only used for development and debugging. The production environment must use an external database (SQLite → PostgreSQL). To switch just replacecheckpointer, the rest of the code remains unchanged.

Return to Module 2 Home Page
Comprehensive practical projects
Project

Intelligent research assistant
AI Research Assistant

An intelligent assistant that supports multiple dialogue rounds, automatic history compression, and cross-session persistent memory. It organically integrates all six core knowledge points of Module 2 and is a LangGraph project skeleton that can be directly used in the production environment.

L1 Pydantic Schema L2 add_messages L3 Multiple Schema L4 trim_messages L5 rolling summary L6 SQLite persistence
Project characteristics
Multiple rounds of dialogue + cross-session memory
Automatic scrolling summary compression history
Precise control of Token usage
Complete isolation between interface and implementation
Multi-user session isolation
Memory is not lost after process restart
System architecture diagram
InputState · L3
user_message: str
SqliteSaver · L6
thread_id isolate session
ResearchState(OverallState · L1+L2)
call_model node
L4trim_messages control token
L5Inject summary as context
L2add_messages append replies
conditional edge
Number of messages > 6?
summarize node
L5LLM generates rolling summaries
L2RemoveMessage deletes old messages
L2Only keep the last 2 original messages
OutputState · L3
messages: list[AnyMessage]  ·  summary: str
Code block 1 / 6
Schema definition - L1 + L3
# ── 依赖导入 ──────────────────────────────────────────────
from typing import Annotated, Optional, Literal
from typing_extensions import TypedDict
from pydantic import BaseModel, field_validator
from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, SystemMessage, RemoveMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import trim_messages
from langgraph.checkpoint.sqlite import SqliteSaver
import sqlite3

# ════════════════════════════════════════════════════════
# 【L3】输入 Schema ── 图的公开入口,只暴露给调用者的字段
# ════════════════════════════════════════════════════════
class InputState(TypedDict):
    user_message: str   # 调用者只需传这一个字段

# ════════════════════════════════════════════════════════
# 【L3】输出 Schema ── 图的公开出口,过滤掉内部中间字段
# ════════════════════════════════════════════════════════
class OutputState(TypedDict):
    messages: list[AnyMessage]  # 调用者可见的对话历史
    summary:  str               # 调用者可见的当前摘要

# ════════════════════════════════════════════════════════
# 【L1 + L2】OverallState ── 图内部完整状态,包含所有字段
#   · TypedDict(L1):零安装成本,字典风格访问
#   · Annotated + add_messages(L2):消息追加 Reducer
# ════════════════════════════════════════════════════════
class ResearchState(TypedDict):
    # 【L2】add_messages Reducer:每轮对话追加,不覆盖
    messages: Annotated[list[AnyMessage], add_messages]

    # 【L5】摘要字段:超过阈值时由 summarize 节点写入
    summary:      str

    # 普通字段:默认覆盖策略(L2:无 Reducer 即覆盖)
    user_message: str

# ════════════════════════════════════════════════════════
# 【L1】Pydantic Schema ── 运行时强校验用户会话配置
#   · @field_validator 确保 session_id 合法
#   · 非法输入在进入图之前就被拦截
# ════════════════════════════════════════════════════════
class UserSession(BaseModel):
    session_id:   str
    max_messages: int = 6   # 触发摘要的消息阈值

    @field_validator("session_id")
    @classmethod
    def validate_session_id(cls, v):
        if not v or len(v) < 4:
            raise ValueError("session_id 至少需要 4 个字符")
        return v.strip()
L3 · Interface isolation of multiple schemas

InputStateThere is only one field, and the caller does not need to know the internal implementation of the graph;OutputStateOnly exposes two result fields, the ones inside the graphuser_messageThe intermediate fields are completely transparent to the caller. This is a direct manifestation of Lesson 3 "Separation of interface and implementation".

L1 · Use in selected combinations

ResearchStateUse TypedDict (lightweight, no dependencies, suitable for the internal state of the graph);UserSessionUse Pydantic (strong validation of external input). The two types are used in combination, each performing its own duties, which is exactly the practice recommended in Lesson 1 selection.

Code block 2 / 6
Persistence configuration - L6
# ════════════════════════════════════════════════════════
# 【L6】外部持久化配置
#   · SqliteSaver 将图的检查点(Checkpoint)写入 SQLite 文件
#   · 进程重启后,同一 thread_id 的对话历史可完整恢复
#   · 对比 MemorySaver:进程退出即全部丢失
# ════════════════════════════════════════════════════════
conn   = sqlite3.connect("research_assistant.db", check_same_thread=False)
memory = SqliteSaver(conn)   # 替换为 PostgresSaver 即可升级到生产级数据库

# LLM 初始化
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
L6 · Why use SqliteSaver instead of MemorySaver?

MemorySaverStored in the RAM of the Python process - the memory is reset when the Notebook is restarted or the service is deployed and updated.SqliteSaverWrite a snapshot of each execution step to a disk file. After the process restarts, just pass in the samethread_idYou can continue from the breakpoint. Here'scheck_same_thread=FalseAllows multiple threads to access the same database connection concurrently - required in asynchronous frameworks such as FastAPI / Starlette.

Code block 3 / 6
call_model node - L4 + L5
def call_model(state: ResearchState) -> dict:
    """主对话节点:整合摘要上下文 + Token 控制 + LLM 调用"""
    summary  = state.get("summary", "")
    messages = state["messages"]

    # ── 【L4】trim_messages:按 Token 裁剪,只保留最近的消息 ──
    # strategy="last" 从最新消息开始保留,不满足 token 上限就截断
    # include_system=True 保留 SystemMessage(优先级最高)
    # 这里裁剪的是发给 LLM 的消息,State 中的完整历史不受影响
    trimmed = trim_messages(
        messages,
        max_tokens=1500,
        strategy="last",
        token_counter=llm,
        include_system=True,
        allow_partial=False,
    )

    # ── 【L5】将摘要注入 SystemMessage 作为背景上下文 ──
    # 当摘要存在时,LLM 能"记住"之前压缩掉的所有对话
    # 这解决了:消息被 RemoveMessage 删掉后如何保留语义信息
    system_content = "你是一个专业的智能研究助手,帮助用户深入探讨各类研究问题。"
    if summary:
        system_content += f"\n\n【对话摘要】以下是之前对话的压缩记录:\n{summary}"

    final_messages = [SystemMessage(content=system_content)] + trimmed
    response = llm.invoke(final_messages)

    # ── 【L2】add_messages Reducer 自动追加 response ──
    # 节点只需返回包含新消息的列表,Reducer 负责拼接到历史末尾
    return {"messages": [response]}
L4 · How trim_messages works

trim_messagesCount tokens forward from the end of the message list (the latest message). When the total exceedsmax_tokens=1500Stop and discard the remaining messages.Key: This only affects the content sent to LLM. The messages field in State still retains the complete history.This is essentially different from physical deletion of RemoveMessage from State.

L5 · Digest injection logic

After the summarize node compresses the history, old messages are deleted from the State but the summary is saved insummaryfield.call_modeldetectedsummaryIf it is not empty, just spell it into SystemMessage - in this way, even if the original message is no longer in State, LLM can still perceive the previous conversation content and implementsemantic continuity

Code block 4 / 6
summarize node + conditional edge - L5 + L2
def should_summarize(state: ResearchState) -> Literal["summarize", "end"]:
    """条件边:判断消息数量是否触发摘要阈值"""
    # 超过 6 条消息就触发摘要节点,否则直接结束
    if len(state["messages"]) > 6:
        return "summarize"
    return "end"


def summarize_conversation(state: ResearchState) -> dict:
    """摘要节点:压缩对话历史,物理删除旧消息"""
    existing_summary = state.get("summary", "")
    messages         = state["messages"]

    # ── 【L5】构建摘要 Prompt ──
    # 若已有摘要则在其基础上扩展(滚动摘要模式)
    # 若没有则从头生成(首次压缩)
    if existing_summary:
        prompt = (
            f"当前已有摘要:\n{existing_summary}\n\n"
            "请在此基础上,将以下新对话内容也纳入摘要,保持信息完整简洁:"
        )
    else:
        prompt = "请简洁总结以下对话的关键信息,保留重要的问答内容:"

    summary_messages = messages + [HumanMessage(content=prompt)]
    response = llm.invoke(summary_messages)

    # ── 【L2】RemoveMessage:物理删除旧消息,只保留最近 2 条 ──
    # messages[:-2] 取"最后 2 条之前"的所有消息作为删除目标
    # add_messages Reducer 识别到 RemoveMessage 后,按 id 从列表中移除
    # 这与 trim_messages(仅过滤发给 LLM 的内容)不同:这里是真正修改 State
    delete_messages = [RemoveMessage(id=m.id) for m in messages[:-2]]

    return {
        "summary":  response.content,  # 覆盖旧摘要(默认覆盖策略,L2)
        "messages": delete_messages,   # add_messages Reducer 执行删除(L2)
    }
L5 · Accumulation mechanism of rolling summary

Each time the digest is triggered, putExcerpt already + new conversationSend them together to LLM to generate an updated summary. This is the core of "rolling digest" - not rewrite every time, but on the old digestCumulative expansion. As the conversation continues, the summary will become more and more complete, while token consumption remains within control.

L2 · Collaboration between RemoveMessage and add_messages

RemoveMessageNot the actual message content, but aDelete operation instructions. When the nodeRemoveMessage(id=x)into the return list,add_messagesAfter reducer receives it, it will be found in the message list of Stateid=xmessage and physically remove it. This is an advanced use of the reducer mechanism.

Code block 5 / 6
Construction and assembly of diagrams - L3 + L6
# ════════════════════════════════════════════════════════
# 【L3】多重 Schema 装配
#   · 第一参数 ResearchState:图内部的完整 Schema
#   · input=InputState:调用者只能传 user_message
#   · output=OutputState:图只返回 messages 和 summary
# ════════════════════════════════════════════════════════
builder = StateGraph(
    ResearchState,
    input=InputState,
    output=OutputState,
)

# 注册节点
builder.add_node("call_model",            call_model)
builder.add_node("summarize_conversation", summarize_conversation)

# 连接边
builder.add_edge(START, "call_model")

# 条件边:call_model 执行后,根据消息数决定走向
builder.add_conditional_edges(
    "call_model",
    should_summarize,
    {"summarize": "summarize_conversation", "end": END}
)
builder.add_edge("summarize_conversation", END)

# ════════════════════════════════════════════════════════
# 【L6】挂载外部检查点,compile 时传入 SqliteSaver
#   · 每个节点执行完后,LangGraph 自动将 State 快照存入数据库
#   · 只需替换 checkpointer=memory 即可切换存储后端
# ════════════════════════════════════════════════════════
graph = builder.compile(checkpointer=memory)
L3 · The meaning of StateGraph multiple Schema parameters

StateGraph(ResearchState, input=InputState, output=OutputState)These three parameters define the three boundaries of the graph respectively:internal state(node reading and writing),Entrance contract(what the caller passes),export contract(what the caller gets). When the caller passes in additional fields (such as directly passingsummary), LangGraph will automatically ignore it to ensure the encapsulation of the graph.

Code block 6 / 6
Running example - L1 + L6
# ── 【L1】Pydantic 校验:确保 session_id 合法再启动 ──
session = UserSession(session_id="researcher_001", max_messages=6)
config  = {"configurable": {"thread_id": session.session_id}}

# ── 第 1 轮对话 ────────────────────────────────────────
result = graph.invoke(
    {"user_message": "深海生物是如何在极端高压环境中生存的?"},
    config=config,
)
# result 只包含 OutputState 的字段:messages 和 summary
print(result["messages"][-1].content)

# ── 第 2 轮:LangGraph 自动从 SQLite 恢复第 1 轮的状态 ──
result = graph.invoke(
    {"user_message": "这与极地生物的耐寒机制有何异同?"},
    config=config,
)

# ── 模拟进程重启后的跨会话恢复 ────────────────────────
# 重新建立数据库连接(模拟重启)
new_conn   = sqlite3.connect("research_assistant.db", check_same_thread=False)
new_memory = SqliteSaver(new_conn)
new_graph  = builder.compile(checkpointer=new_memory)

# 同一个 thread_id → 历史自动恢复,就像从未重启过
result = new_graph.invoke(
    {"user_message": "给我推荐几篇这个领域的经典论文"},
    config=config,  # thread_id 不变,记忆完整继承
)

# ── 多用户隔离示例 ──────────────────────────────────────
user_a_config = {"configurable": {"thread_id": "user_alice"}}
user_b_config = {"configurable": {"thread_id": "user_bob"}}
# alice 和 bob 的对话历史完全独立,互不干扰
L6 · How cross-session recovery works

every callgraph.invoke(), LangGraph is used firstthread_idFind the latest Checkpoint (State snapshot) in SQLite and load it before executing the node. The next time it is called, the State executed by this node is written back to the database.Process restart does not affect the memory, because the memory is on the disk, not in the memory.

L1 · Pydantic value at entry

UserSession(session_id="ab")will be thrown immediatelyValidationError(less than 4 characters), was blocked before entering the graph. This guarantees illegalthread_idIt will not create garbage records in the SQLite database and is a typical application of Pydantic for "boundary verification".

Overview of correspondence between knowledge points
Exact mapping of project parts to 6 sub-courses
Project components Corresponding courses Core API Problem solved
ResearchState(TypedDict) L1 · State Schema class S(TypedDict) Define data contracts for inter-node communication, with zero installation cost
UserSession(Pydantic) L1 · State Schema class S(BaseModel) + @field_validator Strong verification of external input parameters, illegal session_id is intercepted at the entrance
messages field L2 · State Reducers Annotated[list[AnyMessage], add_messages] Additional messages will be added to each round of conversation without overwriting the history.
RemoveMessage deletes old messages L2 · State Reducers RemoveMessage(id=m.id) Physically delete expired messages in State to free up space
summary field update L2 · State Reducers None Annotated (default override) Each time a new summary is generated, the old summary is directly overwritten, as expected.
InputState / OutputState L3 · Multiple Schemas StateGraph(Overall, input=In, output=Out) The caller only sees the entrance and exit, and the internal fields are completely hidden.
trim_messages in call_model L4 · Trim & Filter trim_messages(msgs, max_tokens=1500, ...) Controls the actual number of Tokens issued to LLM, and the State history remains unchanged.
summary injects SystemMessage L5 · Summarization SystemMessage(content=summary) Make LLM aware of compressed and deleted historical conversations
summarize_conversation node L5 · Summarization Conditional edges + LLM generate summary Compress conversations when thresholds are exceeded, preserving semantics while controlling the number of messages
SqliteSaver + thread_id L6 · External Memory SqliteSaver(conn) + compile(checkpointer=) Cross-process persistence, multi-user session isolation
Return to Module 2 Home Page