Skip to content

第18章 · 生产编排:StateGraph 与 LangGraph 对拍 ​

前置要求:掌握 第15章 · Typed Tools、MCP 与可恢复 Agent 的工具编排与 第16章 · 真实大模型 API 实战 的 API 接口调用。

第15章你手写了 Agent 状态机的每一个部件。本章回答一个自然的问题:主流框架(LangGraph)是怎么解决同一批问题的? 你会把手写版的每个零件逐件映射到 LangGraph 的抽象上,真实跑通图编译、reducer、条件边、跨进程 checkpoint 恢复和 interrupt 式人工审批,并用一次故意的故障注入亲眼看到"恢复时节点从头重跑"会怎样重复执行副作用。

映射学完以后你会发现:框架换掉的是样板代码,换不掉的是第15章的纪律——schema、policy、幂等、批准门,仍然是你自己的责任。

版本基线:langgraph 1.2.10 / langgraph-checkpoint-sqlite 3.1.1。全部 lab 用确定性 mock model,只证明框架合同,不证明 live planner 质量。

本章目标 ​

学完后你能做到:

  • 把第15章手写状态机的每个部件逐件映射到 LangGraph:State(TypedDict)+reducer、节点+条件边、@tool+ToolNode、checkpointer+thread_id、interrupt+Command(resume)。
  • 真实跑通 langgraph:图编译与调用、reducer 追加语义、条件边路由、SqliteSaver 跨实例恢复、interrupt/resume。
  • 说清 interrupt 的"节点从头重跑"语义及三条工程规则,并用一次故障注入亲眼看到非幂等副作用被重复执行。
  • 画出 Anthropic《Building effective agents》 五种 workflow 模式的图形态,说清 workflow(预定义代码路径)与 agent(模型自主循环)的决策边界。
  • 知道 LangChain v1 的分工:create_agent + middleware 是生产快捷方式,create_react_agent 已废弃(langgraph 0.2.x 移除,requirements-agent.txt 锁定 langgraph>=1.2,<2,实测 1.2.10——废弃 API 不再存在于当前 pinned 版本),LCEL 退居 langchain-classic(本课不教)。

阶段一:概念映射表(手写 → LangGraph) ​

本章的一切都是这张表的展开。先把它读懂,再动手。

第15章手写件LangGraph 对应关键差异
状态 st(AgentState dataclass)State(TypedDict),字段用 Annotated[list, operator.add] 声明 reducer节点返回部分更新,reducer 决定合并方式(追加 vs 覆盖);手写版是节点内直接 mutate
转移函数 δ / while 主循环StateGraph 的节点 + 边;循环 = 条件边回指上游节点循环不再是 while 关键字,而是图拓扑;终止是路由到 END
validate(a_t)(ToolSpec input_schema)@tool 装饰器从函数签名/类型标注自动生成 JSON Schema + ToolNode 执行与错误回传schema 是"免费"的,但风险分级、policy、幂等键仍要你自己做——框架不替代第15章的 gate
HITL 批准(waiting_approval + approval token)节点内 interrupt(payload) + Command(resume=value) 恢复恢复时整个节点从头重跑(详见阶段五三条规则)
CheckpointStore.save/loadcompile(checkpointer=...) + config={"configurable": {"thread_id": ...}};get_state_history = time-travel同一 thread_id 即恢复;InMemorySaver 进程内,SqliteSaver 落盘跨进程
幂等约束(idempotency key)interrupt 重跑语义同样要求幂等:interrupt 前的副作用必须幂等或移后同一条纪律,框架不会替你执行
抽象成本对比手写 loop 直接控制每一步;LangGraph 在之上加了编译时拓扑校验、节点边界自动 checkpoint、reducer 组合语义框架给你的:图结构可序列化、条件边路由可审计、checkpoint 跨进程零配置;框架替你隐藏的:while 循环的显式控制流、状态 mutation 的时序、异常从节点向上传播的路径;跟框架打架时坏的:在节点内 mutate 外部状态而不走 reducer、在 interrupt 前放非幂等副作用、把图当 while 循环手动跳节点——这些都绕过框架的保证,framework_compare.py 的 fixture 正是用这些场景校验手写版与 LangGraph 版的行为等价性

自研框架 vs LangGraph vs LangChain:5 句话架构裁决 (LL-Agent-1) ​

  1. 控制流透明度:自研框架以极简的显式状态机(While 循环 + 显式状态转移函数 δ)驱动,调用栈纯粹直观,零黑盒调度;LangGraph 将控制流编译为图拓扑结构,依赖隐式条件边与状态派发,获得了图序列化与可视化能力,但让单步调试和原生 Exception 捕获链路大幅复杂化。
  2. 状态更新与合并机制:自研框架通常采用强类型对象或不可变数据快照直接演进,语义明确;LangGraph 强制解耦节点计算与状态写入,通过 Partial Update 增量返回,并高度依赖 State Reducer(如 operator.add)进行合并,若开发者遗漏 Reducer 标注将引发后值覆盖的隐蔽故障。
  3. 副作用与恢复语义:自研框架的状态机能够精确控制中断点与重放计数,易于实现外部幂等;LangGraph 基于快照回放的 Interrupt/Resume 机制具有“节点从头重跑(Node Re-execution)”的物理特性,强制要求 Interrupt 之前的任何操作必须严格幂等或后置,否则必然导致副作用二次执行。
  4. 扩展性与技术债周期:自研框架零外部第三方依赖,代码随团队业务架构灵活内聚演进,无破坏性升级负担;LangChain/LangGraph 生态技术迭代极快、依赖链庞大且存在频繁的 API 废弃与断代(如 LangChain 早期 Agent 废弃、create_react_agent 移除),企业长期维护需承担高昂的版本漂移与升级适配成本。
  5. 系统安全与准入控制:自研框架可原生贯彻防御性编程,在循环核心无缝嵌入微秒级的 Tool Policy、Schema Gate 和准入鉴权拦截;LangGraph 的安全与人机协同必须以特殊节点或图外编排层封装,框架本身不提供业务合规兜底,安全责任依然完全落在自研拦截策略上。

前端类比:StateGraph 就是你写过的 store + reducer + 路由——Annotated[list, operator.add] 等价于 Redux 里 case APPEND: [...state, action.payload],条件边等价于路由守卫,checkpointer 等价于把 store 快照按 session id 持久化。

依赖安装(可选依赖,不进课程基线) ​

bash
.venv/bin/pip install -r requirements-agent.txt   # 追加项:langgraph、langgraph-checkpoint-sqlite

langgraph 会带入 langchain-core(@tool、消息类型来自这里)。测试在依赖缺失时整模块 skip(诚实降级),不会把"没装"伪装成"通过"。

阶段二:从手写状态机到 StateGraph ​

手写 loop 的骨架:while running: action = planner(state); state.step += 1; ...。LangGraph 版(build_loop_graph):

python
class LoopState(TypedDict):
    step: int
    done: bool
    trace: Annotated[list[str], operator.add]  # reducer:追加而非覆盖

graph = StateGraph(LoopState)
graph.add_node("decide", decide)   # = planner
graph.add_node("act", act)         # = executor
graph.add_edge(START, "decide")
graph.add_conditional_edges("decide", route, {"act": "act", "end": END})
graph.add_edge("act", "decide")    # 循环 = 边回指,不是 while
app = graph.compile()
result = app.invoke({"step": 0, "done": False, "trace": []})

State Reducer 的合并机制:在 LangGraph 中,各工作节点仅返回局部状态增量(Partial Update),框架负责将其合并至全局 State。默认机制为“后值覆盖”;若声明 Annotated[list, operator.add] 则按列表追加。若未显式指定 Reducer,状态字段将被末尾节点的返回值直接覆盖。

动手:跑 lab1_stategraph_loop(max_steps=3),数 trace 长度(实测 7 = 3 轮 decide+act + 1 次 done 判定)。然后把 reducer 从 operator.add 改成默认(无 Annotated),观察 trace 只剩最后一个节点的返回值——亲眼看到覆盖故障,再改回来。

阶段三:Typed tools 与 ToolNode ​

第15章的两个 typed tools 框架化(lookup_fact / append_note):

python
@tool
def append_note(note: str, count: int = 1) -> str:
    """Append a note `count` times to the scratchpad."""
    if count < 1 or count > 5:
        raise ValueError("count must be within [1, 5]")
    return f"noted:{note} x{count}"

@tool 从签名生成 JSON Schema(append_note.args_schema.model_json_schema() 可验证:note: string 必填、count: integer)。真实系统里模型节点是 model.bind_tools(tools) 的 chat model;本章用 MockToolModel(按脚本发 AIMessage(tool_calls=[...]))保持确定性。ToolNode 执行工具并把结果作为 ToolMessage 回传——这正是手写版 observe → plan 的那条边。

动手:跑通 lab2_tool_loop(2 次工具调用、6 条消息、最终文本由 tool 结果拼成)。再跑 lab2_tool_error_path:工具抛 ValueError 时图不崩,错误作为 status="error" 的 ToolMessage 回传给模型。注意一个实测坑:ToolNode 默认只捕获部分异常类型,ValueError 会被重新抛出炸掉图——需要显式 ToolNode(..., handle_tool_errors=True) 才把所有异常转成 error 消息。回答:为什么工具错误应该作为 data 回传给模型而不是直接让图失败?(模型可以观察错误并修正参数重试——对应第15章的"schema 失败提示模型重写参数"。)

阶段四:Checkpoint 与恢复 ​

python
from langgraph.checkpoint.sqlite import SqliteSaver
conn = sqlite3.connect("checkpoints.sqlite", check_same_thread=False)
app = graph.compile(checkpointer=SqliteSaver(conn))
config = {"configurable": {"thread_id": "w10c-lab3"}}
app.invoke(input, config=config)          # 进程 A
# ... 进程 A 退出;进程 B 新建连接、重新 compile,同一 thread_id ...
app_b.get_state(config)                   # 完整恢复,零节点重跑
list(app_b.get_state_history(config))     # time-travel:逐步 checkpoint 历史

对照手写版:CheckpointStore.save(state) → checkpointer 在每个节点边界自动落盘;resume(checkpoint_hash) → 同一 thread_id 继续。InMemorySaver 适合开发与测试,SqliteSaver 才能跨进程。

动手:跑 lab3_sqlite_recovery——第一个实例跑完两步图后 conn.close()(模拟进程退出),第二个全新实例用同一 thread_id 读回状态。实测:恢复状态与首次运行完全一致、第二个实例零节点重跑、get_state_history 返回 4 个 checkpoint。进阶:lab3_interrupt_resume_sqlite 演示中断中的执行也能跨实例 resume(Command(resume=True) 在全新进程里继续跑完 before → gate → after)。

阶段五:HITL = interrupt,以及"从头重跑"语义 ​

第15章的批准门在 LangGraph 里是一个插在敏感工具前的节点:

python
from langgraph.types import interrupt, Command

def approval_node(state):
    answer = interrupt({"action": "delete_records", "risk": "high"})  # 挂起,payload 须 JSON 可序列化
    side_effects["delete_records"] += 1   # 副作用放 interrupt 之后
    return {"decision": str(answer)}

first = app.invoke(input, config=config)       # 返回 {"__interrupt__": [...]}
resumed = app.invoke(Command(resume="approved"), config=config)  # 人工批准后恢复

关键语义:恢复时整个节点从头重跑,不是从 interrupt 行继续。 用时间线理解:第一次运行,节点从头执行到 interrupt 行挂起;Command(resume=...) 恢复时,节点再次从头执行,这次 interrupt 直接返回 resume 值,继续往下跑。由此三条规则:

  1. interrupt 之前的副作用必须幂等(或移到 interrupt 之后)——因为它会被执行两次;
  2. 不要用裸 try/except 包住 interrupt——重跑时异常路径会再次触发;
  3. 一个节点内多个 interrupt 按索引匹配 resume 值。

官方不推荐用静态 interrupt_before 做 HITL(它适合调试断点),批准语义应该用节点内 interrupt 表达。

故障题(必做):build_hitl_graph(idempotent=False) 故意把 side_effects["delete_records"] += 1 放在 interrupt 之前。第一次 invoke 挂起时副作用已执行 1 次;Command(resume=...) 恢复后节点从头重跑,副作用执行第 2 次——实测计数 1→2。修正版把同一行移到 interrupt 之后,实测恢复后仍只执行 1 次。这与第15章幂等键解决的是同一类问题:任何"恢复"机制都会重放,副作用必须能安全重放。

阶段六:Workflow 模式谱系 + 何时不用 agent ​

Anthropic《Building effective agents》 把 LLM 系统分两档:workflow(LLM 走预定义代码路径编排)与 agent(模型自主决定过程与工具使用)。决策边界一句话:从最简单的方案起步,仅在任务需要模型级自主决策时才增加自主性——每多一分自主性,就多一分延迟、成本与不可预测性,要用可观测的收益去换。

五种 workflow 模式的图形态(全部能用本章的 StateGraph 直接画出来):

模式图形态适用
prompt chaining线性链 A → B → C,节点间可有程序化的 gate任务可分解为固定子步骤
routing一个分类节点 + 条件边分发到专用分支输入类别决定处理路径
parallelization扇出并行节点 + 聚合节点(sectioning / voting)子任务独立,或需多视角投票
orchestrator-workersorchestrator 动态分解 → worker 并行 → synthesizer 合并子任务无法预先穷举
evaluator-optimizer生成 → 评估 → 条件边回环,达标或轮次用尽退出有明确可计算的评估标准

动手:build_evaluator_optimizer 把手写 loop 改造成 evaluator-optimizer——mock generator 每轮产出 draft-v{n},mock evaluator 给确定性评分,条件边在 score >= target 或 rounds >= max_rounds 时退出,否则回环到 generator。实测 target=3 时 3 轮收敛;max_rounds=2 时不收敛但按预算退出。把它和阶段二的纯循环对比:同样的条件边回指,多了一个"评分驱动"的退出条件——workflow 模式的差别全在图拓扑与路由函数里。

收尾(知道即可,不展开):LangChain v1 的高阶入口 create_agent 就跑在 LangGraph 上,配合 middleware(Summarization、HumanInTheLoop 等预置件)是生产快捷方式;langgraph.prebuilt.create_react_agent 在 langgraph 0.2.x 已移除(当前 pinned 基线 langgraph>=1.2,<2,实测 1.2.10——你安装的版本里不存在这个符号,import 会直接报错)。迁移路径:手写 AgentState + ToolNode + 条件边循环,或跟随 LangGraph 当前推荐的 prebuilt 路径——具体 API 以使用时的官方文档为准。本仓库 framework_compare.py 对照的是手写 loop 与本地 state graph,不锁定任何 deprecated prebuilt API。LCEL 退居 langchain-classic,本课不教。判断不变:快捷方式不替代你在第15章学过的 schema/policy/幂等/批准 gate。

生产级警示:何时坚决不引入 LangGraph (LL-Agent-1) ​

典型反面场景:超低延迟实时风控/API 网关内联 Agent (Inline Risk Inspection Agent) ​

  • 场景特征:部署在金融支付或 API Gateway 实时主路径上,单节点承受数万 QPS,要求 P99 延迟严格压在 5~10ms 内,单次状态流转需以微秒(μs)计算。
  • 为何坚决不引入 LangGraph(底层物理归因):
    1. 状态拷贝与堆内存垃圾回收(GC Pressure):LangGraph 在每次节点流转与 Reducer 合并时,强制执行 TypedDict / Pydantic 深度反序列化与增量合并,创建大量短生命周期临时字典对象,在高并发下引发不可预测的 Python GC 停顿与延迟尖刺;
    2. 事件流调度与中间件开销:LangGraph 内部集成了复杂的异步事件总线(为了支撑追踪与图监听),即使完全关闭外部遥测,其抽象层包裹调用耗时依然在数十微秒级,无法匹敌纯 Python 扁平状态机纳秒级的直接跳转;
    3. 沙箱合规与依赖膨胀风险:高密核心网络有严苛的第三方代码供应链安全审查(SCA)。引入 LangGraph 需拉取庞大的依赖树(langchain-core、pydantic、jsonpatch 等);而自研显式状态机仅需数十行标准库代码,白盒可审计且零供应链攻击面。

Workflow vs Agent:5 维选型决策矩阵 (LL-Agent-4) ​

在做 C 端与生产架构选型时,切忌盲目“言必称 Agent”。下表给出了 5 维度的量化评估基准(分数 1~5 分,分数越高代表系统复杂度、成本或风险越高):

评估维度核心考量确定性 Workflow (如 DAG/链式编排)自主型 Agent (如 ReAct/自主探索)架构选型权衡基准
1. 业务路径确定性路径是否可预先穷举、终态是否清晰固定分支 / 100% 可穷举
例如:客服工单自动归类、订单取消退款
开放式求解 / 动态试错
例如:跨代码仓 Bug 诊断、深度竞品情报调研
凡是业务专家能画出标准流程图的,坚决不用自主 Agent。
2. Token 调用成本单次任务消耗的 Token 放大乘数低成本(1x 基线)
单次执行仅消耗预设 Prompt 的固定 Token
高成本(4x ~ 15x+)
多轮试错、中间思考与历史叠加呈指数放大
评估业务商业价值是否能覆盖 10 倍以上的 Token 溢价。
3. 端到端延迟P99 响应耗时与稳定性波动毫秒~秒级 / 高度可预期(1~3s)
通常仅需 1~2 次模型前向计算
长程 / 高度不确定(10s ~ 120s+)
取决于试错轮次、工具重试与回环次数
在线 C 端同步交互界面尽量避免无界 Agent 探索。
4. 可解释与可审计结果追溯性与合规白盒度100% 确定性可回溯
代码固定分支,各节点具备前置后置断言
概率性轨迹 / 弱解释性
受采样温度影响,同一输入两次轨迹可能完全不同
强监管合规领域(金融合规、医疗诊断)严禁使用自主 Agent。
5. 错误爆炸半径单节点失败对系统全局的破坏性极小且可精细隔离
单节点报错直接触发对应分支的降级兜底
极大且具级联放大效应
一次工具调用幻觉可能引发后续连锁破坏
是否涉及真实资金划扣/不可逆写库决定了能否放权给 Agent。

架构选型决策树 ​

阶段七:动手实验 ​

五节全部真实运行 langgraph,确定性 mock model,无网络、无 API key。参考实现是 python/labs/langgraph_labs.py,测试是 python/tests/test_langgraph_labs.py(13 项,全部真实运行 langgraph,不打桩)。所有 lab 用确定性 mock model / 假决策函数——与第15章 FixtureModel 同一边界。

环境准备 ​

bash
cd <仓库根>
export PYTHONPATH="$PWD/python"
.venv/bin/pip install -r requirements-agent.txt   # langgraph + langgraph-checkpoint-sqlite

命令与预期输出 ​

bash
# 1) 全部 13 项测试(真实跑 langgraph)
.venv/bin/python -m pytest python/tests/test_langgraph_labs.py -q

# 2) 五节 lab 汇总指标(evidence 数据源)
.venv/bin/python python/labs/langgraph_labs.py
text
.............                                                            [100%]
13 passed in 1.90s

lab1: steps=3, trace_length=7(decide/act 交替追加,末位 decide:done@3)
lab2: 2 次工具调用,6 条消息,final 文本由 tool 结果拼成
lab2_error: ToolNode 把 ValueError 回传为 status="error" 的 ToolMessage
lab3: 跨实例恢复 recovered_matches=true,节点重跑 0 次,history 4 个 checkpoint
lab3_hitl: 中断中的执行跨实例 resume 后跑完 ["before","gate","after"]
lab4_faulty: interrupt 前的副作用恢复后执行第 2 次(1→2,故障复现)
lab4_fixed: 副作用移到 interrupt 后,恢复后仍只执行 1 次(0→1,修复验证)
lab5: evaluator-optimizer 3 轮收敛(target=3),超预算时如实报未收敛

判定信号:
  reducer 追加、条件边路由、ToolNode 错误回传全部通过
  SqliteSaver 同一 thread_id 跨实例恢复零重跑
  interrupt/resume 节点重跑语义被故障题实证

概念图 ​

故障注入与预期信号 ​

注入预期失败信号修复后证据
State 字段不写 reducer(无 Annotated[..., operator.add])后一个节点的返回值覆盖前面的累积,trace/messages 只剩最后一项声明 reducer 后 trace 按节点顺序完整追加(lab1 实测 7 项)
ToolNode 默认错误处理 + 工具抛 ValueError异常穿透节点,整个 invoke 抛错handle_tool_errors=True 后错误作为 status="error" 的 ToolMessage 回传,模型可见并继续
interrupt 之前放非幂等副作用Command(resume=...) 恢复后副作用执行第 2 次(实测计数 1→2)副作用移到 interrupt 之后(或加幂等键),恢复后仍只执行 1 次
裸 try/except 包住 interrupt重跑时异常路径再次触发,状态不可预测interrupt 不做异常包装;恢复值校验放在 interrupt 之后
换了进程/实例但 thread_id 写错get_state 读不到历史,从头空跑同一 thread_id 跨实例恢复,零节点重跑(lab3 实测)
无 checkpointer 就用 interrupt图无法挂起/恢复,直接报错compile(checkpointer=InMemorySaver()) 起步,落盘换 SqliteSaver
evaluator-optimizer 只设 target 不设 max_rounds评分永远不达标时图无限回环双退出条件(达标 OR 轮次预算),超预算如实报"未收敛"

本章验收 ​

不看资料,用 5–10 分钟回答:

  • 闭卷解释概念映射表全表:手写 loop 的每个部件在 LangGraph 里是什么,差异在哪。
  • 解释 reducer:为什么 messages 字段不写 add_messages 会被覆盖?State 的更新机制到底是"返回全量"还是"返回增量"?
  • 背出 interrupt 的三条规则,先预测 build_hitl_graph(idempotent=False) 中 interrupt 前的副作用在恢复后会执行几次,再运行 test_faulty_side_effect_before_interrupt_reruns_on_resume 验证预测(实测副作用从 1 变为 2),并解释"节点从头重跑"为什么推出"副作用必须幂等或延后"——用 lab4 的计数 1→2 作证。
  • 说清 checkpointer / thread_id / store 的分工:checkpointer 管"执行到哪儿"(thread 内状态快照),thread_id 标识一段会话/任务,store 管跨 thread 的长期记忆。
  • 用"从最简单方案起步"原则,给三个具体任务分别判定该用 workflow 还是 agent,并说出理由;顺带说清 LangChain v1 与 LangGraph 的分工(create_agent 跑在 LangGraph 上,create_react_agent 已废弃)。
  • 调试题:打开 python/tests/test_langgraph_labs.py 的 test_faulty_side_effect_before_interrupt_reruns_on_resume,在 build_hitl_graph(idempotent=False) 中把副作用移到 interrupt 之后,预测并观察恢复后副作用执行次数从 2 变为 1,说明为什么"节点从头重跑"语义要求 interrupt 之前的副作用必须幂等或延后。

规范与延伸 ​

前端/Agent 迁移 ​

条件边等价于路由守卫;interrupt/resume 等价于一个会序列化自身等待用户操作的弹窗流程。真正的迁移判断在别处:图的拓扑是开发者定义的代码路径(workflow),还是模型运行时自主选择的循环(agent)——这个边界决定你的测试策略(前者可确定性回放,后者必须加预算与 gate)。

资源 / 成本 / 隐私 ​

全部 lab 本地运行、无网络、无真实模型调用,gross cost 为 0。langgraph / langgraph-checkpoint-sqlite 为 MIT 许可的可选依赖,不进入课程核心基线;依赖缺失时测试诚实 skip。trace 与 checkpoint 内容遵循第15章的 redact 纪律:真实凭据、cookie、他人数据一律不进 State、不进 checkpoint 文件——sqlite checkpoint 文件本身按敏感产物处理,不进 git。

Evidence ​

仓库当前机器证据(只读快照) ​

evidence/16-langgraph-v1.json 是当前 checkout 的脱敏机器运行记录:test_langgraph_labs.py 13 项全部通过、reducer/条件边/SqliteSaver 跨实例恢复/interrupt 重跑语义的实测指标(含故障版副作用 1→2 与修复版 0→1 的对照计数)。全部 lab 用确定性 mock model,只证明框架合同,不证明 live planner 质量;本模块已登记进 evidence/module-manifest-v1.json。

学习者提交模板(待填写,不是当前机器证据) ​

复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actual 和 artifacts 尤其不能被当作已运行或已通过。artifacts 必须替换为本次提交中真实存在的仓库相对路径。

yaml
schema: learn-llm.evidence.v1
module: 18-langgraph
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 10
commands:
  - PYTHONPATH=python python -m pytest python/tests/test_langgraph_labs.py -q
  - PYTHONPATH=python python python/labs/langgraph_labs.py
metrics:
  - name: langgraph_tests_passed
    expected: 13
    actual: <recorded-value>
  - name: lab3_recovered_state_matches
    expected: true
    actual: <recorded-value>
  - name: lab3_node_reruns_on_recovery
    expected: 0
    actual: <recorded-value>
  - name: lab4_faulty_side_effects_after_resume
    expected: 2
    actual: <recorded-value>
  - name: lab4_fixed_side_effects_after_resume
    expected: 1
    actual: <recorded-value>
  - name: lab5_rounds_to_converge
    expected: 3
    actual: <recorded-value>
artifacts:
  - <learner-repo-relative-artifact-path>
cost:
  gross_usd: 0
  credit_usd: 0
licenses:
  - source: langgraph
    version: <installed-version>
    license: MIT
    attribution: LangChain Inc.
    redistribution: permitted-by-license
known_failures:
  - <sanitized-failure-or-none>

只有 invoke 成功截图、没有跨实例恢复与 interrupt 重跑故障对照计数时,本章保持 gate。

下一步 ​

进入 第19章 · 全栈流式交付与增量渲染:把第16章的 streaming 解析接到前端——SSE 协议、fetch+ReadableStream 消费、增量渲染。

私有学习站 · 原理从零构建 · 勿提交个人隐私或密钥