作者 | Aparna Dhinakaran
编译 |岳扬
Image by author
智能体(Agents)正迎来辉煌时刻。伴随着众多新框架的涌现和对该领域的持续投资[1],现代 AI 智能体正在跨越起初的不稳定阶段[2],迅速取代 RAG 成为开发首选。那么,2024 年是否会成为 autonomous AI 系统全面接管撰写邮件、预订航班、数据分析等任务的一年呢?
也许吧,但要实现这一点还有很多工作要做。开发人员在构建智能体时,不仅要决定使用何种模型、应用场景和技术架构,还要挑选合适的开发框架。是坚持较为早期的 LangGraph,还是转向新兴的 LlamaIndex Workflows?或者走传统路线,自己编写全部代码呢?
这篇文章的目的就是让您更轻松地做出选择。在过去几周里,我使用多个主流框架构建了相同的智能体,并从技术角度分析了它们各自的优缺点。每个智能体的所有代码都可以在此代码仓库[3]中找到。
本文测试用智能体的基本概述
本次测试所采用的智能体整合了多项功能,包括执行函数调用(function calling)、使用多种工具或技能、与外部资源建立连接,以及实现状态或记忆的共享。
该智能体具备以下几项核心能力:
为了达成上述目标,智能体需要掌握三项基本技能:结合产品文档的 RAG、在相关数据库上生成 SQL 语句的能力,以及数据分析技巧。智能体的用户界面使用 gradio 搭建,而智能体本身则以聊天机器人(chatbot)的形式构建。
01 Code-Based Agent(不使用智能体框架)
在着手开发智能体时,您可以选择不依赖任何框架,而是完全自主构建。在启动这个项目之初,我首先采用了这种方法。
Image by author
1.1 纯代码架构
下面是基于纯代码构建的智能体, 其核心是一个由 OpenAI 提供支持的技能路由器 ,它通过函数调用来确定使用哪项技能。技能执行完毕后,控制权将返回给技能路由器,以便调用其他技能或直接向用户作出回应。
智能体会持续记录用户消息和智能体响应,并在每次调用时将这一完整列表传递给技能路由器,确保在整个交互过程中保留上下文。
各项技能均在独立的类中进行定义(例如“GenerateSQLQuery”类),这些类都保存在 SkillMap 中。技能路由器仅与 SkillMap 进行交互,通过它来加载技能的名称、描述以及可调用的函数。这种设计理念使得向智能体中添加新技能变得非常简单:只需将该技能编写为一个独立的类,并将其加入到 SkillMap 的技能列表即可。这样做的目的是为了在不影响技能路由器代码的前提下,轻松实现新技能的添加。
总的来说,这种实现方式虽然简单易行,但仍然存在一些需要克服的难题。
1.2 使用纯代码智能体面临的挑战
第一个困难在于如何设计技能路由器的系统提示词(system prompt)。 在上面的例子中,技能路由器往往倾向于自行生成 SQL 语句,而不是交给相应的技能模块去处理。如果你有过试图让大语言模型停止执行某项任务的经历,那你可能深知这其中的挫败感;为了找到合适的提示词,我不得不进行了多次调试。此外, 处理每个步骤产生的不同输出格式也是一项复杂的工作。 由于我选择不使用结构化输出,因此必须为技能路由器和各项技能中大语言模型的调用准备多种格式的应对策略。
1.3 纯代码智能体的优点
基于代码的方法提供了一个扎实的基础和出发点,是一种绝佳的学习途径,让我们可以在不依赖现成框架提供的智能体教程的情况下,了解智能体的运作原理。虽然引导大语言模型按既定行为模式运作确实存在难度,但代码结构本身简洁明了,易于操作,对于某些使用场景而言,这种做法是完全合理的(具体分析将在下文展开)。
02 LangGraph
LangGraph 是众多智能体框架中历史最为悠久的之一,它于 2024 年 1 月首次发布。 该框架的设计初衷是为了解决现有流程和链条的非循环性问题,它通过采用 Pregel 图结构来解决这一问题。 LangGraph 通过引入节点(nodes)、边(edges)以及条件边(conditional edges)的概念,简化了在智能体中创建循环流程的过程,使得图的遍历变得更加直观。 LangGraph 是基于 LangChain 构建的,它继承了后者的对象(objects)和类型(types)。
Image by author
2.1 LangGraph 架构
从表面上看,LangGraph 智能体与基于代码的智能体有相似之处,但它们的底层代码却有大不相同。虽然 LangGraph 在技术上也使用了“路由器(router)”这一概念,即通过代码函数调用 OpenAI 并利用其响应来推进到下一个步骤,但程序在不同技能之间的切换控制机制却完全不同。
在此定义的图(graph)中,包含了一个用于初始化 OpenAI 调用的节点,即上文中提到的“agent”,以及一个用于工具处理步骤节点,即“tools”。LangGraph 内置了一个名为 ToolNode 的对象,它能够接收一系列可调用的工具,并根据 ChatMessage 的响应来触发这些工具,完成操作后再次回到“agent”节点。
每当“agent”节点(也可以理解为基于代码的智能体中的技能路由器(router))被调用之后,should_continue 这条边将判断是将响应直接返回给用户,还是转给 ToolNode 来处理工具调用。
在每个节点中,“state” 负责保存与 OpenAI 的交互消息和响应列表,这一点与基于代码的智能体保持上下文的方式相似。
2.2 使用 LangGraph 面临的挑战
在处理 LangGraph 构建的智能体示例时,遇到的主要难题在于必须借助 Langchain 对象才能确保流程的顺畅。
挑战 1:函数调用的 validation 错误
为了能够使用 ToolNode 对象,我不得不对 Skill 代码进行大规模的重构。ToolNode 需要一组可调用的函数列表,我本以为可以直接使用现成的函数,但是函数参数配置出了问题,导致流程受阻。
这些技能(skills)是以类形式定义的,每个类都有一个可调用的成员函数,其中“self”是首个参数。GPT-4o 足够智能,能够在生成函数调用(function call)时自动排除“self”参数,但 LangGraph 却因此认为缺少了必要参数,从而抛出了 validation 错误。
这个问题让我摸索了好几小时才搞清楚,因为错误信息把函数里的第三个参数(数据分析技能中的“args”)错误地标记为缺失参数(missing parameter):
需要指出的是, 这个误导性的错误信息其实来自 Pydantic,而非 LangGraph。
最后,我下定决心,改用 Langchain 的 @tool 装饰器将我的技能(skills)重新编写为基本方法,这样程序就能正常运行了。
挑战 2:Debugging
正如前文所述,在框架中调试非常困难。主要是因为错误信息混乱不清,以及框架中的抽象概念,它们使得追踪和查看变量变得非常复杂。
抽象概念主要体现在尝试跟踪智能体间传递的消息时。LangGraph 会将消息保存在 state[“messages”] 里。Graph 中的一些节点会自动从这些消息(messages)中提取信息,这样的自动化过程可能会让节点在访问消息(messages)时,我们难以把握消息(messages)的具体内容。
智能体行动的顺序视图(图片由作者提供)
2.3 LangGraph 的优点
LangGraph 的最大优势在于其易用性。它的图结构代码简洁且易于理解。对于那些拥有复杂节点逻辑的场景,LangGraph 能够提供一个清晰的图视图,让我们更轻松地把握智能体的连接方式。此外,LangGraph 还可以直接转换以 LangChain 构建的现有应用程序。
2.4 经验之谈
当我们只使用 LangGraph 框架的相关功能时,一切都会运行得非常流畅;但一旦我们尝试跳出框架,就要准备好进行一些令人头疼的调试了。
03 LlamaIndex Workflows
Workflows 是智能体框架领域的新晋成员,它于今年夏初首次亮相。 与 LangGraph 类似,它的设计宗旨是简化可循环智能体的构建过程。此外,Workflows 特别强调其异步执行的能力。
在 Workflows 中,某些设计元素似乎是为了直接对标 LangGraph,尤其是它采用事件(events)而非边(edges)或条件边(conditional edges)作为连接逻辑的方式。在 Workflows 中,智能体逻辑被封装在“步骤(steps)”中(与 LangGraph 中的“节点(nodes)”相对应),而事件(events)的发出和接收则负责在不同的步骤(steps)间传递信息。
Image by author
上述框架与 LangGraph 的结构颇为相似,但有一点不同:我给 Workflow 增加了一个初始化步骤,用于准备智能体的环境上下文,稍后我会详细介绍这一点。尽管两者的结构相似,但它们所依赖的代码实现却截然不同。
3.1 Workflows 架构
以下代码段描绘了 Workflow 的架构。与 LangGraph 相仿,在这一部分,我配置了状态信息(state),并将各项技能(skills)绑定到了 LLM 对象上。
在这里,我还定义了一个额外的步骤——“prepare_agent”。该步骤负责将用户输入转换成 ChatMessage,并将其存储到工作流的记忆存储中。将这一过程作为一个独立的步骤分离出来,意味着智能体在遍历工作步骤(steps)时可以重复回到这一步,从而避免反复将用户信息加入到记忆存储中。
在 LangGraph 的实现案例中,我通过一个位于图(graph)之外的 run_agent 方法实现了相同的功能。这一改变主要是出于风格上的考虑,但我认为,将这一逻辑整合到 Workflow 和图(graph)中,会更加整洁和高效。
在 Workflow 配置完成后,我继续编写了路由代码:
以及工具调用处理代码:
它们的实现方式似乎更接近于纯代码的智能体,而非 LangGraph 智能体。这主要是因为 Workflows 选择在各步骤(steps)中维护条件路由(conditional routing)逻辑,而不是像 LangGraph 那样使用条件边(conditional edge)(第 18-24 行在 LangGraph 中是条件边,而现在它们只是路由步骤的一部分)。另外,LangGraph 中的 ToolNode 对象能够在 tool_call_handler 方法中自动处理大部分任务。
在路由步骤之后,我们能够将 SkillMap 以及基于纯代码的智能体中已有的技能(skills)直接应用于 Workflows。这些技能(skills)无需任何修改即可与 Workflows 配合使用,这大大简化了我的工作。
3.2 使用 Workflows 面临的挑战
挑战 1:Sync vs Async
尽管对于在线运行的智能体来说,异步执行是更优的选择,但调试同步执行的智能体通常更为简便。 Workflows 本身是为了异步操作而设计的,因此尝试将其改为同步执行非常困难。
起初,我以为只需去掉“async”方法标识,并将函数名“achat_with_tools”改为“chat_with_tools”即可。但是,由于 Workflow 类内部的方法同样采用了异步标记,为了实现同步运行,我不得不重新定义这些方法。尽管如此,我最终还是选择了异步处理方式,幸运的是,这并没有增加调试的难度。
智能体行动的顺序视图(图片由作者提供)
挑战 2:Pydantic Validation Errors
与 LangGraph 的问题类似,在智能体的技能(skills)处也出现了令人困惑的 Pydantic Validation Errors。幸运的是,由于 Workflows 能够很好地处理成员函数,这些问题这次比较容易解决。最终,我不得不更加规范地为智能体技能(skills)创建 LlamaIndex FunctionTool 对象:
从构建 FunctionTools 的 AgentFlow.文件中摘录
3.3 Workflows 的优点
与 LangGraph 相比,我在使用 Workflows 构建智能体时要轻松得多,主要原因是 Workflows 并未提供内置功能,而是需要我自己编写路由逻辑和工具操作代码。 这也使得我的 Workflow 智能体与基于纯代码的智能体看起来极为相似。
最大的区别在于事件(events)的使用上。我使用两个自定义事件在智能体中的各个步骤之间移动:
这种基于事件的发射器-接收器架构(emitter-receiver),取代了直接调用智能体中某些方法的做法,例如工具调用处理(tool call handler)。
对于那些步骤(steps)更为复杂、异步触发且可能产生多个事件(events)的系统来说,这种架构就非常有助于干净利落地管理这些步骤。
Workflows 的其他优点还包括其轻量级特性,不会施加过多的结构限制(除了必须使用特定的 LlamaIndex 对象外),并且其基于事件(event-based)的架构为直接函数调用提供了一种有效的替代方案,这对于处理复杂、异步的应用场景尤为有益。
04 对这些方法进行比较
对比这三种方法,各有其独到之处。
无框架方法实施起来最简单。由于所有抽象层都是由开发者自行定义(如前例中的 SkillMap 对象),因此管理不同类型(types)和对象(objects)相对简单。但是,代码的可读性和易用性完全取决于开发者个人,可以预见,如果没有一定的智能体结构约束,智能体的复杂性增加后可能会变得难以驾驭。
LangGraph 提供了丰富的智能体结构支持,使得智能体的定义非常清晰。对于多人协作开发的智能体来说,这种智能体结构设定有助于统一架构规范。LangGraph 也为那些对智能体结构不太熟悉的开发者提供了帮助。不过,这样做也有代价 —— 由于 LangGraph 为你做了许多工作,如果你不完全认同这个框架,它可能会让你头疼不已;代码可能会非常简洁,但你可能要为此进行更多的调试工作。
Workflows 则处于两者之间。基于事件(event-based)的架构在某些项目中可能极具价值,而且因为它对 LlamaIndex 类型的使用要求不高,对于那些没有在应用程序中完全使用该框架的开发者来说,提供了更大的自由度。
Image created by author
归根结底,关键问题可能在于“你是否已经在使用 LlamaIndex 或 LangChain 来组织应用程序?” LangGraph 和 Workflows 都与它们所依赖的框架紧密集成,因此每个特定智能体框架的额外优势可能不足以成为转换使用的理由。
纯代码方法可能永远是一个有吸引力的选择。如果你能够严格地记录并执行所创建的任何抽象概念,那么确保外部框架不会成为你的阻碍就很容易了。
05 在选择智能体框架时需要考虑的关键问题
当然,单纯一句“具体情况具体分析”这样的回答总是让人不太满意。以下三个问题或许能帮你选择下一个智能体项目应该采用哪个框架。
你的项目是否已经深度集成了 LlamaIndex 或 LangChain?
如果是的话,不妨优先考虑这两个选项。
你对智能体的常见架构是否熟悉,还是更希望有人告诉你应该如何构建智能体结构?
如果你倾向于后者,那么 Workflows 可能是个不错的选择。如果你非常倾向于后者,那么 LangGraph 或许更适合你。
你要构建的智能体是否有参考样例?
框架的一个优势在于,每个框架都有大量的教程和实例供你参考。而纯代码构建智能体的参考实例相对较少。
Image created by author
06 Conclusion
选择一个智能体框架只是影响生成式人工智能系统在生产环境中表现众多决策中的一项,建立强大的安全保障和对大语言模型(LLM)的监控[4]是必要的 —— 同时,面对新智能体框架、研究成果和模型对传统技术的颠覆,我们还需保持灵活应对的态度。
Thanks for reading!
Hope you have enjoyed and learned new things from this blog!
About the authors
Aparna Dhinakaran
Co-Founder and CPO of Arize AI. Formerly Computer Vision PhD at Cornell, Uber Machine Learning, UC Berkeley AI Research.
本期互动内容