LangGraph

LangGraph

7 分钟阅读

TL;DR: LangGraph 是构建于 LangChain 之上的模块,旨在更好地创建循环图,这通常是代理运行时所需要的。

简介

在我们的 LangChain v0.1 公告中,我们重点介绍了一个新的库:LangGraph。LangGraph 构建于 LangChain 之上,并与 LangChain 生态系统完全互操作。它主要通过引入一种简单的方法来创建循环图来增加新的价值。这在创建代理运行时时通常很有用。

在这篇博文中,我们将首先探讨 LangGraph 的动机。然后,我们将介绍它提供的基本功能。接着,我们将重点介绍我们已经实现的两个代理运行时。然后,我们将强调一些我们听到的关于这些运行时的常见修改请求,并提供实现这些修改的示例。最后,我们将预览我们接下来将发布的内容。

动机

LangChain 的一大价值主张是能够轻松创建自定义链。我们为此投入了大量的功能,即 LangChain 表达式语言。然而,到目前为止,我们仍然缺乏一种简单的方法将循环引入这些链中。实际上,这些链是有向无环图 (DAGs) —— 大多数 数据编排框架 也是如此。

当我们看到人们创建更复杂的 LLM 应用程序时,常见的模式之一是将循环引入运行时。这些循环通常使用 LLM 来推理下一步在循环中做什么。LLM 的一个重大突破是能够将它们用于这些推理任务。这基本上可以被认为是像在 for 循环中运行 LLM。这些类型的系统通常被称为代理。

当考虑典型的 检索增强生成 (RAG) 应用程序 时,可以找到这种代理行为如此强大的一个例子。在典型的 RAG 应用程序中,会调用检索器以返回一些文档。然后将这些文档传递给 LLM 以生成最终答案。虽然这通常是有效的,但在第一次检索步骤未能返回任何有用的结果时,它就会失效。在这种情况下,如果 LLM 可以推理出从检索器返回的结果很差,并可能向检索器发出第二次(更精细的)查询,并使用这些结果,这通常是理想的。本质上,在循环中运行 LLM 有助于创建更灵活的应用程序,从而可以完成更多可能未预定义的模糊用例。

这些类型的应用程序通常被称为代理。其中最简单——但同时也是最雄心勃勃——的形式是一个本质上包含两个步骤的循环

  1. 调用 LLM 以确定 (a) 要采取哪些操作,或 (b) 要给用户什么响应
  2. 采取给定的操作,然后传回步骤 1

重复这些步骤,直到生成最终响应。这基本上是驱动我们核心 AgentExecutor 的循环,也是导致像 AutoGPT 这样的项目声名鹊起的相同逻辑。这很简单,因为它是一个相对简单的循环。它是最雄心勃勃的,因为它几乎将所有的决策和推理能力都卸载到 LLM。

在我们与社区和公司合作将代理投入生产的过程中,我们发现在实践中,通常需要更多的控制。您可能希望始终强制代理首先调用特定的工具。您可能希望对工具的调用方式有更多的控制。您可能希望根据代理所处的状态,为代理使用不同的提示。

当谈到这些更受控制的流程时,我们在内部将它们称为“状态机”。请参阅我们关于认知架构的博客中的下图。

这些状态机具有循环的能力——允许处理比简单链更模糊的输入。然而,在如何构建该循环方面,仍然存在人为指导的因素。

LangGraph 是一种通过将状态机指定为图来创建这些状态机的方法。

功能

LangGraph 的核心是在 LangChain 之上公开了一个相当狭窄的接口。

StateGraph

StateGraph 是表示图的类。您通过传入 state 定义来初始化此类。此状态定义表示随时间更新的中心状态对象。此状态由图中的节点更新,这些节点返回对该状态属性的操作(以键值对存储的形式)。

可以通过两种方式更新此状态的属性。首先,可以完全覆盖属性。如果您希望节点返回属性的新值,这将非常有用。其次,可以通过添加到其值来更新属性。如果属性是已采取操作的列表(或类似内容),并且您希望节点返回新采取的操作(并让这些操作自动添加到属性中),这将非常有用。

您可以在创建初始状态定义时指定是应覆盖属性还是添加到属性。请参见下面的伪代码示例。

from langgraph.graph import StateGraph
from typing import TypedDict, List, Annotated
import Operator


class State(TypedDict):
    input: str
    all_actions: Annotated[List[str], operator.add]


graph = StateGraph(State)

节点

创建 StateGraph 后,您可以使用 graph.add_node(name, value) 语法添加节点。name 参数应该是一个字符串,我们将在添加边时使用它来引用节点。value 参数应该是函数或 LCEL runnable,将被调用。此函数/LCEL 应该接受与 State 对象相同形式的字典作为输入,并输出一个字典,其中包含要更新的 State 对象的键。

请参见下面的伪代码示例。

graph.add_node("model", model)
graph.add_node("tools", tool_executor)

还有一个特殊的 END 节点,用于表示图的结尾。重要的是,您的循环能够最终结束!

from langgraph.graph import END

添加节点后,您可以添加边来创建图。有几种类型的边。

起始边

这是将图的起始点连接到特定节点的边。这将使该节点成为将输入传递到图时第一个被调用的节点。伪代码如下:

graph.set_entry_point("model")

普通边

这些边是指一个节点应始终在另一个节点之后被调用的边。例如,在基本代理运行时中,我们总是希望在调用工具后调用模型。

graph.add_edge("tools", "model")

条件边

这些边是指使用函数(通常由 LLM 驱动)来确定首先要转到哪个节点的边。要创建此边,您需要传入三件事

  1. 上游节点:将查看此节点的输出以确定下一步做什么
  2. 函数:将调用此函数以确定下一步调用哪个节点。它应该返回一个字符串
  3. 映射:此映射将用于将 (2) 中函数的输出映射到另一个节点。键应该是 (2) 中的函数可能返回的可能值。值应该是如果返回该值则要转到的节点的名称。

例如,在调用模型后,我们可以退出图并返回给用户,或者我们可以调用工具——这取决于用户决定!请参见下面的伪代码示例

graph.add_conditional_edge(
    "model",
    should_continue,
    {
        "end": END,
        "continue": "tools"
    }
)

编译

在我们定义了图之后,我们可以将其编译成 runnable!这只需获取我们目前创建的图定义并返回一个 runnable。此 runnable 公开了与 LangChain runnables 相同的所有方法(.invoke.stream.astream_log 等),使其可以像链一样被调用。

app = graph.compile()

Agent Executor

我们已经使用 LangGraph 重建了规范的 LangChain AgentExecutor。这将允许您使用现有的 LangChain 代理,但允许您更轻松地修改 AgentExecutor 的内部结构。默认情况下,此图的状态包含如果您使用过 LangChain 代理,您应该熟悉的概念:inputchat_historyintermediate_steps(以及 agent_outcome 来表示最近的代理结果)

from typing import TypedDict, Annotated, List, Union
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.messages import BaseMessage
import operator


class AgentState(TypedDict):
   input: str
   chat_history: list[BaseMessage]
   agent_outcome: Union[AgentAction, AgentFinish, None]
   intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]

请参阅 此 notebook 了解如何开始

Chat Agent Executor

我们看到的一个常见趋势是,越来越多的模型是“聊天”模型,这些模型对消息列表进行操作。这些模型通常配备了诸如函数调用之类的功能,这使得类似代理的体验更加可行。当使用这些类型的模型时,将代理的状态表示为消息列表通常是很直观的。

因此,我们创建了一个与此状态一起工作的代理运行时。输入是消息列表,节点只是随着时间的推移简单地添加到此消息列表中。

from typing import TypedDict, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage


class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]

请参阅 此 notebook 了解如何开始

修改

LangGraph 的一大优势在于,它以一种更自然和可修改的方式公开了 AgentExecutor 的逻辑。我们提供了一些我们听到的请求的修改示例

强制调用工具

用于当您始终希望代理首先调用工具时。适用于 Agent ExecutorChat Agent Executor

人机环路

如何在调用工具之前添加人机环路步骤。适用于 Agent ExecutorChat Agent Executor

管理代理步骤

用于添加关于如何处理代理可能采取的中间步骤的自定义逻辑(当步骤很多时很有用)。适用于 Agent ExecutorChat Agent Executor

以特定格式返回输出

如何使代理使用函数调用以特定格式返回输出。仅适用于 Chat Agent Executor

动态直接返回工具的输出

有时您可能希望直接返回工具的输出。我们在 LangChain 中提供了一种使用 return_direct 参数轻松实现此目的的方法。但是,这使得工具的输出始终直接返回。有时,您可能希望让 LLM 选择是直接返回响应还是不返回。仅适用于 Chat Agent Executor

未来工作

我们对 LangGraph 能够实现更自定义和更强大的代理运行时感到非常兴奋。我们希望在不久的将来实现的一些功能包括

  • 来自学术界的更高级代理运行时 (LLM Compiler, plan-and-solve, 等)
  • 有状态工具(允许工具修改某些状态)
  • 更受控制的人机环路工作流程
  • 多代理工作流程

如果其中任何一项与您产生共鸣,请随时在 LangGraph 仓库中添加示例 notebook,或通过 hello@langchain.dev 联系我们进行更深入的合作!