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()
相关推荐
小李不想当小白7 小时前
WDG看门狗(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
lisw0510 小时前
作为人类代理的大语言模型及其角色分类!
人工智能·学习·语言模型
Z21-X10 小时前
云原生运维实战笔记:企业网站综合上云部署与云运维服务全景解析
运维·笔记·学习·云原生
坤坤子吖10 小时前
Linux网络编程:UDP进阶,从字典服务器到简单聊天室
linux·服务器·网络·c++·笔记·学习
Rabitebla10 小时前
【Linux学习笔记】进程篇(九):进程的创建与退出——fork、写时拷贝与退出码
linux·c++·笔记·学习
楚山之石11 小时前
【MRM】CAPLEX 对比剂灌注 MRI 开源术语库:ISMRM OSIPI 共识报告
图像处理·人工智能·学习·mri
前进的李工1 天前
Agent 学习笔记 05:系统提示词应该怎么写
笔记·学习·prompt·agent·个人开发·few-shot·ai应用开发
正在走向自律1 天前
【3/5】人工智能训练师 三级/高级工 考证学习方向:从“独立执行”到“设计、归因与全链条负责”
人工智能·学习·人工智能训练师·注方案设计·数据质控负责人·模型评测与调优辅助·ai训练项目交付
CCIE-Yasuo1 天前
网络安全-学习数据库-Mysql-2
数据库·学习·mysql
La Pulga1 天前
【FreeRTOS源码学习笔记】任务切换
单片机·mcu·学习·嵌入式·freertos·实时操作系统