LangChain居然不香了?一線程序員現身說法,硬核博文剖析LLM應用開發原則
2023年是屬于LLM初創公司的一年,也是屬于LangChain的一年。
這個發布于2022年10月的開源框架可以支持開發者構建由LLM驅動的應用程序,目前依舊是社區中一種不可忽視的開發范式。
更具體地說,基于LLM構建應用程序的過程有點像在搭積木。即使模型本身的能力已經很強大了,我們依舊需要其他的組件和工具才能更好發揮其潛力。
比如聊天模型、提示模板、文本嵌入模型、文本分割器、文檔加載器、檢索器、向量存儲等,這些工具的不同的搭配組合能夠構建出各種的應用鏈,滿足RAG、Agent、存儲&索引、信息提取等不同的應用需求。
舉個例子,你想用GPT-4開發一個旅行顧問機器人,為用戶提供行程方面的規劃和建議。如果只依靠GPT-4在訓練時學到的知識,沒有實時查詢最新的航班、酒店、景區信息,提供的建議就不可能準確實用。
借助LangChain框架,這個機器人就能鏈接到各種API和外部數據庫,并記住用戶的旅行偏好,甚至能根據用戶的對話歷史提供個性化建議。
這樣聽起來,LangChain是一個非常強大的工具,流行起來也是理所應當。
然而,最近一個技術團隊的博文登上了HN熱榜,描述了他們從「入坑」LangChain到「幡然醒悟」,最終決定拋棄這個熱門框架的過程。
「為什么我們不再使用LangChain構建AI agents——當抽象弊大于利時:在生產中使用LangChain的教訓以及我們應該做什么」
底下的評論也紛紛附和,表示這個框架有種「代碼糟糕」的感覺,而且把使用LangChain描述為一條「充滿雷區的道路」。
「LangChain的抽象就是死亡的定義。」
從大受追捧到「人人喊打」,LangChain到底有什么樣的問題?
問題浮現
這個技術團隊在生產中使用LangChain已經超過12個月,開始于2023年初。
當時,LangChain似乎是最佳選擇,因為它擁有一系列令人印象深刻的組件和工具,并且承諾開發者「用一個下午將想法轉變為可執行的代碼」,流行程度飆升。
然而,隨著需求逐漸變得復雜,LangChain的不靈活性開始顯現出來,開始阻礙生產力、成為摩擦的源頭。
團隊不得不深入研究框架的內部結構,以改善系統的底層行為。但因為LangChain有意通過抽象屏蔽細節,編寫底層代碼的嘗試通常也不可行,或至少是十分復雜。
究其根源,作者認為是LangChain的抽象程度過高。在開發的初期階段,較為簡單的需求與框架的假設相一致,因此配合得很好。
但高級抽象很快使之后的代碼變得難以理解、維護,團隊花費在理解和調試LangChain上的時間越來越多,幾乎趕上了真正構建功能所用的時間。
舉個具體的例子,上代碼:用OpenAI的包,將英語單詞翻譯為意大利語(沒錯,就是GPT-4o發布會demo的功能)
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
這段代碼很好理解,包含一個OpenAI類的實例client以及一個函數調用,其余都是標準的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})
涉及到三個類、四個函數調用。但最令人擔憂的是,一個如此簡單的任務需要引入三個抽象概念——
- 提示模板(prompt template):為LLM提供提示
- 輸出解析器(output parser):處理LLM的輸出
- 鏈(chain):LangChain的「LCEL語法」覆蓋了Python的「|」運算符
這似乎徒增代碼復雜性,卻沒有任何額外的好處。
LangChain似乎更適合早期原型,但不適合實際的生產使用。
對于后者而言,開發人員必須理解每一個組件,才能保證代碼不會在真實的使用場景中意外崩潰,但LangChain限制他們必須遵守給定的數據結構和抽象概念,這無疑是額外的負擔。
再舉一個例子,這次是從API獲取JSON。
要使用Python內置的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的過程更加復雜,開發人員還不得不面對大量的堆棧跟蹤信息,并調試那些自己不熟悉的內部框架代碼。
以這個技術團隊自己的開發為例,他們的應用程序使用大量AI agent執行不同類型的任務,比如測試用例發現、Playwright測試生成和自動修復。
當他們想要從只有單個順序代理的架構轉向更復雜架構時,例如,生成sub-agnet并與原始agent交互,或者多個專業agent彼此交互,LangChain就成為了限制因素。
另一個示例中,需要根據業務邏輯和LLM的輸出,動態更改agent可訪問工具的可用性。但LangChain沒有提供從外部觀察agent狀態的方法,導致他們不得不縮小實現范圍,以適應LangChain對agent可用功能的限制。
下決心刪除LangChain之后,技術團隊仿佛得到了真正的「解脫」。不僅工作高效了,內耗也少了。
一旦刪除了它,我們就可以不用先將需求轉化為適合LangChain的解決方案。我們只寫代碼就可以了。
拋棄LangChain,下一個框架用什么?
事后,團隊仔細反思復盤了這個問題。他們認為,長期來看,不使用框架是更好的選擇。
LangChain提供了一長串組件,讓人感覺LLM驅動的應用程序很復雜,但其實并不是。核心組件只有幾樣——
- 用于LLM通信的客戶端
- 函數或調用函數的工具
- 用于RAG的向量數據庫
- 用于追蹤、評估等功能的可觀察平臺
其余的組件,要么是以上核心組件的輔助(比如向量數據庫的分塊和嵌入),要么只是完成常規應用程序的任務(比如使用數據持久化和緩存,以管理文件和應用程序狀態)。
如果不使用任何框架,毫無疑問會增加前期用于學習、調研的工作量,開發者需要更長時間來組建自己的工具箱。
但「磨刀不誤砍柴工」,這些時間是值得的。在即將進入的領域打下基礎,這對你本人和應用程序的未來都是良好的投資。
而且,很多情況下,使用LLM的流程都是非常簡單直接的。開發人員主要編寫順序代碼、迭代提示,并提高輸出的質量和可預測性。絕大多數任務都可以通過簡潔的代碼和較小的外部包集合來實現。
即使用到了agent,也不一定需要框架才能實現。在處理業務邏輯時,一般只需要在預定順序流中進行agent之間的通信,處理它們的狀態和響應,超出這個范圍的工作內容并不多。
雖然agent領域正在迅速發展,并帶來許多令人興奮的用例和可能性,但在代理的使用模式逐漸固化的過程中,我們還是應該遵循簡潔原則。
構建基本塊,「輕裝疾行」
假設技術團隊沒有在生產中混入垃圾代碼,那么創新和迭代的速度是衡量成功的最重要指標,因為AI領域的許多發展都是由實驗和原型設計驅動的。
這意味著,代碼庫需要盡可能精簡且適應性強,才能最大限度提升開發人員的學習速度,每個迭代周期才能產生更多價值。
然而,「框架」的概念與此并不相容,它通常是人為設計出一種代碼結構,為了匹配根據既有的使用模式。
但LLM驅動的應用還在發展階段,沒有固定的使用模式。當你不得不將創新的想法「翻譯」為特定于某個框架的代碼時,就限制了迭代速度。
因此,相比于使用框架,更好的辦法是構建基本塊(builing blocks),通過簡潔的底層代碼和精心挑選的外部依賴包,保持架構的精簡,從而讓開發人員專注于真正需要解決的問題。
「構建基本塊」意味著簡潔、可被完全理解,且不易變動。最典型的例子就是矢量數據庫,它屬于已知類型的模塊化組件,只有基本功能,因此可以輕松被更換或取代。
因此,作者所在團隊目前的策略是,完全不使用任何框架,用盡可能少的抽象進行模塊化構建,從而讓開發過程更快、更流暢。
雖然LangChain的槽點如此之多,但作者還是選擇不過分苛責。某種程度上,這些缺陷都是無法避免的。
在AI和LLM這樣快速變化的領域,每周都會涌現新的概念和想法。因此,想要在如此多新型技術中間創建LangChain這樣的框架,并設計出經得起時間考驗的抽象,是非常困難的。
作者非常坦誠地承認,如果當初是自己去構建LangChain,也不會做得比現在更好。當一個「事后諸葛」指出錯誤總是容易的,這篇博文的目的并不是批評任何LangChain的開發人員或貢獻者,因為每個人都在盡力而為。
即使能很好地理解需求,構建精心設計的抽象也是很困難的。因此在不斷變動的條件下對組件(比如agent)進行建模時,更安全的選擇是僅對底層模塊使用抽象。