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。