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() 暂停当前图,并把审核数据返回给调用方。
  • MemorySaverthread_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

相关推荐
CodeSheep1 小时前
又一个华为天才少年,离职了!
前端·后端·程序员
IT_陈寒3 小时前
Vue的双向绑定把我坑惨了,原来这个场景不能用
前端·人工智能·后端
赵广陆4 小时前
企业实战:主体识别
langchain·pdf·langgraph
31535669134 小时前
DeepSeek Harness 发布后,我没急着跑 Demo,先把 `.agents/` 翻了一遍
前端·后端·github
Nturmoils4 小时前
不在公司,也能连回办公电脑:用 Natapp 打通 Windows 远程桌面
后端
LEE4 小时前
AI Agent 都在疯狂加功能,它说:我全砍了
前端·后端
桦说编程4 小时前
记一次 Coding Agent 改动带来的bug与启示
后端·agent·vibecoding
302wanger4 小时前
Mole-在终端里给Mac做减法
后端
一心只读圣贤书5 小时前
本地后端环境与前端联调总结(前端小白视角)
后端