LangGraph高级特性

流式处理(Streaming)状态持久化(Persistence)时间回溯(Time-Travel)子图(Subgraphs)

流式处理(Streaming)

 LangGraph 里,流式处理的范围更大。它不仅能输出模型生成过程,还能把图执行过程中的状态变化、节点进度、子图过程、自定义消息一边执行一边暴露出来

这也是 LangGraph 流式处理和普通模型流式输出最大的区别:

  • 普通 LLM 流式:更关注“模型文字怎么一点点吐出来”
  • LangGraph 流式:更关注“整张图现在跑到哪一步了,状态发生了什么变化”

可以先把 LangGraph Streaming 理解成:把图执行过程拆开给你看,而不是等整张图完全跑完才给最终结果。

LangGraph 图本身实现了 Runnable 接口,所以自然拥有这些流式能力。这也让它和 LangChain Runnable / LCEL 体系能够衔接起来

  • invoke():等整张图跑完,再返回最终结果
  • stream() / astream():图在运行过程中,就把中间结果分批往外送

多补一个关键点:stream() 不是只有一种固定输出格式。你在调用它时,通常还会配合一个很重要的参数:stream_mode。这个参数决定了“图在执行过程中,到底往外流什么”,比如是流完整状态、流增量更新、流模型消息片段,还是流自定义进度信息

stream_model参数:

模式 含义
values 每一步结束后输出当前完整状态快照
updates 每一步结束后只输出本步的增量更新
messages 输出 LLM 生成过程中的消息片段 / token
custom 输出节点内部主动写出的自定义消息
debug 输出更完整、更底层的调试信息

如果你想同时拿到多种流,可以直接传列表,例如:

stream_mode=["updates", "custom"]

values和update的区别:

  • values:每一步都给你“当前完整 State 长什么样”
  • updates:每一步只给你“这一小步到底改了哪些字段”.

从调试体验上说:

  • values 更像“每一步的全量快照”
  • updates 更像“每一步的增量日志”

案例:流图状态

  

# 流式传输图状态:对比 stream_mode 为 updates 与 values 时,每一步向调用方推送的内容差异
# stream(..., stream_mode="updates")`:每步只推送“本节点本次改了什么”,更像增量日志
# `stream(..., stream_mode="values")`:每步推送“当前完整状态长什么样”,更像全量快照
# 

from typing import TypedDict

from langgraph.graph import StateGraph, START, END
# 声明约束
class DiliState(TypedDict):
    topic: str
    joke: str
# 节点1
def refine_topic(state: DiliState):
    return {"topic": state["topic"] + " and cats"}
# 节点2
def generate_joke(state: DiliState):
    return {"joke": f"This is a joke about {state['topic']}"}

def main():
    graph = (
        StateGraph(DiliState)
        .add_node(refine_topic)
        .add_node(generate_joke)
        .add_edge(START, "refine_topic")
        .add_edge("refine_topic", "generate_joke")
        .add_edge("generate_joke", END)
        .compile()
    )

    # updates:每步结束后只流出「本步对状态的更新」
    for chunk in graph.stream({"topic": "ice cream"}, stream_mode="updates"):
        print(chunk)

    print()

    # values:每步结束后流出「当前完整 state」(未写字段可能仍为空字符串等初始形态)
    for chunk in graph.stream({"topic": "ice cream"}, stream_mode="values"):
        print(chunk)


if __name__ == "__main__":
    main()   
    
"""
【输出示例】
{'refine_topic': {'topic': 'ice cream and cats'}}
{'generate_joke': {'joke': 'This is a joke about ice cream and cats'}}

{'topic': 'ice cream'}
{'topic': 'ice cream and cats'}
{'topic': 'ice cream and cats', 'joke': 'This is a joke about ice cream and cats'}
"""

多模式流与Debug

当你把 stream_mode 设成列表时,一次运行里就能同时拿到多种类型的流。这个案例最值得观察的是:

  • 多种模式一起开时,输出长什么样
  • debug 模式为什么更适合调试,而不是直接拿去做业务 UI
# :同一图依次演示 values、updates、列表组合 [values, updates]、以及 debug 模式
# stream_mode 为列表时,每次迭代得到 (mode, chunk) 元组,便于前端按类型分别处理
# `values` 看“全貌”,`updates` 看“增量”;`debug` 输出更细,适合调试,不适合直接当业务输出
# 这个案例的核心价值是帮你建立“同一张图可以同时暴露多种观察视角”,而不是背住某个模式名
# 节点函数返回的字典仍按 State 的 Reducer 合并;本例字段未显式 Annotated,默认就是覆盖更新

from typing import TypedDict

from langgraph.graph import StateGraph, START, END

class DiliState(TypedDict):
    question: str
    answer: str
    confidence: float  # 置信度分数
    steps: list
    
def think(state: DiliState) -> DiliState:
    """思考节点:模拟多步推理,写入 steps。"""
    question = state["question"]
    steps = [f"分析问题: {question}", "检索相关知识", "形成初步答案"]
    return {"steps": steps}


def respond(state: DiliState) -> DiliState:
    """回应节点:根据关键词生成答案与置信度。"""
    question = state["question"]
    if "天气" in question:
        answer = "今天天气晴朗"
        confidence = 0.9
    elif "时间" in question:
        answer = "现在是上午10点"
        confidence = 0.8
    else:
        answer = "这是一个很好的问题"
        confidence = 0.7

    return {
        "answer": answer,
        "confidence": confidence,
    }
    
def reflect(state: DiliState) -> DiliState:
    """反思节点:在 steps 上追加校验与结论。"""
    answer = state["answer"]
    confidence = state["confidence"]
    steps = state.get("steps", [])

    steps.append(f"验证答案: {answer}")
    steps.append(f"置信度评估: {confidence}")

    if confidence > 0.8:
        conclusion = "高置信度答案"
    elif confidence > 0.5:
        conclusion = "中等置信度答案"
    else:
        conclusion = "低置信度答案"

    steps.append(f"结论: {conclusion}")

    return {"steps": steps}

def main():
    builder = StateGraph(DiliState)
    builder.add_node("think", think)
    builder.add_node("respond", respond)
    builder.add_node("reflect", reflect)

    builder.add_edge(START, "think")
    builder.add_edge("think", "respond")
    builder.add_edge("respond", "reflect")
    builder.add_edge("reflect", END)

    graph = builder.compile()

    print("=== LangGraph 多模式流式传输演示 ===\n")

    input_state = {
        "question": "今天天气怎么样?",
        "answer": "",
        "confidence": 0.0,
        "steps": [],
    }

    print("--- 1. 使用 stream_mode='values' 模式 ---")
    print("显示每一步执行后的完整状态:")
    for chunk in graph.stream(input_state, stream_mode="values"):
        print(f"  {chunk}")

    print("\n" + "=" * 60 + "\n")

    print("--- 2. 使用 stream_mode='updates' 模式 ---")
    print("只显示每一步的状态更新:")
    for chunk in graph.stream(input_state, stream_mode="updates"):
        print(f"  {chunk}")

    print("\n" + "=" * 60 + "\n")

    print("--- 3. 同时使用 stream_mode=[values, updates] 多种流模式 ---")
    print("同时显示完整状态和状态更新:")
    for mode, chunk in graph.stream(input_state, stream_mode=["values", "updates"]):
        print(f"  [{mode}]: {chunk}")

    print("\n" + "=" * 60 + "\n")

    print("--- 4. 使用 debug 模式 ---")
    print("显示详细的调试信息:")
    try:
        for chunk in graph.stream(input_state, stream_mode="debug"):
            print(f"  {chunk}")
    except Exception as e:
        print(f"  Debug模式可能需要特殊配置: {e}")


if __name__ == "__main__":
    main()
    
"""
【输出示例】
=== LangGraph 多模式流式传输演示 ===

--- 1. 使用 stream_mode='values' 模式 ---
显示每一步执行后的完整状态:
  {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': []}
  {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}
  {'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}
  {'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}

============================================================

--- 2. 使用 stream_mode='updates' 模式 ---
只显示每一步的状态更新:
  {'think': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}}
  {'respond': {'answer': '今天天气晴朗', 'confidence': 0.9}}
  {'reflect': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}}

============================================================

--- 3. 同时使用 stream_mode=[values, updates] 多种流模式 ---
同时显示完整状态和状态更新:
  [values]: {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': []}
  [updates]: {'think': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}}
  [values]: {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}
  [updates]: {'respond': {'answer': '今天天气晴朗', 'confidence': 0.9}}
  [values]: {'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}
  [updates]: {'reflect': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}}
  [values]: {'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}

============================================================

--- 4. 使用 debug 模式 ---
显示详细的调试信息:
  {'step': 1, 'timestamp': '2026-03-23T10:18:42.693927+00:00', 'type': 'task', 'payload': {'id': '11d771f2-98ac-b00c-931e-2b5bcb28f2ec', 'name': 'think', 'input': {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': []}, 'triggers': ('branch:to:think',)}}
  {'step': 1, 'timestamp': '2026-03-23T10:18:42.693983+00:00', 'type': 'task_result', 'payload': {'id': '11d771f2-98ac-b00c-931e-2b5bcb28f2ec', 'name': 'think', 'error': None, 'result': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}, 'interrupts': []}}
  {'step': 2, 'timestamp': '2026-03-23T10:18:42.694026+00:00', 'type': 'task', 'payload': {'id': 'a3af09a2-2f1a-d7f5-7957-55d931a52ed7', 'name': 'respond', 'input': {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}, 'triggers': ('branch:to:respond',)}}
  {'step': 2, 'timestamp': '2026-03-23T10:18:42.694074+00:00', 'type': 'task_result', 'payload': {'id': 'a3af09a2-2f1a-d7f5-7957-55d931a52ed7', 'name': 'respond', 'error': None, 'result': {'answer': '今天天气晴朗', 'confidence': 0.9}, 'interrupts': []}}
  {'step': 3, 'timestamp': '2026-03-23T10:18:42.694113+00:00', 'type': 'task', 'payload': {'id': 'c2627962-4fda-05bb-b1f5-5c40e36195a7', 'name': 'reflect', 'input': {'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}, 'triggers': ('branch:to:reflect',)}}
  {'step': 3, 'timestamp': '2026-03-23T10:18:42.694234+00:00', 'type': 'task_result', 'payload': {'id': 'c2627962-4fda-05bb-b1f5-5c40e36195a7', 'name': 'reflect', 'error': None, 'result': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}, 'interrupts': []}}
"""

LLM逐token流式输出:

  如果某个节点里调用了大模型,messages 模式就特别有价值。它可以帮助你在图运行过程中,直接拿到模型生成的消息片段,而不用等节点完全执行完。这也是为什么 LangGraph Streaming 不只是“图状态流”,它还能把模型输出也纳入统一流式体系。

# messages 流模式:从图中调用 LLM 的节点逐 token(或片段)推送输出,便于打字机效果
# stream_mode="messages" 时,每次迭代一般为 (message_chunk, metadata):chunk 为模型输出片段,metadata 标明节点等上下文
# 流式消费侧通常关心 `chunk.content` 和 `metadata`;前者是输出片段,后者帮助你知道这些片段来自哪个节点。
# 需配置环境变量(如 aliQwen-api)与网络;模型、base_url 按你本地教程为准

import os
from typing import TypedDict

from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START

from dotenv import load_dotenv

load_dotenv(encoding="utf-8")

class State(TypedDict):
    query: str
    answer: str

def node(state: State):
    print("开始调用 node 节点")

    model = init_chat_model(
        model="qwen-plus",
        model_provider="openai",
        api_key=os.getenv("aliQwen-api"),
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    )

    llm_result = model.invoke([("user", state["query"])])
    print("llm invoke 结束", end="\n\n")

    return {"answer": llm_result}

def main():
    graph = (
        StateGraph(state_schema=State).add_node(node).add_edge(START, "node").compile()
    )

    inputs = {"query": "帮我生成一个200字的小学生作文,主题为我的一天"}

    # messages:从图内触发的大模型调用处流式输出;(chunk, metadata) 见官方文档
    for chunk, _metadata in graph.stream(inputs, stream_mode="messages"):
        # print(f"type of chunk:{type(chunk)}")  # 调试时可打开
        print(chunk.content, end="")
        # print(chunk, end="")


if __name__ == "__main__":
    main()
    
    

自定义数据流:

有时候你想流的不是状态,也不是 token,而是业务自定义进度。例如:

  • “正在检索知识库”
  • “正在生成回答”
  • “当前进度 60%”

这时就可以在节点内部通过流写入器主动写出自定义数据,然后用 custom 模式接收

这两个案例的关系可以这样理解:

  • StreamCustomDataSimple.py:先看最小可运行版本
  • StreamCustomData.py:再看更贴近真实项目的进度与组合模式写法
# 自定义流(custom)最简版:在节点内通过 get_stream_writer() 写入任意可序列化数据,stream 侧用 custom 接收。
# 这是 `custom` 模式的最小案例,重点不是业务逻辑,而是先看懂“节点内部怎么主动写出一段流式消息”
# `get_stream_writer()` 仅在图执行(stream/astream)过程中有效;调用 `graph.stream` 时,`stream_mode` 里必须包含 `custom`
# 自定义块与状态更新是分开的:前者更适合 UI/日志/进度提示,后者仍然通过 State 和 Reducer 管理。

from typing import TypedDict

from langgraph.config import get_stream_writer
from langgraph.graph import StateGraph, START, END


class State(TypedDict):
    query: str
    answer: str

def nodeStart(state: State):
    writer = get_stream_writer()
    writer({"startMsg", "嘿,是我,你要进我的米奇妙妙屋吗?"})
    return{"answer:", "嗨嗨嗨,来了哈"}

def node(state: State):
    writer = get_stream_writer()
    writer({"custom_key": "欢迎来到线上Agent班级学习,O(∩_∩)O"})
    return {"answer": "some data"}


def main():
    graph = (
        StateGraph(State)
        .add_node(node)
        .add_edge(START, "node")
        .add_edge("node", END)
        .compile()
    )

    # 仅 custom:for chunk in graph.stream({"query": "example"}, stream_mode=["custom"]): print(chunk)
    # custom + updates:for mode, chunk in graph.stream(..., stream_mode=["updates", "custom"]): ...
    for chunk in graph.stream({"query": "example"}, stream_mode=["values", "custom"]):
        print(chunk)


if __name__ == "__main__":
    main()

进阶

# `get_stream_writer()` 负责把“图运行过程中的自定义消息”主动往外推;它和节点返回的状态更新是两条并行通道。
# `stream_mode=["custom", "updates"]` 时,迭代得到 `(mode, chunk)`,非常适合前端一边看业务进度,一边看状态更新。
# 本例最值得观察的是:`writer(...)` 写出的 `custom` 数据不会自动进 State;节点真正写回图状态的,仍然是最后 return 的那份 dict。


from typing import TypedDict

from langgraph.config import get_stream_writer
from langgraph.graph import StateGraph, START, END


class State(TypedDict):
    query: str
    answer: str
    progress: list


def node_with_custom_streaming(state: State) -> State:
    """带自定义流式传输的节点:边写自定义流边更新状态。"""
    writer = get_stream_writer()

    writer({"custom_key": "开始处理查询"})
    writer({"progress": "步骤1: 分析查询内容", "status": "running"})

    query = state["query"]

    writer({"progress": "步骤2: 生成结果", "status": "running"})
    writer({"progress": "步骤3: 完成处理", "status": "completed"})
    writer({"custom_key": "查询处理完成"})

    result = f"处理结果: {query.upper()}"
    return {
        "answer": result,
        "progress": state.get("progress", []) + ["处理完成"],
    }


def main():
    print("=== LangGraph 自定义数据流式传输演示 ===\n")

    graph = (
        StateGraph(State)
        .add_node("node_with_custom_streaming", node_with_custom_streaming)
        .add_edge(START, "node_with_custom_streaming")
        .add_edge("node_with_custom_streaming", END)
        .compile()
    )

    inputs = {"query": "hello world", "answer": "", "progress": []}

    print("--- 1. 单独使用 custom 流模式 ---")
    try:
        for chunk in graph.stream(inputs, stream_mode="custom"):
            print(f"自定义数据块: {chunk}")
    except Exception as e:
        print(f"错误: {e}")
        print(
            "说明: 在 Graph API 中,自定义流数据需在节点中通过 get_stream_writer 发送"
        )

    print("\n" + "=" * 50 + "\n")

    print("--- 2. 单独使用 updates 流模式 ---")
    for chunk in graph.stream(inputs, stream_mode="updates"):
        print(f"状态更新: {chunk}")

    print("\n" + "=" * 50 + "\n")

    print("--- 3. 同时使用 custom 和 updates 流模式 ---")
    try:
        for mode, chunk in graph.stream(inputs, stream_mode=["custom", "updates"]):
            print(f"[{mode}]: {chunk}")
    except Exception as e:
        print(f"错误: {e}")
        print("说明: 请确认 LangGraph 版本支持多模式流")


if __name__ == "__main__":
    main()
    
"""
【输出示例】
=== LangGraph 自定义数据流式传输演示 ===

--- 1. 单独使用 custom 流模式 ---
自定义数据块: {'custom_key': '开始处理查询'}
自定义数据块: {'progress': '步骤1: 分析查询内容', 'status': 'running'}
自定义数据块: {'progress': '步骤2: 生成结果', 'status': 'running'}
自定义数据块: {'progress': '步骤3: 完成处理', 'status': 'completed'}
自定义数据块: {'custom_key': '查询处理完成'}

==================================================

--- 2. 单独使用 updates 流模式 ---
状态更新: {'node_with_custom_streaming': {'answer': '处理结果: HELLO WORLD', 'progress': ['处理完成']}}

==================================================

--- 3. 同时使用 custom 和 updates 流模式 ---
[custom]: {'custom_key': '开始处理查询'}
[custom]: {'progress': '步骤1: 分析查询内容', 'status': 'running'}
[custom]: {'progress': '步骤2: 生成结果', 'status': 'running'}
[custom]: {'progress': '步骤3: 完成处理', 'status': 'completed'}
[custom]: {'custom_key': '查询处理完成'}
[updates]: {'node_with_custom_streaming': {'answer': '处理结果: HELLO WORLD', 'progress': ['处理完成']}}
"""    
    
    

 什么时候用什么流

需求 更适合的模式
想看整张图当前完整状态 values
想看每一步改了什么 updates
想看 LLM token messages
想推送业务自定义进度 custom
想详细调试图内部执行 debug

 

状态持久化

  流式处理解决的是“图在运行过程中怎么被看见”,那状态持久化解决的就是另一件很重要的事:图跑到一半、跑完之后,状态能不能被记住,并在下次继续使用。

LangGraph 的持久化核心围绕 checkpoint(检查点) 展开。官方文档的说法可以概括成一句话:当你给图配置了 checkpointer,图在执行的每一步都可以把当前状态保存成一个检查点

这些检查点会被组织到某个 thread 下面。入门阶段,可以先把 thread 理解成“同一条会话或同一条工作流链路”的 ID 容器,而最常见的就是通过:

{"configurable": {"thread_id": "..."}}

短期记忆- CheckPointer:

这里说的 Checkpointer,本质上是:

  • 按 thread_id 保存图的执行状态
  • 让同一个线程下的多次调用可以继续沿用之前的状态

所以 Checkpointer 更像是:线程内、会话内、工作流运行期的短期记忆

长期记忆- Store/BaseStore

Checkpointer 保存线程内状态,Store 用来保存跨线程共享的长期信息, 所以两者最核心的区别可以这样记:

  • Checkpointer:保存图在某条 thread 里的运行状态
  • Store / BaseStore:保存跨 thread、跨会话仍然要长期保留的数据

    例如:

    • 用户偏好
    • 长期业务事实
    • 跨会话共享的知识片段

    这些都更适合放 Store,而不是硬塞进单条 thread 的 checkpoint 链里。

持久化后端怎么选:

  • 内存:最适合学习和临时验证
  • SQLite:适合本地开发、小型项目、单机轻量部署
  • Postgres / Redis / 其他数据库后端:更适合生产环境

案例-内存检查点

# 内存检查点 InMemorySaver:编译图时传入 checkpointer,用 thread_id 区分会话,演示 get_state / get_state_history / 二次 invoke。
# compile(checkpointer=...) 后,每次 invoke 会在检查点中留下快照;config["configurable"]["thread_id"] 标识一条「对话线程」
# get_state(config) 取当前线程最新状态;get_state_history(config) 取历史快照序列(用于调试或时间回溯)
# `InMemorySaver` 数据仅在进程内存中,进程结束即丢失;它最适合先帮助你理解“checkpoint 到底是什么”
# Persistence 不只是“把结果存起来”,而是把图每一步的状态历史都保留下来,为后面的 Time-Travel 打基础

from typing import Annotated

import operator
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

class PersistenceDemoState(TypedDict):
    # operator.add:列表/数值等按「相加」语义合并(列表相当于拼接)
    messages: Annotated[list, operator.add]
    step_count: Annotated[int, operator.add]
    
def step_one(state: PersistenceDemoState) -> dict:
    print("执行步骤 1")
    return {
        "messages": ["执行了步骤 1"],
        "step_count": 1,
    }
    
def step_two(state: PersistenceDemoState) -> dict:
    print("执行步骤 2")
    return {
        "messages": ["执行了步骤 2"],
        "step_count": 1,
    }


def step_three(state: PersistenceDemoState) -> dict:
    print("执行步骤 3")
    return {
        "messages": ["执行了步骤 3"],
        "step_count": 1,
    }

def create_graph():
    builder = StateGraph(PersistenceDemoState)

    builder.add_node("step_one", step_one)
    builder.add_node("step_two", step_two)
    builder.add_node("step_three", step_three)

    builder.add_edge(START, "step_one")
    builder.add_edge("step_one", "step_two")
    builder.add_edge("step_two", "step_three")
    builder.add_edge("step_three", END)

    return builder    
    
def main():
    print("=== LangGraph 1.0 内存持久化存储演示 ===\n")

    graph = create_graph()
    app = graph.compile(checkpointer=InMemorySaver())

    config = {"configurable": {"thread_id": "user_13811112222"}}

    print("1. 首次执行工作流:")
    result = app.invoke(
        {
            "messages": ["开始执行"],
            "step_count": 0,
        },
        config,
    )

    print(f"执行结果 result: {result}\n")

    print("2. 检查存储的状态:")
    saved_state = app.get_state(config)
    print(f"保存的状态: {saved_state.values}")
    print(f"下一个节点: {saved_state.next}\n")

    # 正序遍历:从最早到最晚的检查点快照
    history = app.get_state_history(config)
    for checkpoint in history:
        print("=" * 50)
        print(f"当前状态: {checkpoint.values}")

    print("=" * 80)
    print("3. 恢复执行工作流:")
    # 工作流若已结束,再次 invoke(None, config) 通常直接返回已落盘的结果
    result2 = app.invoke(None, config)
    print(f"恢复执行结果: {result2}\n")

    print("=== 演示结束 ===")


if __name__ == "__main__":
    main()

SQLite检查点

# 依赖包:项目根目录 `requirements.txt` 已包含 `langgraph-checkpoint-sqlite`;全量安装用 `pip install -r requirements.txt`,或单独 `pip install langgraph-checkpoint-sqlite`。生产环境更常用 Postgres(`langgraph-checkpoint-postgres`)等实现。
# SqliteSaver(conn=...) 与 sqlite3.connect 配合;数据库文件路径需本机可写,目录需事先存在。
# 与 InMemorySaver 用法相同:`compile(checkpointer=...)`、`invoke(..., config)`、`get_state(config)`,区别主要在于存储介质
# 

import sqlite3
import operator
from pathlib import Path
from typing import Annotated, TypedDict

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph, START, END


class MyState(TypedDict):
    messages: Annotated[list, operator.add]


def node_1(state: MyState):
    return {"messages": ["abc", "def"]}


def main():
    # 默认写在项目旁,避免硬编码 Windows 盘符
    db_dir = Path(__file__).resolve().parent / "sqlite_checkpoints"
    db_dir.mkdir(parents=True, exist_ok=True)
    db_path = db_dir / "sqlite_data.db"

    conn = sqlite3.connect(database=str(db_path), check_same_thread=False)
    sqlite_db = SqliteSaver(conn=conn)

    builder = StateGraph(MyState)
    builder.add_node("node_1", node_1)

    builder.add_edge(START, "node_1")
    builder.add_edge("node_1", END)

    graph = builder.compile(checkpointer=sqlite_db)

    # 同一 thread_id 表示同一会话;多次执行会累积检查点,调试时可删 .db 或换 thread_id
    config = {"configurable": {"thread_id": "user-001"}}

    initial_state = graph.get_state(config)
    print(f"Initial state: {initial_state}")

    result = graph.invoke({"messages": []}, config)
    print(f"Result: {result}")

    print()
    print("====================查看执行后的状态====================")
    final_state = graph.get_state(config)
    print()
    print(f"Final state: {final_state}")

    conn.close()


if __name__ == "__main__":
    main()

"""
【输出示例】
Initial state: StateSnapshot(values={}, next=(), config={'configurable': {'thread_id': 'user-001'}}, metadata=None, created_at=None, parent_config=None, tasks=(), interrupts=())
Result: {'messages': ['abc', 'def']}

====================查看执行后的状态====================

Final state: StateSnapshot(values={'messages': ['abc', 'def']}, next=(), config={'configurable': {'thread_id': 'user-001', 'checkpoint_ns': '', 'checkpoint_id': '1f1272f0-d724-675e-8001-bb885d01bb16'}}, metadata={'source': 'loop', 'step': 1, 'parents': {}}, created_at='2026-03-24T03:10:46.773535+00:00', parent_config={'configurable': {'thread_id': 'user-001', 'checkpoint_ns': '', 'checkpoint_id': '1f1272f0-d723-6a48-8000-d3aac2954c9d'}}, tasks=(), interrupts=())
"""

 预建Agent与持久化

  

# create_agent 搭配 InMemorySaver,实现同一 thread_id 下的多轮对话与上下文延续
# `create_agent(..., checkpointer=...)` 说明高层 Agent 接口底层仍然可以吃到 LangGraph 的持久化能力
# 同一 `thread_id` 下的多次 invoke 会连续使用同一条线程状态,这也是“多轮对话为什么能续上”的关键
# 

import os

from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver

from dotenv import load_dotenv

load_dotenv(encoding="utf-8")

def main():
    llm = init_chat_model(
        model="qwen-plus",
        model_provider="openai",
        api_key=os.getenv("aliQwen-api"),
        temperature=0.0,
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    )
    
    checkPointer = InMemmorySaver()
    agent = creat_agnet(model=llm, checkpointer=checkpointer)
    config = {"configurable": {"thread_id": "user-001"}}
    msg1 = agent.invoke(
        {"messages": [("user", "你好,我叫张三,喜欢足球,60字内简洁回复")]},
        config,
    )
    msg1["messages"][-1].pretty_print()

    msg2 = agent.invoke(
        {"messages": [("user", "我叫什么?我喜欢做什么?")]},
        config,
    )
    msg2["messages"][-1].pretty_print()


if __name__ == "__main__":
    main()

    """
【输出示例】
================================== Ai Message ==================================

你好张三!很高兴认识一位足球爱好者,祝你绿茵场上挥洒汗水、享受快乐!
================================== Ai Message ==================================

你叫张三,喜欢足球!⚽
"""
    
    
    
    

时间回溯:

  它解决的问题不是“怎么正常跑一张图”,而是:图已经跑过了,我能不能回到历史中的某一步,从那里重新继续跑,甚至改一改状态再跑。在 LangGraph 里,因为前面已经有了 checkpoint 链,所以时间回溯就变得自然了。

回溯价值:

真实项目里很常见的问题包括:这次为什么回答对了,我想回看中间过程;这次为什么跑偏了,我想定位到底在哪一步开始出问题;如果在某一步换一个状态、换一条分支,后面会发生什么。

所以时间回溯特别适合:调试、复盘、分支探索、人工修正后重跑。

回溯怎么做

  1. 先跑一遍图,生成历史 checkpoint
  2. 用 get_state_history(...) 找到你想回到的那个历史点
  3. 视情况决定是原样恢复,还是先 update_state(...) 改状态
  4. 再从那个历史点继续 invoke(...) 或 stream(...)

如果你把这四步理解成:

  • 先有“历史录像”
  • 再选“从哪一帧切回去”
  • 可选“改一下剧本”
  • 然后“从那里继续往后演”

就会非常好记。

# 在带 checkpointer 的图上先跑完全程,用 get_state_history 选历史快照,update_state 改写状态后 invoke(None, new_config) 从分叉点重跑
#        时间回溯(Time-Travel)
# 基本步骤:(1)invoke/stream 跑图;
#           (2)get_state_history 找 checkpoint_id;
#           (3)可选 update_state 改 values;
#           (4)invoke(None, config) 从指定检查点继续
# update_state 返回的新 config 含新 checkpoint,后续应使用该 config 作为「时间旅行起点」
# NotRequired 表示该键在开始时可不出现,适合分步填满的故事状态
# 

import uuid

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, NotRequired

class StoryState(TypedDict):
    """故事状态:字段均可逐步写入。"""

    character: NotRequired[str]
    setting: NotRequired[str]
    plot: NotRequired[str]
    ending: NotRequired[str]

def create_character(state: StoryState):
    """创建故事角色(模拟 LLM 节点)。"""
    print("执行节点: create_character")

    mock_character = "一只会说话的猫"
    print(f"创建的角色: {mock_character}")
    return {"character": mock_character}


def set_setting(state: StoryState):
    """设置故事背景。"""
    print("执行节点: set_setting")

    mock_setting = "在一个神秘的图书馆里"
    print(f"设置的背景: {mock_setting}")
    return {"setting": mock_setting}


def develop_plot(state: StoryState):
    """发展故事情节。"""
    print("执行节点: develop_plot")

    character = state.get("character", "未知角色")
    setting = state.get("setting", "未知背景")
    mock_plot = f"{character}在{setting}发现了一本会发光的书"
    print(f"发展的剧情: {mock_plot}")
    return {"plot": mock_plot}


def write_ending(state: StoryState):
    """编写故事结局。"""
    print("执行节点: write_ending")

    plot = state.get("plot", "未知剧情")
    mock_ending = f"当{plot}时,整个图书馆都被魔法光芒照亮了"
    print(f"编写的结局: {mock_ending}")
    return {"ending": mock_ending}

def main():
    print("=== LangGraph 高级时间旅行演示 ===\n")
    workflow = StateGraph(StoryState)
    workflow.add_node("create_character", create_character)
    workflow.add_node("set_setting", set_setting)
    workflow.add_node("develop_plot", develop_plot)
    workflow.add_node("write_ending", write_ending)
    
    workflow.add_edge(START, "create_character")
    workflow.add_edge("create_character", "set_setting")
    workflow.add_edge("set_setting", "develop_plot")
    workflow.add_edge("develop_plot", "write_ending")
    workflow.add_edge("write_ending", END)
    
    graph = workflow.compile(checkpointer = InMemorySver())
    
    print("1. 生成第一个故事...")
    config1 = {
        "configurable":{
            "thread_id":str(uuid.uuid4())
        }
    }
    story1 = graph.invoke({}, config1)
    print(f"角色: {story1['character']}")
    print(f"背景: {story1['setting']}")
    
    print("2. 查看第一个故事的历史状态...")
    states1 = list(graph..get_state_history(config1))
    
    print("历史状态:")
    for i, state in enumerate(states1):
        print(f"  {i}. 下一步节点: {state.next}")
        print(f"     检查点ID: {state.config['configurable']['checkpoint_id']}")
        if state.values:
            print(f"     状态值: {state.values}")
        print()

    print("3. 从中间状态恢复执行,创建第二个故事...")
    
    
    # 索引需与 get_state_history 顺序一致;states1[2] 对应 create_character 执行后的快照(请以本地打印为准调整)
    character_state = states1[2]
    print(f"选中的状态值: {character_state.values}")
    new_config = graph.update_state(
        character_state.config,
        values={"character": "一只会飞的龙"},
    )
    print(f"新配置: {new_config}")
    print("4. 从新检查点恢复执行,生成第二个故事...")
    story2 = graph.invoke(None, new_config)
    print(f"新角色: {story2['character']}")
    print(f"背景: {story2['setting']}")
    
    print("5. 比较两个故事:")
    print("  故事1:")
    print(f"    角色: {story1['character']}")
    print(f"    背景: {story1['setting']}")
    
     print("  故事2:")
    print(f"    角色: {story2['character']}")
    print(f"    背景: {story2['setting']}")
    
if __name__ == "__main__":
    main()

子图

当图开始变复杂时,最自然的问题就是:能不能把一整张图,当成另一张图里的一个节点来复用?LangGraph 里的子图,可以理解成:把一个已经编译好的图,嵌入到另一张更大的父图里。所以子图的价值,本质上在于:复杂流程拆分、模块化复用、父子流程解耦

image

三种理解方式:

  1. 最简单模式:把编译后的子图直接当成父图里的节点
  2. 共享字段模式:父图和子图共享部分状态字段
  3. 状态转换模式:父图状态和子图状态结构不同,需要代理节点做转换

1.子图作为节点

# 子图作为节点:将 compile 后的子图直接 add_node 进父图;父子共用同一 State 类型时,由 Reducer 合并 messages。



from operator import add
from typing import Annotated, TypedDict

from langgraph.constants import END
from langgraph.graph import StateGraph, START


class DiliState(TypedDict):
    """
    状态:messages 使用 operator.add 合并策略——新返回的列表与原有列表拼接(非覆盖)。
    """

    messages: Annotated[list[str], add]

    
def sub_node(state: DiliState) -> DiliState:
    return {"messages": ["response from subgraph"]}

# --- 子图 ---
subgraph_builder = StateGraph(DiliState)
subgraph_builder.add_node("sub_node", sub_node)
subgraph_builder.add_edge(START, "sub_node")
subgraph_builder.add_edge("sub_node", END)

subgraph = subgraph_builder.compile()


# --- 父图:节点即子图 ---
builder = StateGraph(DiliState)
builder.add_node("subgraph_node", subgraph)
builder.add_edge(START, "subgraph_node")
builder.add_edge("subgraph_node", END)

graph = builder.compile()

"""
子图调用的状态传递逻辑当主图调用子图节点时,整个过程会触发两次状态合并:
第一步:主图把初始状态 {"messages": ["main-graph"]} 传递给子图

第二步:子图内部执行 sub_node,返回 {"messages": ["response from subgraph"]},
        由于 add 策略,子图会把传入的 ["main-graph"] 和返回的 ["response from subgraph"] 拼接,
        得到 ["main-graph", "response from subgraph"]

第三步:子图执行完成后,主图会再次应用 add 策略,
    把主图原有的 ["main-graph"]
    和子图返回的 ["main-graph", "response from subgraph"] 拼接,
    最终得到 ["main-graph", "main-graph", "response from subgraph"]
"""

print(graph.invoke({"messages": ["main-graph"]}))
print()

父子图共享状态字段:

当父图和子图共享部分字段时,理解重点就变成了:

  • 哪些字段是共享的
  • 哪些字段是子图内部私有的
  • 父图最终能看到哪些结果

它帮助读者意识到:子图不是完全孤立的小黑盒,它可以和父图共享部分状态空间。

# 父图 State 与子图 State 均含 parent_messages;子图内可改共享列表;子图私有字段不会出现在父图最终 state(父 schema 未声明)
# 子图 compile 后作为父图的一个 node;父图 invoke 的初始状态会传入子图(字段对齐时)
# 子图 TypedDict 多出的键(如 sub_message)仅在子图内部可见,父图输出按 ParentState 过滤
# 

from typing import TypedDict

from langgraph.graph import StateGraph, START, END


class ParentState(TypedDict):
    parent_messages: list


class SubgraphState(TypedDict):
    parent_messages: list
    sub_message: str

    
def subgraph_node(state: SubgraphState) -> SubgraphState:
    """子图节点:更新共享列表 + 写入子图私有字段。"""
    state["parent_messages"].append("message from subgraph updateO(∩_∩)O")
    state["sub_message"] = "subgraph private message"
    return state


def parent_node(state: ParentState) -> ParentState:
    """父图首节点:保证 parent_messages 为列表并追加父侧消息。"""
    if not state.get("parent_messages"):
        state["parent_messages"] = []
    state["parent_messages"].append("message from 父亲 node")
    return state


def build_subgraph():
    """构建并返回编译后的子图。"""
    sub_builder = StateGraph(SubgraphState)
    sub_builder.add_node("sub_node", subgraph_node)
    sub_builder.add_edge(START, "sub_node")
    sub_builder.add_edge("sub_node", END)
    return sub_builder.compile()


def build_parent_graph(compiled_subgraph):
    """构建并返回编译后的父图。"""
    builder = StateGraph(ParentState)
    builder.add_node("parent_node", parent_node)
    builder.add_node("subgraph_node", compiled_subgraph)
    builder.add_edge(START, "parent_node")
    builder.add_edge("parent_node", "subgraph_node")
    builder.add_edge("subgraph_node", END)
    return builder.compile()

def main():
    # 构建子图
    compiled_subgraph = build_subgraph()
    # 构建父图
    parent_graph = build_parent_graph(compiled_subgraph)
    initial_state = {"parent_messages": ["我是父消息"]}
    print("初始状态:", initial_state)

    # 父图执行时会进入子图;sub_message 不会出现在父图最终 dict(ParentState 无此键)
    final_state = parent_graph.invoke(initial_state)
    print("\n执行后最终状态:", final_state)


if __name__ == "__main__":
    main()
    
 """
初始状态: {'parent_messages': ['我是父消息']}

执行后最终状态: {'parent_messages': ['我是父消息', 'message from 父亲 node', 'message from subgraph updateO(∩_∩)O']}
"""   
    
    

代理节点与状态转换

很多时候,父图和子图的状态结构根本不是一套:

  • 父图关心用户请求和最终答案
  • 子图关心分析输入、中间步骤、分析结果

这时就不能直接把子图粗暴塞进去,而更推荐用一个父图代理节点完成三件事:

  1. 父状态 → 子图输入
  2. 调用子图
  3. 子图输出 → 父状态

这个模式非常重要,因为它是“跨图状态解耦”的关键

# 父子 State 字段完全不同,不能直接把子图挂成节点;在父图节点里手动构造子图输入、invoke 子图、再把结果写回父 State
# 父状态 ParentState 专注业务(user_query / final_answer),子状态 SubgraphState 专注分析过程,二者无交集字段时必须「代理节点」做映射
# 代理节点签名仍为 (父 state) -> 父 state 的增量/全量;内部调用 compiled_subgraph.invoke(subgraph_input)
# 该模式可扩展任意形状的状态转换,是多智能体、流水线拆图时的常用技巧
# 父子图状态不一致时,不要硬凑,直接用“代理节点”做父→子、子→父的状态转换

from typing import TypedDict

from langgraph.graph import StateGraph, START, END

# 定义不同结构的父子图状态
# 父图状态:仅包含用户查询和最终答案(与子图状态完全不同)
class ParentState(TypedDict):
    user_query: str
    final_answer: str | None
    
# 子图状态:专注于分析逻辑(与父图状态无重叠字段)
class SubgraphState(TypedDict):
    analysis_input: str
    analysis_result: str
    intermediate_steps: list
    
# 定义子图核心逻辑
def subgraph_analysis_node(state: SubgraphState) -> SubgraphState:
    """子图核心节点:模拟分析流水线。"""
    query = state["analysis_input"]
    state["intermediate_steps"] = [f"解析查询:{query}", "执行分析逻辑", "生成结果"]
    state["analysis_result"] = f"针对「{query}」的分析结果:这是子图处理后的内容"
    return state 

# 创建子图节点实例
def build_subgraph() -> StateGraph:
    sub_builder = StateGraph(SubgraphState)
    sub_builder.add_node("subgraph_analysis_node", subgraph_analysis_node)
    sub_builder.add_edge(START, "subgraph_analysis_node")
    sub_builder.add_edge("subgraph_analysis_node", END)
    return sub_builder.compile()


compiled_subgraph = build_subgraph()


# 定义父图代理节点(核心:状态转换+调用子图)从节点调用图
def call_subgraph_proxy(state: ParentState) -> ParentState:
    """
    父图代理节点:
    1) 父 -> 子:拼子图输入;
    2) 调用子图 invoke;
    3) 子 -> 父:把 analysis_result 写入 final_answer。
    """
    subgraph_input = {
        "analysis_input": state["user_query"],
        "intermediate_steps": [],
        "analysis_result": "",
    }

    subgraph_response = compiled_subgraph.invoke(subgraph_input)

    return {
        "user_query": state["user_query"],
        "final_answer": subgraph_response["analysis_result"],
    }

    
    def build_parent_graph():
    parent_builder = StateGraph(ParentState)
    # 添加代理节点(核心:手动处理状态转换+调用子图)
    parent_builder.add_node("call_subgraph_proxy", call_subgraph_proxy)
    # 父图执行链路:START → 代理节点 → END
    parent_builder.add_edge(START, "call_subgraph_proxy")
    parent_builder.add_edge("call_subgraph_proxy", END)
    return parent_builder.compile()


def main():
    # 1. 构建父图
    parent_graph = build_parent_graph()

    # 2. 定义父图初始状态(仅包含user_query,符合父图状态结构)
    initial_state = {
        "user_query": "请分析Python中StateGraph的使用场景",
        "final_answer": None,
    }
    print("父图初始状态:", initial_state)

    # 3. 执行父图,实际而言父图调用了call_subgraph_proxy
    final_state = parent_graph.invoke(initial_state)

    # 4. 输出结果
    print("\n父图最终状态:", final_state)
    print("\n子图处理后的最终答案:", final_state["final_answer"])


if __name__ == "__main__":
    main()

"""
【输出示例】
父图初始状态: {'user_query': '请分析Python中StateGraph的使用场景', 'final_answer': None}

父图最终状态: {'user_query': '请分析Python中StateGraph的使用场景', 'final_answer': '针对「请分析Python中StateGraph的使用场景」的分析结果:这是子图处理后的内容'}

子图处理后的最终答案: 针对「请分析Python中StateGraph的使用场景」的分析结果:这是子图处理后的内容
"""

 

posted @ 2026-05-19 13:45  幻影之舞  阅读(35)  评论(0)    收藏  举报