本指南演示了 LangGraph 的 Graph API 基础。将介绍 state,以及如何组合常见图结构,如 序列、分支 和 循环。还将介绍 LangGraph 的控制功能,包括用于 map-reduce 工作流的 Send API 和用于将状态更新与节点跳转结合的 Command API。
安装 langgraph:
npm install @langchain/langgraph
为更好的调试配置 LangSmith注册 LangSmith 可以帮助快速定位问题并提升 LangGraph 项目的性能。LangSmith 让你使用追踪数据调试、测试和监控基于 LangGraph 的 LLM 应用 — 详见文档 obsservability。
定义与更新状态
这里展示如何在 LangGraph 中定义和更新 state。我们将演示:
- 如何使用 state 定义图的 schema
- 如何使用 reducers 控制状态更新的处理方式
定义状态
LangGraph 中的 State 使用 StateSchema 类定义。该类接受标准 schema(如 Zod)用于字段,并支持 ReducedValue、MessagesValue 和 UntrackedValue 等特殊值类型。
默认情况下,图具有相同的输入与输出 schema,状态决定了该 schema。关于如何定义不同的输入与输出 schema,请参见 定义输入和输出 schema。
下面以消息(messages)为例,这是许多 LLM 应用常用的状态表述形式。详见概念页面 working-with-messages-in-graph-state。
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("Hello!");
return { messages: [newMessage], extraField: 10 };
};
此节点只是向消息列表追加一条消息(reducer 负责连接),并填充一个额外字段。
节点应直接返回对状态的更新,而不是在原地修改状态。
接下来使用 StateGraph 定义包含该节点的图:
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("Hi")], extraField: 0 });
console.log(result);
{ messages: [HumanMessage { content: 'Hi' }, AIMessage { content: 'Hello!' }], extraField: 10 }
注意:
- 我们通过更新单个键启动调用。
- 调用结果包含整个状态。
通常使用如下方式打印消息对象内容:
for (const message of result.messages) {
console.log(`${message.getType()}: ${message.content}`);
}
使用 reducers 处理状态更新
状态中的每个键可以有独立的 reducer,用于控制节点返回的更新如何被应用。如果未显式指定 reducer,则默认采用覆盖策略。
在示例中,我们使用的 MessagesValue 已内置 reducer。对自定义字段,可使用 ReducedValue 定义更新的合并方式。
import { StateSchema, MessagesValue, ReducedValue } from "@langchain/langgraph";
import * as z from "zod";
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("Hello!");
return { messages: [newMessage], extraField: 10 };
};
MessagesValue
在实际中,更新消息列表时还需考虑:
- 是否更新现有消息
- 是否接受消息格式的简写,例如 OpenAI 格式
LangGraph 提供了内置的 MessagesValue 来处理这些考虑事项。
定义输入与输出 schema
默认情况下,StateGraph 使用单一 schema,所有节点使用该 schema 通信。但也可以定义不同的输入与输出 schema。当指定不同 schema 时,内部仍会使用一个内部 schema 在节点间通信;输入 schema 用于校验提供的输入结构,输出 schema 用于过滤返回给调用者的数据。
示例:
import { StateGraph, StateSchema, GraphNode, START, END } from "@langchain/langgraph";
import * as z from "zod";
const InputState = new StateSchema({
question: z.string(),
});
const OutputState = new StateSchema({
answer: z.string(),
});
const OverallState = new StateSchema({
question: z.string(),
answer: z.string(),
});
const answerNode: GraphNode<typeof OverallState> = (state) => {
return { answer: "bye", question: state.question };
};
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: "hi" }));
注意 invoke 的输出仅包含输出 schema。
在节点间传递私有状态
有时,节点间需要传递对中间逻辑重要但不应成为图公共 schema 的私有数据。这类私有数据仅供某些节点之间共享,不应成为整体输入/输出的一部分。
下面示例构建了一个包含三个节点的序列(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(),
});
const Node1Output = new StateSchema({
privateData: z.string(),
});
const Node2Input = new StateSchema({
privateData: z.string(),
});
const node1: GraphNode<typeof OverallState> = (state) => {
const output = { privateData: "set by node1" };
console.log(`Entered node 'node1':\n\tInput: ${JSON.stringify(state)}.\n\tReturned: ${JSON.stringify(output)}`);
return output;
};
const node2: GraphNode<typeof Node2Input> = (state) => {
const output = { a: "set by node2" };
console.log(`Entered node 'node2':\n\tInput: ${JSON.stringify(state)}.\n\tReturned: ${JSON.stringify(output)}`);
return output;
};
const node3: GraphNode<typeof OverallState> = (state) => {
const output = { a: "set by node3" };
console.log(`Entered node 'node3':\n\tInput: ${JSON.stringify(state)}.\n\tReturned: ${JSON.stringify(output)}`);
return output;
};
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: "set at start" });
console.log(`\nOutput of graph invocation: ${JSON.stringify(response)}`);
Entered node 'node1':
Input: {"a":"set at start"}.
Returned: {"privateData":"set by node1"}
Entered node 'node2':
Input: {"privateData":"set by node1"}.
Returned: {"a":"set by node2"}
Entered node 'node3':
Input: {"a":"set by node2"}.
Returned: {"a":"set by node3"}
Output of graph invocation: {"a":"set by node3"}
其他状态定义方式
除了 StateSchema,LangGraph 还支持多种定义状态的方法:Channels API、Annotation.Root、以及使用不同 Zod 版本的方式。请参阅原文档中对应部分以获得更详细的示例和比较表。
添加运行时配置
有时希望在调用图时配置某些运行时参数(例如指定使用哪个 LLM 或系统提示),而不将这些参数写入图状态。步骤:
- 为配置指定 schema
- 在节点或条件边的函数签名中添加配置参数
- 在调用时传入配置
示例(省略部分长示例以保持简洁,原文包含更详细的运行时模型示例)。