LangGraph

LangGraph

7 分钟阅读

TL;DR: LangGraph 是在 LangChain 之上构建的一个模块,可以更好地创建循环图,这对于代理运行时通常是必需的。

引言

在我们 LangChain v0.1 发布公告中,我们强调了引入一个新库:LangGraph。LangGraph 构建在 LangChain 之上,并且与 LangChain 生态系统完全兼容。它主要通过引入一种创建循环图的简便方法来增加新的价值。这在创建代理运行时时非常有用。

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

动机

LangChain 的一个主要价值主张是能够轻松创建自定义链。我们已经通过 LangChain Expression Language 大力投入了这方面的功能。然而,到目前为止,我们还没有一种简便的方法将循环引入这些链中。实际上,这些链是有向无环图(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 定义来初始化这个类。这个 state 定义代表了一个随着时间推移而更新的中央 state 对象。这个 state 由图中的节点更新,这些节点返回操作以更新 state 的属性(以键值对的形式)。

可以通过两种方式更新 state 的属性。首先,一个属性可以被完全覆盖。如果您希望节点返回属性的新值,这很有用。第二,可以通过添加值来更新一个属性。如果一个属性是已执行操作的列表(或类似内容),并且您希望节点返回新执行的操作(并自动添加到该属性),则此方法很有用。

在创建初始 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]

请参阅 此笔记本了解如何开始。

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]

请参阅 此笔记本了解如何开始。

修改

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 Compilerplan-and-solve 等)
  • 有状态工具(允许工具修改某些状态)
  • 更受控的人工干预工作流
  • 多代理工作流

如果您觉得这些内容中有任何引起您的共鸣,请随时在 LangGraph 仓库中添加一个示例笔记本,或者通过 hello@langchain.dev 联系我们以获得更深入的合作!

© . This site is unofficial and not affiliated with LangChain, Inc.