跳到内容

如何在子图中查看和更新状态

先决条件

本指南假定您熟悉以下内容

一旦您添加了持久性,您就可以在任何时间点查看和更新子图的状态。这使得能够实现人工参与的交互模式,例如

  • 您可以在中断期间向用户显示状态,让他们接受操作。
  • 您可以回溯子图以重现或避免问题。
  • 您可以修改状态,以便用户更好地控制其操作。

本指南展示了如何做到这一点。

设置

首先,我们需要安装所需的软件包

npm install @langchain/langgraph @langchain/core @langchain/openai

接下来,我们需要为 OpenAI 设置 API 密钥(我们将在本指南中使用此提供商)

// process.env.OPENAI_API_KEY = "YOUR_API_KEY";

设置 LangSmith 以进行 LangGraph 开发

注册 LangSmith 以快速发现问题并提高 LangGraph 项目的性能。LangSmith 允许您使用跟踪数据来调试、测试和监控使用 LangGraph 构建的 LLM 应用程序 — 阅读此处了解更多关于如何开始的信息。

定义子图

首先,让我们设置我们的子图。为此,我们将创建一个简单的图,它可以获取特定城市的天气。我们将在 weather_node 之前使用断点编译此图

import { z } from "zod";
import { tool } from "@langchain/core/tools";
import { ChatOpenAI } from "@langchain/openai";
import { StateGraph, MessagesAnnotation, Annotation } from "@langchain/langgraph";

const getWeather = tool(async ({ city }) => {
  return `It's sunny in ${city}`;
}, {
  name: "get_weather",
  description: "Get the weather for a specific city",
  schema: z.object({
    city: z.string().describe("A city name")
  })
});

const rawModel = new ChatOpenAI({ model: "gpt-4o-mini" });
const model = rawModel.withStructuredOutput(getWeather);

// Extend the base MessagesAnnotation state with another field
const SubGraphAnnotation = Annotation.Root({
  ...MessagesAnnotation.spec,
  city: Annotation<string>,
});

const modelNode = async (state: typeof SubGraphAnnotation.State) => {
  const result = await model.invoke(state.messages);
  return { city: result.city };
};

const weatherNode = async (state: typeof SubGraphAnnotation.State) => {
  const result = await getWeather.invoke({ city: state.city });
  return {
    messages: [
      {
        role: "assistant",
        content: result,
      }
    ]
  };
};

const subgraph = new StateGraph(SubGraphAnnotation)
  .addNode("modelNode", modelNode)
  .addNode("weatherNode", weatherNode)
  .addEdge("__start__", "modelNode")
  .addEdge("modelNode", "weatherNode")
  .addEdge("weatherNode", "__end__")
  .compile({ interruptBefore: ["weatherNode"] });

定义父图

我们现在可以设置总图。如果需要获取天气,此图将首先路由到子图,否则将路由到正常的 LLM。

import { MemorySaver } from "@langchain/langgraph";

const memory = new MemorySaver();

const RouterStateAnnotation = Annotation.Root({
  ...MessagesAnnotation.spec,
  route: Annotation<"weather" | "other">,
});

const routerModel = rawModel.withStructuredOutput(
  z.object({
    route: z.enum(["weather", "other"]).describe("A step that should execute next to based on the currnet input")
  }),
  {
    name: "router"
  }
);

const routerNode = async (state: typeof RouterStateAnnotation.State) => {
  const systemMessage = {
    role: "system",
    content: "Classify the incoming query as either about weather or not.",
  };
  const messages = [systemMessage, ...state.messages]
  const { route } = await routerModel.invoke(messages);
  return { route };
}

const normalLLMNode = async (state: typeof RouterStateAnnotation.State) => {
  const responseMessage = await rawModel.invoke(state.messages);
  return { messages: [responseMessage] };
};

const routeAfterPrediction = async (state: typeof RouterStateAnnotation.State) => {
  if (state.route === "weather") {
    return "weatherGraph";
  } else {
    return "normalLLMNode";
  }
};

const graph = new StateGraph(RouterStateAnnotation)
  .addNode("routerNode", routerNode)
  .addNode("normalLLMNode", normalLLMNode)
  .addNode("weatherGraph", subgraph)
  .addEdge("__start__", "routerNode")
  .addConditionalEdges("routerNode", routeAfterPrediction)
  .addEdge("normalLLMNode", "__end__")
  .addEdge("weatherGraph", "__end__")
  .compile({ checkpointer: memory });

这是我们刚刚创建的图的图表

让我们用一个正常的查询来测试一下,以确保它按预期工作!

const config = { configurable: { thread_id: "1" } };

const inputs = { messages: [{ role: "user", content: "hi!" }] };

const stream = await graph.stream(inputs, { ...config, streamMode: "updates" });

for await (const update of stream) {
  console.log(update);
}
{ routerNode: { route: 'other' } }
{
  normalLLMNode: {
    messages: [
      AIMessage {
        "id": "chatcmpl-ABtbbiB5N3Uue85UNrFUjw5KhGaud",
        "content": "Hello! How can I assist you today?",
        "additional_kwargs": {},
        "response_metadata": {
          "tokenUsage": {
            "completionTokens": 9,
            "promptTokens": 9,
            "totalTokens": 18
          },
          "finish_reason": "stop",
          "system_fingerprint": "fp_f85bea6784"
        },
        "tool_calls": [],
        "invalid_tool_calls": [],
        "usage_metadata": {
          "input_tokens": 9,
          "output_tokens": 9,
          "total_tokens": 18
        }
      }
    ]
  }
}
太棒了!我们没有询问天气,所以我们从 LLM 收到了正常的响应。

从断点恢复

现在让我们看看断点会发生什么。让我们用一个应该路由到天气子图的查询来调用它,我们在天气子图中设置了中断节点。

const config2 = { configurable: { thread_id: "2" } };

const streamWithBreakpoint = await graph.stream({
  messages: [{
    role: "user",
    content: "what's the weather in sf"
  }]
}, { ...config2, streamMode: "updates" });

for await (const update of streamWithBreakpoint) {
  console.log(update);
}
{ routerNode: { route: 'weather' } }
请注意,图流不包含子图事件。如果我们想要流式传输子图事件,我们可以在配置中传递 subgraphs: True,并像这样返回子图事件

const streamWithSubgraphs = await graph.stream({
  messages: [{
    role: "user",
    content: "what's the weather in sf"
  }]
}, { configurable: { thread_id: "3" }, streamMode: "updates", subgraphs: true });

for await (const update of streamWithSubgraphs) {
  console.log(update);
}
[ [], { routerNode: { route: 'weather' } } ]
[
  [ 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0' ],
  { modelNode: { city: 'San Francisco' } }
]
这次,我们看到流式更新的格式已更改。现在它是一个元组,其中第一项是一个嵌套数组,其中包含有关子图的信息,第二项是实际的状态更新。如果我们现在获取状态,我们可以看到它正如预期的那样在 weatherGraph 上暂停了

const state = await graph.getState({ configurable: { thread_id: "3" } })
state.next
[ 'weatherGraph' ]
如果我们查看当前状态的待处理任务,我们可以看到我们有一个名为 weatherGraph 的任务,它对应于子图任务。

JSON.stringify(state.tasks, null, 2);
[
  {
    "id": "ec67e50f-d29c-5dee-8a80-08723a937de0",
    "name": "weatherGraph",
    "path": [
      "__pregel_pull",
      "weatherGraph"
    ],
    "interrupts": [],
    "state": {
      "configurable": {
        "thread_id": "3",
        "checkpoint_ns": "weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0"
      }
    }
  }
]
但是,由于我们使用父图的配置获取了状态,因此我们无法访问子图状态。如果您查看上面任务的 state 值,您会注意到它只是父图的配置。如果我们想实际填充子图状态,我们可以将 subgraphs: True 传递给 getState 的第二个参数,如下所示

const stateWithSubgraphs = await graph.getState({ configurable: { thread_id: "3" } }, { subgraphs: true })
JSON.stringify(stateWithSubgraphs.tasks, null, 2)
[
  {
    "id": "ec67e50f-d29c-5dee-8a80-08723a937de0",
    "name": "weatherGraph",
    "path": [
      "__pregel_pull",
      "weatherGraph"
    ],
    "interrupts": [],
    "state": {
      "values": {
        "messages": [
          {
            "lc": 1,
            "type": "constructor",
            "id": [
              "langchain_core",
              "messages",
              "HumanMessage"
            ],
            "kwargs": {
              "content": "what's the weather in sf",
              "additional_kwargs": {},
              "response_metadata": {},
              "id": "094b6752-6bea-4b43-b837-c6b0bb6a4c44"
            }
          }
        ],
        "city": "San Francisco"
      },
      "next": [
        "weatherNode"
      ],
      "tasks": [
        {
          "id": "2f2f8b8f-6a99-5225-8ff2-b6c49c3e9caf",
          "name": "weatherNode",
          "path": [
            "__pregel_pull",
            "weatherNode"
          ],
          "interrupts": []
        }
      ],
      "metadata": {
        "source": "loop",
        "writes": {
          "modelNode": {
            "city": "San Francisco"
          }
        },
        "step": 1,
        "parents": {
          "": "1ef7c6ba-3d36-65e0-8001-adc1f8841274"
        }
      },
      "config": {
        "configurable": {
          "thread_id": "3",
          "checkpoint_id": "1ef7c6ba-4503-6700-8001-61e828d1c772",
          "checkpoint_ns": "weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0",
          "checkpoint_map": {
            "": "1ef7c6ba-3d36-65e0-8001-adc1f8841274",
            "weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0": "1ef7c6ba-4503-6700-8001-61e828d1c772"
          }
        }
      },
      "createdAt": "2024-09-27T00:58:43.184Z",
      "parentConfig": {
        "configurable": {
          "thread_id": "3",
          "checkpoint_ns": "weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0",
          "checkpoint_id": "1ef7c6ba-3d3b-6400-8000-fe27ae37c785"
        }
      }
    }
  }
]
现在我们可以访问子图状态了!

要恢复执行,我们可以像往常一样调用外部图

const resumedStream = await graph.stream(null, {
  configurable: { thread_id: "3" },
  streamMode: "values",
  subgraphs: true,
});

for await (const update of resumedStream) {
  console.log(update);
}
[
  [],
  {
    messages: [
      HumanMessage {
        "id": "094b6752-6bea-4b43-b837-c6b0bb6a4c44",
        "content": "what's the weather in sf",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    ],
    route: 'weather'
  }
]
[
  [ 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0' ],
  {
    messages: [
      HumanMessage {
        "id": "094b6752-6bea-4b43-b837-c6b0bb6a4c44",
        "content": "what's the weather in sf",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    ],
    city: 'San Francisco'
  }
]
[
  [ 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0' ],
  {
    messages: [
      HumanMessage {
        "id": "094b6752-6bea-4b43-b837-c6b0bb6a4c44",
        "content": "what's the weather in sf",
        "additional_kwargs": {},
        "response_metadata": {}
      },
      AIMessage {
        "id": "55d7a03f-876a-4887-9418-027321e747c7",
        "content": "It's sunny in San Francisco",
        "additional_kwargs": {},
        "response_metadata": {},
        "tool_calls": [],
        "invalid_tool_calls": []
      }
    ],
    city: 'San Francisco'
  }
]
[
  [],
  {
    messages: [
      HumanMessage {
        "id": "094b6752-6bea-4b43-b837-c6b0bb6a4c44",
        "content": "what's the weather in sf",
        "additional_kwargs": {},
        "response_metadata": {}
      },
      AIMessage {
        "id": "55d7a03f-876a-4887-9418-027321e747c7",
        "content": "It's sunny in San Francisco",
        "additional_kwargs": {},
        "response_metadata": {},
        "tool_calls": [],
        "invalid_tool_calls": []
      }
    ],
    route: 'weather'
  }
]

从特定子图节点恢复

在上面的示例中,我们是从外部图重播的 - 这会自动从子图之前的任何状态(在我们的例子中是在 weatherNode 之前暂停)重播子图,但也可以从子图内部重播。为了做到这一点,我们需要从我们想要重播的确切子图状态获取配置。

我们可以通过浏览子图的状态历史记录,并选择 modelNode 之前的状态来做到这一点 - 我们可以通过过滤 .next 参数来实现。

要获取子图的状态历史记录,我们需要首先传入子图之前的父图状态

let parentGraphStateBeforeSubgraph;

const histories = await graph.getStateHistory({ configurable: { thread_id: "3" } });

for await (const historyEntry of histories) {
  if (historyEntry.next[0] === "weatherGraph") {
    parentGraphStateBeforeSubgraph = historyEntry;
  }
}

let subgraphStateBeforeModelNode;

const subgraphHistories = await graph.getStateHistory(parentGraphStateBeforeSubgraph.tasks[0].state);

for await (const subgraphHistoryEntry of subgraphHistories) {
  if (subgraphHistoryEntry.next[0] === "modelNode") {
    subgraphStateBeforeModelNode = subgraphHistoryEntry;
  }
}

console.log(subgraphStateBeforeModelNode);
{
  values: {
    messages: [
      HumanMessage {
        "id": "094b6752-6bea-4b43-b837-c6b0bb6a4c44",
        "content": "what's the weather in sf",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    ]
  },
  next: [ 'modelNode' ],
  tasks: [
    {
      id: '6d0d44fd-279b-56b0-8160-8f4929f9bfe6',
      name: 'modelNode',
      path: [Array],
      interrupts: [],
      state: undefined
    }
  ],
  metadata: {
    source: 'loop',
    writes: null,
    step: 0,
    parents: { '': '1ef7c6ba-3d36-65e0-8001-adc1f8841274' }
  },
  config: {
    configurable: {
      thread_id: '3',
      checkpoint_ns: 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0',
      checkpoint_id: '1ef7c6ba-3d3b-6400-8000-fe27ae37c785',
      checkpoint_map: [Object]
    }
  },
  createdAt: '2024-09-27T00:58:42.368Z',
  parentConfig: {
    configurable: {
      thread_id: '3',
      checkpoint_ns: 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0',
      checkpoint_id: '1ef7c6ba-3d38-6cf1-ffff-b3912beb00b9'
    }
  }
}
无论嵌套多少层,都可以扩展此模式。

我们可以通过比较 subgraphStateBeforeModelNode.next 参数来确认我们已获得正确的状态。

subgraphStateBeforeModelNode.next;
[ 'modelNode' ]
完美!我们已经获得了正确的状态快照,我们现在可以从子图内部的 modelNode 恢复

const resumeSubgraphStream = await graph.stream(null, {
  ...subgraphStateBeforeModelNode.config,
  streamMode: "updates",
  subgraphs: true
});

for await (const value of resumeSubgraphStream) {
  console.log(value);
}
[
  [ 'weatherGraph:ec67e50f-d29c-5dee-8a80-08723a937de0' ],
  { modelNode: { city: 'San Francisco' } }
]
我们可以看到它重新运行了 modelNode,并在 weatherNode 之前按预期中断。

本小节展示了如何从任何节点重播,无论它在图中嵌套多深 - 这是测试代理确定性的强大工具。

修改状态

更新子图的状态

如果我们想修改子图的状态怎么办?我们可以像我们更新正常图的状态一样做到这一点。我们只需要确保将子图的配置传递给 updateState。让我们像以前一样运行我们的图

const graphStream = await graph.stream({
  messages: [{
    role: "user",
    content: "what's the weather in sf"
  }],
}, {
  configurable: {
    thread_id: "4",
  }
});

for await (const update of graphStream) {
  console.log(update);
}
{ routerNode: { route: 'weather' } }

const outerGraphState = await graph.getState({
  configurable: {
    thread_id: "4",
  }
}, { subgraphs: true })

console.log(outerGraphState.tasks[0].state);
{
  values: {
    messages: [
      HumanMessage {
        "id": "07ed1a38-13a9-4ec2-bc88-c4f6b713ec85",
        "content": "what's the weather in sf",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    ],
    city: 'San Francisco'
  },
  next: [ 'weatherNode' ],
  tasks: [
    {
      id: 'eabfbb82-6cf4-5ecd-932e-ed994ea44f23',
      name: 'weatherNode',
      path: [Array],
      interrupts: [],
      state: undefined
    }
  ],
  metadata: {
    source: 'loop',
    writes: { modelNode: [Object] },
    step: 1,
    parents: { '': '1ef7c6ba-563f-60f0-8001-4fce0e78ef56' }
  },
  config: {
    configurable: {
      thread_id: '4',
      checkpoint_id: '1ef7c6ba-5c71-6f90-8001-04f60f3c8173',
      checkpoint_ns: 'weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681',
      checkpoint_map: [Object]
    }
  },
  createdAt: '2024-09-27T00:58:45.641Z',
  parentConfig: {
    configurable: {
      thread_id: '4',
      checkpoint_ns: 'weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681',
      checkpoint_id: '1ef7c6ba-5641-6800-8000-96bcde048fa6'
    }
  }
}
为了更新内部图的状态,我们需要传递内部图的配置,我们可以通过调用 state.tasks[0].state.config 来获取它 - 由于我们在子图内部中断,因此任务的状态只是子图的状态。

import type { StateSnapshot } from "@langchain/langgraph";

await graph.updateState((outerGraphState.tasks[0].state as StateSnapshot).config, { city: "la" });
{
  configurable: {
    thread_id: '4',
    checkpoint_ns: 'weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681',
    checkpoint_id: '1ef7c6ba-5de0-62f0-8002-3618a75d1fce',
    checkpoint_map: {
      '': '1ef7c6ba-563f-60f0-8001-4fce0e78ef56',
      'weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681': '1ef7c6ba-5de0-62f0-8002-3618a75d1fce'
    }
  }
}
我们现在可以恢复流式传输外部图(这将恢复子图!),并检查我们是否已更新我们的搜索以使用 LA 而不是 SF。

const resumedStreamWithUpdatedState = await graph.stream(null, {
  configurable: {
    thread_id: "4",
  },
  streamMode: "updates",
  subgraphs: true,
})

for await (const update of resumedStreamWithUpdatedState) {
  console.log(JSON.stringify(update, null, 2));
}
[
  [
    "weatherGraph:8d8c9278-bd2a-566a-b9e1-e72286634681"
  ],
  {
    "weatherNode": {
      "messages": [
        {
          "role": "assistant",
          "content": "It's sunny in la"
        }
      ]
    }
  }
]
[
  [],
  {
    "weatherGraph": {
      "messages": [
        {
          "lc": 1,
          "type": "constructor",
          "id": [
            "langchain_core",
            "messages",
            "HumanMessage"
          ],
          "kwargs": {
            "content": "what's the weather in sf",
            "additional_kwargs": {},
            "response_metadata": {},
            "id": "07ed1a38-13a9-4ec2-bc88-c4f6b713ec85"
          }
        },
        {
          "lc": 1,
          "type": "constructor",
          "id": [
            "langchain_core",
            "messages",
            "AIMessage"
          ],
          "kwargs": {
            "content": "It's sunny in la",
            "tool_calls": [],
            "invalid_tool_calls": [],
            "additional_kwargs": {},
            "response_metadata": {},
            "id": "94c29f6c-38b3-420f-a9fb-bd85548f0c03"
          }
        }
      ]
    }
  }
]
太棒了!正如我们预期的那样,AI 回复说“洛杉矶阳光明媚!”。

充当子图节点

除了在 weatherGraph 子图中编辑 weatherNode 之前的状态之外,我们可以更新状态的另一种方法是充当 weatherNode 本身。我们可以通过传递子图配置以及作为第三个位置参数传递的节点名称来做到这一点,这允许我们更新状态,就好像我们是指定的节点一样。

我们将在 weatherNode 之前设置中断,然后使用更新状态函数作为 weatherNode,图本身永远不会直接调用 weatherNode,而是由我们决定 weatherNode 的输出应该是什么。

const streamWithAsNode = await graph.stream({
  messages: [{
    role: "user",
    content: "What's the weather in sf",
  }]
}, {
  configurable: {
    thread_id: "14",
  }
});

for await (const update of streamWithAsNode) {
  console.log(update);
}

// Graph execution should stop before the weather node
console.log("interrupted!");

const interruptedState = await graph.getState({
  configurable: {
    thread_id: "14",
  }
}, { subgraphs: true });

console.log(interruptedState);

// We update the state by passing in the message we want returned from the weather node
// and make sure to pass `"weatherNode"` to signify that we want to act as this node.
await graph.updateState((interruptedState.tasks[0].state as StateSnapshot).config, {
  messages: [{
    "role": "assistant",
    "content": "rainy"
  }]
}, "weatherNode");


const resumedStreamWithAsNode = await graph.stream(null, {
  configurable: {
    thread_id: "14",
  },
  streamMode: "updates",
  subgraphs: true,
});

for await (const update of resumedStreamWithAsNode) {
  console.log(update);
}

console.log(await graph.getState({
  configurable: {
    thread_id: "14",
  }
}, { subgraphs: true }));
{ routerNode: { route: 'weather' } }
interrupted!
{
  values: {
    messages: [
      HumanMessage {
        "id": "90e9ff28-5b13-4e10-819a-31999efe303c",
        "content": "What's the weather in sf",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    ],
    route: 'weather'
  },
  next: [ 'weatherGraph' ],
  tasks: [
    {
      id: 'f421fca8-de9e-5683-87ab-6ea9bb8d6275',
      name: 'weatherGraph',
      path: [Array],
      interrupts: [],
      state: [Object]
    }
  ],
  metadata: {
    source: 'loop',
    writes: { routerNode: [Object] },
    step: 1,
    parents: {}
  },
  config: {
    configurable: {
      thread_id: '14',
      checkpoint_id: '1ef7c6ba-63ac-68f1-8001-5f7ada5f98e8',
      checkpoint_ns: ''
    }
  },
  createdAt: '2024-09-27T00:58:46.399Z',
  parentConfig: {
    configurable: {
      thread_id: '14',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-5f20-6020-8000-1ff649773a32'
    }
  }
}
[ [], { weatherGraph: { messages: [Array] } } ]
{
  values: {
    messages: [
      HumanMessage {
        "id": "90e9ff28-5b13-4e10-819a-31999efe303c",
        "content": "What's the weather in sf",
        "additional_kwargs": {},
        "response_metadata": {}
      },
      AIMessage {
        "id": "af761e6d-9f6a-4467-9a3c-489bed3fbad7",
        "content": "rainy",
        "additional_kwargs": {},
        "response_metadata": {},
        "tool_calls": [],
        "invalid_tool_calls": []
      }
    ],
    route: 'weather'
  },
  next: [],
  tasks: [],
  metadata: {
    source: 'loop',
    writes: { weatherGraph: [Object] },
    step: 2,
    parents: {}
  },
  config: {
    configurable: {
      thread_id: '14',
      checkpoint_id: '1ef7c6ba-69e6-6cc0-8002-1751fc5bdd8f',
      checkpoint_ns: ''
    }
  },
  createdAt: '2024-09-27T00:58:47.052Z',
  parentConfig: {
    configurable: {
      thread_id: '14',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-63ac-68f1-8001-5f7ada5f98e8'
    }
  }
}
完美!代理回复了我们自己传入的消息,并将旧金山的天气识别为 rainy 而不是 sunny

充当整个子图

最后,我们也可以通过充当整个子图来更新图。这与上面的情况类似,但我们不仅仅充当 weatherNode,而是充当整个 weatherGraph 子图。这是通过传入正常的图配置以及 asNode 参数来完成的,我们在其中指定我们充当整个子图节点。

const entireSubgraphExampleStream = await graph.stream({
  messages: [
    {
      role: "user",
      content: "what's the weather in sf"
    }
  ],
}, {
  configurable: {
    thread_id: "8",
  },
  streamMode: "updates",
  subgraphs: true,
});

for await (const update of entireSubgraphExampleStream) {
  console.log(update);
}

// Graph execution should stop before the weather node
console.log("interrupted!");

// We update the state by passing in the message we want returned from the weather graph.
// Note that we don't need to pass in the subgraph config, since we aren't updating the state inside the subgraph
await graph.updateState({
  configurable: {
    thread_id: "8",
  }
}, {
  messages: [{ role: "assistant", content: "rainy" }]
}, "weatherGraph");

const resumedEntireSubgraphExampleStream = await graph.stream(null, {
  configurable: {
    thread_id: "8",
  },
  streamMode: "updates",
});

for await (const update of resumedEntireSubgraphExampleStream) {
  console.log(update);
}

const currentStateAfterUpdate = await graph.getState({
  configurable: {
    thread_id: "8",
  }
});

console.log(currentStateAfterUpdate.values.messages);
[ [], { routerNode: { route: 'weather' } } ]
[
  [ 'weatherGraph:db9c3bb2-5d27-5dae-a724-a8d702b33e86' ],
  { modelNode: { city: 'San Francisco' } }
]
interrupted!
[
  HumanMessage {
    "id": "001282b0-ca2e-443f-b6ee-8cb16c81bf59",
    "content": "what's the weather in sf",
    "additional_kwargs": {},
    "response_metadata": {}
  },
  AIMessage {
    "id": "4b5d49cf-0f87-4ee8-b96f-eaa8716b9e9c",
    "content": "rainy",
    "additional_kwargs": {},
    "response_metadata": {},
    "tool_calls": [],
    "invalid_tool_calls": []
  }
]
同样,代理按预期回复了“rainy”。

双重嵌套子图

无论嵌套级别如何,此相同功能都将继续工作。这是一个使用双重嵌套子图执行相同操作的示例(尽管任何嵌套级别都将起作用)。我们在我们已定义的图之上添加另一个路由器。

首先,让我们重新创建我们上面一直在使用的图。这次我们将使用没有检查点的编译它,因为它本身将是一个子图!

const parentGraph = new StateGraph(RouterStateAnnotation)
  .addNode("routerNode", routerNode)
  .addNode("normalLLMNode", normalLLMNode)
  .addNode("weatherGraph", subgraph)
  .addEdge("__start__", "routerNode")
  .addConditionalEdges("routerNode", routeAfterPrediction)
  .addEdge("normalLLMNode", "__end__")
  .addEdge("weatherGraph", "__end__")
  .compile();

现在让我们声明一个“祖父”图,该图使用此图作为子图

const checkpointer = new MemorySaver();

const GrandfatherStateAnnotation = Annotation.Root({
  ...MessagesAnnotation.spec,
  toContinue: Annotation<boolean>,
});

const grandparentRouterNode = async (_state: typeof GrandfatherStateAnnotation.State) => {
  // Dummy logic that will always continue
  return { toContinue: true };
};

const grandparentConditionalEdge = async (state: typeof GrandfatherStateAnnotation.State) => {
  if (state.toContinue) {
    return "parentGraph";
  } else {
    return "__end__";
  }
};

const grandparentGraph = new StateGraph(GrandfatherStateAnnotation)
  .addNode("routerNode", grandparentRouterNode)
  .addNode("parentGraph", parentGraph)
  .addEdge("__start__", "routerNode")
  .addConditionalEdges("routerNode", grandparentConditionalEdge)
  .addEdge("parentGraph", "__end__")
  .compile({ checkpointer });

这是一个显示其外观的图表

如果我们运行到中断,我们现在可以看到所有三个图的状态快照

const grandparentConfig = {
  configurable: { thread_id: "123" },
};

const grandparentGraphStream = await grandparentGraph.stream({
  messages: [{
    role: "user",
    content: "what's the weather in SF"
  }],
}, {
  ...grandparentConfig,
  streamMode: "updates",
  subgraphs: true
});

for await (const update of grandparentGraphStream) {
  console.log(update);
}
[ [], { routerNode: { toContinue: true } } ]
[
  [ 'parentGraph:095bb8a9-77d3-5a0c-9a23-e1390dcf36bc' ],
  { routerNode: { route: 'weather' } }
]
[
  [
    'parentGraph:095bb8a9-77d3-5a0c-9a23-e1390dcf36bc',
    'weatherGraph:b1da376c-25a5-5aae-82da-4ff579f05d43'
  ],
  { modelNode: { city: 'San Francisco' } }
]

const grandparentGraphState = await grandparentGraph.getState(
  grandparentConfig, 
  { subgraphs: true }
);

const parentGraphState = grandparentGraphState.tasks[0].state as StateSnapshot;
const subgraphState = parentGraphState.tasks[0].state as StateSnapshot;

console.log("Grandparent State:");
console.log(grandparentGraphState.values);
console.log("---------------");
console.log("Parent Graph State:");
console.log(parentGraphState.values);
console.log("---------------");
console.log("Subgraph State:");
console.log(subgraphState.values);
Grandparent State:
{
  messages: [
    HumanMessage {
      "id": "5788e436-a756-4ff5-899a-82117a5c59c7",
      "content": "what's the weather in SF",
      "additional_kwargs": {},
      "response_metadata": {}
    }
  ],
  toContinue: true
}
---------------
Parent Graph State:
{
  messages: [
    HumanMessage {
      "id": "5788e436-a756-4ff5-899a-82117a5c59c7",
      "content": "what's the weather in SF",
      "additional_kwargs": {},
      "response_metadata": {}
    }
  ],
  route: 'weather'
}
---------------
Subgraph State:
{
  messages: [
    HumanMessage {
      "id": "5788e436-a756-4ff5-899a-82117a5c59c7",
      "content": "what's the weather in SF",
      "additional_kwargs": {},
      "response_metadata": {}
    }
  ],
  city: 'San Francisco'
}
我们现在可以继续,充当三层以下的节点

await grandparentGraph.updateState(subgraphState.config, {
  messages: [{
    role: "assistant",
    content: "rainy"
  }]
}, "weatherNode");

const updatedGrandparentGraphStream = await grandparentGraph.stream(null, {
  ...grandparentConfig,
  streamMode: "updates",
  subgraphs: true,
});

for await (const update of updatedGrandparentGraphStream) {
  console.log(update);
}

console.log((await grandparentGraph.getState(grandparentConfig)).values.messages)
[
  [ 'parentGraph:095bb8a9-77d3-5a0c-9a23-e1390dcf36bc' ],
  { weatherGraph: { messages: [Array] } }
]
[ [], { parentGraph: { messages: [Array] } } ]
[
  HumanMessage {
    "id": "5788e436-a756-4ff5-899a-82117a5c59c7",
    "content": "what's the weather in SF",
    "additional_kwargs": {},
    "response_metadata": {}
  },
  AIMessage {
    "id": "1c161973-9a9d-414d-b631-56791d85e2fb",
    "content": "rainy",
    "additional_kwargs": {},
    "response_metadata": {},
    "tool_calls": [],
    "invalid_tool_calls": []
  }
]
与上述情况一样,我们可以看到 AI 按预期回复了“rainy”。

我们可以浏览状态历史记录,以查看祖父图的状态在每个步骤是如何更新的。

const grandparentStateHistories = await grandparentGraph.getStateHistory(grandparentConfig);
for await (const state of grandparentStateHistories) {
  console.log(state);
  console.log("-----");
}
{
  values: {
    messages: [
      HumanMessage {
        "id": "5788e436-a756-4ff5-899a-82117a5c59c7",
        "content": "what's the weather in SF",
        "additional_kwargs": {},
        "response_metadata": {}
      },
      AIMessage {
        "id": "1c161973-9a9d-414d-b631-56791d85e2fb",
        "content": "rainy",
        "additional_kwargs": {},
        "response_metadata": {},
        "tool_calls": [],
        "invalid_tool_calls": []
      }
    ],
    toContinue: true
  },
  next: [],
  tasks: [],
  metadata: {
    source: 'loop',
    writes: { parentGraph: [Object] },
    step: 2,
    parents: {}
  },
  config: {
    configurable: {
      thread_id: '123',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-8560-67d0-8002-2e2cedd7de18'
    }
  },
  createdAt: '2024-09-27T00:58:49.933Z',
  parentConfig: {
    configurable: {
      thread_id: '123',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-7977-62c0-8001-13e89cb2bbab'
    }
  }
}
-----
{
  values: {
    messages: [
      HumanMessage {
        "id": "5788e436-a756-4ff5-899a-82117a5c59c7",
        "content": "what's the weather in SF",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    ],
    toContinue: true
  },
  next: [ 'parentGraph' ],
  tasks: [
    {
      id: '095bb8a9-77d3-5a0c-9a23-e1390dcf36bc',
      name: 'parentGraph',
      path: [Array],
      interrupts: [],
      state: [Object]
    }
  ],
  metadata: {
    source: 'loop',
    writes: { routerNode: [Object] },
    step: 1,
    parents: {}
  },
  config: {
    configurable: {
      thread_id: '123',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-7977-62c0-8001-13e89cb2bbab'
    }
  },
  createdAt: '2024-09-27T00:58:48.684Z',
  parentConfig: {
    configurable: {
      thread_id: '123',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-7972-64a0-8000-243575e3244f'
    }
  }
}
-----
{
  values: {
    messages: [
      HumanMessage {
        "id": "5788e436-a756-4ff5-899a-82117a5c59c7",
        "content": "what's the weather in SF",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    ]
  },
  next: [ 'routerNode' ],
  tasks: [
    {
      id: '00ed334c-47b5-5693-92b4-a5b83373e2a0',
      name: 'routerNode',
      path: [Array],
      interrupts: [],
      state: undefined
    }
  ],
  metadata: { source: 'loop', writes: null, step: 0, parents: {} },
  config: {
    configurable: {
      thread_id: '123',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-7972-64a0-8000-243575e3244f'
    }
  },
  createdAt: '2024-09-27T00:58:48.682Z',
  parentConfig: {
    configurable: {
      thread_id: '123',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-796f-6d90-ffff-25ed0eb5bb38'
    }
  }
}
-----
{
  values: { messages: [] },
  next: [ '__start__' ],
  tasks: [
    {
      id: 'ea62628e-881d-558d-bafc-e8b6a734e8aa',
      name: '__start__',
      path: [Array],
      interrupts: [],
      state: undefined
    }
  ],
  metadata: {
    source: 'input',
    writes: { __start__: [Object] },
    step: -1,
    parents: {}
  },
  config: {
    configurable: {
      thread_id: '123',
      checkpoint_ns: '',
      checkpoint_id: '1ef7c6ba-796f-6d90-ffff-25ed0eb5bb38'
    }
  },
  createdAt: '2024-09-27T00:58:48.681Z',
  parentConfig: undefined
}
-----