LangGraph 入门实战(9)--中断

LangGraph 人工审核:在工具调用前暂停并恢复

LangGraph 不仅可以编排模型和工具,还可以在关键步骤暂停流程,等待人工确认后再继续。本例让模型查询天气,但在真正执行工具前先进行人工审核。

1. 安装与配置

bash 复制代码
python -m pip install -U langgraph langchain-deepseek
export DEEPSEEK_API_KEY="你的 DeepSeek API Key"

2. 完整代码

python 复制代码
import os

from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.runnables.config import RunnableConfig
from langchain_core.tools import tool
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.types import Command, interrupt
from pydantic import SecretStr
from typing_extensions import Literal


@tool
def get_weather_search(city: str) -> str:
    """返回指定城市的天气信息。"""
    return f"{city}的天气是晴朗的,气温是40摄氏度"


deepseek = ChatDeepSeek(
    model="deepseek-v4-flash",
    temperature=0,
    base_url="https://api.deepseek.com",
    api_key=SecretStr(os.environ["DEEPSEEK_API_KEY"]),
)

# 绑定工具后,模型才能生成 tool_calls。
deepseek_with_tools = deepseek.bind_tools([get_weather_search])


class State(MessagesState):
    """以 messages 为核心的图状态。"""


def call_model(state: State):
    """调用模型,让模型决定是否需要查询天气。"""
    response = deepseek_with_tools.invoke(state["messages"])
    return {"messages": [response]}


def humain_review(state: State) -> Command[Literal["call_model", "run_tool"]]:
    """暂停流程,等待人类审核工具调用。"""
    last_message = state["messages"][-1]
    if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
        raise ValueError("人类审核节点需要一条包含工具调用的 AI 消息")
    tool_call = last_message.tool_calls[-1]

    review = interrupt({"question": "是否执行这个工具调用?", "tool_calls": tool_call})
    review_action = review["action"]
    review_answer = review.get("answer")

    if review_action == "continue":
        return Command(goto="run_tool")

    if review_action == "update":
        if not isinstance(review_answer, dict):
            raise ValueError("更新工具调用时,answer 必须是参数字典")
        if last_message.id is None:
            raise ValueError("无法更新没有 ID 的 AI 消息")
        update_message = {
            "role": "ai",
            "content": last_message.content,
            "tool_calls": [
                {
                    "id": tool_call["id"],
                    "name": tool_call["name"],
                    "args": review_answer,
                }
            ],
            "id": last_message.id,
        }
        return Command(goto="run_tool", update={"messages": [update_message]})

    if review_action == "feedback":
        if review_answer is None:
            raise ValueError("反馈内容不能为空")
        tool_message = {
            "role": "tool",
            "content": str(review_answer),
            "name": tool_call["name"],
            "tool_call_id": tool_call["id"],
        }
        return Command(goto="call_model", update={"messages": [tool_message]})

    raise ValueError(f"未知的审核操作: {review_action}")


def run_tool(state: State):
    """执行审核通过的工具调用。"""
    tools = {"get_weather_search": get_weather_search}
    last_message = state["messages"][-1]
    if not isinstance(last_message, AIMessage):
        raise ValueError("工具节点需要一条 AI 消息")

    messages = []
    for tool_call in last_message.tool_calls:
        tool_instance = tools[tool_call["name"]]
        result = tool_instance.invoke(tool_call["args"])
        messages.append(
            {
                "role": "tool",
                "name": tool_call["name"],
                "content": result,
                "tool_call_id": tool_call["id"],
            }
        )
    return {"messages": messages}


def router_after_llm(state: State) -> Literal["humain_review", "end"]:
    """有工具调用就审核,否则结束。"""
    last_message = state["messages"][-1]
    if isinstance(last_message, AIMessage) and last_message.tool_calls:
        return "humain_review"
    return "end"


# 流程:模型 -> 人工审核 -> 工具 -> 模型。
builder = StateGraph(State)
builder.add_node(call_model)
builder.add_node(run_tool)
builder.add_node(humain_review)
builder.add_edge(START, "call_model")
builder.add_conditional_edges(
    "call_model",
    router_after_llm,
    {"humain_review": "humain_review", "end": END},
)
builder.add_edge("run_tool", "call_model")

# checkpointer 保存 interrupt 前后的状态。
graph = builder.compile(checkpointer=MemorySaver())
config = RunnableConfig(configurable={"thread_id": "weather-demo"})

# 第一次运行到 interrupt 处暂停。
for event in graph.stream(
    {"messages": [HumanMessage(content="杭州,上海天气怎么样?")]},
    config=config,
    stream_mode="updates",
):
    print(event)

# 演示"继续执行"。实际项目可将 action 来自网页按钮或审批接口。
for event in graph.stream(
    Command(resume={"action": "continue"}),
    config=config,
    stream_mode="updates",
):
    print(event)

3. 工作流程

text 复制代码
START -> call_model -> humain_review -> run_tool -> call_model -> END
  • interrupt() 暂停当前图,并把审核数据返回给调用方。
  • MemorySaver 和 thread_id 保存暂停时的状态,恢复时必须使用同一个 thread_id。
  • Command(resume=...) 恢复图的执行。
  • continue 直接执行工具;update 修改工具参数后执行;feedback 将人工意见交回模型重新判断。

4. 典型输出

模型返回内容和消息 ID 会变化,输出结构通常如下:

text 复制代码
{'call_model': {'messages': [AIMessage(tool_calls=[
    {'name': 'get_weather_search', 'args': {'city': '杭州'}, ...}
])]}}
{'__interrupt__': (...,)}
{'run_tool': {'messages': [
    ToolMessage(content='杭州的天气是晴朗的,气温是40摄氏度')
]}}
{'call_model': {'messages': [
    AIMessage(content='杭州和上海今天都是晴朗天气,气温约40摄氏度。')
]}}

示例中的 continue 可以替换为:

python 复制代码
Command(resume={"action": "update", "answer": {"city": "上海"}})
Command(resume={"action": "feedback", "answer": "请只查询杭州"})

人工审核适合高风险工具调用,例如发送邮件、执行订单、修改数据库等场景。生产环境应将 MemorySaver 替换为持久化 checkpointer,并由服务端保存 thread_id。

相关推荐
考虑考虑10 小时前
JDK26中的Map.ofLazy
java·后端·java ee
云浪11 小时前
Go 并发实战:从零实现一个高并发安全的任务调度器
javascript·后端·go
苏三的开发日记12 小时前
Docker 启动顺序优化(CentOS / Ubuntu)
后端
高频因子挖掘机13 小时前
同一交易日出现重复 K 线:去重前先确认时间、来源和粒度
后端·github·api
feely020814 小时前
《我写了 31 个静态扫描插件,踩了 7 个「假绿」》
后端
橙子家15 小时前
参数校验类异常(Argument Exception)【C# Exception 详解系列一】
后端
掘金挖土15 小时前
前端手摸手跑路之 AI 应用开发(九)
前端·后端
全栈弄潮儿16 小时前
Python实战第8期:函数
后端·python·编程语言
码事漫谈16 小时前
你让 AI “尽力做”,它反而可能做不好
后端