彻底搞懂LangGraph:构建强大的Multi-Agent多智能体应用的LangChain新利器 【1】

技术

点击上方蓝字关注我们

picture.image

上个月LangChain刚刚发布了正式的0.1稳定版本(没错,是0.1而不是1.0),在版本公告里面首当其冲宣布的最重要更新,是在这个版本里面引入了一个最新库 - LangGraph。 这是一个面向当前LLM开发领域最火热的AI Agent开发与控制的开发库,也是LangChain试图用来 弥补其在Agent开发、特别是复杂的多Agent系统定制方面的不足 的重大尝试,相信也会成为LangChain在2024升级更新的重点领域!

picture.image

我们会用一系列文章深入LangGraph,结合官方例子介绍与剖析其在几个重点Agent方向的应用。

  • LangGraph诞生的动力及设计思想
  • LangGraph应用:增强的RAG应用
  • LangGraph应用:自修复代码助手
  • LangGraph应用:Multi-Agent系统
  • LangGraph应用:构建Web Agents

由于官方文档较为晦涩,加上LangChain一贯的“重量级”风格。为了更好地帮助深入浅出的理解LangGraph,并照顾到没有LangChain基础的朋友,我们首先来了解一些“预备知识”。

PART 01

picture.image

picture.image

预备知识

【LangChain中的链与LCEL】

Chain(链) 是LangChain中最核心的概念之一(看名字就知道)。简单的说,就是把自然语言输入、关联知识检索、Prompt组装、可用Tools信息、大模型调用、输出格式化等这些LLM 应用中的常见动作,组装成一个可以运行的“链”式过程。链可以直接调用,也可以用来进一步构建更强大的Agent。

LCEL即LangChain Express Language, 即LangChain表达语言。这是LangChain提供的一种简洁的、用于组装上述“链”的声明性方式。

我们看一个官方使用LCEL“组装”Chain的例子就明白:


          
prompt = ChatPromptTemplate.from_template("讲一个关于 {topic} 的笑话")model = ChatOpenAI(model="gpt-4")output_parser = StrOutputParser()chain = prompt | model | output_parser
          
#调用chainchain.invoke({"topic": "冰淇淋"})
      

这个官方的例子中,把提示(prompt)、大模型(model)、输出解析(output_parser)几个组件使用管道符号“|”链接在一起,上个组件的输出作为下一个组件的输入,一起形成了一个链。

对于最常见的RAG应用来说,使用LCEL也无非是在此之上增加一个检索相关文档的动作,类似:


        
            

          chain = setup\_and\_retrieval | prompt | model | output\_parser
        
      

这里很清晰地看到一个简单的RAG应用处理过程:检索关联文档 => 组装Prompt => 调用大模型 => 输出处理。

最后总结一下: LCEL就是LangChain提供用来组装Chain的一种简单表示方式。用这种方式组装链,可以自动获得诸如批量、流输出、并行、异步等一系列能力;而且链可以进一步通过LCEL组装成更复杂的链与Agent。

【LCEL构建与调度Agent】

那么如何用LCEL来创建一个AI Agent并调度运行呢?以最常见的React(推理&行动)范式的Agent来说,相对于Chain需要扩展的能力有:

  • 增加工具使用能力 。这体现在Prompt中需要注入可用工具信息,并能自动调用工具获得结果。
  • 增加“循环”能力。 Agent的运行通常需要多次Reason(推理)-Act(行动)的反复与循环,直到完成任务。

以LCEL来组装并创建运行一个Agent的简单过程如下:


        
            

          """  
定义Agent需要使用的Tools  
"""  
@tool  
def search(query: str) -> str:  
  """此处省略"""  
  
"""  
LCEL创建一个Agent,与Chain类似  
"""  
agent = (  
    {input:{输入信息}, agent\_scratchpad:{中间步骤}}  
    | prompt  
    | model  
    | AgentOutputParser()  
)  
  
"""  
注意:Agent需要使用agent\_executor调用,以增加上述两个能力  
"""  
agent\_executor = AgentExecutor(agent=agent, tools=tool\_list, verbose=True)  
agent\_executor.invoke({"input": "whats the weather in New york?"})
        
      

注意到,相对于Chain.invoke()直接运行,这里的 Agent_executor的作用就是为了能够实现多次循环ReAct的动作,以最终完成任务。

【什么是图(Graph)】

图是计算机科学中的一种数据结构。大部分人可能都接触过一些基本的数据结构,比如队列(Queue)、堆栈(Stack)、链表(List)或者树(Tree)等,图(Graph)也是其中的一种相对复杂的数据结构。我们无意在此普及图的数据结构知识,你只需要了解的图的几个基本知识:

  • 图是表示多个元素及其之间关系的一种结构 。其特点是,任何两个元素之间都可以直接发生联系,所以适合表达更复杂的元素关系。
  • 图的基本表示就是N个元素(节点/顶点)及这些元素之间的关系(边)的集合。

picture.image

  • 有向无环图(Directed Acyclic Graph,DAG) :有向指的是图中的“边”有方向;无环指的是无法从某个节点经过若干“边”返回这个节点。

其 他的一些图 的理论,包括不同类型图的存储结构、相关算法等 ,这里对理解LangGraph无关紧要。

PART 02

picture.image

picture.image

LangGraph的驱动力

即然上文介绍的LCEL已经很强大,但是为什么还需要LangGraph呢?基于LCEL构建的Chain与Agent又存在哪些不足呢?

*** 链(Chain):无法满足在循环中调用LLM以完成任务。**

上文中,我们可以轻易地使用LCEL来快速创建一个链,但是很显然的一个问题是:如果我们把链中的组件想象成Graph中的节点,组件之间的联系想象成Graph中的边,那么这个链就是一个有向无环图(DAG)。 即在一次Chain运行中,一个调用节点无法重复/循环进入。

picture.image

那么为什么需要将循环引入运行时呢?考虑一个增强的RAG应用:

picture.image

在这个RAG应用设计中,我们可以对语义检索出来的关联文档(上下文)进行评估:如果评估的文档质量很差,可以对检索的问题进行重写(Rewrite,比如把输入的问题结合对话历史用更精确的方式来表达),并把重写结果重新交给检索器,检索出新的关联文档,这样有助于获得更精确的结果。

这里把Rewrite的问题重新交给检索器,就是一个典型的“循环”动作。 而在目前LangChain的简单链中是无法支持的。

其他一些典型的依赖“循环”的场景包括:

  • 代码生成时的自我纠正: 当借助LLM自动生成软件代码时,根据代码执行的结果进行自我反省,并要求LLM重新生成代码。
  • Web访问自动导航 :每当进入下一界面时,需要借助多模态模型来决定下一步的动作(点击、滚动、输入等),直至完成导航。

*** AgentExecutor:尽管支持“循环”,但缺乏精确控制能力。**

那么,如果我们需要在循环中调用LLM能力,就需要借助于AgentExecutor。其调用的过程主要就是两个步骤:

  1. 通过大模型来决定采取什么行动,使用什么工具,或者向用户输出响应(如运行结束时);
  2. 执行1步骤中的行动,比如调用某个工具,并把结果继续交给大模型来决定,即返回步骤1;

这里的AgentExecute存在的问题是: 过于黑盒,所有的决策过程隐藏在AgentExecutor背后,缺乏更精细的控制能力,在构建复杂Agent的时候受限 。这些精细化的控制要求比如:

  • 某个Agent要求首先强制调用某个Tool
  • 在 Agent运行过程中增加人机交互步骤
  • 能够灵活更换Prompt或者背后的LLM
  • 多Agent(Multi-Agent)智能体构建的需求,即多个Agent协作完成任务的场景支持。(这也是Langchain相对于竞争对手Autogen等最薄弱的能力之一,也是众多开发者千呼万唤的特性)

所以,让我们简单总结LangGraph诞生的动力: LangChain简单的链(Chain)不具备“循环”能力;而AgentExecutor调度的Agent** 运行又过于“黑盒”。因此需要一个具备更精细控制能力的框架来支持更复杂场景的LLM应用。**

PART 03

picture.image

picture.image

LangGraph的设计思想

LangGraph并非一个独立于Langchain的新框架,它是基于Langchain之上构建的一个扩展库,可以与Langchain现有的链、LCEL等无缝协作。LangGraph能够协调多个Chain、Agent、Tool等共同协作来完成输入任务,支持LLM调用“循环”以及Agent过程的更精细化的控制。

LangGraph的实现方式是把之前基于AgentExecutor的黑盒调用过程用一种新的形式来构建:状态图(StateGraph)。 把基于LLM的任务(比如RAG、代码生成等)细节用Graph进行精确的定义(定义图的节点与边),最后基于这个图来编译生成应用;在任务运行过程中,维持一个中央状态对象(state),会根据节点的跳转不断更新,状态包含的属性可自行定义。

我们用官方的一个增强的RAG应用的Graph来帮助理解:

picture.image

这个Graph中体现了LangGraph的几个基本概念:

  • StateGraph :这是代表整个状态图的基础类。
  • Nodes :节点。在有了图之后,可以向图中添加节点,节点通常是一个可调用的函数、一个可运行的Chain或者Agent。有一个特殊的节点叫END,进入这个节点,代表运行结束。

在上图中,推理函数调用、调用检索器、生成响应内容、问题重写等都是其中的任务节点。

  • Edges :边。有了节点后,需要向图中添加边,边代表从上一个节点跳转到下一个节点的关系。目前有三种类型的边:
  • Starting Edge :一种特殊的边。用来定义任务运行的开始节点,所以它没有上一个节点。
  • Normal Edge :普通边。代表上一个节点运行完成后立即进入下一个节点。比如在调用Tools后获得结果后,立刻进入LLM推理节点。
  • Conditional Edge :条件边。代表上一个节点运行完成后,需要根据条件跳转到某个节点,因此这种边不仅需要上游节点、下游节点,还需要一个条件函数,根据条件函数的返回来决定下游节点。

在上图中,Check Relevance就是一个条件边,它的上游节点是检索相关文档,条件函数是判断文档是否相关,如果相关,则进入下游节点【产生回答】;如果不相关,则进入下游节点【重写输入问题】。

在构建好StateGraph,并增加Node和Edge后,可以通过compile编译成可运行的应用:

app = graph.compile()

接下来你就可以调用这个app来完成你的任务。

PART 04

picture.image

picture.image

LangGraph构建基础Agent

我们可以粗暴的认为LangGraph就是把现在黑盒的AgentExecutor揉碎掰开,允许你定义内部的细节结构(用图的方式),从而实现更强大的功能。 那么我们当然可以用LangGraph来重新实现原来的AgentExecutor,即实现一个最基础的ReAct范式的Agent应用。

对应的Graph如下:

picture.image

简单的实现代码如下(省略了部分细节):


        
            

          # 定义一个Graph,传入state定义(参考上图state属性)  
workflow = StateGraph(AgentState)  
  
# 两个节点  
  
#节点1: 推理节点,调用LLM决定action,省略了runreason细节  
workflow.add\_node("reason", run\_reason)  
  
#节点2: 行动节点,调用tools执行action,省略executetools细节  
workflow.add\_node("action", execute\_tools)  
  
#入口节点:总是从推理节点开始  
workflow.set\_entry\_point("reason")  
  
#条件边:根据推理节点的结果决定下一步  
workflow.add\_conditional\_edges(  
    "reason",  
    should\_continue, #条件判断函数(自定义,根据状态中的推理结果判断)  
    {  
        "continue": "action", #如果条件函数返回continue,进action节点  
        "end": END, #如果条件函数返回end,进END节点  
    },  
)  
  
#普通边:action结束后,总是返回reason  
workflow.add\_edge("action", "reason")  
  
#编译成app  
app = workflow.compile()  
  
#可以调用app了,并使用流式输出  
inputs = {"input": "you task description", "chat\_history": []}  
for s in app.stream(inputs):  
    print(list(s.values())[0])  
    print("----")
        
      

代码中的注释对graph构建的细节做了解释。显然,这要比简单的使用agentExecutor要复杂的多,但同时也展示了LangGraph在构建LLM应用时强大的控制能力: 通过Graph的定义,可以对一个LLM应用的处理过程进行非常细节的编排设计,从而满足大量复杂场景的AI Agent应用。

由于LangGraph刚推出不久,一些细节与易用性在后期也会不断完善。比如未来是否会提供更直观的定义界面等,也值得期待。 在后续的文章中,我们将逐渐实践几个代表性场景下的LangGraph的应用,比如代码助手,自省式RAG,多Agent应用等,敬请期待。

picture.image

END

点击下方关注我,不迷路

交流请识别以下名片并说明来源

picture.image

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论