
LangGraph.js 概念介绍
在图中使用共享状态存在一些设计上的权衡。例如,您可能会觉得这就像使用可怕的全局变量(尽管可以通过命名空间参数来缓解)。然而,共享类型化状态在构建 AI 工作流方面提供了许多好处,包括:每个“超级步骤”前后,数据流都可以完全检查。状态是可变的,允许用户或其他软件在超级步骤之间写入相同的状态以控制代理的方向(使用 updateState)。检查点定义明确,便于在任何存储后端中保存和恢复,甚至对整个工作
在图中使用共享状态存在一些设计上的权衡。例如,您可能会觉得这就像使用可怕的全局变量(尽管可以通过命名空间参数来缓解)。然而,共享类型化状态在构建 AI 工作流方面提供了许多好处,包括:
-
每个“超级步骤”前后,数据流都可以完全检查。
-
状态是可变的,允许用户或其他软件在超级步骤之间写入相同的状态以控制代理的方向(使用 updateState)。
-
检查点定义明确,便于在任何存储后端中保存和恢复,甚至对整个工作流执行进行完全的版本控制。
我们将在下一节更详细地讨论检查点。
持久性
任何“智能”系统都需要内存才能运行。AI 代理也不例外,并且需要跨越一个或多个时间框架的内存:
-
它们总是需要记住在这个任务中采取的步骤(以避免在回答给定查询时重复)。
-
它们经常需要记住与用户的前几轮多轮对话(用于共指解析和附加上下文)。
-
理想情况下,它们“记住”与用户之前的交互以及在给定“环境”(例如应用程序上下文)中的行为,以表现得更个性化和高效。
后一种形式的内存涵盖很多内容(个性化、优化、持续学习等),这超出了本文的范围,不过它可以轻松集成到任何 LangGraph.js 工作流中,并且我们正在积极探索以原生方式公开此功能的最佳方法。
StateGraph API 通过检查点原生支持前两种形式的内存。
检查点
检查点代表用户(或用户组或其他系统)之间(可能)的多轮交互中的线程状态。在单个运行中创建的检查点在从该状态恢复时将具有一组要执行的下一个节点。在给定运行结束时创建的检查点相同,只是没有要转换的下一个节点(图正在等待用户输入)。
检查点支持聊天记忆等功能,允许您标记并持久化系统采取的每个状态,无论是在单个运行中还是在多轮交互中。让我们探讨一下为什么这很有用:
单次运行内存
在给定的运行中,在每个步骤都创建检查点。这意味着您可以要求您的代理创造世界和平。当它失败并遇到错误时,您总是可以从保存的检查点恢复其任务。
这也允许您构建人类在环工作流,这在客户支持机器人、编程助手和其他应用程序中很常见。您可以在执行给定节点之前或之后中断图的执行,并将控制权“升级”给用户或支持人员。工作人员可能会立即响应,也可能会在一个月后响应。无论如何,您的工作流可以随时恢复,就好像没有时间过去一样。
多轮记忆
检查点在 thread_id 下保存,以支持用户和系统之间的多轮交互。对于开发人员,在配置图以添加多轮记忆支持时没有区别,因为检查点的工作方式始终相同。
如果您有一些希望在轮次之间保留的状态和一些希望视为“临时”的状态,则可以在图的最终节点中清除相关状态。
使用检查点就像调用 compile({ checkpointer: myCheckpointer }) 一样简单,然后在其可配置参数中使用 thread_id 调用它。您可以在下一节中看到更多!
配置
对于任何给定的图部署,您可能希望在运行时控制一些可配置的值。这些值与图输入不同,因为它们不应被视为状态变量。它们更像是“带外”通信。
一个常见的例子是对话的 thread_id、user_id、要使用的 LLM、检索器中要返回的文档数量等。虽然这些可以在状态中传递,但最好将它们与常规数据流分开。
示例
让我们回顾另一个示例,看看我们的多轮记忆是如何工作的!您能猜出运行此图的结果和 result2 会是什么吗?
配置
import { END, MemorySaver, START, StateGraph } from "@langchain/langgraph"; interface State { total: number; turn?: string; } function addF(existing: number, updated?: number) { return existing + (updated?? 0); } const builder = new StateGraph<State>({ channels: { total: { value: addF, default: () => 0, }, }, }) .addNode("add_one", (_state) => ({ total: 1 })) .addEdge(START, "add_one") .addEdge("add_one", END); const memory = new MemorySaver(); const graphG = builder.compile({ checkpointer: memory }); let threadId = "some-thread"; let config = { Configurable: { thread_id: threadId } }; const result = await graphG.invoke({ total: 1, turn: "First Turn" }, config); const result2 = await graphG.invoke({ turn: "Next Turn" }, config); const result3 = await graphG.invoke({ total: 5 }, config); const result4 = await graphG.invoke( { total: 5 }, { Configurable: { thread_id: "new-thread-id" } } ); console.log(result); // { total: 2, turn: 'First Turn' } console.log(result2); // { total: 3, turn: 'Next Turn' } console.log(result3); // { total: 9, turn: 'Next Turn' } console.log(result4); // { total: 6 }
对于第一次运行,未找到检查点,因此图基于原始输入运行。总值从 1 增加到 2,并且轮次设置为“First Turn”。
对于第二次运行,用户提供了“轮次”的更新,但没有提供总值!由于我们从状态加载,上一个结果增加 1(在我们的“add_one”节点中),并且“轮次”被用户覆盖。
对于第三次运行,“轮次”保持不变,因为它是从检查点加载的,但未被用户覆盖。总值由用户提供的值递增,因为它使用 add 函数更新现有值。
对于第四次运行,我们使用新的线程 ID,未找到检查点,因此结果只是用户提供的总值加 1。
您可能会注意到,这种面向用户的行为等同于在没有检查点的情况下运行以下命令。
配置
const graphB = builder.compile(); const resultB1 = await graphB.invoke({ total: 1, turn: "First Turn" }); const resultB2 = await graphB.invoke({...result, turn: "Next Turn" }); const resultB3 = await graphB.invoke({...result2, total: result2.total + 5 }); const resultB4 = await graphB.invoke({ total: 5 }); console.log(resultB1); // { total: 2, turn: 'First Turn' } console.log(resultB2); // { total: 3, turn: 'Next Turn' } console.log(resultB3); // { total: 9, turn: 'Next Turn' } console.log(resultB4); // { total: 6 }
自己运行它以确认等效性。用户输入和检查点加载被视为任何其他状态更新。
既然我们已经介绍了 LangGraph.js 的核心概念,通过一个端到端的示例来了解所有部分如何组合在一起可能会更有帮助。
StateGraph 单次执行数据流
作为工程师,在不知道“幕后”发生了什么之前,我们永远不会满意。在前面的部分中,我们解释了 LangGraph.js 的一些核心概念。现在是时候展示它们是如何组合在一起的了。
让我们用一个条件边扩展我们的玩具示例,并遍历两次连续的调用。
数据流
import { START, END, StateGraph, MemorySaver } from "@langchain/langgraph"; interface State { total: number; } function addG(existing: number, updated?: number) { return existing + (updated?? 0); } const builderH = new StateGraph<State>({ channels: { total: { value: addG, default: () => 0, }, }, }) .addNode("add_one", (_state) => ({ total: 1 })) .addNode("double", (state) => ({ total: state.total })) .addEdge(START, "add_one"); function route(state: State) { if (state.total < 6) { return "double"; } return END; } builderH.addConditionalEdges("add_one", route); builderH.addEdge("double", "add_one"); const memoryH = new MemorySaver(); const graphH = builderH.compile({ checkpointer: memoryH }); const threadId = "some-thread"; const config = { Configurable: { thread_id: threadId } }; for await (const step of await graphH.stream( { total: 1 }, {...config, streamMode: "values" } )) { console.log(step); } // 0 checkpoint { total: 1 } // 1 task null // 1 task_result null // 1 checkpoint { total: 2 } // 2 task null // 2 task_result null // 2 checkpoint { total: 4 } // 3 task null // 3 task_result null // 3 checkpoint { total: 5 } // 4 task null // 4 task_result null // 4 checkpoint { total: 10 } // 5 task null // 5 task_result null // 5 checkpoint { total: 11 }
要跟踪此运行,请查看 LangSmith 链接。我们将逐步解释执行过程:
-
首先,图查找检查点。未找到,因此状态以总值为 0 初始化。
-
接下来,图将用户输入作为状态更新应用。缩减器将输入(1)添加到现有值(0)。在此超级步骤结束时,总值为(1)。
-
之后,调用“add_one”节点,返回 1。
-
接下来,缩减器将此更新应用于现有总值(1)。状态现在为 2。
-
然后,调用条件边“route”。由于值小于 6,我们继续到“double”节点。
-
“double”节点获取现有状态(2)并返回。然后调用缩减器将其添加到现有状态。状态现在为 4。
-
图然后循环回到“add_one”(5),检查条件边,并由于小于 6 继续。加倍后,总值为(10)。
-
固定边循环回到“add_one”(11),检查条件边,由于大于 6,程序终止。
对于第二次运行,我们将使用相同的配置:
数据流
const resultH2 = await graphH.invoke({ total: -2, turn: "First Turn" }, config); console.log(resultH2); // { total: 10 }
要跟踪此运行,请查看 LangSmith 链接。我们将逐步解释执行过程:
-
首先,图查找检查点。将其加载到内存中作为初始状态。总值为之前的(11)。
-
接下来,它应用用户输入更新。add 缩减器将总值从 11 更新为 -9。
-
之后,调用“add_one”节点,返回 1。
-
使用缩减器应用此更新,将值提高到 10。
-
接下来,触发“route”条件边。由于值大于 6,我们终止程序,结束时为(11)。
结论
就是这样!我们已经探索了 LangGraph.js 的核心概念,并了解了如何使用它来创建可靠、容错的代理系统。通过将代理建模为状态机,LangGraph.js 为组合可扩展和可控的 AI 工作流提供了强大的抽象。
使用 LangGraph.js 时,请记住这些关键思想:
-
节点执行工作;边决定控制流。
-
缩减器精确定义了在每个步骤如何应用状态更新。
-
检查点支持单次运行和多轮交互中的内存。
-
中断允许您为人类在环工作流暂停、检索和更新图的状态。
-
可配置参数允许独立于常规数据流的运行时控制。
凭借这些原则,您可以充分利用 LangGraph.js 的强大功能来构建高级 AI 代理系统。
更多推荐
所有评论(0)