在这篇文章中,作者分享了他们在采用 LangChain 过程中遇到的挑战,以及如何通过采用模块化的构建块来替代其僵化的高级抽象,从而简化了代码库,提升了团队的工作效率。我们一起来看看作者怎么说的。
#01
背景故事
我们自 2023 年初开始在生产环境中使用 LangChain,到 2024 年决定将其移除,使用 LangChain 超过 12 个月。
在 2023 年时,LangChain 对我们来说似乎是理想选择,它拥有丰富的组件和工具,并且大众对 LangChain 的认可度也持续提升。LangChain 承诺 “能够使开发人员在短短一个下午内将创意转化为可执行代码。”
但随着我们的需求变得越来越复杂,问题也慢慢出现了,LangChain 逐渐从助力变为障碍。
随着 LangChain 不灵活性问题的显现,我们不得不深入探索其内部结构,以改善我们系统的底层行为。但由于 LangChain 有意将许多细节抽象掉,我们几乎不可能编写我们需要的底层代码。
#02
早期框架的风险
AI 和 LLM 领域变化瞬息万变,新概念和想法层出不穷。因此,围绕这些新兴技术构建一个能够经受时间考验的框架(如 LangChain)是非常困难的。
说明一下,如果我在 LangChain 创建之初尝试构建一个框架,可能也不会做得更好。批评总是容易的,而行动则困难得多。本文并非批评 LangChain 的核心开发者或其贡献者。因为每个人都在努力追求卓越。
**设计良好的抽象是困难的,尤其是在需求不明确的情况下。**当你在一个如此变幻莫测的状态下建模组件时(例如 AI Agent),使用低级构建块进行抽象可能是更安全的选择。
#03
LangChain 抽象的问题
起初,LangChain 与我们的简单需求相匹配,为我们提供了极大的帮助。但随着时间的推移,其高级抽象使得代码变得更加难以理解和维护。当我们的团队花费越来越多的时间来理解 LangChain,而不是开发新功能时,这显然是不可取的。
LangChain 的抽象问题可以通过一个简单的英文到意大利语的翻译示例来说明。
这是一个仅使用 OpenAI 包的 Python 示例:
from openai import OpenAI
client = OpenAI(api_key="<your_api_key>")
text = "hello!"
language = "Italian"
messages = [
{"role": "system", "content": "You are an expert translator"},
{"role": "user", "content": f"Translate the following from English into {language}"},
{"role": "user", "content": f"{text}"},
]
response = client.chat.completions.create(model="gpt-4o", messages=messages)
result = response.choices[0].message.content
这段简单易懂的代码包含一个类和一个函数调用,剩下的是标准的 Python 代码。
让我们对比一下 LangChain 的版本:
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
os.environ["OPENAI_API_KEY"] = "<your_api_key>"
text = "hello!"
language = "Italian"
prompt_template = ChatPromptTemplate.from_messages(
[("system", "You are an expert translator"),
("user", "Translate the following from English into {language}"),
("user", "{text}")]
)
parser = StrOutputParser()
chain = prompt_template | model | parser
result = chain.invoke({"language": language, "text": text})
代码大致相同,但也仅仅是大致相同。
我们现在有三个类和四个函数调用。但最令人担忧的是,LangChain 引入了三个新的抽象:
-
提示模板(Prompt templates):提供给 LLM 的提示
-
输出解析器(Output parsers):处理 LLM 的输出
-
链(Chains):LangChain 的 “LCEL 语法” 覆盖了 Python 的 | 运算符
LangChain 增加了代码的复杂性,而没有带来明显的好处。
这段代码可能适合早期阶段的原型开发。但对于生产使用,每个组件都必须能够合理理解,以便在现实世界使用条件下不会意外崩溃。你必须遵循给定的数据结构,并围绕这些抽象设计你的应用程序。
让我们再看看另一个 Python 中的抽象对比,这次是从 API 获取 JSON 数据。
使用内置的 http 包:
import http.client
import json
conn = http.client.HTTPSConnection("api.example.com")
conn.request("GET", "/data")
response = conn.getresponse()
data = json.loads(response.read().decode())
conn.close()
使用 requests 包:
import requests
response = requests.get("/data")
data = response.json()
好坏一目了然。这就是一个好的抽象的感觉。
当然,这些都是简单的例子。但我要表达的是,好的抽象简化了你的代码,减少了理解它所需的认知负担。
LangChain 试图通过用更少的代码做更多的事情来让你的工作更轻松。但当这以牺牲简单性和灵活性为代价时,抽象就失去了价值。
LangChain 还有一个习惯,就是在其他抽象之上使用抽象,因此你经常被迫以嵌套抽象的方式来思考如何正确使用 API。这不可避免地导致理解大量的堆栈跟踪和调试你没有编写的内部框架代码,而不是实现新功能。
#04
LangChain 对我们开发团队的影响
我们的应用程序大量使用 AI Agent 来执行不同类型的任务,例如测试用例发现、Playwright 测试生成和自动修复。
当我们想从单一 Sequential Agent 架构迁移到更复杂的架构时,LangChain 成了限制因素。例如,生成 Sub-Agent 并让它们与原始 Agent 交互。或者多个专业 Agent 相互交互。
在另一个例子中,我们需要根据业务逻辑和 LLM 的输出动态更改 Agent 可以访问的工具的可用性。但 LangChain 并没有提供从外部观察 Agent 状态的方法,导致我们不得不减少实现的范围,以适应 LangChain Agent 提供的有限功能。
一旦我们移除它,我们不再需要将我们的需求转换为适合 LangChain 的解决方案,我们只需写代码即可。
那么,如果不用 LangChain,你应该使用什么框架?也许你根本不需要框架。
#05
构建 AI 应用程序是否需要框架?
LangChain 最初通过提供 LLM 功能帮助我们专注于构建应用程序。但现在看起来,如果没有框架,我们可能会发展得更好。
LangChain 的组件列表给人一种构建 LLM 驱动应用程序很复杂的印象。但大多数应用程序所需的核心组件通常是:
-
一个用于 LLM 通信的客户端
-
用于函数调用的函数 / 工具
-
一个用于 RAG 的向量数据库
-
一个用于追踪和评估的可观察性平台
其余的则是围绕这些组件的辅助工具(例如,向量数据库的分块和嵌入),或者是常规应用程序任务,如数据持久化和缓存管理。
如果你在没有框架的情况下开始 AI 开发,你可能需要花更多的时间来整合自己的工具箱,并且需要更多的前期学习和研究。但这些投资是值得的,因为你将学习到你将要操作的领域的基础知识。
在大多数情况下,你对 LLM 的使用是简单而直接的。你将主要编写顺序代码,迭代提示,提高输出的质量和可预测性。大多数任务可以通过简单的代码和相对较少的外部包来实现。
即使使用 Agent,你也不太可能做超出简单的 Agent 间通信的事情,除了在预定的顺序流程中使用业务逻辑来处理 Agent 状态及其响应。你不需要一个框架来实现这一点。
虽然 Agent 领域正在迅速发展,出现了很多激动人心的可能性和有趣的用例,但我们建议现在保持简单,直到 Agent 使用模式稳定下来。
#06
使用构建块保持快速和精简
假设你没有将垃圾代码投入生产,那么团队创新和迭代的速度是成功的关键。AI 领域的许多开发工作是由实验和原型驱动的。
但是,框架通常是为基于已建立的使用模式强制结构而设计的 ——LLM 驱动的应用程序尚未具备这种模式。必须将新想法转换为特定于框架的代码,这会限制你的迭代速度。
构建块方法偏爱简单的低级代码,配以精心挑选的外部包,保持你的架构精简,以便开发人员可以集中精力解决他们试图解决的问题。
构建块是你感觉完全理解且不太可能改变的简单东西。例如,一个向量数据库。这是一种已知类型的模块化组件,具有基本功能集,因此可以轻松替换。你的代码库需要保持精简和可适应,以最大化你的学习速度和每个迭代周期的价值。
我希望我客观地描述了我们在 LangChain 上的挑战,以及为什么完全放弃框架对我们的团队有巨大的好处。
我们目前采用最小抽象的模块化构建块策略,使我们能够更迅速、更轻松地进行开发。