跳到内容

如何等待用户输入

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

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

另一种方法是设置一个专门用于获取用户输入的节点。这在笔记本设置中很容易实现 - 你只需在节点中放置一个 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

# 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}]}}