跳到内容

如何等待用户输入

人工参与交互模式的主要模式之一是等待人工输入。一个关键用例是向用户提出澄清问题。实现此目的的一种方法是直接转到 END 节点并退出图。然后,任何用户响应都会作为图的新调用返回。这基本上只是创建了一个聊天机器人架构。

这样做的问题是很难在图中的特定点恢复。通常,代理正在进行某个过程的一半,只需要一点用户输入。虽然可以以某种方式设计您的图,使其具有 conditional_entry_point 以将用户消息路由回正确的位置,但这不是很可扩展(因为它本质上涉及具有一个路由函数,该函数可能最终在几乎任何地方)。

另一种方法是专门设置一个节点来获取用户输入。这在 notebook 环境中很容易实现 - 您只需在节点中放置一个 input() 调用即可。但这并不是真正的生产就绪。

幸运的是,LangGraph 使在生产环境中执行类似操作成为可能。基本思路是

  • 设置一个代表人工输入的节点。这可以具有特定的传入/传出边(根据您的需要)。此节点内部实际上不应有任何逻辑。
  • 在节点之前添加一个断点。这将在此节点执行之前停止图形(这很好,因为无论如何其中没有真正的逻辑)
  • 使用 .update_state 来更新图的状态。传入您获得的任何人工响应。这里的关键是使用 as_node 参数来应用此更新,就像您是该节点一样。这将产生这样的效果:当您下次恢复执行时,它会像该节点刚刚执行一样恢复,而不是从头开始。

设置

我们不会展示我们托管的图的完整代码,但如果您愿意,可以在此处查看。一旦托管了此图,我们就可以调用它并等待用户输入。

SDK 初始化

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

from langgraph_sdk import get_client
client = get_client(url=<DEPLOYMENT_URL>)
# Using the graph deployed with the name "agent"
assistant_id = "agent"
thread = await client.threads.create()
import { Client } from "@langchain/langgraph-sdk";

const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
// Using the graph deployed with the name "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": "Use the search tool to ask the user where they are, then look up the weather there",
        }
    ]
}

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: "Use the search tool to ask the user where they are, then look up the weather there"
    }
  ]
};

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': "Certainly! I'll use the AskHuman function to ask the user about their location, and then I'll use the search function to look up the weather for that location. Let's start by asking the user where they are.", 'type': 'text'}, {'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR', 'input': {'question': 'Where are you currently located?'}, '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': 'Where are you currently located?'}, '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']

# We now create the tool call with the id and the response we want
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;

// We now create the tool call with the id and the response we want
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"

     # Construct the JSON payload
     JSON_PAYLOAD=$(printf "{\"messages\": [{\"tool_call_id\": \"%s\", \"type\": \"tool\", \"content\": \"san francisco\"}], \"as_node\": \"ask_human\"}" "$TOOL_CALL_ID")

     # Send the updated state
     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': "Thank you for letting me know that you're in San Francisco. Now, I'll use the search function to look up the weather in San Francisco.", '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': "Based on the search results, I can provide you with information about the current weather in San Francisco:\n\nThe weather in San Francisco is currently sunny. It's a beautiful day in the city! \n\nHowever, I should note that the search result included an unusual comment about Gemini zodiac signs. This appears to be either a joke or potentially irrelevant information added by the search engine. For accurate and detailed weather information, you might want to check a reliable weather service or app for San Francisco.\n\nIs there anything else you'd like to know about the weather or San Francisco?", '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}]}}

评论