本指南演示 LangGraph 的 Graph API 基础。它会介绍状态,以及如何组合常见的图结构,例如序列分支循环。它还涵盖 LangGraph 的控制功能,包括用于 map-reduce 工作流的 Send API,以及用于将状态更新与跨节点“跳转”结合起来的 Command API

设置

安装 langgraph
npm install @langchain/langgraph
设置 LangSmith 以便更好地调试注册 LangSmith,可以快速发现问题并提升 LangGraph 项目的性能。LangSmith 允许你使用 trace 数据来调试、测试和监控使用 LangGraph 构建的 LLM 应用——更多入门信息请阅读文档

定义和更新状态

这里我们展示如何在 LangGraph 中定义和更新状态。我们将演示:
  1. 如何使用状态来定义图的 schema
  2. 如何使用 reducers 控制状态更新的处理方式。

定义状态

LangGraph 中的状态使用 StateSchema 类定义。它提供了统一的 API,可以接受单个字段的标准 schema(例如 Zod),以及 ReducedValueMessagesValueUntrackedValue 等特殊值类型。 默认情况下,图会具有相同的输入和输出 schema,而状态决定了该 schema。有关如何定义不同的输入和输出 schema,请参阅定义输入和输出 schema 我们来看一个使用 messages 的简单示例。对于许多 LLM 应用来说,这是一种通用的状态表达方式。更多细节请参阅我们的概念页面
import { StateSchema, MessagesValue } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  messages: MessagesValue,
  extraField: z.number(),
});
这个状态会跟踪一个消息对象列表,以及一个额外的整数字段。

更新状态

我们来构建一个只有单个节点的示例图。我们的节点只是一个 TypeScript 函数,它读取图的状态并对其进行更新。这个函数的第一个参数始终是状态:
import { AIMessage } from "@langchain/core/messages";
import { GraphNode } from "@langchain/langgraph";

const node: GraphNode<typeof State> = (state) => {
  const messages = state.messages;
  const newMessage = new AIMessage("你好!");
  return { messages: [newMessage], extraField: 10 };
};
这个节点只是向消息列表追加一条消息(reducer 会处理拼接),并填充一个额外字段。
节点应该直接返回对状态的更新,而不是修改状态本身。
接下来我们定义一个包含此节点的简单图。我们使用 StateGraph 来定义一个对该状态进行操作的图。然后使用 addNode 填充图。
import { StateGraph } from "@langchain/langgraph";

const graph = new StateGraph(State)
  .addNode("node", node)
  .addEdge("__start__", "node")
  .compile();
LangGraph 提供了内置工具来可视化你的图。我们来查看这个图。有关可视化的详细信息,请参阅可视化你的图
import * as fs from "node:fs/promises";

const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());

await fs.writeFile("graph.png", imageBuffer);
在这个例子中,我们的图只执行单个节点。我们继续做一次简单调用:
import { HumanMessage } from "@langchain/core/messages";

const result = await graph.invoke({ messages: [new HumanMessage("你好")], extraField: 0 });
console.log(result);
{ messages: [HumanMessage { content: '你好' }, AIMessage { content: '你好!' }], extraField: 10 }
请注意:
  • 我们通过更新状态中的单个 key 来启动调用。
  • 我们会在调用结果中收到完整状态。
为方便起见,我们经常通过日志查看消息对象的内容:
for (const message of result.messages) {
  console.log(`${message.getType()}: ${message.content}`);
}
human: 你好
ai: 你好!

使用 reducer 处理状态更新

状态中的每个 key 都可以拥有自己独立的 reducer 函数,用于控制如何应用来自节点的更新。如果没有显式指定 reducer 函数,则默认认为对该 key 的所有更新都会覆盖原值。 在前面的示例中,我们使用了 MessagesValue,它已经内置了 reducer。对于自定义字段,你可以使用 ReducedValue 定义如何应用更新。 在前面的示例中,我们的节点通过追加一条消息来更新状态中的 "messages" key。MessagesValue reducer 会自动处理这一点:
import { StateSchema, MessagesValue, ReducedValue } from "@langchain/langgraph";
import * as z from "zod";

// MessagesValue 已经内置了 reducer
const State = new StateSchema({
  messages: MessagesValue,
  extraField: z.number(),
});
我们的节点可以只返回新消息(reducer 会处理拼接):
import { GraphNode } from "@langchain/langgraph";

const node: GraphNode<typeof State> = (state) => {
  const newMessage = new AIMessage("你好!");
  return { messages: [newMessage], extraField: 10 };
};
import { START } from "@langchain/langgraph";

const graph = new StateGraph(State)
  .addNode("node", node)
  .addEdge(START, "node")
  .compile();

const result = await graph.invoke({ messages: [new HumanMessage("你好")] });

for (const message of result.messages) {
  console.log(`${message.getType()}: ${message.content}`);
}
human: 你好
ai: 你好!

MessagesValue

在实践中,更新消息列表时还有一些额外注意事项:
  • 我们可能希望更新状态中已有的消息。
  • 我们可能希望接受消息格式的简写形式,例如 OpenAI 格式
LangGraph 包含内置的 MessagesValue,可以处理这些情况:
import { StateSchema, StateGraph, MessagesValue, GraphNode, START } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  messages: MessagesValue,
  extraField: z.number(),
});

const node: GraphNode<typeof State> = (state) => {
  const newMessage = new AIMessage("你好!");
  return { messages: [newMessage], extraField: 10 };
};

const graph = new StateGraph(State)
  .addNode("node", node)
  .addEdge(START, "node")
  .compile();
const inputMessage = { role: "user", content: "你好" };

const result = await graph.invoke({ messages: [inputMessage] });

for (const message of result.messages) {
  console.log(`${message.getType()}: ${message.content}`);
}
human: 你好
ai: 你好!
对于涉及聊天模型的应用来说,这是一种通用的状态表示方式。为了方便使用,LangGraph 内置了 MessagesValue,因此我们可以这样写:
import { StateSchema, MessagesValue } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  messages: MessagesValue,
  extraField: z.number(),
});

定义输入和输出 schema

默认情况下,StateGraph 使用单个 schema 运行,并且期望所有节点都使用该 schema 进行通信。不过,也可以为图定义不同的输入和输出 schema。 当指定了不同的 schema 时,节点之间的通信仍会使用内部 schema。输入 schema 确保提供的输入符合预期结构,而输出 schema 会过滤内部数据,只返回根据已定义输出 schema 相关的信息。 下面我们将了解如何定义不同的输入和输出 schema。
import { StateGraph, StateSchema, GraphNode, START, END } from "@langchain/langgraph";
import * as z from "zod";

// 定义输入的 schema
const InputState = new StateSchema({
  question: z.string(),
});

// 定义输出的 schema
const OutputState = new StateSchema({
  answer: z.string(),
});

// 定义整体 schema,将输入和输出合并
const OverallState = new StateSchema({
  question: z.string(),
  answer: z.string(),
});

// 定义处理输入的节点
const answerNode: GraphNode<typeof OverallState> = (state) => {
  // 示例答案和一个额外 key
  return { answer: "再见", question: state.question };
};

// 构建图,并指定输入和输出 schema
const graph = new StateGraph({
  input: InputState,
  output: OutputState,
  state: OverallState,
})
  .addNode("answerNode", answerNode)
  .addEdge(START, "answerNode")
  .addEdge("answerNode", END)
  .compile();

// 使用输入调用图并打印结果
console.log(await graph.invoke({ question: "你好" }));
{ answer: '再见' }
请注意,invoke 的输出只包含输出 schema。

在节点之间传递私有状态

在某些情况下,你可能希望节点交换一些对中间逻辑至关重要、但不需要成为图主 schema 一部分的信息。这些私有数据与图的整体输入/输出无关,并且应该只在特定节点之间共享。 下面,我们将创建一个由三个节点(node_1、node_2 和 node_3)组成的顺序图示例,其中私有数据在前两个步骤(node_1 和 node_2)之间传递,而第三个步骤(node_3)只能访问公开的整体状态。
import { StateGraph, StateSchema, GraphNode, START, END } from "@langchain/langgraph";
import * as z from "zod";

// 图的整体状态(这是跨节点共享的公共状态)
const OverallState = new StateSchema({
  a: z.string(),
});

// node1 的输出包含不属于整体状态的私有数据
const Node1Output = new StateSchema({
  privateData: z.string(),
});

// 节点 2 的输入只请求 node1 之后可用的私有数据
const Node2Input = new StateSchema({
  privateData: z.string(),
});

// 私有数据只在 node1 和 node2 之间共享
const node1: GraphNode<typeof OverallState> = (state) => {
  const output = { privateData: "由 node1 设置" };
  console.log(`进入节点 'node1':\n\t输入:${JSON.stringify(state)}\n\t返回:${JSON.stringify(output)}`);
  return output;
};

const node2: GraphNode<typeof Node2Input> = (state) => {
  const output = { a: "由 node2 设置" };
  console.log(`进入节点 'node2':\n\t输入:${JSON.stringify(state)}\n\t返回:${JSON.stringify(output)}`);
  return output;
};

// 节点 3 只能访问整体状态(不能访问来自 node1 的私有数据)
const node3: GraphNode<typeof OverallState> = (state) => {
  const output = { a: "由 node3 设置" };
  console.log(`进入节点 'node3':\n\t输入:${JSON.stringify(state)}\n\t返回:${JSON.stringify(output)}`);
  return output;
};

// 按顺序连接节点
// node2 接收来自 node1 的私有数据,而
// node3 看不到这些私有数据。
const graph = new StateGraph(OverallState)
  .addNode("node1", node1)
  .addNode("node2", node2, { input: Node2Input })
  .addNode("node3", node3)
  .addEdge(START, "node1")
  .addEdge("node1", "node2")
  .addEdge("node2", "node3")
  .addEdge("node3", END)
  .compile();

// 使用初始状态调用图
const response = await graph.invoke({ a: "在开始时设置" });

console.log(`\n图调用的输出:${JSON.stringify(response)}`);
进入节点 'node1':
	Input: {"a":"在开始时设置"}.
	Returned: {"privateData":"由 node1 设置"}
进入节点 'node2':
	Input: {"privateData":"由 node1 设置"}.
	Returned: {"a":"由 node2 设置"}
进入节点 'node3':
	Input: {"a":"由 node2 设置"}.
	Returned: {"a":"由 node3 设置"}

图调用的输出:{"a":"由 node3 设置"}

替代的状态定义方式

虽然 StateSchema 是定义状态的推荐方式,但 LangGraph 也支持其他几种方法。本节涵盖所有可用选项。

Channels API

channels API 提供了对状态管理的底层控制。LangGraph 提供了几种内置通道类型:
Channel TypeBehaviorUse Case
LastValue存储最新的值会被覆盖的简单字段
BinaryOperatorAggregate使用 reducer 函数组合值累积值(计数器、列表)
Topic将所有值收集为一个序列事件流、审计日志
EphemeralValue在 superstep 之间重置的值临时计算状态
使用对象简写: 当你传入一个带有 reducerdefault 的对象时,它会创建一个 BinaryOperatorAggregate 通道。传入 null 会创建一个 LastValue 通道:
import { BaseMessage } from "@langchain/core/messages";
import { StateGraph } from "@langchain/langgraph";

interface WorkflowState {
  messages: BaseMessage[];
  question: string;
  answer: string;
}

const workflow = new StateGraph<WorkflowState>({
  channels: {
    // BinaryOperatorAggregate:使用 reducer 组合值
    messages: {
      reducer: (current, update) => current.concat(update),
      default: () => [],
    },
    // LastValue:存储最新的值(null = 无 reducer)
    question: null,
    answer: null,
  },
});
直接使用通道类: 如需更多控制,可以直接实例化通道类:
import { BaseMessage } from "@langchain/core/messages";
import { StateGraph, LastValue, BinaryOperatorAggregate, Topic } from "@langchain/langgraph";

interface WorkflowState {
  messages: BaseMessage[];
  question: string;
  events: string[];
}

const workflow = new StateGraph<WorkflowState>({
  channels: {
    messages: new BinaryOperatorAggregate<BaseMessage[]>(
      (current, update) => current.concat(update),
      () => []
    ),
    question: new LastValue<string>(),
    // Topic 会收集执行期间推送的所有值
    events: new Topic<string>(),
  },
});

Annotation.Root

Annotation.Root 提供了一种声明式方式来定义带有 reducer 的状态。它类似于 StateSchema,但语法不同:
import { BaseMessage } from "@langchain/core/messages";
import { Annotation, StateGraph, messagesStateReducer } from "@langchain/langgraph";

const State = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: messagesStateReducer,
    default: () => [],
  }),
  question: Annotation<string>(),
  count: Annotation<number>({
    reducer: (current, update) => current + update,
    default: () => 0,
  }),
});

const graph = new StateGraph(State);

使用 Zod v3 的 Zod 对象

使用 Zod v3 时,可以用普通的 z.object() schema 定义状态。LangGraph 通过 .langgraph 插件扩展了 Zod v3,该插件提供 .reducer().metadata() 方法:
import { z } from "zod/v3";
import { BaseMessage } from "@langchain/core/messages";
import { StateGraph, messagesStateReducer } from "@langchain/langgraph";

const State = z.object({
  // 使用 .langgraph.reducer() 附加 reducer 函数
  messages: z
    .array(z.custom<BaseMessage>())
    .default([])
    .langgraph.reducer(messagesStateReducer),
  // 简单字段可直接工作(最后写入者获胜)
  question: z.string().optional(),
  answer: z.string().optional(),
  // 用于累积值的自定义 reducer
  count: z
    .number()
    .default(0)
    .langgraph.reducer((current, update) => current + update),
});

const graph = new StateGraph(State);

使用 Zod v4 的 Zod 对象

Zod v4 使用基于 registry 的方式。使用 LangGraph registry 为 schema 字段附加元数据:
import * as z from "zod";
import { BaseMessage } from "@langchain/core/messages";
import { StateGraph, MessagesZodMeta, messagesStateReducer } from "@langchain/langgraph";
import { registry } from "@langchain/langgraph/zod";

const State = z.object({
  // 使用 .register() 搭配 LangGraph registry 和 MessagesZodMeta
  messages: z
    .array(z.custom<BaseMessage>())
    .default([])
    .register(registry, MessagesZodMeta),
  // 简单字段可直接工作(最后写入者获胜)
  question: z.string().optional(),
  answer: z.string().optional(),
  // 通过 registry 元数据使用自定义 reducer
  count: z
    .number()
    .default(0)
    .register(registry, { reducer: (current: number, update: number) => current + update }),
});

const graph = new StateGraph(State);

对比表

ApproachReducersType SafetyZod VersionRecommended
StateSchema✅ 内置✅ 完整v3 或 v4✅ 是
Channels API✅ 手动⚠️ 部分N/A适用于高级场景
Annotation.Root✅ 内置✅ 完整N/A旧版
Zod v3 + .langgraph✅ 通过插件✅ 完整仅 v3旧版
Zod v4 + registry✅ 通过 registry✅ 完整仅 v4旧版

添加运行时配置

有时你希望在调用图时能够配置它。例如,你可能希望能够在运行时指定要使用哪个 LLM 或系统提示,而不把这些参数污染到图状态中 要添加运行时配置:
  1. 为配置指定 schema
  2. 将配置添加到节点或条件边的函数签名中
  3. 将配置传入图。
下面是一个简单示例:
import { StateGraph, StateSchema, GraphNode, END, START } from "@langchain/langgraph";
import * as z from "zod";

// 1. 指定配置 schema
const ContextSchema = z.object({
  myRuntimeValue: z.string(),
});

// 2. 定义一个在节点中访问配置的图
const State = new StateSchema({
  myStateValue: z.number(),
});

const node: GraphNode<typeof State> = (state, runtime) => {
  if (runtime?.context?.myRuntimeValue === "a") {
    return { myStateValue: 1 };
  } else if (runtime?.context?.myRuntimeValue === "b") {
    return { myStateValue: 2 };
  } else {
    throw new Error("未知值。");
  }
};

const graph = new StateGraph(State, ContextSchema)
  .addNode("node", node)
  .addEdge(START, "node")
  .addEdge("node", END)
  .compile();

// 3. 在运行时传入配置:
console.log(await graph.invoke({}, { context: { myRuntimeValue: "a" } }));
console.log(await graph.invoke({}, { context: { myRuntimeValue: "b" } }));
{ myStateValue: 1 }
{ myStateValue: 2 }
下面我们演示一个实用示例:在运行时配置要使用的 LLM。我们将同时使用 OpenAI 和 Anthropic 模型。
import { ChatOpenAI } from "@langchain/openai";
import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, StateSchema, MessagesValue, GraphNode, START, END } from "@langchain/langgraph";
import * as z from "zod";

const ConfigSchema = z.object({
  modelProvider: z.string().default("anthropic"),
});

const State = new StateSchema({
  messages: MessagesValue,
});

const MODELS = {
  anthropic: new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }),
  openai: new ChatOpenAI({ model: "gpt-5.4-mini" }),
};

const callModel: GraphNode<typeof State> = async (state, config) => {
  const modelProvider = config?.configurable?.modelProvider || "anthropic";
  const model = MODELS[modelProvider as keyof typeof MODELS];
  const response = await model.invoke(state.messages);
  return { messages: [response] };
};

const graph = new StateGraph(State, ConfigSchema)
  .addNode("model", callModel)
  .addEdge(START, "model")
  .addEdge("model", END)
  .compile();

// 用法
const inputMessage = { role: "user", content: "你好" };
// 不传配置时,使用默认值(Anthropic)
const response1 = await graph.invoke({ messages: [inputMessage] });
// 或者,可以设置为 OpenAI
const response2 = await graph.invoke(
  { messages: [inputMessage] },
  { configurable: { modelProvider: "openai" } },
);

console.log(response1.messages.at(-1)?.response_metadata?.model);
console.log(response2.messages.at(-1)?.response_metadata?.model);
claude-haiku-4-5-20251001
gpt-5.4-mini
下面我们演示一个实用示例:在运行时配置两个参数:要使用的 LLM 和系统消息。
import { ChatOpenAI } from "@langchain/openai";
import { ChatAnthropic } from "@langchain/anthropic";
import { SystemMessage } from "@langchain/core/messages";
import { StateGraph, StateSchema, MessagesValue, GraphNode, START, END } from "@langchain/langgraph";
import * as z from "zod";

const ConfigSchema = z.object({
  modelProvider: z.string().default("anthropic"),
  systemMessage: z.string().optional(),
});

const State = new StateSchema({
  messages: MessagesValue,
});

const MODELS = {
  anthropic: new ChatAnthropic({ model: "claude-haiku-4-5-20251001" }),
  openai: new ChatOpenAI({ model: "gpt-5.4-mini" }),
};

const callModel: GraphNode<typeof State> = async (state, config) => {
  const modelProvider = config?.configurable?.modelProvider || "anthropic";
  const systemMessage = config?.configurable?.systemMessage;

  const model = MODELS[modelProvider as keyof typeof MODELS];
  let messages = state.messages;

  if (systemMessage) {
    messages = [new SystemMessage(systemMessage), ...messages];
  }

  const response = await model.invoke(messages);
  return { messages: [response] };
};

const graph = new StateGraph(State, ConfigSchema)
  .addNode("model", callModel)
  .addEdge(START, "model")
  .addEdge("model", END)
  .compile();

// 用法
const inputMessage = { role: "user", content: "你好" };
const response = await graph.invoke(
  { messages: [inputMessage] },
  {
    configurable: {
      modelProvider: "openai",
      systemMessage: "请用意大利语回答。"
    }
  }
);

for (const message of response.messages) {
  console.log(`${message.getType()}: ${message.content}`);
}
human: 你好
ai: Ciao! Come posso aiutarti oggi?

添加重试策略

在很多使用场景中,你可能希望节点拥有自定义重试策略,例如调用 API、查询数据库或调用 LLM 等。LangGraph 允许你为节点添加重试策略。 要配置重试策略,请将 retryPolicy 参数传给 addNoderetryPolicy 参数接收一个 RetryPolicy 对象。下面我们使用默认参数实例化一个 RetryPolicy 对象,并将它关联到一个节点:
import { RetryPolicy } from "@langchain/langgraph";

const graph = new StateGraph(State)
  .addNode("nodeName", nodeFunction, { retryPolicy: {} })
  .compile();
默认情况下,重试策略会对除以下异常之外的任何异常进行重试:
  • TypeError
  • SyntaxError
  • ReferenceError
考虑一个从 SQL 数据库读取数据的示例。下面我们向节点传入两个不同的重试策略:
import Database from "better-sqlite3";
import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, StateSchema, MessagesValue, GraphNode, START, END } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";

const State = new StateSchema({
  messages: MessagesValue,
});

// 创建一个内存数据库
const db: typeof Database.prototype = new Database(":memory:");

const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" });

const callModel: GraphNode<typeof State> = async (state) => {
  const response = await model.invoke(state.messages);
  return { messages: [response] };
};

const queryDatabase: GraphNode<typeof State> = async (state) => {
  const queryResult: string = JSON.stringify(
    db.prepare("SELECT * FROM Artist LIMIT 10;").all(),
  );

  return { messages: [new AIMessage({ content: "queryResult" })] };
};

const workflow = new StateGraph(State)
  // 定义我们将在其间循环的两个节点
  .addNode("call_model", callModel, { retryPolicy: { maxAttempts: 5 } })
  .addNode("query_database", queryDatabase, {
    retryPolicy: {
      retryOn: (e: any): boolean => {
        if (e instanceof Database.SqliteError) {
          // 对 "SQLITE_BUSY" 错误进行重试
          return e.code === "SQLITE_BUSY";
        }
        return false; // 不对其他错误进行重试
      },
    },
  })
  .addEdge(START, "call_model")
  .addEdge("call_model", "query_database")
  .addEdge("query_database", END);

const graph = workflow.compile();

在节点内部访问执行信息

你可以通过 runtime.executionInfo 访问执行身份和重试信息。它会暴露线程、运行和检查点标识符以及重试状态,而无需直接从 config 中读取。
属性类型描述
threadIdstring | undefined当前执行的线程 ID。
runIdstring | undefined当前执行的运行 ID。
checkpointIdstring当前执行的检查点 ID。
checkpointNsstring当前执行的检查点命名空间。
taskIdstring当前执行的任务 ID。
nodeAttemptnumber当前执行尝试次数(从 1 开始计数)。
nodeFirstAttemptTimenumber | undefined第一次尝试开始时的 Unix 时间戳(秒)。在多次重试之间保持不变。

访问线程 ID 和运行 ID

使用 executionInfo 在节点内部访问线程 ID、运行 ID 和其他身份字段:
import { StateGraph, StateSchema, START, END } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  result: z.string(),
});

const myNode: GraphNode<typeof State> = async (state, runtime) => {
  const info = runtime.executionInfo;
  console.log(`Thread: ${info.threadId}, Run: ${info.runId}`);
  return { result: "done" };
};

const graph = new StateGraph(State)
  .addNode("my_node", myNode)
  .addEdge(START, "my_node")
  .addEdge("my_node", END)
  .compile();

根据重试状态调整行为

当节点有重试策略时,使用 executionInfo 检查当前尝试次数,并在第一次尝试失败后切换到备用方案:
import { StateGraph, StateSchema, START, END } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  result: z.string(),
});

const myNode: GraphNode<typeof State> = async (state, runtime) => {
  const info = runtime.executionInfo;
  if (info.nodeAttempt > 1) {
    // 在重试时使用备用方案
    return { result: await callFallbackApi() };
  }
  return { result: await callPrimaryApi() };
};

const graph = new StateGraph(State)
  .addNode("my_node", myNode, { retryPolicy: { maxAttempts: 3 } })
  .addEdge(START, "my_node")
  .addEdge("my_node", END)
  .compile();
即使没有重试策略,Runtime 对象上也可以使用 executionInfo —— nodeAttempt 默认值为 1nodeFirstAttemptTime 会设置为节点开始执行的时间。

在节点内部访问服务器信息

当你的图运行在 LangGraph Server 上时,可以通过 runtime.serverInfo 访问服务器特定的元数据。
属性类型描述
assistantIdstring当前部署的助手 ID。
graphIdstring当前部署的图 ID。
userBaseUser | null已认证用户(如果配置了 custom auth)。
const myNode: GraphNode<typeof State> = async (state, runtime) => {
  const server = runtime.serverInfo;
  if (server != null) {
    console.log(`Assistant: ${server.assistantId}, Graph: ${server.graphId}`);
    if (server.user != null) {
      console.log(`User: ${server.user.identity}`);
    }
  }
  return { result: "done" };
};
当图未运行在 LangGraph Server 上时,serverInfonull
runtime.executionInforuntime.serverInfo 需要 deepagents>=1.9.0(或 @langchain/langgraph>=1.2.8)。

创建一系列步骤

前置条件 本指南假设你熟悉上文关于 state 的部分。
这里我们演示如何构建一个简单的步骤序列。我们将展示:
  1. 如何构建顺序图
  2. 用于构建类似图的内置简写方式。
要添加一系列节点,我们使用 graph.addNode.addEdge 方法:
import { START, StateGraph } from "@langchain/langgraph";

const builder = new StateGraph(State)
  .addNode("step1", step1)
  .addNode("step2", step2)
  .addNode("step3", step3)
  .addEdge(START, "step1")
  .addEdge("step1", "step2")
  .addEdge("step2", "step3");
LangGraph 让你可以轻松地为应用添加底层持久化层。 这允许在节点执行之间对状态进行检查点保存,因此你的 LangGraph 节点会决定:它们还决定执行步骤如何被 streamed,以及如何使用 Studio 可视化和调试你的应用。我们来演示一个端到端示例。我们将创建一个包含三个步骤的序列:
  1. 在状态的某个键中填充一个值
  2. 更新同一个值
  3. 填充另一个不同的值
首先定义我们的 state。它决定了 图的 schema,也可以指定如何应用更新。更多细节请参阅 使用 reducer 处理状态更新在本例中,我们只跟踪两个值:
import { StateSchema, GraphNode } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  value1: z.string(),
  value2: z.number(),
});
我们的 nodes 只是一些 TypeScript 函数,它们读取图的状态并对其进行更新。这个函数的第一个参数始终是状态:
const step1: GraphNode<typeof State> = (state) => {
  return { value1: "a" };
};

const step2: GraphNode<typeof State> = (state) => {
  const currentValue1 = state.value1;
  return { value1: `${currentValue1} b` };
};

const step3: GraphNode<typeof State> = (state) => {
  return { value2: 10 };
};
请注意,在向状态发出更新时,每个节点只需要指定它希望更新的键的值。默认情况下,这会覆盖对应键的值。你也可以使用 reducers 来控制更新的处理方式——例如,你可以将后续更新追加到某个键上。更多细节请参阅 使用 reducer 处理状态更新
最后,我们定义图。我们使用 StateGraph 来定义一个在该状态上运行的图。然后我们将使用 addNodeaddEdge 来填充图并定义其控制流。
import { START, StateGraph } from "@langchain/langgraph";

const graph = new StateGraph(State)
  .addNode("step1", step1)
  .addNode("step2", step2)
  .addNode("step3", step3)
  .addEdge(START, "step1")
  .addEdge("step1", "step2")
  .addEdge("step2", "step3")
  .compile();
指定自定义名称 你可以使用 .addNode 为节点指定自定义名称:
const graph = new StateGraph(State)
.addNode("myNode", step1)
.compile();
请注意:
  • .addEdge 接收节点名称;对于函数,默认是 node.name
  • 我们必须指定图的入口点。为此,我们添加一条带有 START node 的边。
  • 当没有更多节点可执行时,图会停止。
接下来,我们 compile 图。这会对图结构进行一些基本检查(例如识别孤立节点)。如果我们通过 checkpointer 为应用添加持久化,也会在这里传入它。LangGraph 提供了用于可视化图的内置工具。我们来检查一下这个序列。有关可视化的详细信息,请参阅 可视化你的图
import * as fs from "node:fs/promises";

const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());

await fs.writeFile("graph.png", imageBuffer);
接下来进行一次简单调用:
const result = await graph.invoke({ value1: "c" });
console.log(result);
{ value1: 'a b', value2: 10 }
请注意:
  • 我们通过为单个状态键提供一个值来启动调用。我们必须始终为至少一个键提供值。
  • 我们传入的值被第一个节点覆盖了。
  • 第二个节点更新了该值。
  • 第三个节点填充了另一个不同的值。

创建分支

节点的并行执行对于加快整体图运行至关重要。LangGraph 原生支持节点的并行执行,可以显著提升基于图的工作流性能。这种并行化通过 fan-out 和 fan-in 机制实现,并同时利用标准边和 conditional_edges。下面是一些示例,展示如何创建适合你的分支数据流。

并行运行图节点

在这个示例中,我们从 Node A 扇出到 B and C,然后扇入到 D。在状态中,我们指定 reducer add 操作。它会对 State 中特定键的值进行合并或累积,而不是简单地覆盖现有值。对于列表,这意味着将新列表与现有列表连接起来。有关使用 reducer 更新状态的更多细节,请参阅上文 state reducers 部分。
import { StateGraph, StateSchema, ReducedValue, GraphNode, START, END } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  // reducer 使其只能追加
  aggregate: new ReducedValue(
    z.array(z.string()).default(() => []),
    { reducer: (x, y) => x.concat(y) }
  ),
});

const nodeA: GraphNode<typeof State> = (state) => {
  console.log(`Adding "A" to ${state.aggregate}`);
  return { aggregate: ["A"] };
};

const nodeB: GraphNode<typeof State> = (state) => {
  console.log(`Adding "B" to ${state.aggregate}`);
  return { aggregate: ["B"] };
};

const nodeC: GraphNode<typeof State> = (state) => {
  console.log(`Adding "C" to ${state.aggregate}`);
  return { aggregate: ["C"] };
};

const nodeD: GraphNode<typeof State> = (state) => {
  console.log(`Adding "D" to ${state.aggregate}`);
  return { aggregate: ["D"] };
};

const graph = new StateGraph(State)
  .addNode("a", nodeA)
  .addNode("b", nodeB)
  .addNode("c", nodeC)
  .addNode("d", nodeD)
  .addEdge(START, "a")
  .addEdge("a", "b")
  .addEdge("a", "c")
  .addEdge("b", "d")
  .addEdge("c", "d")
  .addEdge("d", END)
  .compile();
import * as fs from "node:fs/promises";

const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());

await fs.writeFile("graph.png", imageBuffer);
通过 reducer,你可以看到每个节点中添加的值会被累积起来。
const result = await graph.invoke({
  aggregate: [],
});
console.log(result);
Adding "A" to []
Adding "B" to ['A']
Adding "C" to ['A']
Adding "D" to ['A', 'B', 'C']
{ aggregate: ['A', 'B', 'C', 'D'] }
在上面的示例中,节点 "b""c" 会在同一个 superstep 中并发执行。因为它们位于同一步中,所以节点 "d" 会在 "b""c" 都完成之后执行。重要的是,来自并行 superstep 的更新顺序可能并不总是一致。如果你需要并行 superstep 中更新具有一致且预先确定的顺序,应将输出写入状态中的单独字段,并附带一个可用于排序的值。
LangGraph 在 supersteps 中执行节点,这意味着虽然并行分支是并行执行的,但整个 superstep 是事务性的。如果其中任何一个分支抛出异常,不会将任何更新应用到状态(整个 superstep 会报错)。重要的是,在使用 checkpointer 时,superstep 中成功节点的结果会被保存,并且在恢复时不会重复执行。如果你有容易出错的操作(比如想处理不稳定的 API 调用),LangGraph 提供了两种方式来解决:
  1. 你可以在节点中编写普通的 python 代码来捕获并处理异常。
  2. 你可以设置一个 retry_policy,指示图对抛出特定类型异常的节点进行重试。只有失败的分支会被重试,因此你无需担心执行冗余工作。
这两者结合起来,可以让你执行并行流程,并完全控制异常处理。
设置最大并发数 你可以在调用图时,通过在 configuration 中设置 max_concurrency 来控制最大并发任务数。
const result = await graph.invoke({ value1: "c" }, {configurable: {max_concurrency: 10}});

条件分支

如果你的 fan-out 需要在运行时根据状态变化,可以使用 addConditionalEdges 通过图状态选择一条或多条路径。请看下面的示例,其中节点 a 生成一个状态更新,用来决定后续节点。
import { StateGraph, StateSchema, ReducedValue, GraphNode, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  aggregate: new ReducedValue(
    z.array(z.string()).default(() => []),
    { reducer: (x, y) => x.concat(y) }
  ),
  // 向状态添加一个键。我们将设置这个键来决定
  // 如何分支。
  which: z.string(),
});

const nodeA: GraphNode<typeof State> = (state) => {
  console.log(`Adding "A" to ${state.aggregate}`);
  return { aggregate: ["A"], which: "c" };
};

const nodeB: GraphNode<typeof State> = (state) => {
  console.log(`Adding "B" to ${state.aggregate}`);
  return { aggregate: ["B"] };
};

const nodeC: GraphNode<typeof State> = (state) => {
  console.log(`Adding "C" to ${state.aggregate}`);
  return { aggregate: ["C"] };
};

const conditionalEdge: ConditionalEdgeRouter<typeof State, "b" | "c"> = (state) => {
  // 在这里填写使用状态的任意逻辑
  // 来决定下一个节点
  return state.which as "b" | "c";
};

const graph = new StateGraph(State)
  .addNode("a", nodeA)
  .addNode("b", nodeB)
  .addNode("c", nodeC)
  .addEdge(START, "a")
  .addEdge("b", END)
  .addEdge("c", END)
  .addConditionalEdges("a", conditionalEdge)
  .compile();
import * as fs from "node:fs/promises";

const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());

await fs.writeFile("graph.png", imageBuffer);
const result = await graph.invoke({ aggregate: [] });
console.log(result);
Adding "A" to []
Adding "C" to ['A']
{ aggregate: ['A', 'C'], which: 'c' }
你的条件边可以路由到多个目标节点。例如:
const routeBcOrCd: ConditionalEdgeRouter<typeof State, "b" | "c" | "d"> = (state) => {
  if (state.which === "cd") {
    return ["c", "d"];
  }
  return ["b", "c"];
};

Map-Reduce 和 send API

LangGraph 支持使用 Send API 实现 map-reduce 和其他高级分支模式。下面是一个使用示例:
import { StateGraph, StateSchema, ReducedValue, GraphNode, START, END, Send } from "@langchain/langgraph";
import * as z from "zod";

const OverallState = new StateSchema({
  topic: z.string(),
  subjects: z.array(z.string()),
  jokes: new ReducedValue(
    z.array(z.string()).default(() => []),
    { reducer: (x, y) => x.concat(y) }
  ),
  bestSelectedJoke: z.string(),
});

const generateTopics: GraphNode<typeof OverallState> = (state) => {
  return { subjects: ["lions", "elephants", "penguins"] };
};

const generateJoke: GraphNode<typeof OverallState> = (state) => {
  const jokeMap: Record<string, string> = {
    lions: "Why don't lions like fast food? Because they can't catch it!",
    elephants: "Why don't elephants use computers? They're afraid of the mouse!",
    penguins: "Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice."
  };
  return { jokes: [jokeMap[state.subject]] };
};

const continueToJokes: ConditionalEdgeRouter<typeof OverallState, "generateJoke"> = (state) => {
  return state.subjects.map((subject) => new Send("generateJoke", { subject }));
};

const bestJoke: GraphNode<typeof OverallState> = (state) => {
  return { bestSelectedJoke: "penguins" };
};

const graph = new StateGraph(OverallState)
  .addNode("generateTopics", generateTopics)
  .addNode("generateJoke", generateJoke)
  .addNode("bestJoke", bestJoke)
  .addEdge(START, "generateTopics")
  .addConditionalEdges("generateTopics", continueToJokes)
  .addEdge("generateJoke", "bestJoke")
  .addEdge("bestJoke", END)
  .compile();
import * as fs from "node:fs/promises";

const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());

await fs.writeFile("graph.png", imageBuffer);
// 调用图:这里我们调用它来生成一个笑话列表
for await (const step of await graph.stream({ topic: "animals" })) {
  console.log(step);
}
{ generateTopics: { subjects: [ 'lions', 'elephants', 'penguins' ] } }
{ generateJoke: { jokes: [ "Why don't lions like fast food? Because they can't catch it!" ] } }
{ generateJoke: { jokes: [ "Why don't elephants use computers? They're afraid of the mouse!" ] } }
{ generateJoke: { jokes: [ "Why don't penguins like talking to strangers at parties? Because they find it hard to break the ice." ] } }
{ bestJoke: { bestSelectedJoke: 'penguins' } }

创建和控制循环

创建带循环的图时,我们需要一种终止执行的机制。最常见的做法是添加一个 conditional edge,当达到某个终止条件时路由到 END 节点。 你也可以在调用或流式处理图时设置图的递归限制。递归限制设置的是图在抛出错误之前允许执行的 super-steps 数量。请阅读更多关于 recursion limit concept 的内容。 我们来看一个带循环的简单图,以便更好地理解这些机制如何工作。
如果想返回状态的最后一个值,而不是收到递归限制错误,请参阅 下一节
创建循环时,你可以包含一条指定终止条件的条件边:
const route: ConditionalEdgeRouter<typeof State, "b"> = (state) => {
  if (terminationCondition(state)) {
    return END;
  } else {
    return "b";
  }
};

const graph = new StateGraph(State)
  .addNode("a", nodeA)
  .addNode("b", nodeB)
  .addEdge(START, "a")
  .addConditionalEdges("a", route)
  .addEdge("b", "a")
  .compile();
要控制递归限制,请在配置中指定 "recursionLimit"。这会抛出一个 GraphRecursionError,你可以捕获并处理它:
import { GraphRecursionError } from "@langchain/langgraph";

try {
  await graph.invoke(inputs, { recursionLimit: 3 });
} catch (error) {
  if (error instanceof GraphRecursionError) {
    console.log("递归错误");
  }
}
让我们定义一个带有简单循环的图。请注意,我们使用条件边来实现终止条件。
import { StateGraph, StateSchema, ReducedValue, GraphNode, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  // reducer 会让它只能追加
  aggregate: new ReducedValue(
    z.array(z.string()).default(() => []),
    { reducer: (x, y) => x.concat(y) }
  ),
});

const nodeA: GraphNode<typeof State> = (state) => {
  console.log(`节点 A 看到 ${state.aggregate}`);
  return { aggregate: ["A"] };
};

const nodeB: GraphNode<typeof State> = (state) => {
  console.log(`节点 B 看到 ${state.aggregate}`);
  return { aggregate: ["B"] };
};

// 定义边
const route: ConditionalEdgeRouter<typeof State, "b"> = (state) => {
  if (state.aggregate.length < 7) {
    return "b";
  } else {
    return END;
  }
};

const graph = new StateGraph(State)
  .addNode("a", nodeA)
  .addNode("b", nodeB)
  .addEdge(START, "a")
  .addConditionalEdges("a", route)
  .addEdge("b", "a")
  .compile();
import * as fs from "node:fs/promises";

const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());

await fs.writeFile("graph.png", imageBuffer);
这个架构类似于一个 ReAct agent,其中节点 "a" 是一个调用工具的模型,节点 "b" 表示这些工具。 在我们的 route 条件边中,我们指定当状态中的 "aggregate" 列表超过某个长度阈值后就应该结束。 调用该图时,我们可以看到在达到终止条件之前,会在节点 "a""b" 之间交替执行。
const result = await graph.invoke({ aggregate: [] });
console.log(result);
节点 A 看到 []
节点 B 看到 ['A']
节点 A 看到 ['A', 'B']
节点 B 看到 ['A', 'B', 'A']
节点 A 看到 ['A', 'B', 'A', 'B']
节点 B 看到 ['A', 'B', 'A', 'B', 'A']
节点 A 看到 ['A', 'B', 'A', 'B', 'A', 'B']
{ aggregate: ['A', 'B', 'A', 'B', 'A', 'B', 'A'] }

施加递归限制

在某些应用中,我们可能无法保证一定会达到给定的终止条件。在这些情况下,我们可以设置图的递归限制。在经过给定数量的超级步后,这会抛出一个 GraphRecursionError。然后我们可以捕获并处理这个异常:
import { GraphRecursionError } from "@langchain/langgraph";

try {
  await graph.invoke({ aggregate: [] }, { recursionLimit: 4 });
} catch (error) {
  if (error instanceof GraphRecursionError) {
    console.log("递归错误");
  }
}
节点 A 看到 []
节点 B 看到 ['A']
节点 A 看到 ['A', 'B']
节点 B 看到 ['A', 'B', 'A']
节点 A 看到 ['A', 'B', 'A', 'B']
递归错误

使用 Command 组合控制流和状态更新

将控制流(边)和状态更新(节点)组合起来会很有用。例如,你可能希望在同一个节点中既执行状态更新,又决定接下来要前往哪个节点。LangGraph 提供了一种方式:从节点函数返回一个 Command 对象:
import { Command } from "@langchain/langgraph";

const myNode = (state: State): Command => {
  return new Command({
    // 状态更新
    update: { foo: "bar" },
    // 控制流
    goto: "myOtherNode"
  });
};
下面我们展示一个端到端示例。让我们创建一个包含 3 个节点的简单图:A、B 和 C。我们会先执行节点 A,然后根据节点 A 的输出决定接下来前往节点 B 还是节点 C。
import { StateGraph, StateSchema, GraphNode, Command, START } from "@langchain/langgraph";
import * as z from "zod";

// 定义图状态
const State = new StateSchema({
  foo: z.string(),
});

// 定义节点

const nodeA: GraphNode<typeof State, "nodeB" | "nodeC"> = (state) => {
  console.log("调用了 A");
  const value = Math.random() > 0.5 ? "b" : "c";
  // 这是条件边函数的替代方案
  const goto = value === "b" ? "nodeB" : "nodeC";

  // 注意 Command 如何允许你同时更新图状态并路由到下一个节点
  return new Command({
    // 这是状态更新
    update: { foo: value },
    // 这是边的替代方案
    goto,
  });
};

const nodeB: GraphNode<typeof State> = (state) => {
  console.log("调用了 B");
  return { foo: state.foo + "b" };
};

const nodeC: GraphNode<typeof State> = (state) => {
  console.log("调用了 C");
  return { foo: state.foo + "c" };
};
现在我们可以使用上面的节点创建 StateGraph。请注意,这个图没有用于路由的条件边!这是因为控制流是在 nodeA 内部通过 Command 定义的。
const graph = new StateGraph(State)
  .addNode("nodeA", nodeA, {
    ends: ["nodeB", "nodeC"],
  })
  .addNode("nodeB", nodeB)
  .addNode("nodeC", nodeC)
  .addEdge(START, "nodeA")
  .compile();
你可能已经注意到,我们使用 ends 来指定 nodeA 可以导航到哪些节点。这对于图渲染是必需的,它告诉 LangGraph nodeA 可以导航到 nodeBnodeC
import * as fs from "node:fs/promises";

const drawableGraph = await graph.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());

await fs.writeFile("graph.png", imageBuffer);
如果多次运行该图,我们会看到它会根据节点 A 中的随机选择走不同路径(A -> B 或 A -> C)。
const result = await graph.invoke({ foo: "" });
console.log(result);
调用了 A
调用了 C
{ foo: 'cc' }

导航到父图中的节点

如果你正在使用子图,你可能希望从子图中的某个节点导航到另一个子图(也就是父图中的另一个节点)。为此,你可以在 Command 中指定 graph=Command.PARENT
const myNode = (state: State): Command => {
  return new Command({
    update: { foo: "bar" },
    goto: "otherSubgraph",  // 其中 `otherSubgraph` 是父图中的一个节点
    graph: Command.PARENT
  });
};
让我们用上面的示例来演示这一点。我们会把上面示例中的 nodeA 改成一个单节点图,然后把它作为子图添加到父图中。
使用 Command.PARENT 进行状态更新 当你从子图节点向父图节点发送更新,并且更新的键同时存在于父图和子图的状态模式中时,你必须为父图状态中要更新的键定义一个 reducer。请看下面的示例。
import { StateGraph, StateSchema, ReducedValue, GraphNode, Command, START } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  // 注意:我们在这里定义了一个 reducer
  foo: new ReducedValue(  
    z.string().default(""),
    { reducer: (x, y) => x + y }
  ),
});

const nodeA: GraphNode<typeof State, "nodeB" | "nodeC"> = (state) => {
  console.log("调用了 A");
  const value = Math.random() > 0.5 ? "nodeB" : "nodeC";

  // 注意 Command 如何允许你同时更新图状态并路由到下一个节点
  return new Command({
    update: { foo: "a" },
    goto: value,
    // 这会告诉 LangGraph 导航到父图中的 nodeB 或 nodeC
    // 注意:这会导航到相对于子图最近的父图
    graph: Command.PARENT,
  });
};

const subgraph = new StateGraph(State)
  .addNode("nodeA", nodeA, { ends: ["nodeB", "nodeC"] })
  .addEdge(START, "nodeA")
  .compile();

const nodeB: GraphNode<typeof State> = (state) => {
  console.log("调用了 B");
  // 注意:因为我们已经定义了 reducer,所以不需要手动追加
  // 新字符到现有的 'foo' 值中。相反,reducer 会自动追加这些字符
  // automatically
  return { foo: "b" };
};

const nodeC: GraphNode<typeof State> = (state) => {
  console.log("调用了 C");
  return { foo: "c" };
};

const graph = new StateGraph(State)
  .addNode("subgraph", subgraph, { ends: ["nodeB", "nodeC"] })
  .addNode("nodeB", nodeB)
  .addNode("nodeC", nodeC)
  .addEdge(START, "subgraph")
  .compile();
const result = await graph.invoke({ foo: "" });
console.log(result);
调用了 A
调用了 C
{ foo: 'ac' }

在工具内部使用

一个常见用例是在工具内部更新图状态。例如,在客户支持应用中,你可能希望在对话开始时根据客户的账号或 ID 查询客户信息。要从工具更新图状态,你可以从工具返回 Command(update={"my_custom_key": "foo", "messages": [...]})
import { tool } from "@langchain/core/tools";
import { Command } from "@langchain/langgraph";
import * as z from "zod";

const lookupUserInfo = tool(
  async (input, runtime) => {
    const userId = runtime.serverInfo?.user?.identity;
    const userInfo = getUserInfo(userId);
    return new Command({
      update: {
        // 更新状态键
        userInfo: userInfo,
        // 更新消息历史
        messages: [{
          role: "tool",
          content: "已成功查询用户信息",
          tool_call_id: runtime.toolCall.id
        }]
      }
    });
  },
  {
    name: "lookupUserInfo",
    description: "使用此工具查询用户信息,以便更好地帮助他们解决问题。",
    schema: z.object({}),
  }
);
当从工具返回 Command 时,你必须在 Command.update 中包含 messages(或任何用于消息历史的状态键),并且 messages 中的消息列表必须包含一个 ToolMessage。这是生成有效消息历史所必需的(LLM 提供商要求带有工具调用的 AI 消息后面必须跟随工具结果消息)。
如果你使用的是通过 Command 更新状态的工具,我们建议使用预构建的 ToolNode,它会自动处理返回 Command 对象的工具,并将其传播到图状态。如果你正在编写一个调用工具的自定义节点,则需要手动将工具返回的 Command 对象作为节点更新进行传播。

可视化你的图

这里我们演示如何可视化你创建的图。 你可以可视化任意 Graph,包括 StateGraph 让我们创建一个简单的示例图来演示可视化。
import { StateGraph, StateSchema, MessagesValue, ReducedValue, GraphNode, ConditionalEdgeRouter, START, END } from "@langchain/langgraph";
import * as z from "zod";

const State = new StateSchema({
  messages: MessagesValue,
  value: new ReducedValue(
    z.number().default(0),
    { reducer: (x, y) => x + y }
  ),
});

const node1: GraphNode<typeof State> = (state) => {
  return { value: state.value + 1 };
};

const node2: GraphNode<typeof State> = (state) => {
  return { value: state.value * 2 };
};

const router: ConditionalEdgeRouter<typeof State, "node2"> = (state) => {
  if (state.value < 10) {
    return "node2";
  }
  return END;
};

const app = new StateGraph(State)
  .addNode("node1", node1)
  .addNode("node2", node2)
  .addEdge(START, "node1")
  .addConditionalEdges("node1", router)
  .addEdge("node2", "node1")
  .compile();

Mermaid

我们也可以将图类转换为 Mermaid 语法。
const drawableGraph = await app.getGraphAsync();
console.log(drawableGraph.drawMermaid());
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
    tart__([<p>__start__</p>]):::first
    e1(node1)
    e2(node2)
    nd__([<p>__end__</p>]):::last
    tart__ --> node1;
    e1 -.-> node2;
    e1 -.-> __end__;
    e2 --> node1;
    ssDef default fill:#f2f0ff,line-height:1.2
    ssDef first fill-opacity:0
    ssDef last fill:#bfb6fc

PNG

如果愿意,我们也可以把 Graph 渲染成 .png。这里会使用 Mermaid.ink API 来生成图表。
import * as fs from "node:fs/promises";

const drawableGraph = await app.getGraphAsync();
const image = await drawableGraph.drawMermaidPng();
const imageBuffer = new Uint8Array(await image.arrayBuffer());

await fs.writeFile("graph.png", imageBuffer);