LangGraph学习-(3)人工审批

重点学习:

  1. 人工审批如何实现中断
  2. 人工审核之后如何恢复流程的

以下内容参照代码学习

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()
相关推荐
MartinYeung51 小时前
[论文学习]LoRA-Leak:针对 LoRA 微调语言模型的成员推断攻击深度分析与隐私风险评估
人工智能·学习·语言模型
极客侃科技1 小时前
线上课程学习平台选型指南:2026五大主流平台综合解析
学习
晓py2 小时前
Windows 本地挂载阿里云 ECS,并使用 Claude 操作挂载路径学习文档
windows·学习·阿里云
babe小鑫2 小时前
2026工商管理专业学习数据分析的价值分析
学习·数据挖掘·数据分析
一尘之中2 小时前
基于架构的软件开发方法
学习·架构·ai写作
chase。2 小时前
【学习笔记】面向机器人食物舀取的 spillage-aware 引导扩散策略
笔记·学习·机器人
踏着七彩祥云的小丑2 小时前
嵌入式测试学习第 34 天:常见bug类型:死机、重启、数据错乱、通信丢包
单片机·嵌入式硬件·学习
zhonghaoxincekj2 小时前
基于 168MHz MCU 的直流继电器全参数自动化测试方案解析
经验分享·功能测试·科技·学习·测试工具·创业创新·制造
爱睡懒觉的焦糖玛奇朵2 小时前
【从视频到数据集:焦糖玛奇朵的魔法工具Dataset Cleaner】
人工智能·python·学习·算法·yolo·音视频