重点学习:
- 人工审批如何实现中断
- 人工审核之后如何恢复流程的
以下内容参照代码学习
1. 人工审批如何实现中断
观察human_review函数中的interrupt();
第一次执行到这里是,就会触发中断
2. 人工审核之后如何恢复流程的
第一次图流程执行到human_review中的interrupt()时触发中断,

然后输入人工审批结果, raw_decision

注意两个地方,normalize_decision 和 graph.invoke(Command(resume=resume_value), config)
normalize_decision 中返回
{ "decision": "approve", "comment": f"人工确认结果:{raw_decision.strip() or 'approve'}"}
这两个字段并不是HumanInterruptState中的
同时注意graph.invoke(Command(resume=resume_value), config)使用的是Command(resume=resume_value) 而不是 resume_value
原因是 Command(resume=resume_value) 是将数据传给human_review中的 review_result,并不是将resume_value转成 图状态(HumanInterruptState), human_review中的return 才是真的更新图的状态; 此时图的状态中包含了 审批结果 "approved",
最后, 这个审批状态在条件边中,通过route_after_review 决定走通过流程,还是拒绝流程
参考代码
bash
from __future__ import annotations
from typing import Any, Literal, TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, StateGraph
from langgraph.graph.state import CompiledStateGraph
from langgraph.types import Command, RunnableConfig, StateSnapshot, interrupt
class HumanInterruptState(TypedDict):
"""动态中断 demo 使用的状态结构。"""
request: str
draft: str
approved: bool
review_note: str
final_message: str
steps: list[str]
def build_config(thread_id: str) -> RunnableConfig:
"""构造 thread_id 配置。"""
return {"configurable": {"thread_id": thread_id}}
def build_initial_state(request: str) -> HumanInterruptState:
"""构造动态中断 demo 的初始状态。"""
return {
"request": request,
"draft": "",
"approved": False,
"review_note": "",
"final_message": "",
"steps": [],
}
def print_snapshot(thread_id: str, snapshot: StateSnapshot) -> None:
"""打印当前 checkpoint 快照的关键信息。"""
print(f"[checkpoint] thread_id = {thread_id}")
print(f"[checkpoint] 当前完整状态 values = {snapshot.values}")
print(f"[checkpoint] 下一步待执行节点 next = {snapshot.next}")
print()
def normalize_decision(raw_decision: str) -> dict[str, str]:
"""把终端输入整理成结构化审批结果。"""
normalized = raw_decision.strip().lower()
approved_words = {"approve", "approved", "yes", "y", "同意", "通过"}
if normalized in approved_words:
return {
"decision": "approve",
"comment": f"人工确认结果:{raw_decision.strip() or 'approve'}",
}
return {
"decision": "reject",
"comment": f"人工确认结果:{raw_decision.strip() or 'reject'}",
}
def plan_action(state: HumanInterruptState) -> dict[str, Any]:
"""先生成一个待审批的行动建议。"""
print(f"[plan_action] 收到请求:{state['request']}")
draft = f"建议动作:为"{state['request']}"整理执行清单,并在提交前请人工确认。"
print(f"[plan_action] 生成草案:{draft}")
return {
"draft": draft,
"steps": state["steps"] + ["plan_action"],
}
def human_review(state: HumanInterruptState) -> dict[str, Any]:
"""动态中断节点:向人工发起审批请求。"""
print("[human_review] 进入人工确认节点。")
print("[human_review] 注意:恢复执行时,本节点会从头再次执行。")
review_result = interrupt(
{
"kind": "human_review",
"message": "请审批这份行动建议:输入 approve 或 reject。",
"draft": state["draft"],
}
)
print(f"[human_review] 收到人工审批结果:{review_result}")
approved = review_result.get("decision") == "approve"
review_note = review_result.get("comment", "")
return {
"approved": approved,
"review_note": review_note,
"steps": state["steps"] + ["human_review"],
}
def route_after_review(state: HumanInterruptState) -> Literal["approved", "rejected"]:
"""根据人工审批结果选择后续路径。"""
if state["approved"]:
print("[route_after_review] 审批通过 -> 进入 apply_action")
return "approved"
print("[route_after_review] 审批拒绝 -> 进入 reject_action")
return "rejected"
def apply_action(state: HumanInterruptState) -> dict[str, Any]:
"""模拟审批通过后的执行动作。"""
final_message = f"审批通过,开始执行:{state['draft']};备注:{state['review_note']}"
print(f"[apply_action] {final_message}")
return {
"final_message": final_message,
"steps": state["steps"] + ["apply_action"],
}
def reject_action(state: HumanInterruptState) -> dict[str, Any]:
"""模拟审批拒绝后的结束动作。"""
final_message = f"审批未通过,本次不执行;备注:{state['review_note']}"
print(f"[reject_action] {final_message}")
return {
"final_message": final_message,
"steps": state["steps"] + ["reject_action"],
}
def finish_human_flow(state: HumanInterruptState) -> dict[str, Any]:
"""动态中断 demo 的终结节点。"""
print(f"[finish_human_flow] 最终结果:{state['final_message']}")
print(f"[finish_human_flow] 执行路径:{state['steps']}")
return {
"steps": state["steps"] + ["finish_human_flow"],
}
def build_graph(checkpointer: InMemorySaver) -> CompiledStateGraph:
"""构建动态中断图。"""
builder = StateGraph(HumanInterruptState)
builder.add_node("plan_action", plan_action)
builder.add_node("human_review", human_review)
builder.add_node("apply_action", apply_action)
builder.add_node("reject_action", reject_action)
builder.add_node("finish_human_flow", finish_human_flow)
builder.set_entry_point("plan_action")
builder.add_edge("plan_action", "human_review")
builder.add_conditional_edges(
"human_review",
route_after_review,
{"approved": "apply_action", "rejected": "reject_action"},
)
builder.add_edge("apply_action", "finish_human_flow")
builder.add_edge("reject_action", "finish_human_flow")
builder.add_edge("finish_human_flow", END)
return builder.compile(checkpointer=checkpointer)
def run_demo() -> None:
"""使用 invoke 演示动态中断与恢复。"""
thread_id = "day3-invoke-dynamic-thread"
question = "提交发布前的变更说明"
checkpointer = InMemorySaver()
graph = build_graph(checkpointer)
config = build_config(thread_id)
print("=" * 60)
print("Day 3 Demo:graph.invoke + 动态中断 interrupt()")
print("目标:第一次 invoke 命中人工中断,第二次 invoke 用 Command(resume=...) 恢复")
print("=" * 60)
print()
print(">>> 第 1 次 invoke:传入初始状态,运行到 human_review 中断")
first_result = graph.invoke(build_initial_state(question), config)
print(f"[invoke] 第 1 次返回结果 = {first_result}")
print_snapshot(thread_id, graph.get_state(config))
if "__interrupt__" not in first_result:
print("未命中动态中断,本次演示提前结束。")
return
print(">>> 命中的中断载荷:")
print(first_result["__interrupt__"])
print()
raw_decision = input("请输入人工审批结果(approve / reject):").strip()
resume_value = normalize_decision(raw_decision)
print()
print(">>> 第 2 次 invoke:传入 Command(resume=...) 恢复执行")
print(f">>> 恢复载荷:{resume_value}")
second_result = graph.invoke(Command(resume=resume_value), config)
print(f"[invoke] 第 2 次返回结果 = {second_result}")
print_snapshot(thread_id, graph.get_state(config))
print(">>> 观察结论:")
print("1. 第一次 invoke 命中 interrupt() 时,返回结果中会包含 __interrupt__。")
print("2. 第二次 invoke 传入 Command(resume=...) 后,resume 值会成为 interrupt() 的返回值。")
print("3. 图状态通过同一个 thread_id 从 checkpoint 恢复,不需要再次传入初始状态。")
def main() -> None:
"""脚本入口。"""
run_demo()
if __name__ == "__main__":
main()