如何等待用户输入¶
一种主要的人机交互模式是等待用户输入。一个关键用例是向用户提出澄清问题。一种实现方式是简单地跳到END
节点并退出流程图。然后,任何用户响应都会作为流程图的新调用返回。这基本上就是创建一个聊天机器人架构。
这种方法的问题在于很难从流程图中的某个特定点继续执行。通常情况下,代理在处理某些流程时中途停下来,只需要一点用户输入。虽然可以设计流程图,使其具有conditional_entry_point
来路由用户消息回到正确的位置,但这并不特别可扩展(因为它本质上涉及一个路由函数,该函数可能几乎出现在任何地方)。
另一种方法是设置一个专门用于获取用户输入的节点。这在笔记本环境中很容易实现——你只需在节点中放置一个input()
调用即可。但这并不是生产环境就绪的。
幸运的是,LangGraph使得可以在生产环境中做类似的事情。基本思路是:
- 设置一个表示用户输入的节点。该节点可以具有特定的输入/输出边(如你所愿)。实际上,该节点内部不应有任何逻辑。
- 在该节点之前添加一个断点。这将使流程图在该节点执行之前停止(这是好的,因为该节点本身并没有实际逻辑)。
- 使用
.update_state
更新流程图的状态。传入你从用户那里获得的任何响应。关键在于使用as_node
参数来应用此更新,好像你就是那个节点一样。这将使流程图在下次恢复执行时,从该节点执行的结果继续,而不是从头开始。
设置¶
我们不会展示托管图表的完整代码,但如果你想查看,可以在这里查看:此处。一旦图表托管完成,我们就可以调用它并等待用户输入。
SDK 初始化¶
首先,我们需要设置客户端,以便能够与托管的图表进行通信:
等待用户输入¶
初始调用¶
现在,让我们通过在ask_human
节点之前中断来调用我们的图:
const input = {
messages: [
{
role: "human",
content: "使用搜索工具询问用户他们在哪里,然后查找那里的天气"
}
]
};
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: input,
streamMode: "updates",
interruptBefore: ["ask_human"]
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Use the search tool to ask the user where they are, then look up the weather there\"}]},
\"interrupt_before\": [\"ask_human\"],
\"stream_mode\": [
\"updates\"
]
}" | \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
sub(/^event: /, "", $0)
event_type = $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
}
'
输出:
{'agent': {'messages': [{'content': [{'text': "当然!我将使用AskHuman函数询问用户的位置,然后使用搜索功能查找该位置的天气。让我们从询问用户他们在哪里开始。", 'type': 'text'}, {'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR', 'input': {'question': '您目前位于何处?'}, 'name': 'AskHuman', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-a8422215-71d3-4093-afb4-9db141c94ddb', 'example': False, 'tool_calls': [{'name': 'AskHuman', 'args': {'question': '您目前位于何处?'}, 'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
向状态添加用户输入¶
我们现在希望用用户的回复更新这个线程。然后我们可以启动另一个运行。
因为我们将其视为工具调用,所以我们需要像响应工具调用一样更新状态。为此,我们需要检查状态以获取工具调用的ID。
state = await client.threads.get_state(thread['thread_id'])
tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']
# 我们现在使用ID和所需的响应创建工具调用
tool_message = [{"tool_call_id": tool_call_id, "type": "tool", "content": "san francisco"}]
await client.threads.update_state(thread['thread_id'], {"messages": tool_message}, as_node="ask_human")
const state = await client.threads.getState(thread["thread_id"]);
const toolCallId = state.values.messages[state.values.messages.length - 1].tool_calls[0].id;
// 我们现在使用ID和所需的响应创建工具调用
const toolMessage = [
{
tool_call_id: toolCallId,
type: "tool",
content: "san francisco"
}
];
await client.threads.updateState(
thread["thread_id"],
{ values: { messages: toolMessage } },
{ asNode: "ask_human" }
);
curl --request GET \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
| jq -r '.values.messages[-1].tool_calls[0].id' \
| sh -c '
TOOL_CALL_ID="$1"
# 构造JSON负载
JSON_PAYLOAD=$(printf "{\"messages\": [{\"tool_call_id\": \"%s\", \"type\": \"tool\", \"content\": \"san francisco\"}], \"as_node\": \"ask_human\"}" "$TOOL_CALL_ID")
# 发送更新的状态
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
--header "Content-Type: application/json" \
--data "${JSON_PAYLOAD}"
' _
输出:
{'configurable': {'thread_id': 'a9f322ae-4ed1-41ec-942b-38cb3d342c3a',
'checkpoint_ns': '',
'checkpoint_id': '1ef58e97-a623-63dd-8002-39a9a9b20be3'}}
接收人类输入后继续¶
现在我们可以告诉代理继续。我们可以将None作为图的输入传递,因为不需要额外的输入:
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"stream_mode\": [
\"updates\"
]
}"| \
sed 's/\r$//' | \
awk '
/^event:/ {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
sub(/^event: /, "", $0)
event_type = $0
data_content = ""
}
/^data:/ {
sub(/^data: /, "", $0)
data_content = $0
}
END {
if (data_content != "" && event_type != "metadata") {
print data_content "\n"
}
}
'
输出:
{'agent': {'messages': [{'content': [{'text': "感谢您告诉我您在旧金山。现在,我将使用搜索功能查找旧金山的天气。", 'type': 'text'}, {'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-241baed7-db5e-44ce-ac3c-56431705c22b', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
{'action': {'messages': [{'content': '["I looked up: current weather in San Francisco. Result: It\'s sunny in San Francisco, but you better look out if you\'re a Gemini 😈."]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '8b699b95-8546-4557-8e66-14ea71a15ed8', 'tool_call_id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}]}}
{'agent': {'messages': [{'content': "根据搜索结果,我可以为您提供旧金山当前天气的信息:\n\n旧金山目前的天气是晴朗。这是一个美丽的日子!\n\n然而,我应该指出搜索结果中包含一个关于双子座星座的不寻常评论。这似乎是一个笑话或搜索引擎添加的可能无关的信息。为了获得准确和详细的天气信息,您可能需要查看可靠天气服务或应用程序中的旧金山天气。\n\n您是否还有其他想了解的关于天气或旧金山的问题?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-b4d7309f-f849-46aa-b6ef-475bcabd2be9', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}