上个月LangChain刚刚发布了正式的0.1稳定版本(没错,是0.1而不是1.0),在版本公告里面首当其冲宣布的最重要更新,是在这个版本里面引入了一个最新库 - LangGraph。这是一个面向当前LLM开发领域最火热的AI Agent开发与控制的开发库,也是LangChain试图用来弥补其在Agent开发、特别是复杂的多Agent系统定制方面的不足的重大尝试,相信也会成为LangChain在2024升级更新的重点领域! 我们会用一系列文章深入LangGraph,结合官方例子介绍与剖析其在几个重点Agent方向的应用。
由于官方文档较为晦涩,加上LangChain一贯的“重量级”风格。为了更好地帮助深入浅出的理解LangGraph,并照顾到没有LangChain基础的朋友,我们首先来了解一些“预备知识”。 PART 01 预备知识 【LangChain中的链与LCEL】 Chain(链)是LangChain中最核心的概念之一(看名字就知道)。简单的说,就是把自然语言输入、关联知识检索、Prompt组装、可用Tools信息、大模型调用、输出格式化等这些LLM 应用中的常见动作,组装成一个可以运行的“链”式过程。链可以直接调用,也可以用来进一步构建更强大的Agent。 LCEL即LangChain Express Language,即LangChain表达语言。这是LangChain提供的一种简洁的、用于组装上述“链”的声明性方式。 我们看一个官方使用LCEL“组装”Chain的例子就明白: prompt = ChatPromptTemplate.from_template('讲一个关于 {topic} 的笑话') #调用chain 这个官方的例子中,把提示(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需要扩展的能力有:
以LCEL来组装并创建运行一个Agent的简单过程如下: ''' 注意到,相对于Chain.invoke()直接运行,这里的Agent_executor的作用就是为了能够实现多次循环ReAct的动作,以最终完成任务。 【什么是图(Graph)】 图是计算机科学中的一种数据结构。大部分人可能都接触过一些基本的数据结构,比如队列(Queue)、堆栈(Stack)、链表(List)或者树(Tree)等,图(Graph)也是其中的一种相对复杂的数据结构。我们无意在此普及图的数据结构知识,你只需要了解的图的几个基本知识:
PART 02 LangGraph的驱动力 即然上文介绍的LCEL已经很强大,但是为什么还需要LangGraph呢?基于LCEL构建的Chain与Agent又存在哪些不足呢? * 链(Chain):无法满足在循环中调用LLM以完成任务。 上文中,我们可以轻易地使用LCEL来快速创建一个链,但是很显然的一个问题是:如果我们把链中的组件想象成Graph中的节点,组件之间的联系想象成Graph中的边,那么这个链就是一个有向无环图(DAG)。即在一次Chain运行中,一个调用节点无法重复/循环进入。 那么为什么需要将循环引入运行时呢?考虑一个增强的RAG应用: 在这个RAG应用设计中,我们可以对语义检索出来的关联文档(上下文)进行评估:如果评估的文档质量很差,可以对检索的问题进行重写(Rewrite,比如把输入的问题结合对话历史用更精确的方式来表达),并把重写结果重新交给检索器,检索出新的关联文档,这样有助于获得更精确的结果。 这里把Rewrite的问题重新交给检索器,就是一个典型的“循环”动作。而在目前LangChain的简单链中是无法支持的。 其他一些典型的依赖“循环”的场景包括:
* AgentExecutor:尽管支持“循环”,但缺乏精确控制能力。 那么,如果我们需要在循环中调用LLM能力,就需要借助于AgentExecutor。其调用的过程主要就是两个步骤:
这里的AgentExecute存在的问题是:过于黑盒,所有的决策过程隐藏在AgentExecutor背后,缺乏更精细的控制能力,在构建复杂Agent的时候受限。这些精细化的控制要求比如:
所以,让我们简单总结LangGraph诞生的动力:LangChain简单的链(Chain)不具备“循环”能力;而AgentExecutor调度的Agent运行又过于“黑盒”。因此需要一个具备更精细控制能力的框架来支持更复杂场景的LLM应用。 PART 03 LangGraph的设计思想 LangGraph并非一个独立于Langchain的新框架,它是基于Langchain之上构建的一个扩展库,可以与Langchain现有的链、LCEL等无缝协作。LangGraph能够协调多个Chain、Agent、Tool等共同协作来完成输入任务,支持LLM调用“循环”以及Agent过程的更精细化的控制。 LangGraph的实现方式是把之前基于AgentExecutor的黑盒调用过程用一种新的形式来构建:状态图(StateGraph)。把基于LLM的任务(比如RAG、代码生成等)细节用Graph进行精确的定义(定义图的节点与边),最后基于这个图来编译生成应用;在任务运行过程中,维持一个中央状态对象(state),会根据节点的跳转不断更新,状态包含的属性可自行定义。 我们用官方的一个增强的RAG应用的Graph来帮助理解: 这个Graph中体现了LangGraph的几个基本概念:
在上图中,推理函数调用、调用检索器、生成响应内容、问题重写等都是其中的任务节点。
在上图中,Check Relevance就是一个条件边,它的上游节点是检索相关文档,条件函数是判断文档是否相关,如果相关,则进入下游节点【产生回答】;如果不相关,则进入下游节点【重写输入问题】。 在构建好StateGraph,并增加Node和Edge后,可以通过compile编译成可运行的应用: app = graph.compile() 接下来你就可以调用这个app来完成你的任务。 PART 04 LangGraph构建基础Agent 我们可以粗暴的认为LangGraph就是把现在黑盒的AgentExecutor揉碎掰开,允许你定义内部的细节结构(用图的方式),从而实现更强大的功能。那么我们当然可以用LangGraph来重新实现原来的AgentExecutor,即实现一个最基础的ReAct范式的Agent应用。 对应的Graph如下: 简单的实现代码如下(省略了部分细节):
代码中的注释对graph构建的细节做了解释。显然,这要比简单的使用agentExecutor要复杂的多,但同时也展示了LangGraph在构建LLM应用时强大的控制能力:通过Graph的定义,可以对一个LLM应用的处理过程进行非常细节的编排设计,从而满足大量复杂场景的AI Agent应用。 由于LangGraph刚推出不久,一些细节与易用性在后期也会不断完善。比如未来是否会提供更直观的定义界面等,也值得期待。在后续的文章中,我们将逐渐实践几个代表性场景下的LangGraph的应用,比如代码助手,自省式RAG,多Agent应用等,敬请期待。 END |
|