FDE W4D2 学习手册 · LangGraph
W4 Week1 Day2 · A 级(必须掌握,面试核心)· 4h · 学完能讲清 LangGraph 如何用 State/Node/Edge/Conditional Edge 组成图,以及 Checkpointer/Interrupt 在 HITL 中的作用
本日定位:Day1 讲了 Agent Loop 的"朴素本质"(while 循环)。Day2 升级到工业级编排框架 LangGraph:用"图"(StateGraph)把节点、边、条件分支、工具调用、断点续跑、人工介入组织起来。这是把 Day1 的循环变成"可持久化、可中断、可审计"的生产系统的关键。
学完能回答:① State/Node/Edge/Conditional Edge 怎么组成一张图;② ToolNode 如何把工具挂进图;③ Checkpointer 与 Interrupt 在"人在回路(HITL)"里分别起什么作用;④ 结合特药理赔 Agent 说明哪里用条件边、哪里要断点等人审批。
使用方法:通读原理 → 重点看「面试话术」「易错点」→ 跑一遍 s10 的 StateGraph 代码 → 做自测清单 → 配合《FDE-W4D2-评测题.md》。选中不熟的词可标注(左下★重要 / 右下📌待查)。
一、为什么需要 LangGraph(图编排动机)
1.1 朴素 Loop 的局限
Day1 的手写 Agent Loop 在"玩具场景"够用,但生产里会遇到:循环状态怎么持久化(进程崩了怎么办)?中途要"等人审批"怎么暂停又恢复?多分支怎么表达?这些手写很痛苦。
1.2 LangGraph 的解法
LangGraph 把 Agent 抽象成一张有向图(StateGraph):
- Node(节点):一个计算步骤(调模型 / 调工具 / 校验)。
- Edge(边):节点间的流转,普通边是固定走向,条件边按 State 动态选分支。
- State(状态):贯穿整张图的共享数据结构,每个节点读写它。
- Checkpointer / Interrupt:让图能"存盘、暂停、恢复",支撑持久化与人工介入。
LangGraph 不是"又一个 Agent 框架",而是"把控制流显式变成一张图"。它的价值在于:① 流程可可视化、可审计(每笔理赔就是图里一条执行轨迹);② 原生支持持久化(Checkpointer)和中断恢复(Interrupt),这是企业上 Agent 的硬需求;③ 用 Conditional Edge 表达复杂分支,比手写 if/else 清爽且可控——本质是 Day1 循环的"工程化升级"。
LangGraph 把 Agent 画成一张图:节点=步骤,边=流转,状态=共享数据。它解决了手写循环的三大痛点:状态持久化、中途暂停等人、复杂分支。是 Day1 循环的工业级升级。
① LangGraph 不是"比手写 Loop 更牛",而是"更可控、可持久化"——小脚本用手写 Loop 也行,企业系统才需要它。② 别把 LangGraph 和 LangChain 混为一谈:LangChain 是"组件库",LangGraph 是"编排框架",两者互补。③ 图≠一定更灵活,图是开发者编排的确定性骨架,配合条件边才让模型参与分支决策。
二、State(状态)
2.1 原理
State 是一份贯穿整张图的共享数据。每个节点执行后返回对 State 的更新,LangGraph 按Reducer(归约函数)合并到全局 State。最常见的是 messages 字段,用"追加(append)"作为 reducer——这就是 Day1 里那个不断增长的 messages 列表。
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
class ClaimState(TypedDict):
messages: Annotated[list, add_messages] # 追加式 reducer
policy_no: str
risk_level: str
need_human: bool
2.2 Reducer 的关键
- append(如 messages):返回的新值追加到旧值后面(不直接覆盖)。
- replace(默认):返回的字段覆盖旧值。
State 的设计决定"节点之间怎么传数据"。生产里把"会变长、要累积"的东西(对话、工具结果)用 append reducer;把"单值结论"(risk_level、need_human)用 replace。特药理赔里 messages 累积每轮对话,risk_level 是最终结论覆盖即可。Reducer 选错(本该 append 用了 replace)会丢历史,是经典 bug。
State 是图的共享数据,节点返回更新、按 reducer 合并。messages 用 append(累积),结论字段用 replace(覆盖)。Reducer 选错会丢历史。
① append reducer 是 LangGraph 的"魔法点",新手常以为节点返回会整体覆盖 State,其实 append 字段是累加——若想覆盖要显式返回新列表或用 replace。② State 字段越多越重,每步都序列化,太大的字段(如 50KB OCR 原文)会拖慢且占存储,应只存 id/摘要。③ TypedDict 只是静态类型提示,运行时靠 reducer 决定合并方式,两者要一致。
三、Node(节点)
3.1 定义
Node 是一个 Python 函数(或 Runnable),签名 (state) -> 部分更新。它读 State、做计算、返回要合并进 State 的字段。
def call_model(state: ClaimState):
resp = model.invoke(state["messages"])
return {"messages": [resp]} # append 到 messages
def check_completeness(state: ClaimState):
ok = bool(state.get("policy_no"))
return {"need_human": not ok} # replace 覆盖
3.2 节点类型
| 节点 | 作用 |
| LLM 节点 | 调模型决策 / 生成 |
| Tool 节点 | 执行工具(常配合 ToolNode) |
| 逻辑/校验节点 | 纯函数:完整性检查、规则校验 |
| 人工节点(human node) | 中断点,等人输入(配合 Interrupt) |
Node 就是一个 (state)→更新 的函数。图里可以有 LLM 节点、工具节点、校验节点、人工节点。每个节点只干一件事,职责清晰。
四、Edge(边)与 Conditional Edge(条件边)
4.1 普通 Edge
固定从一个节点走到另一个节点:graph.add_edge("node_a", "node_b")。对应"必然的下一步"。
4.2 Conditional Edge(条件边)
根据 State 动态选择下一个节点:add_conditional_edges("node_a", 路由函数, {"分支1":"node_b","分支2":"node_c"})。路由函数读 State,返回分支名。
def route_after_model(state):
last = state["messages"][-1]
if last.tool_calls: # 模型要调工具
return "tools"
return "risk_check" # 否则去风险校验
builder.add_conditional_edges("call_model", route_after_model,
{"tools":"call_tools", "risk_check":"risk_check"})
4.3 为什么重要
条件边是"让模型参与控制流"的机制:模型输出决定走哪条边,这正是 Day1 说的"模型动态决策下一步"在图里的表达。普通边=确定性 Workflow,条件边=把决策点交给模型=Agentic Workflow。
普通边是"写死的流程"(Workflow),条件边是"按 State 选分支"(让模型/逻辑决策)。特药理赔里:call_model 后用量边——要工具走 tools,否则走 risk_check;check_completeness 后按 need_human 决定走人工节点还是继续。条件边让图既可控又灵活。
普通边固定走向(Workflow 部分);条件边按 State 动态选分支(Agent 部分,模型决策体现在路由函数的依据上)。图 = 普通边(骨架) + 条件边(决策点)。
① 条件边路由函数必须返回在映射表里声明的分支名,返回未注册的分支会报错。② 路由函数应是纯函数、可重放(中断恢复时会再跑一次),不能有副作用(别在里面发短信)。③ 条件边不等于"模型直接选节点"——是"函数读 State(含模型输出)来选",模型间接驱动,这点面试要讲清。
五、StateGraph 如何组成一张图
5.1 拼装步骤
定义 State → 建 StateGraph(State) → add_node 加节点 → add_edge / add_conditional_edges 连边 → add_edge(START, 首节点) / add_edge(末节点, END) → compile(checkpointer=...) 得到可运行图
builder = StateGraph(ClaimState)
builder.add_node("call_model", call_model)
builder.add_node("call_tools", ToolNode(tools))
builder.add_node("risk_check", risk_check)
builder.add_node("human_approve", human_node)
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", route_after_model,
{"tools":"call_tools", "risk_check":"risk_check"})
builder.add_edge("call_tools", "call_model") # 工具结果回填后再决策
builder.add_conditional_edges("risk_check", route_after_risk,
{"human":"human_approve", "done":"END"})
builder.add_edge("human_approve", "risk_check")
graph = builder.compile(checkpointer=memory) # 持久化
组图四步:① 定义 State;② add_node 加节点;③ add_edge/add_conditional_edges 连边(含 START/END);④ compile 时挂 checkpointer。一张图就成形了。
六、ToolNode(工具节点)
6.1 作用
ToolNode 是 LangGraph 内置节点,自动:读取 State 里最后一条 assistant 消息的 tool_calls → 逐个分发到对应工具函数 → 把结果包装成 tool
6.2 工具从哪来
工具通常是 LangChain 的 @tool 装饰函数,或 OpenAI 风格的 function schema。你的 Java 微服务可以包成一个 @tool 函数(内部 HTTP 调你的服务)。
from langchain_core.tools import tool
@tool
def ocr_scan(file_url: str) -> str:
"""识别理赔材料图片,返回结构化字段"""
return requests.post("http://your-java-svc/ocr", json={"url":file_url}).text
tools = [ocr_scan, query_policy]
tool_node = ToolNode(tools)
ToolNode 让"复用你已有工具"变得自然:Java 微服务、规则引擎包成 @tool,直接挂进图,不用重写。这正是 W4D3 特药理赔 Agent 的基石——编排而非重写。
ToolNode 自动解析 tool_calls、执行工具、回填结果(append 到 messages),省去手写回填。你的 Java 服务包成 @tool 就能挂进去。
① ToolNode 执行失败默认会抛异常中断图——生产要配合 try/except 或 handle_tool_errors 把错误当 tool 消息回填(对应 Day1 的"错误也回填"铁律)。② 工具必须幂等(Day1 考点),因为图可能重放。③ 工具里不要做"人工审批"这类需要暂停的事,那要用 Interrupt,不是 ToolNode。
七、Checkpointer(检查点 / 持久化)
7.1 原理
Checkpointer 是图的"存档器":每执行一步,把当前 State 快照按 thread_id 存起来。这样:
- 崩溃恢复:进程挂了,用同一 thread_id 重跑可从断点继续,不重头来。
- 多轮对话:同一 thread_id 的多次
invoke 共享 State(不丢上下文)。
- 中断恢复:配合 Interrupt 实现"暂停等人、再继续"。
7.2 使用
graph = builder.compile(checkpointer=MemorySaver()) # 或 Sqlite/Postgres
cfg = {"configurable": {"thread_id": "claim_12345"}}
graph.invoke({"messages":[("user","审核这张单")]}, cfg) # 同 thread_id 共享状态
Checkpointer 是企业敢用 Agent 的底座:① 理赔这种长流程,中途服务重启不能重跑 10 步;② 一笔理赔一个 thread_id,全程状态可追溯、可审计;③ 它是 Interrupt/HITL 的前提。生产用 Postgres/Redis 后端,别用 MemorySaver(重启即丢)。
Checkpointer 按 thread_id 给图"存档"。作用:崩溃断点续跑、同线程多轮共享状态、支撑中断恢复。生产用持久化后端(Postgres/Redis)。
① 不传 checkpointer,图也能跑但不能中断/恢复;要 HITL 必须传。② MemorySaver 仅测试用,生产用 PostgresSaver 等,否则重启丢全部进度。③ thread_id 要业务唯一(如理赔单号),混用会串状态。
八、Interrupt(中断)与 Command(命令)
8.1 原理
interrupt() 在节点里调用,会让图暂停执行并把控制权交回调用方,当前 State 已被 Checkpointer 存下。等人工/外部输入后,用 Command(resume=...) 带着输入"唤醒"图,从断点继续。
def human_approve(state):
decision = interrupt({"question":"是否批准该特药理赔?", "risk":state["risk_level"]})
# 图在此暂停;外部调用 graph.invoke(Command(resume="approved"), cfg) 后继续
return {"approved": decision == "approved"}
graph.invoke(Command(resume=user_input), cfg) # 带输入恢复
8.2 与 Checkpointer 的关系
Interrupt 负责"暂停",Checkpointer 负责"把暂停前的状态存好",二者配合才能实现 HITL。没有 Checkpointer,Interrupt 一暂停状态就没了,恢复不了。
Interrupt 是"人工审批"的技术实现:高风险的特药理赔不能让模型自己拍板,要在人工节点 interrupt() 暂停、把案件推给审核员,审核员在系统点"批准/驳回"后,前端用 Command(resume=...) 唤醒图继续。整个过程中 Checkpointer 保证状态不丢、可审计。
Interrupt 让图暂停等人(断点),Command(resume=...) 带人工输入唤醒图继续。它必须配 Checkpointer 才能存住暂停前的状态。这是 HITL 的实现方式。
① Interrupt 必须在节点函数里调用,不是在边里;② 恢复时必须用同一个 thread_id 和 Command(resume=...),否则从不了断点;③ 路由函数/节点会被重放,Interrupt 前的节点可能再执行一次,所以节点要幂等、不能有副作用;④ 别把"人工输入"塞进 ToolNode,那不是暂停,是同步等返回值。
九、HITL(人在回路)的工程含义
- 为什么需要:特药理赔涉及真金白银和医保合规,高金额/高风险案件必须由人拍板;模型只做初审与信息整理。
- 怎么实现:在风险判断后加条件边,
risk=="high" 走 human_approve 节点(interrupt 暂停);低风险的自动过。
- 可审计:每次中断/恢复都留 Checkpointer 轨迹,谁在何时批的、批了什么,全链路可查——合规刚需。
- 超时与升级:人工节点可设超时,超时未批自动升级到上级或转工单系统。
- 降级:审核员不在线时,可先挂起(不丢状态)或转应急通道,绝不卡死流程。
HITL = 高风险节点让人工拍板。LangGraph 用"条件边路由到 human 节点 + interrupt 暂停 + Checkpointer 存状态 + Command 恢复"实现,且全程可审计,满足医保合规。
十、实战:构建特药理赔 Agent 的 LangGraph
把 Day1 的 Agent Loop 升级为图,含工具调用、条件分支、人工审批中断。
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import ToolNode, tools_condition
builder = StateGraph(ClaimState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_node("risk_check", risk_check)
builder.add_node("human_approve", human_approve)
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition) # 内置:有tool_calls→tools
builder.add_edge("tools", "call_model")
builder.add_conditional_edges("risk_check", route_after_risk,
{"human":"human_approve", "done":END})
builder.add_edge("human_approve", "risk_check")
graph = builder.compile(checkpointer=MemorySaver())
# 运行
cfg = {"configurable": {"thread_id": "claim_8848"}}
graph.invoke({"messages":[("user","审核 /tmp/claim.png,保单 P123")]}, cfg)
# 若中断在 human_approve,前端拿审核员输入后:
# graph.invoke(Command(resume="approved"), cfg)
对照 Day1:这里的 call_model→tools→call_model 循环正是 Day1 的 Tool Calling Loop;tools_condition 是内置条件边(对应你的 route_after_model);human_approve + interrupt 是 Day1 "降级转人工"的工程化实现。图把这些"显式表达"了。
十一、LangGraph 高频易错点
① Reducer 选错:messages 本该 append 却用 replace,历史被覆盖。② 条件边返回未注册分支:路由函数返回值不在映射表,运行时报错。③ 路由函数有副作用:图会重放节点,在里面发短信会重复发送。④ ToolNode 错误不回填:工具异常直接中断图,应配 handle_tool_errors。⑤ Interrupt 没配 Checkpointer:暂停即丢状态,无法恢复。⑥ 恢复用错 thread_id:Command(resume) 必须同一 thread_id。⑦ 节点不幂等:重放时重复副作用。⑧ 把人工审批塞进 ToolNode:那不是暂停 HITL,要用 interrupt。
十二、学习资源(中文 / 非 OpenAI)
先看 B站视频建立"图"的直觉,再用官方 Quick Start 跑通最小例子,最后翻 GitHub 查 Checkpointer/Interrupt 的精确 API。三件套覆盖本日面试。
十三、面试达标线①:State / Node / Edge / Conditional Edge 如何组成图
| 组件 | 作用 | 在理赔图中举例 |
| State | 共享数据,按 reducer 合并 | messages(append) / risk_level(replace) |
| Node | 一个计算步骤 (state)→更新 | call_model / tools / risk_check |
| Edge | 固定流转 | START→call_model;tools→call_model |
| Conditional Edge | 按 State 动态选分支 | call_model 后:有 tool_calls→tools,否则→risk_check |
State(共享数据) + Node(步骤) + Edge(固定走向) + Conditional Edge(动态分支) → compile(checkpointer) → 可运行图
达标线①核心:能讲清"State 是共享数据(reducer 合并)、Node 是步骤、Edge 固定走向、Conditional Edge 按 State 选分支",并用一句"四者 compile 成图"收尾,最好结合理赔图举例。
十四、面试达标线②:Checkpointer 与 Interrupt 在 HITL 中的作用
| 机制 | 角色 | 在 HITL 里的作用 |
| Checkpointer | 存档器 | 按 thread_id 存每步 State 快照,支撑断点续跑、状态共享、可审计 |
| Interrupt | 暂停器 | 在 human 节点暂停,把控制权交回调用方,等人工输入 |
| Command(resume) | 唤醒器 | 带人工输入恢复图,从断点继续 |
human 节点调用 interrupt() → 图暂停 + Checkpointer 存状态 → 审核员输入 → Command(resume=决策) 同 thread_id 唤醒 → 图从断点继续
三者关系一句话:Interrupt 负责"暂停",Checkpointer 负责"把暂停前的状态存好",Command 负责"带输入唤醒"。没有 Checkpointer,Interrupt 一暂停状态就没了,HITL 无法实现。特药理赔用它在高风险节点让审核员拍板,且全程留痕可审计。
达标线②核心:能说清"Interrupt 暂停、Checkpointer 存状态、Command 带输入恢复"三者协作,并指出"Checkpointer 是 Interrupt 能恢复的前提",结合理赔人工审批举例。
十五、W4D2 自测清单
- 能说清为什么需要 LangGraph(手写 Loop 的三大痛点:持久化、暂停、分支)。
- 能区分 LangGraph 与 LangChain(编排框架 vs 组件库)。
- 能讲清 State 的作用与 Reducer(append / replace)的区别及选错后果。
- 能定义 Node 是 (state)→更新的函数,并说出常见节点类型。
- 能区分普通 Edge(固定走向)与 Conditional Edge(按 State 选分支)。
- 能讲清 Conditional Edge 是"模型间接参与控制流"的机制(Agentic Workflow)。
- 能口述 StateGraph 组图四步(定义 State→add_node→加边→compile 挂 checkpointer)。
- 能说明 ToolNode 自动做的事(解析 tool_calls→执行→回填),及工具要幂等。
- 能讲清 Checkpointer 的三种作用(崩溃恢复 / 多轮共享 / 支撑中断)。
- 能讲清 Interrupt + Command(resume) 实现 HITL 的流程,及必须配 Checkpointer。
- 能说清 HITL 在特药理赔里的工程含义(高风险人工拍板、可审计、超时升级)。
- 能列出 LangGraph 高频易错点(reducer 选错 / 分支未注册 / 路由副作用 / 没配 checkpointer 等)。
十六、高频面试题速记卡
Q:LangGraph 的 State 是什么?
贯穿整张图的共享数据,节点返回更新、按 reducer 合并。messages 用 append(累积),结论字段用 replace(覆盖)。
Q:普通 Edge 和 Conditional Edge 区别?
普通边固定走向(Workflow);条件边按 State 动态选分支(模型间接驱动,Agent 部分)。图=普通边骨架+条件边决策点。
Q:ToolNode 帮你做了什么?
自动解析最后一条 assistant 消息的 tool_calls、逐个执行工具、把结果 append 回 messages。省去手写回填,你的 Java 服务包成 @tool 即可挂载。
Q:Checkpointer 有什么用?
按 thread_id 存每步 State 快照:崩溃断点续跑、同线程多轮共享状态、支撑 Interrupt 中断恢复。生产用 Postgres/Redis,别用 MemorySaver。
Q:Interrupt 和 Checkpointer 什么关系?
Interrupt 负责暂停把控制权交回调用方,Checkpointer 负责把暂停前状态存好;没 Checkpointer 就恢复不了。两者配合实现 HITL。
Q:怎么实现人工审批(HITL)?
高风险节点用条件边路由到 human 节点,节点内 interrupt() 暂停,审核员输入后用 Command(resume=决策) 同一 thread_id 唤醒继续。
Q:为什么路由函数不能有副作用?
图会重放节点,路由函数可能再执行一次,里面发短信会重复发送。路由要纯函数。
Q:LangGraph 和手写 Agent Loop 比有何优势?
显式图可可视化/可审计、原生持久化(Checkpointer)、中断恢复(Interrupt),适合企业长流程(如理赔)而非玩具脚本。
Q:工具在图里要注意什么?
必须幂等(图会重放);ToolNode 错误要配置回填而非中断;别把"人工审批"塞进工具,那要用 interrupt。
Q:reducer 选错会怎样?
本该 append 的 messages 用了 replace 会被整体覆盖,丢历史;结论字段反之会不断累积。按"是否累积"选 reducer。
FDE W4D2 学习手册 · LangGraph(面试级)· 配合《FDE-W4D2-评测题.md》自测
📌 待查★ 重要