Week 3 --Day 2:LangGraph 进阶

引言:从二元循环到复杂工作流

在 Day 1 我们构建了一个完整的 ReAct Agent,LLM 调用节点和工具执行节点通过条件边交替运行,直到模型认为可以给出最终答案。这套"思考→行动→思考"的二元循环是 Agent 的最简骨架,但真实世界的应用场景远比这复杂。想象你要构建一个内容生产流水线,它需要先研究某个话题的背景资料,然后对收集到的信息进行分析提炼,接着基于分析结果撰写初稿,最后交给审核节点把关,这不是两个节点的简单循环能承载的。更进一步,你可能希望流程在关键节点上暂停、等待人工确认后再继续,或者在某一步失败后能从断点恢复而不是全部重来。这些需求指向 LangGraph 的三个进阶特性,复杂工作流设计状态持久化与检查点人工参与机制

在进入具体技术之前,有一个重要的概念更新需要先行说明。Day 1 中我们手写了 MessagesState 来理解 reducer 的工作方式,但从 LangGraph v1 开始,官方提供了更便捷的预置类型和 API。MessagesState 可以直接从 langgraph.graph 导入而无需手写,ToolNode 预置类可以替代我们手动实现的工具执行逻辑,create_agent() 高层封装底层也是 LangGraph 图。本文中我们聚焦于低层图 API 的进阶用法,因为这些才是构建可定制工作流的真正基石。理解底层之后,高层 API 的种种行为你将了然于胸。

复杂工作流设计:从图到网络

Day 1 的图只有两个节点,这是最容易理解的拓扑。但在复杂场景中,图的节点数可以任意增加,边的连接方式也远不止循环一种。LangGraph 不限制图的形状,你完全可以构建 DAG(有向无环图)流水线、带分支的决策树、甚至是包含多个子图的层级结构。本节我们从最简单的多节点流水线出发,逐步引入条件路由、Command 合并路由和 Send 并行分发,这些构成了复杂工作流设计的核心工具箱。

多节点顺序流水线

多节点顺序流水线是多 Agent 协作的最基本形态。以"研究→分析→写作→审核"的内容生产流水线为例,每个节点是一个独立的处理函数,它们按照预定顺序一个接一个地执行,前一个节点的输出成为后一个节点的输入依据。这种模式在 LangGraph 中用 add_edge 串联即可实现:

python 复制代码
from dotenv import load_dotenv
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langchain.messages import HumanMessage
from langchain.chat_models import init_chat_model

load_dotenv()

# 扩展 MessagesState,增加流水线特有的状态字段
# MessagesState 内部已对 messages 字段使用 add_messages reducer,
# 继承后自动获得消息追加能力,无需手动声明
class PipelineState(MessagesState):
    research_notes: str          # 研究阶段产出的资料
    analysis_result: str         # 分析阶段产出的结论
    draft: str                   # 写作阶段产出的初稿
    review_feedback: str         # 审核阶段产出的反馈
    final_output: str            # 最终产出

model = init_chat_model("Qwen/Qwen3.6-27B",model_provider="openai",temperature=0.7)

def research_node(state: PipelineState) -> dict:
    """研究节点:基于用户的问题,收集和整理相关资料。"""
    topic = state["messages"][-1].content
    response = model.invoke(
        f"你是一个研究员。请针对以下话题收集关键背景信息,输出结构化的研究笔记:\n{topic}"
    )
    return {"research_notes": response.content}

def analysis_node(state: PipelineState) -> dict:
    """分析节点:基于研究笔记,提炼核心观点和逻辑框架。"""
    response = model.invoke(
        f"你是一个分析师。请基于以下研究笔记,提炼出3-5个核心观点和一个逻辑框架:\n{state['research_notes']}"
    )
    return {"analysis_result": response.content}

def writing_node(state: PipelineState) -> dict:
    """写作节点:基于分析结果,撰写完整的内容初稿。"""
    response = model.invoke(
        f"你是一个写作者。请基于以下分析结果,撰写一篇结构完整、语言流畅的文章初稿:\n{state['analysis_result']}"
    )
    return {"draft": response.content}

def review_node(state: PipelineState) -> dict:
    """审核节点:对初稿进行质量把关,输出修改建议或最终确认。"""
    response = model.invoke(
        f"你是一个审核者。请审查以下文章,指出问题或确认通过。如果通过请回复'PASS':\n{state['draft']}"
    )
    is_approved = "PASS" in response.content
    return {"review_feedback": response.content, "final_output": state["draft"] if is_approved else ""}

# 构建流水线图
pipeline = StateGraph(PipelineState)
pipeline.add_node("research", research_node)
pipeline.add_node("analysis", analysis_node)
pipeline.add_node("writing", writing_node)
pipeline.add_node("review", review_node)

# 用固定边串联四步流水线
pipeline.add_edge(START, "research")
pipeline.add_edge("research", "analysis")
pipeline.add_edge("analysis", "writing")
pipeline.add_edge("writing", "review")
pipeline.add_edge("review", END)

app = pipeline.compile()
result = app.invoke({"messages": [HumanMessage(content="请写一篇关于LangGraph持久化机制的技术文章")]})
print(result.get("final_output", "审核未通过,反馈:" + result.get("review_feedback", "")))

这段代码定义了一个五字段的 PipelineState,每个字段对应流水线中一个阶段的产出。四个节点按照 research → analysis → writing → review 的顺序线性推进,数据和业务逻辑的流动一目了然。值得特别解释的是状态字段的设计思路,你可能注意到了,PipelineState 的代码里没有出现 Annotatedadd_messages。这是因为 MessagesState 在 LangGraph 源码内部已经把 messages 字段声明为 Annotated[list[AnyMessage], add_messages]add_messages 是一个智能 reducer,新消息会被追加到列表末尾,而已有消息(通过 ID 匹配)则原地更新。PipelineState 通过继承 MessagesState 自动获得了这个能力,你不需要在子类中再写一遍。相比之下,research_notesanalysis_resultdraft 等五个自定义字段没有指定 reducer,LangGraph 采用默认的覆盖模式:每个阶段产出的新值直接替换旧值。这就形成了一种混合 reducer 的设计------积累型数据(消息历史)用父类的追加 reducer,阶段产出型数据用子类的覆盖 reducer,各取所需、互不干扰。

条件路由

条件路由打破了流水线的线性约束,让图获得了"决策能力"。以上一小节的内容生产流水线为例,审核节点之后不应该总是结束,如果审核不通过应该回到写作节点重新修改,而不是直接输出不合格的内容。这种"审核通过则输出,不通过则返工"的逻辑,正是条件边的经典应用场景:

python 复制代码
from typing import Literal

def route_after_review(state: PipelineState) -> Literal["writing", END]:
    """审核通过则结束,不通过则返回写作节点修改。"""
    if "PASS" in state.get("review_feedback", ""):
        return END
    else:
        return "writing"

# 替换 review 后面的固定边为条件边
pipeline.add_conditional_edges(
    "review",
    route_after_review,
    {"writing": "writing", END: END}
)

route_after_review 函数的逻辑非常简洁,检查 review_feedback 字段中是否包含 "PASS" 标记,包含就返回 END 终止图执行,否则返回 "writing" 让流程跳回写作节点重新生成。更重要的是第三个参数,路由映射表 {"writing": "writing", END: END}。这个字典的键(左侧)是路由函数的返回值值(右侧)是图中实际的节点名称 。在这个例子里,函数返回的字符串恰好和节点名相同,所以左右两边看起来一模一样,这可能会让人困惑"两个 writing 和两个 END 是什么意思"。拆开来看就清楚了,当 route_after_review 返回 "writing" 时,映射表把它翻译为图中名为 "writing" 的节点,当返回 END(它本身就是一个特殊常量,指向图终止)时,映射表确认路由到 END。如果哪天你想让函数返回更语义化的标识,比如 "pass""fail",那么映射表就可以写成 {"pass": END, "fail": "writing"},此时函数的返回值与目标节点名完全解耦。映射表的另一个作用是编译时校验,LangGraph 会检查所有右侧的节点名是否都已通过 add_node 注册,如果某个目标节点不存在,编译阶段就会报错而非运行时才暴露。如果你省略映射表,LangGraph 会直接把函数的返回值当作节点名来用,这就要求返回值必须与图中注册的节点名称完全一致。

条件路由的能力远不止审核重试这一种。在更复杂的场景中,你可以在分析节点之后根据内容复杂度选择不同的写作策略,简单话题直接生成,复杂话题先拆分大纲再逐节撰写。这种"根据数据特征动态选择执行路径"的能力,让 LangGraph 图从静态管道升级为自适应流程。但需要注意一条重要约束,每个节点只能选择一种出边机制,要么用普通边(add_edge),要么用条件边(add_conditional_edges),要么用 Command 返回路由,不要混用,否则多条路径可能同时被激活,导致不可预测的行为。

Command:合并状态更新与路由

在 Day 1 中,节点函数只返回状态更新字典 {"messages": [...]},路由决策由独立的条件边函数负责。这在简单场景中够了,但有时节点内部既需要更新状态,又需要根据内部逻辑动态指定下一步去向,比如审核节点在输出 "PASS" 时要直接结束、输出修改建议时要跳到写作节点。Command 正是为此设计的,它让节点一次返回同时完成"更新状态"和"指定路由"两件事:

python 复制代码
from langgraph.types import Command

def review_node(state: PipelineState) -> Command[Literal["writing", END]]:
    response = model.invoke(
        f"你是一个审核者。请审查以下文章并给出PASS或修改建议:\n{state['draft']}"
    )
    if "PASS" in response.content:
        return Command(
            update={"review_feedback": response.content, "final_output": state["draft"]},
            goto=END
        )
    else:
        return Command(
            update={"review_feedback": response.content},
            goto="writing"
        )

# 使用 Command 的节点,后面不需要 add_edge 或 add_conditional_edges

Command 的两个核心字段是 update(状态更新字典,与普通节点返回值格式一致)和 goto(路由目标,可以是节点名字符串、END、甚至是 Send 对象)。返回 Command(goto=END) 的节点相当于说"我的任务完成了,图到此结束",返回 Command(goto="writing") 的节点则是"我分析了结果,请写作节点重新处理"。还有两个进阶字段,resume 用于从 interrupt() 中断中恢复(后面会详细讲解),graph 用于跨图跳转。使用 Command 的一个重要约束是,返回 Command 的节点不能再 通过 add_edgeadd_conditional_edges 添加出边,这是因为路由完全由节点内部决定。

读到这里你可能会有一个疑问,Command 和条件边看起来都能实现"根据情况跳到不同节点",它们到底有什么区别?表面上看确实相似,但二者的路由决策的归属权 完全不同。条件边的路由逻辑住在节点外部 的独立函数中,节点只负责执行并更新状态,执行完毕后由一个外部的 route_* 函数检查状态、决定下一步。这种"节点干活、别人指路"的模式适合路由逻辑比较固定、且决策依据完全来自状态字段的场景。Command 则把路由权收回到了节点内部 ,节点在处理业务逻辑的过程中自己决定下一步去哪,并把目的地直接写在返回值里。这就说明节点的内部中间变量、LLM 响应的即时内容、甚至是处理过程中的异常情况,都可以直接驱动路由决策,而不需要把这些信息先写回状态再让外部函数读取。Command 还能做条件边做不到的两件事,一是通过 resume 字段配合 interrupt() 实现人工审批后的恢复路由,二是通过 graph 字段实现跨图跳转(把执行流交给另一张完全不同的图)。简单总结,如果你的路由逻辑很简单、完全基于状态字段、且和节点业务逻辑没有耦合,用条件边更清晰,如果路由决策需要节点内部的运行时信息、或者需要和状态更新打包在一起,用 Command 更合适。

Send 与并行分发

Send 与并行分发是多节点工作流的另一个维度。当一个节点需要将不同的数据片段分发给多个下游实例并行处理时,比如研究节点检索到了 5 篇文章,需要 5 个分析实例各分析一篇,使用普通边或 Command 都难以胜任,因为目标节点的数量在运行时才能确定。Send 正是为这种 map-reduce 模式设计的,它允许一个节点的条件边函数返回多个 Send 对象,每个 Send 携带不同的状态片段发往同一个目标节点:

python 复制代码
from langgraph.types import Send

def continue_to_analysis(state: PipelineState):
    """研究完成后,为每篇资料创建独立的分析任务。"""
    articles = state.get("articles", [])
    return [Send("analysis", {"current_article": article}) for article in articles]

pipeline.add_conditional_edges("research", continue_to_analysis)

每个 Send("analysis", {"current_article": article}) 都会触发一次分析节点的执行,且这些执行在同一个 super-step 中并行发生。所有并行任务完成后,图继续向后推进。Send 非常适合搜索聚合、多文档分析、批量评估等需要"一份输入、多份处理"的场景。

状态持久化与检查点:让图拥有记忆

Day 1 中的 Agent 每次调用 invoke() 都是从零开始,消息列表为空,状态字段为默认值,上一轮对话的内容完全不可见。如果你用相同的 thread_id 连续调用两次,第一次的输出不会自动出现在第二次的上下文中。要让 Agent 记住"刚才说了什么",你需要持久化,而这正是 LangGraph 检查点机制的核心价值。

检查点的设计哲学非常简洁,当你在 compile() 时传入一个 checkpointer 对象,LangGraph 会在每个 super-step 的边界 自动保存一份状态快照(即 checkpoint),以 thread_id 为主键组织在同一会话的时间线上。每个 checkpoint 包含该时刻的完整状态值、下一步要执行的节点、元数据和时间戳,以及指向前一个 checkpoint 的 parent 引用,这些 checkpoint 串起来就形成了该 thread 的完整执行历史。官方文档总结了持久化的四大价值,跨轮记忆(同一 thread 的多轮对话自动累积上下文)、人工介入(暂停后能从断点精确恢复)、时间旅行(回放到任意历史 checkpoint 并分叉出新路径)、以及容错恢复(节点失败后从上一个成功 checkpoint 重试而不重复执行已完成的工作)。

LangGraph 提供了三个层级的 checkpointer 实现,从轻量到生产依次递进。InMemorySaverlanggraph 核心包内置,将所有 checkpoint 保存在内存字典中,重启即丢失,适合开发和单元测试。SqliteSaver 来自独立包 langgraph-checkpoint-sqlite,将 checkpoint 持久化到 SQLite 数据库文件,适合本地实验和小规模部署。PostgresSaver 来自 langgraph-checkpoint-postgres,面向生产环境,支持异步操作、连接池和加密序列化。三者实现了同一个 BaseCheckpointSaver 接口,切换只需替换实例:

python 复制代码
# 开发阶段:内存存储
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()

# 本地持久化:SQLite(需 pip install langgraph-checkpoint-sqlite)
from langgraph.checkpoint.sqlite import SqliteSaver
import sqlite3
conn = sqlite3.connect("checkpoints.db", check_same_thread=False)
checkpointer = SqliteSaver(conn)

# 生产环境:PostgreSQL(需 pip install langgraph-checkpoint-postgres)
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string("postgresql://user:pass@host/db")
checkpointer.setup()  # 创建必要的表结构

# 编译时注入 checkpointer(三种方式通用)
app = pipeline.compile(checkpointer=checkpointer)
thread_id

thread_id 是持久化的钥匙。每个 checkpoint 都与一个 thread_id 绑定,这个 ID 是你通过 config 传入的运行时配置。同一个 thread_id 的多次 invoke() 调用沿着同一条时间线累积状态,不同 thread_id 彼此完全隔离,这让你可以用一套图同时服务成百上千个独立会话,只需为每个会话分配唯一的 thread_id。如果你不传 thread_id,checkpointer 无法关联状态,每次调用仍然从零开始:

python 复制代码
config = {"configurable": {"thread_id": "user-session-001"}}

# 第一轮对话
result_1 = app.invoke(
    {"messages": [HumanMessage(content="北京天气怎么样?")]},
    config
)

# 第二轮对话------模型能看到"北京天气怎么样?"这条历史消息
result_2 = app.invoke(
    {"messages": [HumanMessage(content="我刚才问了什么?")]},
    config  # 相同的 thread_id,自动恢复历史状态
)
print(result_2["messages"][-1].content)  # 模型能正确回答"你问了北京天气"

第二轮调用时,LangGraph 通过 thread_id 找到上一个 checkpoint,将其中保存的完整消息历史加载到当前状态中,然后在此基础上追加新消息。这使得跨轮对话记忆对开发者完全透明,你不需要手动管理消息历史列表,checkpointer 替你做了这一切。在生产环境中,thread_id 通常映射到用户 ID 或会话 ID,并持久化在业务数据库中以便长期追踪。

查看状态历史

查看状态历史是持久化赋予的另一项能力。graph.get_state(config) 返回最新的 StateSnapshot 对象,其中 values 字段包含当前所有状态通道的值,next 字段告诉你如果现在继续执行、下一个节点是什么(空元组表示图已结束)。graph.get_state_history(config) 则返回该 thread 的全部 checkpoint 列表,按时间倒序排列(最新的在最前面)。每个 StateSnapshot 还暴露了 metadata(包含来源、节点输出和 super-step 编号)、parent_config(上一个 checkpoint 的引用)和 tasks(待执行的任务及其中断信息):

python 复制代码
# 查看最新状态
snapshot = app.get_state(config)
print(snapshot.values)   # 所有状态字段的当前值
print(snapshot.next)     # 下一步要执行的节点(空元组表示已结束)

# 查看完整历史
for s in app.get_state_history(config):
    print(f"Step {s.metadata['step']}: next={s.next}, values keys={list(s.values.keys())}")

这些 API 不仅是调试工具,它们为时间旅行 提供了技术基础。如果你在 invoke() 时额外传入 checkpoint_id,LangGraph 会从指定的历史 checkpoint 开始重放,跳过之前的节点(它们的结果已保存在 checkpoint 中),只重新执行之后的节点,在调试失败步骤和探索"如果当时做了不同选择会怎样"的替代路径时非常有价值。你还可以通过 graph.update_state(config, values, as_node="某个节点名") 手动编辑任意 checkpoint 的状态,创建一条分叉时间线。在人工介入场景中非常实用,比如审核者可以直接修改初稿字段,然后让图从修改后的状态继续执行,而不是退回写作节点重新生成。

容错与持久化模式

容错与持久化模式也是检查点机制的重要组成部分。LangGraph 的 pending writes 机制保证了故障恢复的精确性,在一个 super-step 中,如果部分节点成功执行而另一个节点失败,成功节点的输出已经被持久化到 checkpoint_writes 表中,下次恢复时不会重复执行它们。另外,LangGraph 支持三种持久化模式(通过 durability 参数控制),"exit" 仅在图退出时保存(最快但中间状态不持久)、"async" 异步保存(在下一步执行期间异步写入)、"sync" 同步保存(每步执行前必须完成写入,最安全但有一定性能开销)。对于需要强一致性的生产环境,推荐 "sync" 模式。

人工参与:暂停、审批、恢复

持久化保存了状态,但真正让"人"参与到自动化流程中,你需要的是中断机制。LangGraph 提供了两种粒度的中断方式,编译时的静态断点interrupt_before / interrupt_after)用于调试和步进执行,运行时的动态中断interrupt() 函数)用于构建灵活的人工审批工作流。两者的本质区别在于静态断点在图的固定位置无条件暂停,适合开发阶段逐节点观察,动态中断可以在节点内部的任意代码位置、基于任意条件触发,适合生产环境中真正需要人工决策的场景。

动态中断

动态中断是官方推荐的人工参与方式。在节点函数内部调用 interrupt(payload) 后,LangGraph 会抛出一个特殊的内部异常来暂停图执行,将当前状态通过 checkpointer 持久化保存,然后把 payload(必须是 JSON 可序列化的值,如字符串、字典、数字等)返回给调用方。调用方拿到 payload 后展示给人类用户,收集人类的决策输入,再通过 Command(resume=value) 重新调用 invoke()stream_events() 来恢复执行。恢复时,resume 的值会作为 interrupt() 调用的返回值注入到节点中,节点从头开始重新执行(不是从 interrupt 那一行继续),之前的所有代码会再次运行:

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

def approval_node(state: PipelineState) -> dict:
    """在最终输出前暂停,等待人工审批。"""
    # 将当前稿件展示给审核者
    decision = interrupt({
        "action": "review_draft",
        "content": state.get("draft", ""),
        "message": "请审核以上内容。输入 'approve' 通过,或输入修改意见。"
    })
    
    if decision == "approve":
        return {"final_output": state["draft"], "review_feedback": "人工审核通过"}
    else:
        return {"review_feedback": f"人工修改意见:{decision}"}

当你使用 invoke() 调用含 interrupt() 的图时,中断信息会出现在返回结果的 __interrupt__ 字段中。但官方强烈推荐使用 stream_events(version="v3") 来处理中断,因为它提供了更清晰的类型化接口,stream.interrupted 布尔值表明图是否因中断而暂停,stream.interrupts 包含所有待处理的中断负载,stream.output 等待最终结果:

python 复制代码
config = {"configurable": {"thread_id": "approval-thread-1"}}

# 首次执行:会触发 interrupt 并暂停
stream = app.stream_events(
    {"messages": [HumanMessage(content="写一篇关于AI安全的文章")]},
    config=config,
    version="v3"
)
final = stream.output  # 阻塞等待,直到图结束或中断

if stream.interrupted:
    # 展示中断信息给用户
    for interrupt_item in stream.interrupts:
        print(f"需要审批:{interrupt_item.value}")
    
    # 收集用户输入后恢复
    user_decision = input("请输入决定 (approve / 修改意见): ")
    resumed_stream = app.stream_events(
        Command(resume=user_decision),
        config=config,
        version="v3"
    )
    result = resumed_stream.output
    print("最终产出:", result.get("final_output", ""))

这段代码展示了典型的人工审批循环。第一次调用 stream_events 驱动图执行直到 approval_node 中的 interrupt() 被触发,stream.interrupted 变为 Truestream.interrupts 中包含我们传入的审查提示字典。程序将提示展示给用户、收集决策输入,然后以 Command(resume=user_decision) 作为输入再次调用 stream_events,这次的 resume 值会成为节点内 interrupt() 的返回值(即 decision 变量),节点据此选择通过或退回修改。

stream_events(v3) 还支持同时订阅多种事件类型,让你在一个循环中既处理 LLM 的 token 流(stream.messages)、又观察每步的状态快照(stream.values)、还能响应中断。这对于构建需要实时展示 LLM 生成过程同时又能暂停等待人工输入的交互式应用非常理想,聊天界面可以逐字展示模型输出,遇到需要确认的操作时弹出审批框,用户确认后无缝继续。

静态断点

静态断点主要用于调试。在 compile() 时通过 interrupt_beforeinterrupt_after 参数指定要在哪些节点之前或之后暂停:

python 复制代码
# 在 analysis 之前、review 之后自动暂停
app = pipeline.compile(
    checkpointer=checkpointer,
    interrupt_before=["analysis"],   # analysis 节点执行前暂停
    interrupt_after=["review"]        # review 节点执行后暂停
)

# 执行到 analysis 前会自动暂停
app.invoke(input_data, config)
# 此时 state 中 research_notes 已有值,analysis_result 为空

# 恢复执行(传 None 表示"从当前状态继续")
app.invoke(None, config)
# 继续执行 analysis → writing,然后在 review 后再次暂停

# 再次恢复,直到结束
app.invoke(None, config)

静态断点的恢复方式是用 invoke(None, config)stream_events(None, config, version="v3"),其中 None 表示"不追加新输入,仅从当前断点恢复执行"。需要注意的是,interrupt_beforeinterrupt_after 是编译时参数而非运行时调用,断点位置是固定的,所有使用该编译图的调用都会在相同位置暂停。这就是为什么它们适合调试而非生产级的人工审批,你不可能为每一个需要审批的场景重新编译一张图。生产环境中应该使用 interrupt() 动态中断,它让审批逻辑成为节点业务代码的一部分,可以基于条件触发、可以有多个审批点、不同审批点可以传递不同的 payload。

常见的人工参与模式

除了上面展示的基本审批模式,LangGraph 中断机制还支持几种在实践中非常实用的变体。审批/拒绝模式在关键操作(API 调用、数据库变更、金融交易)前暂停,人类返回 True/False 决定是否继续,节点通过 Command(goto=...) 将流导向不同的下游路径。状态审查编辑模式让人类直接修改图状态中的某个字段(如 LLM 生成的文本),修改后的值直接替换原始内容继续流动。工具内中断模式将 interrupt() 放在工具函数内部,使得任何调用该工具的 Agent 都会自动触发审批,审批逻辑与工具绑定、跨图复用。输入校验模式在一个循环中反复调用 interrupt() 直到收到合法输入,每次不合法的输入都会触发新的中断、附带更明确的提示信息。

使用中断时有四条关键规则必须牢记。第一,不能用 try/except 包裹 interrupt() 调用 ,因为 interrupt() 是通过抛特殊内部异常来实现暂停的,except Exception 会捕获这个异常导致中断不被传递到运行时。第二,一个节点内有多个 interrupt() 时,每次恢复都会从头执行整个节点 ,中断恢复值按索引匹配,所以 interrupt() 的调用顺序和数量必须在每次执行时保持一致,不能有条件跳过或循环中断。第三,interrupt() 的 payload 必须是 JSON 可序列化的简单值 ,不能传函数、类实例等复杂对象。第四,interrupt() 之前的副作用代码必须是幂等的 ,因为恢复时节点从头重跑会导致这些代码再次执行,例如在 interrupt() 之前创建数据库记录会造成重复插入,应该将副作用移到 interrupt() 之后、或者使用 upsert 等幂等操作。

练习任务

  • 实现一个多Agent协作工作流(研究→分析→写作→审核)
  • 添加检查点功能,实现工作流中断恢复
  • 设计带人工审批节点的流程

考核点 ✅

  1. 多节点工作流:提交含 4 个节点(研究/分析/写作/审核)的 LangGraph 代码
  2. 检查点验证:演示中断恢复功能------中途停止再继续执行,状态不丢失
  3. HITL 设计 :提交带 interrupt_before 的人工审批节点代码,解释触发流程
  4. 状态设计 :口头解释 TypedDict 定义 AgentState 的关键字段选择理由
相关推荐
装不满的克莱因瓶1 小时前
自然语言处理中的词嵌入——从离散符号到语义向量空间
人工智能·python·深度学习·ai·自然语言处理·nlp
蜂蜜黄油呀土豆1 小时前
Reflexion:让 Agent 用「言语」做强化学习
python·ai·大模型·reflexion
搬石头的马农1 小时前
御三家旗舰模型混战下的企业选型策略:GPT-5.6、Fable 5、Gemini 3.5 Pro 怎么选? - 微元算力(weytoken)
java·人工智能·python·gpt·ai编程
小白学大数据1 小时前
知网数据实战:爬虫 + 网络分析打造论文关键词图谱
爬虫·python·scrapy
SilentSamsara1 小时前
Prompt 工程实战:System Prompt 设计、Few-shot 与 Chain-of-Thought
人工智能·python·青少年编程·prompt
零陵上将军_xdr1 小时前
大模型开发02 - 提示词工程
人工智能·python
veminhe1 小时前
解决了调用deepseek接口报的错
python
2501_928945522 小时前
命题GTFE-1:修正爱因斯坦场方程的张量形式推导
python
SilentSamsara2 小时前
LLM API 工程化:OpenAI/DeepSeek/国产模型统一调用层设计
开发语言·人工智能·python