langgraph笔记

AI 智能体实战速成指南:从零到企业级落地 | ai-agents-from-zero

节点重试

比如正常你建一个图是这样,直接写死。

python 复制代码
def build_graph():
    builder = StateGraph(DiliState)

    builder.add_node(
        "unstable_api",
        unstable_api_call,
        retry_policy=RetryPolicy(max_attempts=5),
    )

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

    return builder.compile()

你想建一个节点重试的,并且通用的,就直接建一个方法,你想建什么样的,给什么节点建什么样的策略,直接传入不同的参数即可。

python 复制代码
# 工具函数
def build_retry_graph(node_name: str, node_func, retry_policy: RetryPolicy):
    builder = StateGraph(DiliState)
    # 为节点添加重试策略,需要在add_node中设置retry_policy参数。
    # retry_policy参数接受一个RetryPolicy命名元组对象。
    # 默认情况下,retry_on参数使用default_retry_on函数,该函数会在遇到任何异常时重试
    builder.add_node(node_name, node_func, retry_policy=retry_policy)
    builder.add_edge(START, node_name)
    builder.add_edge(node_name, END)
    return builder.compile()

然后就可以这样,就很灵活,用build_retry_graph来创建有重试策略的图。

python 复制代码
# 工具函数
def build_retry_graph(node_name: str, node_func, retry_policy: RetryPolicy):
    builder = StateGraph(DiliState)
    # 为节点添加重试策略,需要在add_node中设置retry_policy参数。
    # retry_policy参数接受一个RetryPolicy命名元组对象。
    # 默认情况下,retry_on参数使用default_retry_on函数,该函数会在遇到任何异常时重试
    builder.add_node(node_name, node_func, retry_policy=retry_policy)
    builder.add_edge(START, node_name)
    builder.add_edge(node_name, END)
    return builder.compile()



# 模拟不稳定的API调用,使用全局变量跟踪尝试次数
def unstable_api_call(state: DiliState) -> Dict[str, Any]:
    """模拟不稳定API:前2次失败,第3次成功(全局计数器记录尝试次数)"""
    global attempt_counter
    attempt_counter += 1
    # 纯文本打印尝试次数
    print(f"尝试调用API,这是第 {attempt_counter} 次尝试")

    # 模拟失败/成功逻辑:前2次抛异常,第3次返回结果
    if attempt_counter < 3:
        raise Exception(f"模拟API调用失败abcd (尝试 {attempt_counter})")
    return {"result": f"API调用成功,经过 {attempt_counter} 次尝试"}



# 总的就看这个测试方法:默认重试策略
def test_default_retry():
    global attempt_counter
    print("1. 使用默认重试策略:")
    print("   默认策略会对除特定异常外的所有异常进行重试")
    print("   不会重试的异常包括: ValueError, TypeError, ArithmeticError, ImportError,")
    print("                     LookupError, NameError, SyntaxError, RuntimeError,")
    print(
        "                     ReferenceError, StopIteration, StopAsyncIteration, OSError\n"
    )

    print("测试默认重试策略:")
    attempt_counter = 0  # 重置计数器
    default_graph = build_retry_graph(
        node_name="unstable_api",
        node_func=unstable_api_call,
        retry_policy=RetryPolicy(max_attempts=5),  # 最多5次尝试,足够重试成功
    )
    try:
        result = default_graph.invoke({"result": ""})
        print(f"最终结果: {result}\n")
    except Exception as e:
        print(f"最终失败: {type(e).__name__}: {e}\n")

入口点和条件入口点

从 START 开始就根据状态分支 示例:

python 复制代码
"""
【案例】条件入口点:从 START 开始就根据状态分支,使用 add_conditional_edges(START, route_fn, mapping),根据初始输入(如 user_input)决定进入哪个处理节点。

对应教程章节:第 24 章 - LangGraph API:节点、边与进阶 → 2、Graph API 之 Edge(边)

知识点速览:
- add_conditional_edges(START, route_input, {"greeting": "greeting_node", ...}):invoke 传入的 state 先交给 route_input,返回值作为 key 在 mapping 中查下一节点,实现「不同输入走不同入口」。
- 与「条件边」区别:条件边是"某节点执行完后"再分支;条件入口点是"图一启动"就分支,常用于做一级路由。
- 本例重点是理解"图从哪里开始"可以由输入动态决定;至于问候、告别、问题这三类文案本身,只是为了帮助观察路由效果。
"""

from typing import TypedDict
from langgraph.graph import StateGraph, START, END


# 1. 定义简单的状态
class SimpleState(TypedDict):
    user_input: str
    response: str
    node_visited: str


# 2. 路由函数 - 决定从START去哪
def route_input(state: SimpleState) -> str:
    """根据用户输入决定去哪个节点"""
    text = state["user_input"].lower()

    if "hello" in text or "hi" in text:
        return "greeting"  # 返回路由键
    elif "bye" in text or "exit" in text:
        return "farewell"  # 返回路由键
    else:
        return "question"  # 返回路由键


# 3. 各个处理节点
def handle_greeting(state: SimpleState) -> SimpleState:
    """处理问候"""
    state["response"] = "你好!很高兴见到你!"
    state["node_visited"] = "greeting_node"
    return state


def handle_farewell(state: SimpleState) -> SimpleState:
    """处理告别"""
    state["response"] = "再见!祝你有个美好的一天!"
    state["node_visited"] = "farewell_node"
    return state


def handle_question(state: SimpleState) -> SimpleState:
    """处理问题"""
    state["response"] = "我听到了你的问题,需要更多帮助吗?"
    state["node_visited"] = "question_node"
    return state


# 4. 创建图
def create_simple_graph():
    """创建一个简单的图"""
    stateGraph = StateGraph(SimpleState)

    # 添加节点
    stateGraph.add_node("greeting_node", handle_greeting)
    stateGraph.add_node("farewell_node", handle_farewell)
    stateGraph.add_node("question_node", handle_question)

    # 条件入口点:图从 START 进入后,先调用 route_input,再根据 mapping 决定第一跳去哪个业务节点
    stateGraph.add_conditional_edges(
        START,  # 起点
        route_input,  # 路由函数
        # 路由映射(可选):路由函数的返回值 -> 节点名
        {
            "greeting": "greeting_node",  # route_input返回"greeting"时,去greeting_node
            "farewell": "farewell_node",  # route_input返回"farewell"时,去farewell_node
            "question": "question_node",  # route_input返回"question"时,去question_node
        },
    )

    # 所有节点都到END
    stateGraph.add_edge("greeting_node", END)
    stateGraph.add_edge("farewell_node", END)
    stateGraph.add_edge("question_node", END)

    return stateGraph.compile()


# 5. 使用示例
def run_example():
    # 创建图
    graph = create_simple_graph()
    # 测试不同的输入
    test_inputs = ["Hello everyone!", "Goodbye now", "What time is it?"]

    for user_input in test_inputs:
        print(f"\n输入: {user_input}")
        print("-" * 30)

        # 创建初始状态
        initial_state = SimpleState(user_input=user_input, response="", node_visited="")

        # 执行图
        result = graph.invoke(initial_state)

        print(f"路由决策: {route_input(initial_state)}")
        print(f"访问的节点: {result['node_visited']}")
        print(f"响应: {result['response']}")

    print()
    # 打印图的ascii可视化结构
    print(graph.get_graph().print_ascii())
    print("=================================")
    print()
    # 打印图的可视化结构,生成更加美观的Mermaid 代码,通过processon 编辑器查看
    print(graph.get_graph().draw_mermaid())


# 运行示例
if __name__ == "__main__":
    print("简单条件入口点示例")
    print("=" * 40)
    run_example()


"""
【输出示例】
简单条件入口点示例
========================================

输入: Hello everyone!
------------------------------
路由决策: greeting
访问的节点: greeting_node
响应: 你好!很高兴见到你!

输入: Goodbye now
------------------------------
路由决策: farewell
访问的节点: farewell_node
响应: 再见!祝你有个美好的一天!

输入: What time is it?
------------------------------
路由决策: question
访问的节点: question_node
响应: 我听到了你的问题,需要更多帮助吗?

                              +-----------+                                
                              | __start__ |.                               
                         .....+-----------+ .....                          
                     ....           .            ....                      
                .....               .                .....                 
             ...                    .                     ...              
+---------------+           +---------------+           +---------------+  
| farewell_node |           | greeting_node |           | question_node |  
+---------------+****       +---------------+        ***+---------------+  
                     ****           *            ****                      
                         *****      *       *****                          
                              ***   *    ***                               
                               +---------+                                 
                               | __end__ |                                 
                               +---------+                                 
None
=================================

---
config:
  flowchart:
    curve: linear
---
graph TD;
        __start__([<p>__start__</p>]):::first
        greeting_node(greeting_node)
        farewell_node(farewell_node)
        question_node(question_node)
        __end__([<p>__end__</p>]):::last
        __start__ -. &nbsp;farewell&nbsp; .-> farewell_node;
        __start__ -. &nbsp;greeting&nbsp; .-> greeting_node;
        __start__ -. &nbsp;question&nbsp; .-> question_node;
        farewell_node --> __end__;
        greeting_node --> __end__;
        question_node --> __end__;
        classDef default fill:#f2f0ff,line-height:1.2
        classDef first fill-opacity:0
        classDef last fill:#bfb6fc
"""

send

案例:

python 复制代码
"""
【案例】Send 与 Map-Reduce 模式:条件边函数返回 Sequence[Send],每个 Send(节点名, 状态) 触发一次该节点的执行,LangGraph 并行执行后按 Reducer 汇总(如列表合并),适合「动态数量子任务」并行再汇总。

对应教程章节:第 24 章 - LangGraph API:节点、边与进阶 → 3、Send、Command 与 Runtime 上下文

知识点速览:
- 条件边若返回 List[Send](或 Sequence[Send]),每个 Send 指定「下一节点 + 传入该节点的 state」,框架会并行执行这些分支并合并结果。
- Map 阶段:生成主题列表 → 为每个主题构造 Send("make_joke", {"subject": 主题});Reduce 阶段:jokes 字段用列表合并 Reducer,多路结果合并成一条列表。
- 适合「一批输入拆成多份、并行处理、再汇总」的流程。
- 本例最值得观察的是:每个 Send 分支拿到的是"自己的那份状态",而最终能不能顺利汇总,取决于下游字段有没有设计好对应的 Reducer。
"""

from typing import Annotated, List, Sequence
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send


# 定义状态
class DiliState(TypedDict):
    subjects: List[str]
    jokes: Annotated[List[str], lambda x, y: x + y]  # 使用列表合并的方式


# 第一个节点:生成需要处理的主题列表
def generate_subjects(state: DiliState) -> dict:
    """生成需要处理的主题列表"""
    print("执行节点(第一个节点:生成需要处理的主题列表): generate_subjects")
    subjects = ["猫", "狗", "程序员"]
    print(f"生成主题列表: {subjects}")
    return {"subjects": subjects}


# Map节点:为每个主题生成笑话
def make_joke(state: DiliState) -> dict:
    """为单个主题生成笑话"""
    subject = state.get("subject", "未知")
    print(f"执行节点: make_joke,处理主题: {subject}")

    # 根据主题生成相应笑话
    jokes_map = {
        "猫": "为什么猫不喜欢在线购物?因为它们更喜欢实体店!",
        "狗": "为什么狗不喜欢计算机?因为它们害怕被鼠标咬!",
        "程序员": "为什么程序员喜欢洗衣服?因为他们在寻找bugs!",
        "未知": "这是一个关于未知主题的神秘笑话。",
    }

    joke = jokes_map.get(subject, f"这是一个关于{subject}的即兴笑话。")
    print(f"生成笑话: {joke}")
    return {"jokes": [joke]}


# 条件边函数:根据主题列表生成Send对象列表
def map_subjects_to_jokes(state: DiliState) -> List[Send]:
    """将主题列表映射到joke生成任务"""
    print("执行条件边函数: map_subjects_to_jokes")
    subjects = state["subjects"]
    print(f"映射主题到joke任务: {subjects}")

    # 为每个主题创建一个Send对象,指向make_joke节点
    # 每个Send对象包含节点名称和传递给该节点的状态
    send_list = [Send("make_joke", {"subject": subject}) for subject in subjects]
    print(f"生成Send对象列表: {send_list}")
    return send_list


def main():
    """演示Map-Reduce模式"""
    print("=== Map-Reduce 模式演示 ===\n")

    # 创建图
    builder = StateGraph(DiliState)

    # 添加节点
    builder.add_node("generate_subjects", generate_subjects)
    builder.add_node("make_joke", make_joke)

    # 添加边
    builder.add_edge(START, "generate_subjects")

    # 添加条件边,使用Send对象实现map-reduce
    builder.add_conditional_edges(
        "generate_subjects",  # 源节点
        map_subjects_to_jokes,  # 路由函数,返回Send对象列表
    )

    # 从make_joke到结束
    builder.add_edge("make_joke", END)

    # 编译图
    graph = builder.compile()
    print(graph.get_graph().print_ascii())

    # 执行图
    initial_state = {"subjects": [], "jokes": []}
    print("初始状态:", initial_state)
    print("\n开始执行图...")

    result = graph.invoke(initial_state)
    print(f"\n最终结果: {result}")

    print("\n=== 演示完成 ===")


if __name__ == "__main__":
    main()

"""
【输出示例】
=== Map-Reduce 模式演示 ===

    +-----------+      
    | __start__ |      
    +-----------+      
          *            
          *            
          *            
+-------------------+  
| generate_subjects |  
+-------------------+  
          *            
          *            
          *            
     +---------+       
     | __end__ |       
     +---------+       
None
初始状态: {'subjects': [], 'jokes': []}

开始执行图...
执行节点(第一个节点:生成需要处理的主题列表): generate_subjects
生成主题列表: ['猫', '狗', '程序员']
执行条件边函数: map_subjects_to_jokes
映射主题到joke任务: ['猫', '狗', '程序员']
生成Send对象列表: [Send(node='make_joke', arg={'subject': '猫'}), Send(node='make_joke', arg={'subject': '狗'}), Send(node='make_joke', arg={'subject': '程序员'})]
执行节点: make_joke,处理主题: 猫
生成笑话: 为什么猫不喜欢在线购物?因为它们更喜欢实体店!
执行节点: make_joke,处理主题: 狗
生成笑话: 为什么狗不喜欢计算机?因为它们害怕被鼠标咬!
执行节点: make_joke,处理主题: 程序员
生成笑话: 为什么程序员喜欢洗衣服?因为他们在寻找bugs!

最终结果: {'subjects': ['猫', '狗', '程序员'], 'jokes': ['为什么猫不喜欢在线购物?因为它们更喜欢实体店!', '为什么狗不喜欢计算机?因为它们害怕被鼠标咬!', '为什么程序员喜欢洗衣服?因为他们在寻找bugs!']}

=== 演示完成 ===
"""

command:更新并跳转

如果说条件边只负责"去哪",那 Command 解决的是另一个很常见的需求:某个节点在做完判断后,既想更新状态,又想直接决定下一步去哪。

Command 可以把状态更新和控制流放到同一次返回里。

最常见的两个参数是:

  • update:当前节点希望写回 State 的局部更新内容,仍然会按字段对应的 Reducer 规则合并。

  • goto:当前节点执行完后,希望图下一步跳转到哪个节点;也可以直接跳到 END 结束流程。

一个节点可以:

  • 一边返回新的状态更新

  • 一边直接告诉图"下一步去哪个节点"

这和条件边的边界要分清楚:

  • 条件边:通常更适合"节点做完了,再单独根据状态决定去哪"

  • Command:更适合"这个节点本身就是决策点,离开时把状态和去向一起交代清楚"

案例:

python 复制代码
"""
【案例】Command 对象:节点可返回 Command(update=..., goto=节点或END),在「更新状态」的同时「指定下一跳」,实现状态更新与路由一步完成,适合人机闭环与多智能体交接。

对应教程章节:第 24 章 - LangGraph API:节点、边与进阶 → 3、Send、Command 与 Runtime 上下文

知识点速览:
- `Command(update=..., goto=...)` 可以先按 Reducer 规则把 update 合并回 State,再决定下一跳;这正是它和普通节点返回 dict 的关键区别。
- 与条件边的区别:条件边更像"节点执行完后再单独路由",而 Command 更像"这个节点自己就是决策点,离场时把状态和去向一起交代清楚"。
- 本例还顺手演示了一个工程上很重要的点:带循环或回跳的图,最好配合明确的终止条件与递归上限,避免流程跑飞。
"""

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command

# 全局常量:统一递归限制,便于维护
RECURSION_LIMIT = 50


# 定义状态
class AgentState(TypedDict):
    messages: Annotated[list, lambda x, y: x + y]  # 自动合并消息
    current_agent: str
    task_completed: bool


# 决策代理(核心路由节点)
def decision_agent(state: AgentState) -> Command[AgentState]:
    """根据消息内容路由代理,任务完成则直接终止"""
    print("执行节点: decision_agent")
    # 优先终止流程(核心防循环逻辑)
    if state["task_completed"]:
        print("✅ 检测到任务已完成,直接终止流程")
        return Command(
            update={"messages": [("system", "所有任务处理完成,流程正常结束")]},
            goto=END,
        )
    # 提取消息文本(兼容空消息)
    last_message = state["messages"][-1] if state["messages"] else ("", "")
    last_msg_content = last_message[1]
    print(f"最新消息文本: {last_msg_content}")

    # 动态路由
    if "数学" in last_msg_content:
        print("✅ 检测到数学任务,路由到数学代理")
        return Command(
            update={
                "messages": [("system", "路由到数学代理")],
                "current_agent": "math_agent",
            },
            goto="math_agent",
        )
    elif "翻译" in last_msg_content:
        print("✅ 检测到翻译任务,路由到翻译代理")
        return Command(
            update={
                "messages": [("system", "路由到翻译代理")],
                "current_agent": "translation_agent",
            },
            goto="translation_agent",
        )
    else:
        print("❌ 未识别任务类型,标记任务完成并终止")
        return Command(
            update={"messages": [("system", "任务完成")], "task_completed": True},
            goto=END,
        )


# 数学代理(业务节点)
def math_agent(state: AgentState) -> Command[AgentState]:
    """处理数学计算任务,完成后返回决策代理"""
    print("执行节点: math_agent")
    result = "2 + 2 = 4"
    print(f"计算结果: {result}")
    return Command(
        update={
            "messages": [("assistant", f"数学计算结果: {result}")],
            "current_agent": "decision_agent",
            "task_completed": True,
        },
        goto="decision_agent",
    )


# 翻译代理(业务节点)
def translation_agent(state: AgentState) -> Command[AgentState]:
    """处理中英翻译任务,完成后返回决策代理"""
    print("执行节点: translation_agent")
    translation = "Hello -> 你好"
    print(f"翻译结果: {translation}")
    return Command(
        update={
            "messages": [("assistant", f"翻译结果: {translation}")],
            "current_agent": "decision_agent",
            "task_completed": True,
        },
        goto="decision_agent",
    )


def main():
    """演示Command基础用法:状态更新+动态路由+流程终止"""
    print("=== Command 基础演示(LangGraph 1.0.6)===\n")

    # 1. 构建状态图
    builder = StateGraph(AgentState)
    builder.add_node("decision_agent", decision_agent)
    builder.add_node("math_agent", math_agent)
    builder.add_node("translation_agent", translation_agent)

    # 2. 定义边(完整节点关系)
    builder.add_edge(START, "decision_agent")
    builder.add_edge("math_agent", "decision_agent")
    builder.add_edge("translation_agent", "decision_agent")
    builder.add_edge("decision_agent", END)

    # 3. 编译图
    graph = builder.compile()

    # 测试1:数学任务
    print("【测试1: 数学任务】")
    initial_state = {
        "messages": [("user", "我需要计算数学题")],
        "current_agent": "user",
        "task_completed": False,
    }
    print("初始状态:", initial_state)
    result = graph.invoke(initial_state, recursion_limit=RECURSION_LIMIT)
    print(
        "最终状态(简化):", {k: v for k, v in result.items() if k != "messages"}
    )  # 简化输出
    print("\n" + "-" * 50 + "\n")

    # 测试2:翻译任务
    print("【测试2: 翻译任务】")
    initial_state = {
        "messages": [("user", "我需要翻译文本")],
        "current_agent": "user",
        "task_completed": False,
    }
    print("初始状态:", initial_state)
    result = graph.invoke(initial_state, recursion_limit=RECURSION_LIMIT)
    print("最终状态(简化):", {k: v for k, v in result.items() if k != "messages"})
    print("\n" + "-" * 50 + "\n")

    # 测试3:未识别任务
    print("【测试3: 未识别任务类型】")
    initial_state = {
        "messages": [("user", "你好")],
        "current_agent": "user",
        "task_completed": False,
    }
    print("初始状态:", initial_state)
    result = graph.invoke(initial_state, recursion_limit=RECURSION_LIMIT)
    print("最终状态(简化):", {k: v for k, v in result.items() if k != "messages"})

    # 新增:可视化图结构(教学演示必备)
    print("\n=== 图结构可视化 ===")
    print(graph.get_graph().draw_mermaid())


if __name__ == "__main__":
    main()

"""
【输出示例】
=== Command 基础演示(LangGraph 1.0.6)===

【测试1: 数学任务】
初始状态: {'messages': [('user', '我需要计算数学题')], 'current_agent': 'user', 'task_completed': False}
执行节点: decision_agent
最新消息文本: 我需要计算数学题
✅ 检测到数学任务,路由到数学代理
执行节点: math_agent
计算结果: 2 + 2 = 4
执行节点: decision_agent
✅ 检测到任务已完成,直接终止流程
最终状态(简化): {'current_agent': 'decision_agent', 'task_completed': True}

--------------------------------------------------

【测试2: 翻译任务】
初始状态: {'messages': [('user', '我需要翻译文本')], 'current_agent': 'user', 'task_completed': False}
执行节点: decision_agent
最新消息文本: 我需要翻译文本
✅ 检测到翻译任务,路由到翻译代理
执行节点: translation_agent
翻译结果: Hello -> 你好
执行节点: decision_agent
✅ 检测到任务已完成,直接终止流程
最终状态(简化): {'current_agent': 'decision_agent', 'task_completed': True}

--------------------------------------------------

【测试3: 未识别任务类型】
初始状态: {'messages': [('user', '你好')], 'current_agent': 'user', 'task_completed': False}
执行节点: decision_agent
最新消息文本: 你好
❌ 未识别任务类型,标记任务完成并终止
最终状态(简化): {'current_agent': 'user', 'task_completed': True}

=== 图结构可视化 ===
---
config:
  flowchart:
    curve: linear
---
graph TD;
        __start__(<p>__start__</p>)
        decision_agent(decision_agent)
        math_agent(math_agent)
        translation_agent(translation_agent)
        __end__(<p>__end__</p>)
        __start__ --> decision_agent;
        decision_agent --> __end__;
        classDef default fill:#f2f0ff,line-height:1.2
        classDef first fill-opacity:0
        classDef last fill:#bfb6fc
"""

Runtime:配置与状态分开

前面我们一直在强调 State,但真实项目里并不是所有数据都该放进 State。比如下面这些东西:模型名、API Key、数据库连接、用户环境配置、当前运行的外部依赖对象。

它们通常都不属于"图在节点间流转的业务状态",而更像是这次运行的静态上下文。这时就更适合放进 Runtime 上下文,而不是硬塞进 State。

官方 Graph API 和 Use the graph API 指南都强调了这一点:运行时配置可以通过 context_schema 声明,并在调用图时通过 context=... 传入。

所以可以把两者的区别记成:

  • State:会随着图运行不断变化的共享业务数据
  • Runtime Context:本次运行里节点可读、但不应混入业务状态的静态依赖或配置

custom自定义数据流

writer就是推送到正在调用 graph.stream() / graph.astream() 的那一端代码。

它不是自动推送到浏览器、数据库、消息队列或用户;writer(...) 只是把自定义事件放进当前 LangGraph 的流输出中。谁在迭代这个流,谁就收到。

python 复制代码
"""
【案例】自定义流 + 状态更新组合:节点内多次 writer(...) 推送进度,同时返回 dict 更新 State;演示 custom / updates / 组合。

对应教程章节:第 25 章 - LangGraph 高级特性 → 1、流式处理(Streaming)

知识点速览:
- `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': ['处理完成']}}
"""

2.4 Checkpointer 与 thread_id

要让图保存状态,通常有三步:

  1. 创建一个 checkpointer,例如 InMemorySaver()SqliteSaver(...)
  2. 编译图时传入:builder.compile(checkpointer=checkpointer)
  3. 调用图时传入 config={"configurable": {"thread_id": "..."}}

thread_id 很容易被误解。它不是 Python 线程 ID,也不表示操作系统线程;在 LangGraph 语境里,它更像是状态空间的隔离 ID 。同一个 thread_id 下,图可以接上历史状态;换一个新的 thread_id,就是另一条独立会话或任务链。

从故障中恢复时,还会看到一个很常见的写法:

复制代码
graph.invoke(None, config={"configurable": {"thread_id": "user-001"}})

这里传 None 的意思不是"没有输入",而是告诉图:不要重新初始化一份新状态,而是沿用这个 thread 最近保存的状态继续推进。 当然,真实项目里是否可以这样恢复,还取决于你的节点是否幂等、错误是否已修复、checkpoint 是否已经保存到了可靠后端。

2.5 历史状态

持久化还带来一个直接收益:你可以查看某条 thread 的状态历史。常见 API 是:

  • graph.get_state(config):获取最近一次状态快照。
  • graph.get_state_history(config):获取历史状态快照序列。

这些状态快照可以帮助你回答几个排障问题:

  • 当前图最后停在什么状态?
  • 下一步准备执行哪个节点?
  • 中间某一步到底写入了哪些字段?
  • 如果要做时间回溯,应该从哪一个 checkpoint 开始?

这和第 23 章讲过的 StateSnapshot 是一条线:values 让你看到当前状态,next 让你知道下一步,config / parent_config 帮你串起 checkpoint 链,interrupts 用来记录等待处理的人机中断。

2.6 短期记忆:Checkpointer

在 LangGraph 语境里,最常先接触到的是 Checkpointer

这里说的 Checkpointer,主要做两件事:

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

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

这和你前面学过"消息历史""短期记忆"的主线,是能够串起来的。区别在于,这里不是单独记消息,而是记整张图的状态快照

2.7 长期记忆:Store / BaseStore

只用 Checkpointer 还不够,因为它更偏"同一条线程内部的连续状态"。那如果我们想跨线程、跨会话保存长期信息呢?

这就轮到 Store 出场了。

官方 Persistence 文档里把它解释得很清楚:Checkpointer 保存线程内状态,Store 用来保存跨线程共享的长期信息。

所以两者最核心的区别可以这样记:

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

例如:

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

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

内存检查点案例
python 复制代码
"""
【案例】内存检查点 InMemorySaver:编译图时传入 checkpointer,用 thread_id 区分会话,演示 get_state / get_state_history / 二次 invoke。

对应教程章节:第 25 章 - LangGraph 高级特性 → 2、状态持久化(Persistence)

知识点速览:
- 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()

"""
【输出示例】
=== LangGraph 1.0 内存持久化存储演示 ===

1. 首次执行工作流:
执行步骤 1
执行步骤 2
执行步骤 3
执行结果 result: {'messages': ['开始执行', '执行了步骤 1', '执行了步骤 2', '执行了步骤 3'], 'step_count': 3}

2. 检查存储的状态:
保存的状态: {'messages': ['开始执行', '执行了步骤 1', '执行了步骤 2', '执行了步骤 3'], 'step_count': 3}
下一个节点: ()

==================================================
当前状态: {'messages': ['开始执行', '执行了步骤 1', '执行了步骤 2', '执行了步骤 3'], 'step_count': 3}
==================================================
当前状态: {'messages': ['开始执行', '执行了步骤 1', '执行了步骤 2'], 'step_count': 2}
==================================================
当前状态: {'messages': ['开始执行', '执行了步骤 1'], 'step_count': 1}
==================================================
当前状态: {'messages': ['开始执行'], 'step_count': 0}
==================================================
当前状态: {'messages': [], 'step_count': 0}
================================================================================
3. 恢复执行工作流:
恢复执行结果: {'messages': ['开始执行', '执行了步骤 1', '执行了步骤 2', '执行了步骤 3'], 'step_count': 3}

=== 演示结束 ===
"""
SQLite检查点案例:
python 复制代码
"""
【案例】SQLite 检查点 SqliteSaver:把检查点写入本地 .db 文件,进程重启仍可恢复同 thread_id 的会话。

对应教程章节:第 25 章 - LangGraph 高级特性 → 2、状态持久化(Persistence)

知识点速览:
- 依赖包:项目根目录 `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)`,区别主要在于存储介质。
- 这个案例更像"从学习版持久化走向接近真实部署版"的过渡,重点是理解后端替换而不是 API 换了一套。
"""

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

人机协作

实际代码里,interrupt 常见长这样:

python 复制代码
from langgraph.types import Command, interrupt

def review_node(state: TransferState):
    user_review = interrupt({
        "title": "转账审核",
        "recipient": state["recipient"],
        "amount": state["amount"],
        "memo": state["memo"],
    })
    return {
        "approved": bool(user_review.get("approved")),
        "amount": user_review.get("amount", state["amount"]),
    }

first = graph.invoke(initial_state, config=config)
final = graph.invoke(Command(resume={"approved": True, "amount": 80}), config=config)
相关推荐
·醉挽清风·2 小时前
学习笔记—算法—算法题
笔记·学习·算法
西岸行者2 小时前
硬刚DeepSeek: UDPC与MTFAA的血缘辩论
笔记
a1117762 小时前
机器人导航入门指南(从 0 到 1)
笔记·学习·slam
Nebula_g3 小时前
大模型应用技术速通笔记
笔记·深度学习·机器学习·大模型
xian_wwq3 小时前
【学习笔记】开源 vs 闭源:Llama / Qwen / DeepSeek 生态博弈(34/35)
笔记·学习·开源
浩瀚地学4 小时前
【面试算法笔记】0302-哈希表-哈希表实现
java·经验分享·笔记·算法·面试
hj2862514 小时前
K8S 核心组件与资源概念 + 带注释命令 + 实操案例版笔记
笔记·容器·kubernetes
Z5998178415 小时前
c#软件开发学习笔记--分组、游标与临时表、分页
笔记·学习·c#
He BianGu6 小时前
【笔记】WPF中 Brush 的类型、继承关系、使用方式和注意事项
笔记·wpf