LangGraph 词汇表¶
图¶
LangGraph 的核心是将代理工作流建模为图。您可以使用三个关键组件定义代理的行为
-
节点
:JavaScript/TypeScript 函数,用于编码代理的逻辑。它们接收当前状态
作为输入,执行一些计算或副作用,并返回更新后的状态
。 -
边
:JavaScript/TypeScript 函数,用于根据当前状态
确定接下来执行哪个节点
。它们可以是条件分支或固定转换。
通过组合 节点
和 边
,您可以创建复杂的、循环的工作流,随着时间的推移演变 状态
。然而,真正的力量来自于 LangGraph 如何管理 状态
。 强调一下:节点
和 边
只不过是 JavaScript/TypeScript 函数 - 它们可以包含 LLM 或仅仅是优秀的 JavaScript/TypeScript 代码。
简而言之:节点完成工作。边决定下一步做什么。
LangGraph 的底层图算法使用 消息传递 来定义通用程序。当一个节点完成其操作时,它会沿着一条或多条边向其他节点发送消息。这些接收节点然后执行其函数,将结果消息传递到下一组节点,并且该过程继续。受到 Google 的 Pregel 系统的启发,程序以离散的“超级步骤”进行。
一个超级步骤可以被认为是图节点上的单个迭代。并行运行的节点是同一超级步骤的一部分,而顺序运行的节点属于不同的超级步骤。在图执行开始时,所有节点都处于 非活动
状态。当节点在其任何传入边(或“通道”)上接收到新消息(状态)时,节点变为 活动
状态。然后,活动节点运行其函数并响应更新。在每个超级步骤结束时,没有传入消息的节点通过将自身标记为 非活动
来投票 停止
。当所有节点都处于 非活动
状态且没有消息在传输中时,图执行终止。
StateGraph¶
StateGraph
类是要使用的主要图类。它通过用户定义的 状态
对象参数化。(使用 注解
对象定义并作为第一个参数传递)
MessageGraph(旧版)¶
MessageGraph
类是一种特殊类型的图。MessageGraph
的 状态
仅是消息数组。除非用于聊天机器人,否则很少使用此类,因为大多数应用程序需要 状态
比消息数组更复杂。
编译你的图¶
要构建你的图,你首先定义 状态,然后添加 节点 和 边,然后编译它。编译你的图究竟是什么,为什么需要它?
编译是一个非常简单的步骤。它提供了对你的图结构的几个基本检查(没有孤立节点等)。您还可以在这里指定运行时参数,如检查点和 断点。您只需调用 .compile
方法即可编译你的图
你必须在可以使用图之前编译它。
状态 (State)¶
定义图时,你做的第一件事是定义图的 状态
。状态
包括有关图结构的信息,以及 归约器
函数,这些函数指定如何将更新应用于状态。状态
的模式将是图中所有 节点
和 边
的输入模式,应使用 注解
对象定义。所有 节点
都将发出对 状态
的更新,然后使用指定的 归约器
函数应用这些更新。
注解 (Annotation)¶
指定图模式的方法是定义一个根 注解
对象,其中每个键都是状态中的一个项。
多重模式¶
通常,所有图节点都与单个状态注解通信。这意味着它们将读取和写入相同的状态通道。但是,在某些情况下,我们希望对此进行更多控制
- 内部节点可以传递图中输入/输出不需要的信息。
- 我们可能还想为图使用不同的输入/输出模式。例如,输出可能只包含一个相关的输出键。
可以使节点写入图内的私有状态通道以进行内部节点通信。我们可以简单地定义一个私有注解 PrivateState
。有关更多详细信息,请参阅 此笔记本。
也可以为图定义显式输入和输出模式。在这些情况下,我们定义一个“内部”模式,其中包含与图操作相关的所有键。但是,我们还定义了 输入
和 输出
模式,它们是“内部”模式的子集,以约束图的输入和输出。有关更多详细信息,请参阅 本指南。
让我们看一个例子
import {
Annotation,
START,
StateGraph,
StateType,
UpdateType,
} from "@langchain/langgraph";
const InputStateAnnotation = Annotation.Root({
user_input: Annotation<string>,
});
const OutputStateAnnotation = Annotation.Root({
graph_output: Annotation<string>,
});
const OverallStateAnnotation = Annotation.Root({
foo: Annotation<string>,
bar: Annotation<string>,
user_input: Annotation<string>,
graph_output: Annotation<string>,
});
const node1 = async (state: typeof InputStateAnnotation.State) => {
// Write to OverallStateAnnotation
return { foo: state.user_input + " name" };
};
const node2 = async (state: typeof OverallStateAnnotation.State) => {
// Read from OverallStateAnnotation, write to OverallStateAnnotation
return { bar: state.foo + " is" };
};
const node3 = async (state: typeof OverallStateAnnotation.State) => {
// Read from OverallStateAnnotation, write to OutputStateAnnotation
return { graph_output: state.bar + " Lance" };
};
// Most of the time the StateGraph type parameters are inferred by TypeScript,
// but this is a special case where they must be specified explicitly in order
// to avoid a type error.
const graph = new StateGraph<
typeof OverallStateAnnotation["spec"],
StateType<typeof OverallStateAnnotation["spec"]>,
UpdateType<typeof OutputStateAnnotation["spec"]>,
typeof START,
typeof InputStateAnnotation["spec"],
typeof OutputStateAnnotation["spec"]
>({
input: InputStateAnnotation,
output: OutputStateAnnotation,
stateSchema: OverallStateAnnotation,
})
.addNode("node1", node1)
.addNode("node2", node2)
.addNode("node3", node3)
.addEdge("__start__", "node1")
.addEdge("node1", "node2")
.addEdge("node2", "node3")
.compile();
await graph.invoke({ user_input: "My" });
请注意,我们将 state: typeof InputStateAnnotation.State
作为输入模式传递给 node1
。但是,我们写入 foo
,这是 OverallStateAnnotation
中的一个通道。我们如何写入未包含在输入模式中的状态通道?这是因为节点可以写入图状态中的任何状态通道。 图状态是在初始化时定义的状态通道的联合,其中包括 OverallStateAnnotation
和过滤器 InputStateAnnotation
和 OutputStateAnnotation
。
归约器 (Reducers)¶
归约器是理解如何将来自节点的更新应用于 状态
的关键。状态
中的每个键都有其自己的独立归约器函数。如果未显式指定归约器函数,则假定对该键的所有更新都应覆盖它。让我们看几个例子来更好地理解它们。
示例 A
import { StateGraph, Annotation } from "@langchain/langgraph";
const State = Annotation.Root({
foo: Annotation<number>,
bar: Annotation<string[]>,
});
const graphBuilder = new StateGraph(State);
在此示例中,未为任何键指定归约器函数。让我们假设图的输入是 { foo: 1, bar: ["hi"] }
。然后让我们假设第一个 节点
返回 { foo: 2 }
。这被视为对状态的更新。请注意,节点
不需要返回整个 状态
模式 - 只需要更新。应用此更新后,状态
将变为 { foo: 2, bar: ["hi"] }
。如果第二个节点返回 { bar: ["bye"] }
,则 状态
将变为 { foo: 2, bar: ["bye"] }
示例 B
import { StateGraph, Annotation } from "@langchain/langgraph";
const State = Annotation.Root({
foo: Annotation<number>,
bar: Annotation<string[]>({
reducer: (state: string[], update: string[]) => state.concat(update),
default: () => [],
}),
});
const graphBuilder = new StateGraph(State);
在此示例中,我们已将 bar
字段更新为包含 reducer
函数的对象。此函数将始终接受两个位置参数:state
和 update
,其中 state
表示当前状态值,update
表示从 节点
返回的更新。请注意,第一个键保持不变。让我们假设图的输入是 { foo: 1, bar: ["hi"] }
。然后让我们假设第一个 节点
返回 { foo: 2 }
。这被视为对状态的更新。请注意,节点
不需要返回整个 状态
模式 - 只需要更新。应用此更新后,状态
将变为 { foo: 2, bar: ["hi"] }
。如果第二个节点返回 { bar: ["bye"] }
,则 状态
将变为 { foo: 2, bar: ["hi", "bye"] }
。请注意,此处的 bar
键通过将两个数组连接在一起进行更新。
在图状态中使用消息¶
为什么要使用消息?¶
大多数现代 LLM 提供商都有一个聊天模型接口,该接口接受消息列表作为输入。LangChain 的 ChatModel
尤其接受 Message
对象列表作为输入。这些消息有多种形式,例如 HumanMessage
(用户输入)或 AIMessage
(LLM 响应)。要阅读有关消息对象是什么的更多信息,请参阅 此 概念指南。
在你的图中使用消息¶
在许多情况下,将先前的对话历史记录存储为图状态中的消息列表很有帮助。为此,我们可以向图状态添加一个键(通道),该键存储 Message
对象列表,并使用归约器函数对其进行注解(请参阅以下示例中的 messages
键)。归约器函数对于告诉图如何使用每个状态更新来更新状态中的 Message
对象列表至关重要(例如,当节点发送更新时)。如果您未指定归约器,则每个状态更新都会将消息列表覆盖为最近提供的值。
但是,您可能还想手动更新图状态中的消息(例如,人机环路)。如果您使用类似 (a, b) => a.concat(b)
的归约器,则您发送到图的手动状态更新将附加到现有消息列表,而不是更新现有消息。为避免这种情况,您需要一个可以跟踪消息 ID 并在更新时覆盖现有消息的归约器。为了实现这一点,您可以使用预构建的 messagesStateReducer
函数。对于全新的消息,它只会附加到现有列表,但它也会正确处理现有消息的更新。
序列化¶
除了跟踪消息 ID 之外,当在 messages
通道上收到状态更新时,messagesStateReducer
函数还会尝试将消息反序列化为 LangChain Message
对象。这允许以以下格式发送图输入/状态更新
// this is supported
{
messages: [new HumanMessage({ content: "message" })];
}
// and this is also supported
{
messages: [{ role: "user", content: "message" }];
}
以下是使用 messagesStateReducer
作为其归约器函数的图状态注解的示例。
import type { BaseMessage } from "@langchain/core/messages";
import { Annotation, type Messages } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[], Messages>({
reducer: messagesStateReducer,
}),
});
MessagesAnnotation¶
由于在状态中包含消息列表非常常见,因此存在一个名为 MessagesAnnotation
的预构建注解,它可以轻松地将消息用作图状态。MessagesAnnotation
使用单个 messages
键定义,该键是 BaseMessage
对象列表,并使用 messagesStateReducer
归约器。
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
const graph = new StateGraph(MessagesAnnotation)
.addNode(...)
...
等效于像这样手动初始化你的状态
import { BaseMessage } from "@langchain/core/messages";
import { Annotation, StateGraph, messagesStateReducer } from "@langchain/langgraph";
export const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});
const graph = new StateGraph(StateAnnotation)
.addNode(...)
...
MessagesAnnotation
的状态具有一个名为 messages
的键。这是一个 BaseMessage
的数组,使用 messagesStateReducer
作为归约器。messagesStateReducer
基本上将消息添加到现有列表(它还做了一些不错的额外事情,例如从 OpenAI 消息格式转换为标准 LangChain 消息格式,处理基于消息 ID 的更新等)。
我们经常看到消息数组是状态的关键组成部分,因此此预构建状态旨在使其易于使用消息。通常,要跟踪的状态不仅仅是消息,因此我们看到人们扩展此状态并添加更多字段,例如
import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
const StateWithDocuments = Annotation.Root({
...MessagesAnnotation.spec, // Spread in the messages state
documents: Annotation<string[]>,
});
节点 (Nodes)¶
在 LangGraph 中,节点通常是 JavaScript/TypeScript 函数(同步或 async
),其中第一个位置参数是 状态,并且(可选)第二个位置参数是“config”,其中包含可选的 可配置参数(例如 thread_id
)。
类似于 NetworkX
,您可以使用 addNode 方法将这些节点添加到图中
import { RunnableConfig } from "@langchain/core/runnables";
import { StateGraph, Annotation } from "@langchain/langgraph";
const GraphAnnotation = Annotation.Root({
input: Annotation<string>,
results: Annotation<string>,
});
// The state type can be extracted using `typeof <annotation variable name>.State`
const myNode = (state: typeof GraphAnnotation.State, config?: RunnableConfig) => {
console.log("In node: ", config.configurable?.user_id);
return {
results: `Hello, ${state.input}!`
};
};
// The second argument is optional
const myOtherNode = (state: typeof GraphAnnotation.State) => {
return state;
};
const builder = new StateGraph(GraphAnnotation)
.addNode("myNode", myNode)
.addNode("myOtherNode", myOtherNode)
...
在幕后,函数被转换为 RunnableLambda's,这为您的函数添加了批处理和流式处理支持,以及本机跟踪和调试。
START
节点¶
START
节点是一个特殊节点,表示节点将用户输入发送到图。引用此节点的主要目的是确定应首先调用哪些节点。
END
节点¶
END
节点是一个特殊节点,表示终端节点。当您想表示哪些边在完成后没有操作时,会引用此节点。
边 (Edges)¶
边定义了逻辑的路由方式以及图如何决定停止。这是您的代理如何工作以及不同节点如何相互通信的重要组成部分。有几种关键类型的边
- 普通边:直接从一个节点到下一个节点。
- 条件边:调用函数以确定接下来要转到哪个节点。
- 入口点:当用户输入到达时,首先调用哪个节点。
- 条件入口点:调用函数以确定当用户输入到达时,首先调用哪个节点。
一个节点可以有多个传出边。如果一个节点有多个传出边,则所有这些目标节点将在下一个超级步骤中并行执行。
普通边¶
如果您始终要从节点 A 转到节点 B,则可以直接使用 addEdge 方法。
条件边¶
如果您想有条件地路由到一个或多个边(或有条件地终止),则可以使用 addConditionalEdges 方法。此方法接受节点的名称和一个“路由函数”,以便在该节点执行后调用
与节点类似,routingFunction
接受图的当前 状态
并返回值。
默认情况下,返回值 routingFunction
用作要将状态发送到的下一个节点的名称(或节点数组)。所有这些节点将在下一个超级步骤中并行运行。
您可以选择提供一个对象,该对象将 routingFunction
的输出映射到下一个节点的名称。
提示
如果您想在单个函数中组合状态更新和路由,请使用 Command
而不是条件边。
入口点¶
入口点是在图启动时运行的第一个节点。您可以使用虚拟 START
节点的 addEdge
方法到要执行的第一个节点,以指定在哪里进入图。
条件入口点¶
条件入口点允许您根据自定义逻辑从不同的节点开始。您可以使用虚拟 START
节点的 addConditionalEdges
来完成此操作。
您可以选择提供一个对象,该对象将 routingFunction
的输出映射到下一个节点的名称。
发送
¶
默认情况下,节点
和 边
是预先定义的,并且在同一共享状态下运行。但是,在某些情况下,确切的边是未知的,并且/或者您可能希望同时存在不同版本的 状态
。这种情况的一个常见示例是使用 map-reduce
设计模式。在此设计模式中,第一个节点可能会生成对象数组,您可能希望将其他节点应用于所有这些对象。对象的数量可能是预先未知的(意味着边的数量可能是未知的),并且下游 节点
的输入 状态
应该是不同的(每个生成的对象一个)。
为了支持这种设计模式,LangGraph 支持从条件边返回 Send
对象。Send
接受两个参数:第一个是节点的名称,第二个是要传递给该节点的状态。
const continueToJokes = (state: { subjects: string[] }) => {
return state.subjects.map(
(subject) => new Send("generate_joke", { subject })
);
};
graph.addConditionalEdges("nodeA", continueToJokes);
命令
¶
提示
此功能需要 @langchain/langgraph>=0.2.31
。
将控制流(边)和状态更新(节点)结合起来可能很方便。例如,您可能想在同一个节点中同时执行状态更新和决定接下来转到哪个节点,而不是使用条件边。LangGraph 提供了一种通过从节点函数返回 Command
对象来实现此目的的方法
import { StateGraph, Annotation, Command } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
foo: Annotation<string>,
});
const myNode = (state: typeof StateAnnotation.State) => {
return new Command({
// state update
update: {
foo: "bar",
},
// control flow
goto: "myOtherNode",
});
};
使用 Command
,您还可以实现动态控制流行为(与 条件边 相同)
const myNode = async (state: typeof StateAnnotation.State) => {
if (state.foo === "bar") {
return new Command({
update: {
foo: "baz",
},
goto: "myOtherNode",
});
}
// ...
};
重要提示
在节点函数中返回 Command
时,您还必须添加一个 ends
参数,其中包含节点路由到的节点名称列表,例如 .addNode("myNode", myNode, { ends: ["myOtherNode"] })
。这对于图编译和验证是必要的,并表明 myNode
可以导航到 myOtherNode
。
查看此 操作指南,了解如何使用 Command
的端到端示例。
何时应该使用命令而不是条件边?¶
当您需要同时更新图状态和路由到不同节点时,请使用 Command
。例如,在实现 多代理切换 时,路由到不同的代理并将一些信息传递给该代理非常重要。
使用 条件边 在节点之间有条件地路由,而无需更新状态。
导航到父图中的节点¶
如果您正在使用 子图,您可能想从子图中的节点导航到不同的子图(即父图中的不同节点)。为此,您可以在 Command
中指定 graph: Command.PARENT
const myNode = (state: typeof StateAnnotation.State) => {
return new Command({
update: { foo: "bar" },
goto: "other_subgraph", // where `other_subgraph` is a node in the parent graph
graph: Command.PARENT,
});
};
注意
将 graph
设置为 Command.PARENT
将导航到最近的父图。
这在实现 多代理切换 时特别有用。
在工具内部使用¶
一个常见的用例是从工具内部更新图状态。例如,在客户支持应用程序中,您可能希望在对话开始时根据客户的帐号或 ID 查找客户信息。要从工具更新图状态,您可以从工具返回 Command({ update: { my_custom_key: "foo", messages: [...] } })
import { tool } from "@langchain/core/tools";
const lookupUserInfo = tool(async (input, config) => {
const userInfo = getUserInfo(config);
return new Command({
// update state keys
update: {
user_info: userInfo,
messages: [
new ToolMessage({
content: "Successfully looked up user information",
tool_call_id: config.toolCall.id,
}),
],
},
});
}, {
name: "lookup_user_info",
description: "Use this to look up user information to better assist them with their questions.",
schema: z.object(...)
});
重要提示
当从工具返回 Command
时,您必须在 Command.update
中包含 messages
(或用于消息历史记录的任何状态键),并且 messages
中的消息列表必须包含 ToolMessage
。这对于使结果消息历史记录有效是必要的(LLM 提供商要求 AI 消息与工具调用一起使用,后跟工具结果消息)。
如果您正在使用通过 Command
更新状态的工具,我们建议使用预构建的 ToolNode
,它可以自动处理返回 Command
对象的工具并将它们传播到图状态。如果您正在编写调用工具的自定义节点,则需要手动传播工具返回的 Command
对象作为来自节点的更新。
人机环路¶
Command
是人机环路工作流的重要组成部分:当使用 interrupt()
收集用户输入时,Command
然后用于提供输入并通过 new Command({ resume: "User input" })
恢复执行。查看 此概念指南 以获取更多信息。
持久化¶
LangGraph 使用 检查点为您的代理状态提供内置持久性。检查点在每个超级步骤保存图状态的快照,从而允许随时恢复。这实现了诸如人机环路交互、内存管理和容错等功能。您甚至可以使用适当的 get
和 update
方法在图执行后直接操作图的状态。有关更多详细信息,请参阅 概念指南 以获取更多信息。
线程¶
LangGraph 中的线程表示您的图与用户之间的各个会话或对话。当使用检查点时,单个对话中的轮次(甚至单个图执行中的步骤)由唯一的线程 ID 组织。
存储¶
LangGraph 通过 BaseStore 接口提供内置文档存储。与按线程 ID 保存状态的检查点不同,存储使用自定义命名空间来组织数据。这实现了跨线程持久性,允许代理维护长期记忆,从过去的交互中学习,并随着时间的推移积累知识。常见的用例包括存储用户个人资料、构建知识库以及管理所有线程的全局首选项。
图迁移¶
即使在使用检查点来跟踪状态时,LangGraph 也可以轻松处理图定义(节点、边和状态)的迁移。
- 对于图末尾的线程(即未中断),您可以更改图的整个拓扑结构(即所有节点和边,删除、添加、重命名等)
- 对于当前中断的线程,我们支持除重命名/删除节点之外的所有拓扑更改(因为该线程现在可能即将进入不再存在的节点) - 如果这是一个阻碍因素,请联系我们,我们可以优先解决该问题。
- 对于修改状态,我们具有添加和删除键的完全向后和向前兼容性
- 重命名的状态键会在现有线程中丢失其保存的状态
- 类型以不兼容方式更改的状态键当前可能会在具有更改前状态的线程中引起问题 - 如果这是一个阻碍因素,请联系我们,我们可以优先解决该问题。
配置¶
创建图时,您还可以标记图的某些部分是可配置的。这通常用于轻松地在模型或系统提示之间切换。这允许您创建单个“认知架构”(图),但拥有它的多个不同实例。
然后,您可以使用 configurable
config 字段将此配置传递到图中。
然后,您可以在节点内部访问和使用此配置
const nodeA = (state, config) => {
const llmType = config?.configurable?.llm;
let llm: BaseChatModel;
if (llmType) {
const llm = getLlm(llmType);
}
...
};
有关配置的完整细分,请参阅 本指南
递归限制¶
递归限制设置单个执行期间图可以执行的最大 超级步骤 数。一旦达到限制,LangGraph 将引发 GraphRecursionError
。默认情况下,此值设置为 25 步。递归限制可以在运行时在任何图上设置,并通过 config 字典传递给 .invoke
/.stream
。重要的是,recursionLimit
是一个独立的 config
键,不应像所有其他用户定义的配置一样在 configurable
键内传递。请参阅以下示例
阅读 此操作指南,以了解有关递归限制如何工作的更多信息。
interrupt
¶
使用 interrupt 函数在特定点暂停图以收集用户输入。interrupt
函数将中断信息传递到客户端,允许开发人员收集用户输入、验证图状态或在恢复执行之前做出决策。
import { interrupt } from "@langchain/langgraph";
const humanApprovalNode = (state: typeof StateAnnotation.State) => {
...
const answer = interrupt(
// This value will be sent to the client.
// It can be any JSON serializable value.
{ question: "is it ok to continue?"},
);
...
恢复图是通过将 Command
对象传递给图,并将 resume
键设置为 interrupt
函数返回的值来完成的。
在 人机环路概念指南中阅读有关如何将 interrupt
用于人机环路工作流的更多信息。
注意: interrupt
函数目前在 Web 环境中不可用。
断点¶
断点在特定点暂停图执行,并允许逐步执行。断点由 LangGraph 的 持久化层 提供支持,该持久化层在每个图步骤之后保存状态。断点还可以用于启用 人机环路 工作流,尽管我们建议为此目的使用 interrupt
函数。
在 断点概念指南中阅读有关断点的更多信息。
子图¶
子图是 图,它用作另一个图中的 节点。这只不过是将应用于 LangGraph 的古老封装概念。使用子图的一些原因是
- 构建 多代理系统
- 当您想在多个图中重用一组节点时,这些图可能共享一些状态,您可以在子图中定义它们一次,然后在多个父图中使用它们
- 当您希望不同的团队独立处理图的不同部分时,您可以将每个部分定义为子图,并且只要子图接口(输入和输出模式)得到遵守,就可以构建父图而无需了解子图的任何详细信息
有两种方法可以将子图添加到父图
- 添加具有编译子图的节点:当父图和子图共享状态键,并且您不需要在输入或输出时转换状态时,这非常有用
- 添加一个使用函数调用子图的节点:当父图和子图具有不同的状态模式,并且你需要在调用子图之前或之后转换状态时,这非常有用
const subgraph = subgraphBuilder.compile();
const callSubgraph = async (state: typeof StateAnnotation.State) => {
return subgraph.invoke({ subgraph_key: state.parent_key });
};
builder.addNode("subgraph", callSubgraph);
让我们看看每个示例。
作为编译图¶
创建子图节点最简单的方法是直接使用编译后的子图。这样做时,重要的是父图和子图的状态模式至少共享一个键,以便它们可以使用该键进行通信。如果你的图和子图不共享任何键,则应使用编写一个调用子图的函数来代替。
注意
如果你将额外的键传递给子图节点(即,除了共享键之外的键),它们将被子图节点忽略。 同样地,如果你从子图返回额外的键,它们将被父图忽略。
import { StateGraph, Annotation } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
foo: Annotation<string>,
});
const SubgraphStateAnnotation = Annotation.Root({
foo: Annotation<string>, // note that this key is shared with the parent graph state
bar: Annotation<string>,
});
// Define subgraph
const subgraphNode = async (state: typeof SubgraphStateAnnotation.State) => {
// note that this subgraph node can communicate with
// the parent graph via the shared "foo" key
return { foo: state.foo + "bar" };
};
const subgraph = new StateGraph(SubgraphStateAnnotation)
.addNode("subgraph", subgraphNode);
...
.compile();
// Define parent graph
const parentGraph = new StateGraph(StateAnnotation)
.addNode("subgraph", subgraph)
...
.compile();
作为函数¶
你可能想要定义一个具有完全不同模式的子图。在这种情况下,你可以创建一个调用子图的节点函数。此函数将需要转换输入(父)状态为子图状态,然后在调用子图之前,并将结果转换回父状态,然后再从节点返回状态更新。
import { StateGraph, Annotation } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
foo: Annotation<string>,
});
const SubgraphStateAnnotation = Annotation.Root({
// note that none of these keys are shared with the parent graph state
bar: Annotation<string>,
baz: Annotation<string>,
});
// Define subgraph
const subgraphNode = async (state: typeof SubgraphStateAnnotation.State) => {
return { bar: state.bar + "baz" };
};
const subgraph = new StateGraph(SubgraphStateAnnotation)
.addNode("subgraph", subgraphNode);
...
.compile();
// Define parent graph
const subgraphWrapperNode = async (state: typeof StateAnnotation.State) => {
// transform the state to the subgraph state
const response = await subgraph.invoke({
bar: state.foo,
});
// transform response back to the parent state
return {
foo: response.bar,
};
}
const parentGraph = new StateGraph(StateAnnotation)
.addNode("subgraph", subgraphWrapperNode)
...
.compile();
可视化¶
能够可视化图通常很好,尤其是在图变得更复杂时。LangGraph 配备了一种很好的内置方法,可以将图渲染为 Mermaid 图表。你可以像这样使用 getGraph()
方法
const representation = graph.getGraph();
const image = await representation.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);
你还可以查看 LangGraph Studio,这是一个定制的 IDE,包含强大的可视化和调试功能。
流式处理¶
LangGraph 构建时首先考虑了对流式处理的支持。LangGraph 支持几种不同的流式处理模式
"values"
:这会在图的每个步骤之后流式传输状态的完整值。"updates
:这会在图的每个步骤之后流式传输状态的更新。如果在同一步骤中进行了多次更新(例如,运行了多个节点),则这些更新将分别进行流式传输。
此外,你可以使用 streamEvents
方法来流式传输节点内部发生的事件。这对于流式传输 LLM 调用的令牌非常有用。
LangGraph 构建时首先考虑了对流式处理的支持,包括在执行期间从图节点流式传输更新、从 LLM 调用流式传输令牌等等。有关更多信息,请参阅此概念指南。