Skip to content

如何等待用户输入

一种主要的人机交互模式是等待用户输入。一个关键用例是向用户提出澄清问题。一种实现方式是简单地跳到END节点并退出流程图。然后,任何用户响应都会作为流程图的新调用返回。这基本上就是创建一个聊天机器人架构。

这种方法的问题在于很难从流程图中的某个特定点继续执行。通常情况下,代理在处理某些流程时中途停下来,只需要一点用户输入。虽然可以设计流程图,使其具有conditional_entry_point来路由用户消息回到正确的位置,但这并不特别可扩展(因为它本质上涉及一个路由函数,该函数可能几乎出现在任何地方)。

另一种方法是设置一个专门用于获取用户输入的节点。这在笔记本环境中很容易实现——你只需在节点中放置一个input()调用即可。但这并不是生产环境就绪的。

幸运的是,LangGraph使得可以在生产环境中做类似的事情。基本思路是:

  • 设置一个表示用户输入的节点。该节点可以具有特定的输入/输出边(如你所愿)。实际上,该节点内部不应有任何逻辑。
  • 在该节点之前添加一个断点。这将使流程图在该节点执行之前停止(这是好的,因为该节点本身并没有实际逻辑)。
  • 使用.update_state更新流程图的状态。传入你从用户那里获得的任何响应。关键在于使用as_node参数来应用此更新,好像你就是那个节点一样。这将使流程图在下次恢复执行时,从该节点执行的结果继续,而不是从头开始。

设置

我们不会展示托管图表的完整代码,但如果你想查看,可以在这里查看:此处。一旦图表托管完成,我们就可以调用它并等待用户输入。

SDK 初始化

首先,我们需要设置客户端,以便能够与托管的图表进行通信:

from langgraph_sdk import get_client
client = get_client(url=<DEPLOYMENT_URL>)
# 使用名为 "agent" 的部署图表
assistant_id = "agent"
thread = await client.threads.create()
import { Client } from "@langchain/langgraph-sdk";

const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// 使用名为 "agent" 的部署图表
const assistantId = "agent";
const thread = await client.threads.create();
curl --request POST \
  --url <DEPLOYMENT_URL>/threads \
  --header 'Content-Type: application/json' \
  --data '{}'

等待用户输入

初始调用

现在,让我们通过在ask_human节点之前中断来调用我们的图:

input = {
    "messages": [
        {
            "role": "user",
            "content": "使用搜索工具询问用户他们在哪里,然后查找那里的天气",
        }
    ]
}

async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id,
    input=input,
    stream_mode="updates",
    interrupt_before=["ask_human"],
):
    if chunk.data and chunk.event != "metadata": 
        print(chunk.data)
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作为图的输入传递,因为不需要额外的输入:

async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id,
    input=None,
    stream_mode="updates",
):
    if chunk.data and chunk.event != "metadata": 
        print(chunk.data)
const streamResponse = client.runs.stream(
  thread["thread_id"],
  assistantId,
  {
    input: null,
    streamMode: "updates"
  }
);

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\",
   \"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}]}}

Comments