Skip to content

如何从先前状态重新运行和分支

使用LangGraph Cloud,您可以返回到任何先前的状态,并重新运行图以重现测试期间注意到的问题,或者从先前状态中以不同的方式分支出去。在此指南中,我们将展示如何快速重新运行过去的状态以及如何从以前的状态中分支出去。

继续阅读

重新运行过去的状态

从先前状态分支

设置

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

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 '{}'

重新播放状态

初始调用

在重新播放一个状态之前,我们需要创建一些可以重新播放的状态!为了实现这一点,让我们使用简单的消息来调用我们的图:

input = {"messages": [{"role": "user", "content": "Please search the weather in SF"}]}

async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id,
    input=input,
    stream_mode="updates",
):
    if chunk.data and chunk.event != "metadata": 
        print(chunk.data)
const input = { "messages": [{ "role": "user", "content": "Please search the weather in SF" }] }

const streamResponse = client.runs.stream(
  thread["thread_id"],
  assistantId,
  {
    input: input,
    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\",
   \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Please search the weather in SF\"}]},
   \"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_011vroKUtWU7SBdrngpgpFMn',
                                      'input': {'query': 'current weather in San Francisco'},
                                      'name': 'search',
                                      'type': 'tool_use'}],
                         'additional_kwargs': {},
                         'response_metadata': {},
                         'type': 'ai',
                         'name': None,
                         'id': 'run-ee639877-d97d-40f8-96dc-d0d1ae22d203',
                         'example': False,
                         'tool_calls': [{'name': 'search',
                                         'args': {'query': 'current weather in San Francisco'},
                                         'id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}],
                         'invalid_tool_calls': [],
                         'usage_metadata': None}]}}
{'action': {'messages': [{'content': '["我查到了:旧金山当前的天气。结果:旧金山今天阳光明媚,但如果你是双子座的话就要小心了 😈。"]',
                          'additional_kwargs': {},
                          'response_metadata': {},
                          'type': 'tool',
                          'name': 'search',
                          'id': '7bad0e72-5ebe-4b08-9b8a-b99b0fe22fb7',
                          'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}} 
{'agent': {'messages': [{'content': "根据搜索结果,我可以为您提供有关旧金山当前天气的信息:\n\n旧金山目前天气晴朗。这对户外活动和享受城市美景来说是个好消息。\n\n值得注意的是,搜索结果中包含了一个关于双子座的不寻常评论,这通常不会出现在天气报告中。这可能是由于搜索引擎包括了一些占星信息或笑话。然而,为了回答您关于天气的问题,我们可以专注于旧金山目前天气晴朗的事实。\n\n如果您需要更多关于旧金山天气的具体信息,如温度、风速或未来几天的预报,请告诉我,我很乐意为您查询这些信息。",
                          'additional_kwargs': {},
                          'response_metadata': {},
                          'type': 'ai',
                          'name': None,
                          'id': 'run-dbac539a-33c8-4f0c-9e20-91f318371e7c',
                          'example': False,
                          'tool_calls': [],
                          'invalid_tool_calls': [],
                          'usage_metadata': None}]}}

现在我们获取状态列表,并从第三个状态(工具调用之前的那个)开始调用:

states = await client.threads.get_history(thread['thread_id'])

# 我们可以通过检查“next”属性并确认它是工具调用节点来验证此状态是否正确
state_to_replay = states[2]
print(state_to_replay['next'])
const states = await client.threads.getHistory(thread['thread_id']);

// 我们可以通过检查“next”属性并确认它是工具调用节点来验证此状态是否正确
const stateToReplay = states[2];
console.log(stateToReplay['next']);
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -r '.[2].next'

输出:

['action']

要从某个状态重新运行,我们首先需要向线程状态发送一个空更新。然后我们需要传递生成的checkpoint_id如下所示:

state_to_replay = states[2]
updated_config = await client.threads.update_state(
    thread["thread_id"],
    {"messages": []},
    checkpoint_id=state_to_replay["checkpoint_id"]
)
async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id, # graph_id
    input=None,
    stream_mode="updates",
    checkpoint_id=updated_config["checkpoint_id"]
):
    if chunk.data and chunk.event != "metadata": 
        print(chunk.data)
const stateToReplay = states[2];
const config = await client.threads.updateState(thread["thread_id"], { values: {"messages": [] }, checkpointId: stateToReplay["checkpoint_id"] });
const streamResponse = client.runs.stream(
  thread["thread_id"],
  assistantId,
  {
    input: null,
    streamMode: "updates",
    checkpointId: config["checkpoint_id"]
  }
);
for await (const chunk of streamResponse) {
  if (chunk.data && chunk.event !== "metadata") {
    console.log(chunk.data);
  }
}
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -c '
    .[2] as $state_to_replay |
    {
        values: { messages: .[2].values.messages[-1] },
        checkpoint_id: $state_to_replay.checkpoint_id
    }' | \
curl --request POST \
    --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
    --header 'Content-Type: application/json' \
    --data @- | jq .checkpoint_id | \
curl --request POST \
 --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
 --header 'Content-Type: application/json' \
 --data "{
   \"assistant_id\": \"agent\",
   \"checkpoint_id\": \"$1\",
   \"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"
     }
 }
 '

输出:

{'action': {'messages': [{'content': '["我查到了:旧金山当前的天气。结果:旧金山今天阳光明媚,但如果你是双子座的话就要小心了 😈。"]',
                          'additional_kwargs': {},
                          'response_metadata': {},
                          'type': 'tool',
                          'name': 'search',
                          'id': 'eba650e5-400e-4938-8508-f878dcbcc532',
                          'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}} 
{'agent': {'messages': [{'content': "根据搜索结果,我可以为您提供有关旧金山当前天气的信息:\n\n旧金山目前天气晴朗。这对于计划户外活动或仅仅想享受一个愉快的日子来说都是个好消息。\n\n值得注意的是,搜索结果中包含了一个关于双子座的不寻常评论,这似乎与天气无关。这似乎是天气报告中的一个有趣或幽默的添加,可能来自提供该信息的来源。\n\n您还需要了解有关旧金山天气的其他信息吗?或者您需要其他信息?",
                          'additional_kwargs': {},
                          'response_metadata': {},
                          'type': 'ai',
                          'name': None,
                          'id': 'run-bc6dca3f-a1e2-4f59-a69b-fe0515a348bb',
                          'example': False,
                          'tool_calls': [],
                          'invalid_tool_calls': [],
                          'usage_metadata': None}]}}

正如我们所见,图从工具节点重新启动,输入与我们最初的图运行相同。

从先前状态分支

使用LangGraph的检查点功能,您可以做的不仅仅是重放过去的某个状态。您还可以从先前的位置分支出去,让代理探索不同的轨迹,或者让用户对工作流中的更改进行“版本控制”。

让我们展示如何在特定时间点编辑状态。我们将更新状态以更改工具的输入。

# 现在获取状态中的最后一条消息
# 这是包含我们想要更新的工具调用的消息
last_message = state_to_replay['values']['messages'][-1]

# 更新该工具调用的参数
last_message['tool_calls'][0]['args'] = {'query': 'current weather in SF'}

config = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id'])
// 现在获取状态中的最后一条消息
// 这是包含我们想要更新的工具调用的消息
let lastMessage = stateToReplay['values']['messages'][-1];

// 更新该工具调用的参数
lastMessage['tool_calls'][0]['args'] = { 'query': 'current weather in SF' };

const config = await client.threads.updateState(thread['thread_id'], { values: { "messages": [lastMessage] }, checkpointId: stateToReplay['checkpoint_id'] });
curl -s --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | \
jq -c '
    .[2] as $state_to_replay |
    .[2].values.messages[-1].tool_calls[0].args.query = "current weather in SF" |
    {
        values: { messages: .[2].values.messages[-1] },
        checkpoint_id: $state_to_replay.checkpoint_id
    }' | \
curl --request POST \
    --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
    --header 'Content-Type: application/json' \
    --data @-

现在我们可以使用这个新的配置重新运行我们的图,从new_state开始,这是state_to_replay的一个分支:

async for chunk in client.runs.stream(
    thread["thread_id"],
    assistant_id,
    input=None,
    stream_mode="updates",
    checkpoint_id=config['checkpoint_id']
):
    if chunk.data and chunk.event != "metadata": 
        print(chunk.data)
const streamResponse = client.runs.stream(
  thread["thread_id"],
  assistantId,
  {
    input: null,
    streamMode: "updates",
    checkpointId: config['checkpoint_id'],
  }
);
for await (const chunk of streamResponse) {
  if (chunk.data && chunk.event !== "metadata") {
    console.log(chunk.data);
  }
}
curl -s --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
jq -c '.checkpoint_id' | \
curl --request POST \
 --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
 --header 'Content-Type: application/json' \
 --data "{
   \"assistant_id\": \"agent\",
   \"checkpoint_id\": \"$1\",
   \"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"
     }
 }
 '

输出结果:

{
  "action": {
    "messages": [{
      "content": '["I looked up: current weather in SF. 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": "2baf9941-4fda-4081-9f87-d76795d289f1",
      "tool_call_id": "toolu_011vroKUtWU7SBdrngpgpFMn"
    }]
  }
}
{
  "agent": {
    "messages": [{
      "content": "Based on the search results, I can provide you with information about the current weather in San Francisco (SF):\n\nThe weather in San Francisco is currently sunny. This means it's a clear day with plenty of sunshine. \n\nIt's worth noting that the specific temperature wasn't provided in the search result, but sunny weather in San Francisco typically means comfortable temperatures. San Francisco is known for its mild climate, so even on sunny days, it's often not too hot.\n\nThe search result also included a playful reference to astrological signs, mentioning Gemini. However, this is likely just a joke or part of the search engine's presentation and not related to the actual weather conditions.\n\nIs there any specific information about the weather in San Francisco you'd like to know more about? I'd be happy to perform another search if you need details on temperature, wind conditions, or the forecast for the coming days.",
      "additional_kwargs": {},
      "response_metadata": {},
      "type": "ai",
      "name": None,
      "id": "run-a83de52d-ed18-4402-9384-75c462485743",
      "example": False,
      "tool_calls": [],
      "invalid_tool_calls": [],
      "usage_metadata": None
    }]
  }
}

如我们所见,搜索查询已从旧金山更改为SF,正如我们希望的那样!

Comments