这节我们探讨下如何使用 interrupt
等待用户输入,虽然前面都有讲过,但是有部分同学还是一知半解,我决定把这一块详细的逻辑整理出来,从头到尾给大家讲述一下。
人机交互 (HIL) 交互对于 agentic
系统至关重要。等待人工输入是一种常见的 HIL
交互模式,允许 agentic
询问用户澄清问题并在继续之前等待输入。我们可以使用 interrupt()
函数在 LangGraph
中实现这一点。
interrupt
允许我们停止图形执行以收集用户的输入,并使用收集的输入继续执行。
安装
首先我们需要安装需要的包
pip install --quiet -U langgraph langchain_openai
或者
pip install --quiet -U langgraph langchain_anthropic
接下来,我们需要为 Anthropic 和/或 OpenAI(我们将使用的 LLM)设置 API 密钥
import getpass
import osdef
_set_env(var: str):
if not os.environ.get(var):
os.environ[var] = getpass.getpass(f"{var}: ")
_set_env("ANTHROPIC_API_KEY") # _set_env("OPENAI_API_KEY")
大家这里可以注册 LangSmith
以快速发现问题并提高 LangGraph
项目的性能。 LangSmith
允许我们使用跟踪数据来调试、测试和监控使用 LangGraph
构建的 LLM
应用程序 。这个根据大家的实际需要来使用。
简单的例子
让我们探讨一下使用人类反馈的基本示例。创建一个节点 human_feeback
,专门设计用于获取用户输入。这让我们能够在图 graph
中特定的选定点收集反馈。
在 human_feedback
节点内调用 interrupt()
。
设置一个检查指针以将图形的状态保存到该节点。使用 Command(resume=...)
向 human_feedback
节点提供请求的值并恢复执行。
如下面例子:
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
from langgraph.checkpoint.memory import MemorySaver
from IPython.display import Image, display
class State(TypedDict):
input: str
user_feedback: str
def step_1(state):
print("---Step 1---")
pass
def human_feedback(state):
print("---human_feedback---")
feedback = interrupt("Please provide feedback:")
return {"user_feedback": feedback}
def step_3(state):
print("---Step 3---")
pass
builder = StateGraph(State)
builder.add_node("step_1", step_1)
builder.add_node("human_feedback", human_feedback)
builder.add_node("step_3", step_3)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "human_feedback")
builder.add_edge("human_feedback", "step_3")
builder.add_edge("step_3", END)
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
display(Image(graph.get_graph().draw_mermaid_png()))
运行直到 human_feedback
处的断点 interrupt
:
initial_input = {"input": "hello world"}
thread = {"configurable": {"thread_id": "1"}}
for event in graph.stream(initial_input, thread, stream_mode="updates"):
print(event)
print("\n")
现在,我们可以使用用户的输入来手动更新图形状态:
for event in graph.stream(
Command(resume="go to step 3!"), thread, stream_mode="updates"
):
print(event)
print("\n")
现在看下我们的状态:
graph.get_state(thread).values
我们反馈的状态就已经更新好啦。
交互模式下的Agent 的ReAct
在 Agent
的背景下,等待用户反馈对于提出澄清问题特别有用。同学们可能不太明白,我们接下来解释一下,我们创建一个能够进行工具调用的简单 ReAct
风格的代理。
在下面这个例子中,比较贴近我们实际的 agent
开发,我们将使用 OpenAi
的聊天模型以及模拟工具。需要注意的是我们用的是 Pydantic v2 BaseModel
,它需要 langchain-core >= 0.3
。由于 Pydantic v1
和 v2 BaseModels
的混合,使用 langchain-core < 0.3
会导致错误。大家一定要注意自己的版本。
# 设定MessagesState 为State状态
from langgraph.graph import MessagesState, START
# 定义一个真实的搜索工具
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode
from langgraph.types import interrupt
from IPython.display import Image, display
# 这里有多种方式 可以用@tool 修饰,也可以继承扩展 BaseTool
@tool
def search(query: str):
"""添加说明"""
return f"我查了一下: {query}. Result:上海天气非常好,阳光明媚气温25度,如果你想要去滑雪,你最好别呆在这 😈."
# 这种方式之前讲到过,注意这两者的区别
class RequestSearch(BaseModel):
"""添加说明"""
request: f"我查了一下: {query}. Result:上海天气非常好,阳光明媚气温25度,如果你想要去滑雪,你最好别呆在这 😈."
# 定义搜索工具
tools = [search]
tool_node = ToolNode(tools)
# 设置大模型
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini")
from pydantic import BaseModel
# 我们将把所有工具“bind”到模型,我们上面定义了实际工具,但我们还需要一个模拟工具来询问人类,由于 `bind_tools` 不仅接受工具,还接受工具自定义,我们可以为 `ask_human` 定义一个自定义工具
class AskHuman(BaseModel):
"""问人类一个问题"""
question: str
model = model.bind_tools(tools + [AskHuman])
# 定义节点和条件边并定义确定是否继续的函数
def should_continue(state):
messages = state["messages"]
last_message = messages[-1]
# 如果没有函数调用,则结束
if not last_message.tool_calls:
return END
# 如果工具调用询问人类,我们将返回该节点
# 我们还可以在此处添加逻辑,让其他系统知道有些事情需要人类输入。例如,发送一些通知消息,发送一些工单给其他系统等
elif last_message.tool_calls[0]["name"] == "AskHuman":
# sendMessages("🔔 需要人工处理的新请求", “info”, last_message)
return "ask_human"
# 否则,我们将继续处理自动化操作
else:
return "action"
# 定义调用模型的函数
def call_model(state):
messages = state["messages"]
response = model.invoke(messages)
# 我们返回一个列表,因为这将添加到存在的现有列表中
return {"messages": [response]}
# 我们定义一个假节点来询问人类
def ask_human(state):
tool_call_id = state["messages"][-1].tool_calls[0]["id"]
location = interrupt("请输入您的位置:")
tool_message = [{"tool_call_id": tool_call_id, "type": "tool", "content": location}]
return {"messages": tool_message}
# 构件图 graph
from langgraph.graph import END, StateGraph
# 定义一个图 graph
workflow = StateGraph(MessagesState)
# 定义三个节点
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
workflow.add_node("ask_human", ask_human)
# 把agent设置成开始节点,这个节点是第一个被调用的节点
workflow.add_edge(START, "agent")
# 我们现在添加一个条件边
workflow.add_conditional_edges(
# 首先,我们定义起始节点。我们使用`agent`。这些是在调用`agent`节点后获取的边。
"agent",
# 接下来,我们传入确定下一步是调用哪个节点的should_continue函数。
should_continue,
)
# 我们现在从 `tools` 到 `agent` 添加一条普通边。在调用 `tools` 之后,接下来调用 `agent` 节点。
workflow.add_edge("action", "agent")
# 收到人工回复后,我们返回给agent
workflow.add_edge("ask_human", "agent")
# 设置内存
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
# 最后,我们开始编译!这会将其编译为 LangChain Runnable,这意味着我们可以像使用任何其他 Runnable 一样使用它,我们在 `ask_human` 节点之前添加一个断点,这样它就永远不会执行
app = workflow.compile(checkpointer=memory)
display(Image(app.get_graph().draw_mermaid_png()))
上面代码有点长,但是我都加了注释,大家可以直接复制下来尝试跑一下,下面是图的节点展示:
与代理交互
我们现在可以与 agent
进行交互。我们让它直接询问用户他们在什么位置,然后告诉用户天气。
使它首先使用 ask_human
工具,然后使用普通工具。
config = {"configurable": {"thread_id": "2"}}
for event in app.stream(
{
"messages": [
(
"user",
"使用 search tool 询问用户他们在哪里,然后查找那里的天气",
)
]
},
config,
stream_mode="values",
):
event["messages"][-1].pretty_print()
这个时候无哦们看下图的状态:
app.get_state(config).next
得到下面结果
('ask_human',)
从这里我们可以看到我们的图在 ask_human
节点内被中断,该节点现在正在等待提供 location
。我们可以通过使用 Command(resume="<location>")
输入调用图表来提供该值:
for event in app.stream(Command(resume="上海"), config, stream_mode="values"):
event["messages"][-1].pretty_print()
到这里就完成了 agent
怎么等待用户输入了,大家是不是清晰很多了。实际上我们实现这一步除了依赖 Command
和 interrupt
之外,主要还是将所有交互都保存在内存中,在内存里进行恢复中断。