本章我们将展示如何在 LangGraph 中实现用户确认机制,让用户能够控制 AI 助手的敏感操作。
第二部分:添加操作确认机制
在讲之前,问大家一个问题呢,为什么需要确认机制?当助手代表用户执行操作时,用户应该(几乎)始终对是否执行这些操作拥有最终决定权。否则,助手的任何小错误(或它可能遭受的提示注入攻击)都可能给用户造成实际损害。
所以在本节中,我们将使用 interrupt_before
来暂停图的执行,并在执行任何工具之前将控制权返回给用户。我们构造的结构如下所示:
css
用户输入 → 获取用户信息 → 助手 → [中断点] → 工具执行 → 助手
步骤 1:定义状态和助手
我们的图状态和 LLM 调用与上一章几乎相同,但有一个例外:我们添加了一个 user_info
字段,它将在图开始时就被填充,我们可以直接在助手对象中使用状态,而不是使用可配置参数。
python
from typing import Annotated
from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import Runnable, RunnableConfig
from typing_extensions import TypedDict
from langgraph.graph.message import AnyMessage, add_messages
# 定义状态
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
user_info: str # 新增:用户信息字段
# 定义助手类
class Assistant:
def __init__(self, runnable: Runnable):
self.runnable = runnable
def __call__(self, state: State, config: RunnableConfig):
while True:
result = self.runnable.invoke(state)
# 如果 LLM 恰好返回空响应,我们会重新提示它给出实际响应
if not result.tool_calls and (
not result.content
or isinstance(result.content, list)
and not result.content[0].get("text")
):
messages = state["messages"] + [("user", "Respond with a real output.")]
state = {**state, "messages": messages}
else:
break
return {"messages": result}
# 配置 LLM
llm = ChatAnthropic(model="claude-3-sonnet-20240229", temperature=1)
# 定义助手提示词
assistant_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a helpful customer support assistant for Swiss Airlines. "
"Use the provided tools to search for flights, company policies, and other information to assist the user's queries. "
"When searching, be persistent. Expand your query bounds if the first search returns no results. "
"If a search comes up empty, expand your search before giving up."
"\n\nCurrent user:\n<User>\n{user_info}\n</User>"
"\nCurrent time: {time}.",
),
("placeholder", "{messages}"),
]
).partial(time=datetime.now)
# 定义工具列表
part_2_tools = [
TavilySearchResults(max_results=1),
fetch_user_flight_information,
search_flights,
lookup_policy,
update_ticket_to_new_flight,
cancel_ticket,
search_car_rentals,
book_car_rental,
update_car_rental,
cancel_car_rental,
search_hotels,
book_hotel,
update_hotel,
cancel_hotel,
search_trip_recommendations,
book_excursion,
update_excursion,
cancel_excursion,
]
# 绑定工具到 LLM
part_2_assistant_runnable = assistant_prompt | llm.bind_tools(part_2_tools)
步骤 2:定义图结构
现在创建图。与上一章相比,我们做了 2 个重要改变来解决之前的问题:在使用工具之前添加中断点,在第一个节点中显式填充用户状态,这样助手无需使用工具就能了解用户信息。
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
from langgraph.prebuilt import tools_condition
builder = StateGraph(State)
# 定义用户信息节点
def user_info(state: State):
return {"user_info": fetch_user_flight_information.invoke({})}
# 新增:fetch_user_info 节点首先运行
# 这意味着我们的助手无需执行操作就能看到用户的航班信息
builder.add_node("fetch_user_info", user_info)
builder.add_edge(START, "fetch_user_info")
# 添加助手节点
builder.add_node("assistant", Assistant(part_2_assistant_runnable))
# 添加工具节点
builder.add_node("tools", create_tool_node_with_fallback(part_2_tools))
# 定义边
builder.add_edge("fetch_user_info", "assistant")
builder.add_conditional_edges("assistant", tools_condition)
builder.add_edge("tools", "assistant")
# 配置检查点和中断
memory = InMemorySaver()
part_2_graph = builder.compile(
checkpointer=memory,
# 新增:图将始终在执行 "tools" 节点之前暂停
# 用户可以在助手继续之前批准或拒绝(甚至更改请求)
interrupt_before=["tools"],
)
步骤 3:运行对话示例
现在我们运行一下新改进的聊天机器人!让我们在以下对话轮次列表上运行它。
python
import shutil
import uuid
# 更新备份文件,以便我们可以在每个部分从原始位置重新启动
db = update_dates(db)
thread_id = str(uuid.uuid4())
config = {
"configurable": {
# passenger_id 用于我们的航班工具来获取用户的航班信息
"passenger_id": "3442 587242",
# 检查点通过 thread_id 访问
"thread_id": thread_id,
}
}
_printed = set()
# 我们可以重用第 1 部分的教程问题来看看它的表现
for question in tutorial_questions:
# 流式处理事件
events = part_2_graph.stream(
{"messages": ("user", question)},
config,
stream_mode="values"
)
for event in events:
_print_event(event, _printed)
# 获取当前状态快照
snapshot = part_2_graph.get_state(config)
# 处理中断
while snapshot.next:
# 我们有一个中断!代理正在尝试使用工具,用户可以批准或拒绝
# 注意:此代码完全在图之外。通常,你会将输出流式传输到 UI
# 然后,当用户提供输入时,前端会通过 API 调用触发新的运行
try:
user_input = input(
"Do you approve of the above actions? Type 'y' to continue;"
" otherwise, explain your requested changed.\n\n"
)
except:
user_input = "y"
if user_input.strip() == "y":
# 直接继续
result = part_2_graph.invoke(None, config)
else:
# 通过提供有关请求更改/改变主意的说明来满足工具调用
result = part_2_graph.invoke(
{
"messages": [
ToolMessage(
tool_call_id=event["messages"][-1].tool_calls[0]["id"],
content=f"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.",
)
]
},
config,
)
# 更新快照
snapshot = part_2_graph.get_state(config)
现在我们的助手能够节省一个步骤来响应我们的航班详细信息。我们还完全控制了执行哪些操作。这一切都是使用 LangGraph 的中断机制 和检查点实现的。中断机制就是暂停图的执行,其状态使用你配置的检查点安全地持久化,用户随后可以通过使用正确的配置运行图来随时启动它,而状态从检查点加载,就好像它从未被中断过一样。
这里还有个问题,我们实际上不需要参与每一个助手操作,下面我们将重新组织图,以便我们只在实际写入数据库的"敏感"操作上中断。
第三部分:条件中断

现在我们将通过将工具分类为安全工具 (只读)和敏感工具(修改数据)来优化我们的中断策略。我们将仅对敏感工具应用中断,允许机器人自主处理简单查询。这在用户控制和对话流畅性之间取得了平衡。但随着我们添加更多工具,我们的单一图可能会因这种"扁平"结构而变得过于复杂。
大致的图结构如下:
scss
用户输入 → 获取用户信息 → 助手 → 路由决策
↓
┌───────────┴───────────┐
↓ ↓
安全工具 敏感工具
(直接执行) [需要确认]
↓ ↓
└───────────┬───────────┘
↓
助手
步骤 1:定义状态(与第 2 部分相同)
我们的状态和 LLM 调用与第 2 部分完全相同,这里就不演示代码了。
步骤 2:将工具分类
这里涉及到关键变更:将工具分为两类。
python
# "只读"工具(如检索器)不需要用户确认即可使用
part_3_safe_tools = [
TavilySearchResults(max_results=1),
fetch_user_flight_information,
search_flights,
lookup_policy,
search_car_rentals,
search_hotels,
search_trip_recommendations,
]
# 这些工具都会更改用户的预订
# 用户有权控制做出什么决定
part_3_sensitive_tools = [
update_ticket_to_new_flight,
cancel_ticket,
book_car_rental,
update_car_rental,
cancel_car_rental,
book_hotel,
update_hotel,
cancel_hotel,
book_excursion,
update_excursion,
cancel_excursion,
]
# 创建敏感工具名称集合(用于路由判断)
sensitive_tool_names = {t.name for t in part_3_sensitive_tools}
# 我们的 LLM 不必知道它必须路由到哪些节点
# 在它的"思维"中,它只是在调用函数
part_3_assistant_runnable = assistant_prompt | llm.bind_tools(
part_3_safe_tools + part_3_sensitive_tools
)
步骤 3:定义图结构
现在创建图。我们的图与第 2 部分几乎相同,只是我们将工具拆分为 2 个单独的节点。我们只在实际更改用户预订的工具之前中断。
python
from typing import Literal
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
from langgraph.prebuilt import tools_condition
builder = StateGraph(State)
# 定义用户信息节点
def user_info(state: State):
return {"user_info": fetch_user_flight_information.invoke({})}
# 新增:fetch_user_info 节点首先运行
builder.add_node("fetch_user_info", user_info)
builder.add_edge(START, "fetch_user_info")
# 添加助手节点
builder.add_node("assistant", Assistant(part_3_assistant_runnable))
# 添加两个工具节点
builder.add_node("safe_tools", create_tool_node_with_fallback(part_3_safe_tools))
builder.add_node("sensitive_tools", create_tool_node_with_fallback(part_3_sensitive_tools))
# 定义逻辑
builder.add_edge("fetch_user_info", "assistant")
# 定义工具路由函数
def route_tools(state: State):
"""根据工具类型路由到不同节点"""
next_node = tools_condition(state)
# 如果没有调用工具,返回到用户
if next_node == END:
return END
# 获取最后一条 AI 消息
ai_message = state["messages"][-1]
# 这里假设单个工具调用
# 要处理并行工具调用,你需要使用 ANY 条件
first_tool_call = ai_message.tool_calls[0]
# 判断是否为敏感工具
if first_tool_call["name"] in sensitive_tool_names:
return "sensitive_tools"
return "safe_tools"
# 添加条件边
builder.add_conditional_edges(
"assistant",
route_tools,
["safe_tools", "sensitive_tools", END]
)
# 工具节点返回助手
builder.add_edge("safe_tools", "assistant")
builder.add_edge("sensitive_tools", "assistant")
# 配置检查点和中断
memory = InMemorySaver()
part_3_graph = builder.compile(
checkpointer=memory,
# 新增:图将仅在执行 "sensitive_tools" 节点之前暂停
# 用户可以在助手继续之前批准或拒绝(甚至更改请求)
interrupt_before=["sensitive_tools"],
)
步骤 4:运行对话示例
现在我调试一下新改进的聊天机器人!让我们在以下对话轮次列表上运行它。这次,我们将有更少的确认。
python
import shutil
import uuid
db = update_dates(db)
thread_id = str(uuid.uuid4())
config = {
"configurable": {
"passenger_id": "3442 587242",
"thread_id": thread_id,
}
}
tutorial_questions = [
"Hi there, what time is my flight?",
"Am i allowed to update my flight to something sooner? I want to leave later today.",
"Update my flight to sometime next week then",
"The next available option is great",
"what about lodging and transportation?",
"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.",
"OK could you place a reservation for your recommended hotel? It sounds nice.",
"yes go ahead and book anything that's moderate expense and has availability.",
"Now for a car, what are my options?",
"Awesome let's just get the cheapest option. Go ahead and book for 7 days",
"Cool so now what recommendations do you have on excursions?",
"Are they available while I'm there?",
"interesting - i like the museums, what options are there?",
"OK great pick one and book it for my second day there.",
]
_printed = set()
for question in tutorial_questions:
events = part_3_graph.stream(
{"messages": ("user", question)},
config,
stream_mode="values"
)
for event in events:
_print_event(event, _printed)
snapshot = part_3_graph.get_state(config)
while snapshot.next:
# 我们有一个中断!代理正在尝试使用工具
try:
user_input = input(
"Do you approve of the above actions? Type 'y' to continue;"
" otherwise, explain your requested changed.\n\n"
)
except:
user_input = "y"
if user_input.strip() == "y":
# 直接继续
result = part_3_graph.invoke(None, config)
else:
# 提供更改说明
result = part_3_graph.invoke(
{
"messages": [
ToolMessage(
tool_call_id=event["messages"][-1].tool_calls[0]["id"],
content=f"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.",
)
]
},
config,
)
snapshot = part_3_graph.get_state(config)
现在是不是好多了!
然而这种设计还存在问题:我们给单个提示施加了很大的压力。如果我们想添加更多工具,或者如果每个工具变得更复杂(更多过滤器、更多约束行为的业务逻辑等),工具使用和机器人的整体行为可能会开始下降。
这一部分留在我们下一节中,我们将展示如何通过根据用户意图路由到专家代理或子图来更好地控制不同的用户体验。
总结
通过上面的优化例子,在 AI 自主性和用户控制权之间找到平衡点,让 AI 既能高效工作,又不会越权行事。记住:
AI 应该是助手,而不是替代者。用户始终应该掌握最终决策权。
通过合理使用中断机制,你可以构建出既智能又可控的 AI 应用,让用户真正信任并依赖你的系统。