点击上方蓝字关注我们
上个月LangChain刚刚发布了正式的0.1稳定版本(没错,是0.1而不是1.0),在版本公告里面首当其冲宣布的最重要更新,是在这个版本里面引入了一个最新库 - LangGraph。 这是一个面向当前LLM开发领域最火热的AI Agent开发与控制的开发库,也是LangChain试图用来 弥补其在Agent开发、特别是复杂的多Agent系统定制方面的不足 的重大尝试,相信也会成为LangChain在2024升级更新的重点领域!
我们会用一系列文章深入LangGraph,结合官方例子介绍与剖析其在几个重点Agent方向的应用。
- LangGraph诞生的动力及设计思想
- LangGraph应用:增强的RAG应用
- LangGraph应用:自修复代码助手
- LangGraph应用:Multi-Agent系统
- LangGraph应用:构建Web Agents
由于官方文档较为晦涩,加上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} 的笑话")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个元素(节点/顶点)及这些元素之间的关系(边)的集合。
- 有向无环图(Directed Acyclic Graph,DAG) :有向指的是图中的“边”有方向;无环指的是无法从某个节点经过若干“边”返回这个节点。
其 他的一些图 的理论,包括不同类型图的存储结构、相关算法等 ,这里对理解LangGraph无关紧要。
PART 02
LangGraph的驱动力
即然上文介绍的LCEL已经很强大,但是为什么还需要LangGraph呢?基于LCEL构建的Chain与Agent又存在哪些不足呢?
*** 链(Chain):无法满足在循环中调用LLM以完成任务。**
上文中,我们可以轻易地使用LCEL来快速创建一个链,但是很显然的一个问题是:如果我们把链中的组件想象成Graph中的节点,组件之间的联系想象成Graph中的边,那么这个链就是一个有向无环图(DAG)。 即在一次Chain运行中,一个调用节点无法重复/循环进入。
那么为什么需要将循环引入运行时呢?考虑一个增强的RAG应用:
在这个RAG应用设计中,我们可以对语义检索出来的关联文档(上下文)进行评估:如果评估的文档质量很差,可以对检索的问题进行重写(Rewrite,比如把输入的问题结合对话历史用更精确的方式来表达),并把重写结果重新交给检索器,检索出新的关联文档,这样有助于获得更精确的结果。
这里把Rewrite的问题重新交给检索器,就是一个典型的“循环”动作。 而在目前LangChain的简单链中是无法支持的。
其他一些典型的依赖“循环”的场景包括:
- 代码生成时的自我纠正: 当借助LLM自动生成软件代码时,根据代码执行的结果进行自我反省,并要求LLM重新生成代码。
- Web访问自动导航 :每当进入下一界面时,需要借助多模态模型来决定下一步的动作(点击、滚动、输入等),直至完成导航。
*** AgentExecutor:尽管支持“循环”,但缺乏精确控制能力。**
那么,如果我们需要在循环中调用LLM能力,就需要借助于AgentExecutor。其调用的过程主要就是两个步骤:
- 通过大模型来决定采取什么行动,使用什么工具,或者向用户输出响应(如运行结束时);
- 执行1步骤中的行动,比如调用某个工具,并把结果继续交给大模型来决定,即返回步骤1;
这里的AgentExecute存在的问题是: 过于黑盒,所有的决策过程隐藏在AgentExecutor背后,缺乏更精细的控制能力,在构建复杂Agent的时候受限 。这些精细化的控制要求比如:
- 某个Agent要求首先强制调用某个Tool
- 在 Agent运行过程中增加人机交互步骤
- 能够灵活更换Prompt或者背后的LLM
- 多Agent(Multi-Agent)智能体构建的需求,即多个Agent协作完成任务的场景支持。(这也是Langchain相对于竞争对手Autogen等最薄弱的能力之一,也是众多开发者千呼万唤的特性)
所以,让我们简单总结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的几个基本概念:
- 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
LangGraph构建基础Agent
我们可以粗暴的认为LangGraph就是把现在黑盒的AgentExecutor揉碎掰开,允许你定义内部的细节结构(用图的方式),从而实现更强大的功能。 那么我们当然可以用LangGraph来重新实现原来的AgentExecutor,即实现一个最基础的ReAct范式的Agent应用。
对应的Graph如下:
简单的实现代码如下(省略了部分细节):
# 定义一个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应用等,敬请期待。
END
点击下方关注我,不迷路
交流请识别以下名片并说明来源