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
模块总结

从 Schema 定义到外部持久化,Module 2 构建了一套完整的
LangGraph 状态管理与对话记忆体系。

6 子课程
2 核心主题
3 Schema 类型
4 Reducer 策略
模块学习路径
Schema 定义
L1 · TypedDict
Dataclass · Pydantic
Reducer 机制
L2 · 覆盖 / 追加
并行冲突解决
多重 Schema
L3 · 私有状态
Input/Output 过滤
消息管理
L4 · filter / trim
Token 控制
摘要记忆
L5 · 滚动摘要
MemorySaver
外部持久化
L6 · SQLite
跨会话记忆

📌 模块两大主线

State(状态定义)
  • L1 三种 Schema 定义:TypedDict / Dataclass / Pydantic
  • L2 Reducer 控制字段更新策略
  • L3 多重 Schema 实现接口隔离
  • L4 消息裁剪避免上下文爆炸
Memory(记忆管理)
  • L5 滚动摘要压缩对话历史
  • L6 外部数据库实现跨会话持久化
  • • In-memory → SQLite → PostgreSQL
  • • Thread ID 隔离多用户会话
6 个子课程详解
1

State Schema

TypedDict · Dataclass · Pydantic BaseModel

State Schema 是图的"数据契约",定义了所有节点共享的数据结构。LangGraph 支持三种 Python 数据类型作为 Schema。

  • TypedDict:字典风格 state["key"],最轻量,无运行时校验,官方示例默认用法
  • Dataclass:属性风格 state.key,原生支持 field(default_factory=...) 默认值
  • Pydantic BaseModel:实例化时强校验,@field_validator 自定义规则,生产环境首选
  • 共同规则:节点始终返回字典;字段默认更新策略为覆盖(Overwrite)
核心洞察:State 是节点间唯一的通信介质——Node A 写入,Node B 读出。Schema 是这块共享内存的"格式说明书"。
深度讲解 →
2

State Reducers

operator.add · add_messages · RemoveMessage

Reducer 定义了字段的更新规则:reducer(旧值, 新值) → 最终值。它是解决并行节点写入冲突的核心机制。

  • 默认覆盖:无注解,新值直接替换旧值(单节点写入适用)
  • operator.addAnnotated[list[int], add],列表追加,解决并行冲突
  • 自定义 Reducer:处理 None 等边界情况,任意合并逻辑
  • add_messages:专为消息列表设计,支持追加、ID 覆写、RemoveMessage 删除
并行陷阱:同一步骤内的并行节点若同时写入无 Reducer 字段,会触发 InvalidUpdateError,必须用 Annotated 注解指定 Reducer。
深度讲解 →
3

Multiple Schemas

私有状态 · Input Schema · Output Schema

当所有节点共用同一 Schema 时,内部中间数据会污染公开接口。多重 Schema 机制让你精确控制数据边界。

  • 私有状态(Private State):节点间传递内部中间数据,不暴露给调用者
  • Input Schema:过滤调用者传入的字段,图只接受声明的输入
  • Output Schema:过滤图的输出字段,调用者只看到最终结果
  • OverallState:图内部完整 Schema,包含所有中间状态字段
设计原则:Input/Output Schema 实现了"接口与实现分离"——调用者只看到问题和答案,内部推理草稿、搜索关键词等实现细节完全隐藏。
深度讲解 →
4

Trim & Filter Messages

filter_messages · trim_messages · Token 控制

LLM 上下文窗口有上限,而对话历史会无限增长。三种方案各有取舍,控制发给模型的消息范围。

  • RemoveMessage:在 State 层物理删除消息,历史真正减少
  • filter_messages:节点内过滤,按类型/name 筛选,State 保持完整
  • trim_messages:按 Token 数量精确裁剪,保留最近 N token 的消息
  • 策略选择:后两种仅影响发给 LLM 的消息,不修改 State 历史记录
Token 经济:把全部历史消息都发给模型 = 费用激增 + 延迟飙高 + 超 token 上限报错。消息管理是生产 Chatbot 的必备技能。
深度讲解 →
5

Chatbot Summarization

滚动摘要 · MemorySaver · 条件边触发

用 LLM 自动生成对话"滚动摘要",让 Chatbot 既记得全程对话,又不消耗大量 Token。

  • summary 字段:在 MessagesState 中扩展,存储 LLM 生成的对话摘要
  • call_model 节点:将现有摘要注入 SystemMessage,与最新消息一起送给 LLM
  • summarize_conversation 节点:超过阈值时触发,生成新摘要并删除旧消息
  • MemorySaver:内存检查点,同一进程内跨轮对话保持状态
工作原理:消息数 > 6 → 触发摘要节点 → LLM 压缩历史 → 只保留最近 2 条原始消息,其余压缩成 summary。下轮对话时 summary 作为上下文背景注入。
深度讲解 →
6

Chatbot External Memory

SQLite · 外部检查点 · 跨会话持久化

MemorySaver 将状态存在内存中,进程重启后全部消失。外部数据库检查点让对话记忆真正跨会话持久化。

  • MemorySaver 的局限:进程退出即清空,不适合生产环境
  • SqliteSaver:将检查点写入本地 SQLite 文件,进程重启后可恢复
  • AsyncSqliteSaver:异步版本,适合高并发场景
  • Thread ID:每个用户会话分配唯一 thread_id,隔离多用户状态
升级路径:开发阶段用 MemorySaver → 本地测试用 SqliteSaver → 生产环境换 PostgresSaver 或 Redis,切换只需替换 checkpointer 参数,图代码零修改。
深度讲解 →
核心概念横向对比
主题 核心 API / 语法 解决的问题 适用场景
TypedDict Schema class S(TypedDict): x: str 字典风格访问,零安装成本 原型、教学、简单 Agent
Pydantic Schema class S(BaseModel): @field_validator 运行时数据合法性强校验 生产 Agent,数据来自外部
operator.add Reducer Annotated[list[int], add] 并行节点写同一字段不报错 Map-Reduce、并行分支收集结果
add_messages Reducer Annotated[list[AnyMessage], add_messages] 消息追加 + 覆写 + 删除 所有对话类 Agent(最常用)
Private State 节点间传递单独的 Schema 类 内部中间数据不污染公开接口 复杂多节点图的中间结果传递
Input/Output Schema StateGraph(Overall, input=In, output=Out) 精确过滤图的输入输出字段 对外提供 API 的图,接口设计
trim_messages trim_messages(msgs, max_tokens=100, ...) 按 Token 数控制发给 LLM 的消息量 长对话节省 Token,防止上限报错
Chatbot Summarization summary 字段 + 条件边触发摘要节点 压缩历史,保留语义,不丢信息 需要长期记忆的对话 Agent
External Memory SqliteSaver / AsyncPostgresSaver 进程重启后对话状态仍可恢复 生产环境,真实用户系统
模块核心收获
🗂️

State 是一切的基础

State Schema 是图的"数据契约",所有节点通过它通信。选对 Schema 类型(TypedDict / Dataclass / Pydantic)决定了开发效率与运行安全性的平衡点。

⚙️

Reducer 控制更新行为

通过 Annotated[T, fn] 为字段指定 Reducer,是解决并行节点冲突的唯一官方方式。add_messages 是对话 Agent 的标配 Reducer。

🔒

多重 Schema 实现接口隔离

Private State 隔离内部实现细节,Input/Output Schema 精确定义图的公开接口,让复杂图保持整洁的边界设计。

✂️

消息管理是必备技能

生产 Chatbot 必须控制发给 LLM 的消息量。filter_messages 按类型过滤,trim_messages 按 Token 裁剪,两者都不修改 State 历史。

🧠

滚动摘要延伸记忆上限

让 LLM 自动压缩对话历史为摘要,实现"记住全程对话但只发少量 Token"的效果。摘要注入 SystemMessage 作为背景上下文。

💾

外部持久化是生产必要

MemorySaver 仅用于开发调试,生产环境必须换用外部数据库(SQLite → PostgreSQL)。切换只需替换 checkpointer,其余代码不变。

返回 Module 2 首页
综合实战项目
Project

智能研究助手
AI Research Assistant

一个支持多轮对话、自动压缩历史、跨会话持久记忆的智能助手。它将 Module 2 的全部 6 个核心知识点有机整合,是一个可直接用于生产环境的 LangGraph 项目骨架。

L1 Pydantic Schema L2 add_messages L3 多重 Schema L4 trim_messages L5 滚动摘要 L6 SQLite 持久化
项目特性
多轮对话 + 跨会话记忆
自动滚动摘要压缩历史
Token 用量精确控制
接口与实现完全隔离
多用户会话隔离
进程重启后记忆不丢失
系统架构图
InputState · L3
user_message: str
SqliteSaver · L6
thread_id 隔离会话
ResearchState(OverallState · L1+L2)
call_model 节点
L4 trim_messages 控制 Token
L5 注入 summary 为上下文
L2 add_messages 追加回复
条件边
消息数 > 6?
summarize 节点
L5 LLM 生成滚动摘要
L2 RemoveMessage 删旧消息
L2 仅保留最近 2 条原始消息
OutputState · L3
messages: list[AnyMessage]  ·  summary: str
代码块 1 / 6
Schema 定义 —— 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 · 多重 Schema 的接口隔离

InputState 只有一个字段,调用者无需了解图的内部实现;OutputState 只暴露两个结果字段,图内部的 user_message 等中间字段对调用者完全透明。这是 Lesson 3 "接口与实现分离"的直接体现。

L1 · 选型组合使用

ResearchState 用 TypedDict(轻量、无依赖,适合图内部状态);UserSession 用 Pydantic(对外部输入强校验)。两种类型组合使用,各司其职,这正是 Lesson 1 选型建议的实践。

代码块 2 / 6
持久化配置 —— 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 · 为什么用 SqliteSaver 而不是 MemorySaver?

MemorySaver 存在 Python 进程的 RAM 里——Notebook 重启或服务部署更新,记忆归零。SqliteSaver 将每个执行步骤的快照写入磁盘文件,进程重启后只需传入相同的 thread_id 即可从断点继续。此处的 check_same_thread=False 允许多线程并发访问同一数据库连接——在 FastAPI / Starlette 等异步框架中必需。

代码块 3 / 6
call_model 节点 —— 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 · trim_messages 的工作原理

trim_messages 从消息列表末尾(最新消息)向前计数 Token,当累计超过 max_tokens=1500 时停止,把剩余消息丢弃。关键:这只影响发给 LLM 的内容,State 中 messages 字段依然保留完整历史。 这与 RemoveMessage 从 State 物理删除是本质区别。

L5 · 摘要注入的逻辑

在 summarize 节点压缩历史后,旧消息从 State 删除,但摘要保存在 summary 字段。call_model 检测到 summary 非空,就把它拼入 SystemMessage——这样即使原始消息已经不在 State 里,LLM 仍能感知之前的对话内容,实现语义连续性

代码块 4 / 6
summarize 节点 + 条件边 —— 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 · 滚动摘要的累积机制

每次触发摘要时,把已有摘要 + 新对话一起发给 LLM,生成更新后的摘要。这是"滚动摘要"的核心——不是每次重写,而是在旧摘要上累积扩展。随着对话持续,摘要会越来越完整,而 Token 消耗始终维持在可控范围内。

L2 · RemoveMessage 与 add_messages 的协作

RemoveMessage 不是真正的消息内容,而是一个删除操作指令。当节点把 RemoveMessage(id=x) 放入返回列表,add_messages Reducer 收到后会在 State 的消息列表中找到 id=x 的消息并物理移除它。这是 Reducer 机制的高级用法。

代码块 5 / 6
图的构建与装配 —— 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 · StateGraph 多重 Schema 参数的含义

StateGraph(ResearchState, input=InputState, output=OutputState) 这三个参数分别定义了图的三个边界:内部状态(节点读写)、入口契约(调用者传什么)、出口契约(调用者拿到什么)。当调用者传入额外字段(比如直接传 summary)时,LangGraph 会自动忽略它,保证图的封装性。

代码块 6 / 6
运行示例 —— 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 · 跨会话恢复的工作机制

每次调用 graph.invoke(),LangGraph 先用 thread_id 在 SQLite 中查找最新的 Checkpoint(State 快照),加载后再执行节点。下次调用时,这个节点执行完的 State 又被写回数据库。进程重启不影响记忆,因为记忆在磁盘里,不在内存里。

L1 · Pydantic 在入口的价值

UserSession(session_id="ab") 会立即抛出 ValidationError(不足 4 字符),在进入图之前就被拦截。这保证了非法的 thread_id 不会在 SQLite 数据库中创建垃圾记录,是 Pydantic 做"边界校验"的典型应用。

知识点对应关系总览
项目各部分与 6 个子课程的精确对应
项目组成部分 对应课程 核心 API 解决的问题
ResearchState(TypedDict) L1 · State Schema class S(TypedDict) 定义节点间通信的数据契约,零安装成本
UserSession(Pydantic) L1 · State Schema class S(BaseModel) + @field_validator 外部入参强校验,非法 session_id 在入口被拦截
messages 字段 L2 · State Reducers Annotated[list[AnyMessage], add_messages] 每轮对话追加消息,不覆盖历史记录
RemoveMessage 删除旧消息 L2 · State Reducers RemoveMessage(id=m.id) 物理删除 State 中的过期消息,释放空间
summary 字段更新 L2 · State Reducers 无 Annotated(默认覆盖) 每次生成新摘要直接覆盖旧摘要,符合预期
InputState / OutputState L3 · Multiple Schemas StateGraph(Overall, input=In, output=Out) 调用者只见入口和出口,内部字段完全隐藏
trim_messages in call_model L4 · Trim & Filter trim_messages(msgs, max_tokens=1500, ...) 控制实际发给 LLM 的 Token 数,State 历史不变
summary 注入 SystemMessage L5 · Summarization SystemMessage(content=summary) 让 LLM 感知被压缩删除的历史对话
summarize_conversation 节点 L5 · Summarization 条件边 + LLM 生成摘要 超过阈值时压缩对话,保留语义同时控制消息数量
SqliteSaver + thread_id L6 · External Memory SqliteSaver(conn) + compile(checkpointer=) 跨进程持久化,多用户会话隔离
返回 Module 2 首页