Appearance
第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 对应 | 关键差异 |
|---|---|---|
状态 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/load | compile(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)
- 控制流透明度:自研框架以极简的显式状态机(While 循环 + 显式状态转移函数
)驱动,调用栈纯粹直观,零黑盒调度;LangGraph 将控制流编译为图拓扑结构,依赖隐式条件边与状态派发,获得了图序列化与可视化能力,但让单步调试和原生 Exception 捕获链路大幅复杂化。 - 状态更新与合并机制:自研框架通常采用强类型对象或不可变数据快照直接演进,语义明确;LangGraph 强制解耦节点计算与状态写入,通过 Partial Update 增量返回,并高度依赖 State Reducer(如
operator.add)进行合并,若开发者遗漏 Reducer 标注将引发后值覆盖的隐蔽故障。 - 副作用与恢复语义:自研框架的状态机能够精确控制中断点与重放计数,易于实现外部幂等;LangGraph 基于快照回放的 Interrupt/Resume 机制具有“节点从头重跑(Node Re-execution)”的物理特性,强制要求 Interrupt 之前的任何操作必须严格幂等或后置,否则必然导致副作用二次执行。
- 扩展性与技术债周期:自研框架零外部第三方依赖,代码随团队业务架构灵活内聚演进,无破坏性升级负担;LangChain/LangGraph 生态技术迭代极快、依赖链庞大且存在频繁的 API 废弃与断代(如 LangChain 早期 Agent 废弃、
create_react_agent移除),企业长期维护需承担高昂的版本漂移与升级适配成本。 - 系统安全与准入控制:自研框架可原生贯彻防御性编程,在循环核心无缝嵌入微秒级的 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-sqlitelanggraph 会带入 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 值,继续往下跑。由此三条规则:
- interrupt 之前的副作用必须幂等(或移到 interrupt 之后)——因为它会被执行两次;
- 不要用裸
try/except包住 interrupt——重跑时异常路径会再次触发; - 一个节点内多个 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-workers | orchestrator 动态分解 → 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 内,单次状态流转需以微秒(
)计算。 - 为何坚决不引入 LangGraph(底层物理归因):
- 状态拷贝与堆内存垃圾回收(GC Pressure):LangGraph 在每次节点流转与 Reducer 合并时,强制执行 TypedDict / Pydantic 深度反序列化与增量合并,创建大量短生命周期临时字典对象,在高并发下引发不可预测的 Python GC 停顿与延迟尖刺;
- 事件流调度与中间件开销:LangGraph 内部集成了复杂的异步事件总线(为了支撑追踪与图监听),即使完全关闭外部遥测,其抽象层包裹调用耗时依然在数十微秒级,无法匹敌纯 Python 扁平状态机纳秒级的直接跳转;
- 沙箱合规与依赖膨胀风险:高密核心网络有严苛的第三方代码供应链安全审查(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.pytext
............. [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 之前的副作用必须幂等或延后。
规范与延伸
- LangGraph 官方文档(以执行日为准)。
- Anthropic: Building effective agents——五模式与 workflow/agent 边界的原始出处。
- LangChain v1 agents(create_agent + middleware)。
前端/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 消费、增量渲染。