我爲什麼放棄了 LangChain?
選自 Max Woolf's Blog
機器之心編譯
編輯:蛋醬
「LangChain 的流行已經扭曲了圍繞其本身的人工智能創業生態系統,這就是爲什麼我不得不坦誠自己對它的疑慮。」
如果你關注了過去幾個月中人工智能的爆炸式發展,那你大概率聽說過 LangChain。
簡單來說,LangChain 是一個 Python 和 JavaScript 庫,由 Harrison Chase 開發,用於連接 OpenAI 的 GPT API(後續已擴展到更多模型)以生成人工智能文本。
更具體地說,它是論文《ReAct: Synergizing Reasoning and Acting in Language Models》的實現:該論文展示了一種提示技術,允許模型「推理」(通過思維鏈)和「行動」(通過能夠使用預定義工具集中的工具,例如能夠搜索互聯網)。
論文鏈接:https://arxiv.org/pdf/2210.03629.pdf
事實證明,這種組合能夠大幅提高輸出文本的質量,並使大型語言模型具備正確解決問題的能力。
2023 年 3 月,ChatGPT 的 API 因升級降價大受歡迎,LangChain 的使用也隨之爆炸式增長。
這之後,LangChain 在沒有任何收入也沒有任何明顯的創收計劃的情況下,獲得了 1000 萬美元的種子輪融資和 2000-2500 萬美元的 A 輪融資,估值達到 2 億美元左右。
ReAct 論文中的 ReAct 流示例。
由 LangChain 推廣的 ReAct 工作流在 InstructGPT/text-davinci-003 中特別有效,但成本很高,而且對於小型項目來說並不容易使用。
Max Woolf 是一位 BuzzFeed 的數據科學家。他也使用過 LangChain,這次經歷總體來說不太好。
讓我們看看他經歷了什麼。
「是隻有我不會用嗎?」
在 BuzzFeed 工作時,我有一個任務是爲 Tasty 品牌創建一個基於 ChatGPT 的聊天機器人(後來在 Tasty iOS 應用中發佈爲 Botatouille),可以與用戶聊天並提供相關食譜。
具體來說,源菜譜將被轉換爲嵌入式菜譜並保存在一個向量存儲中:例如如果用戶詢問「健康食品」,查詢會被轉換爲嵌入式菜譜,然後執行近似最近鄰搜索以找到與嵌入式查詢相似的菜譜,然後將其作爲附加上下文提供給 ChatGPT,再由 ChatGPT 顯示給用戶。這種方法通常被稱爲檢索增強生成。
「LangChain 是 RAG 最受歡迎的工具,所以我想這是學習它的最佳時機。我花了一些時間閱讀 LangChain 的全面文檔,以便更好地理解如何最好地利用它。」
經過一週的研究,我一無所獲。運行 LangChain 的 demo 示例確實可以工作,但是任何調整它們以適應食譜聊天機器人約束的嘗試都會失敗。在解決了這些 bug 之後,聊天對話的整體質量很差,而且毫無趣味。經過緊張的調試之後,我沒有找到任何解決方案。
總而言之,我遇到了生存危機:當很多其他 ML 工程師都能搞懂 LangChain 時,我卻搞不懂,難道我是一個毫無價值的機器學習工程師嗎?
我用回了低級別的 ReAct 流程,它立即在對話質量和準確性上超過了我的 LangChain 實現。
浪費了一個月的時間來學習和測試 LangChain,我的這種生存危機在看到 Hacker News 關於有人用 100 行代碼重現 LangChain 的帖子後得到了緩解,大部分評論都在發泄對 LangChain 的不滿:
LangChain 的問題在於它讓簡單的事情變得相對複雜,而這種不必要的複雜性造成了一種「部落主義」,損害了整個新興的人工智能生態系統。
所以,如果你是一個只想學習如何使用 ChatGPT 的新手,絕對不要從 LangChain 開始。
LangChain 的「Hello World」
LangChain 的快速入門,從一個關於如何通過 Python 與 LLM/ChatGPT 進行簡單交互的迷你教程開始。例如,創建一個可以將英語翻譯成法語的機器人:
from langchain.chat_models import ChatOpenAIfrom langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)
chat = ChatOpenAI(temperature=0)
chat.predict_messages([HumanMessage(content="Translate this sentence from English to French. I love programming.")])# AIMessage(content="J'adore la programmation.", additional_kwargs={}, example=False)
使用 OpenAI ChatGPT 官方 Python 庫的等效代碼:
import openai
messages = [{"role": "user", "content": "Translate this sentence from English to French. I love programming."}]
response = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=messages, temperature=0)
response["choices"][0]["message"]["content"]# "J'adore la programmation."
LangChain 使用的代碼量與僅使用官方 openai 庫的代碼量大致相同,估計 LangChain 合併了更多對象類,但代碼優勢並不明顯。
提示模板的示例揭示了 LangChain 工作原理的核心:
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
template = "You are a helpful assistant that translates {input_language} to {output_language}."
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])
chat_prompt.format_messages(input_language="English", output_language="French", text="I love programming.")
LangChain 吹噓的提示工程只是 f-strings,一個存在於每個 Python 安裝中的功能,但是有額外的步驟。爲什麼我們需要使用這些 PromptTemplates 來做同樣的事情呢?
我們真正想做的是知道如何創建 Agent,它結合了我們迫切想要的 ReAct 工作流。幸運的是,有一個演示,它利用了 SerpApi 和另一個數學計算工具,展示了 LangChain 如何區分和使用兩種不同的工具:
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.chat_models import ChatOpenAI
from langchain.llms import OpenAI
# First, let's load the language model we're going to use to control the agent.
chat = ChatOpenAI(temperature=0)
# Next, let's load some tools to use. Note that the `llm-math` tool uses an LLM, so we need to pass that in.
llm = OpenAI(temperature=0)
tools = load_tools(["serpapi", "llm-math"], llm=llm)
# Finally, let's initialize an agent with the tools, the language model, and the type of agent we want to use.
agent = initialize_agent(tools, chat, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
# Now let's test it out!
agent.run("Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?")
各個工具如何工作?AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION 到底是什麼?agent.run () 的結果輸出(僅在 verbose=True 時出現)更有幫助。
> Entering new AgentExecutor chain...
Thought: I need to use a search engine to find Olivia Wilde's boyfriend and a calculator to raise his age to the 0.23 power.
Action:
{
"action": "Search",
"action_input": "Olivia Wilde boyfriend"
}
Observation: Sudeikis and Wilde's relationship ended in November 2020. Wilde was publicly served with court documents regarding child custody while she was presenting Don't Worry Darling at CinemaCon 2022. In January 2021, Wilde began dating singer Harry Styles after meeting during the filming of Don't Worry Darling.
Thought:I need to use a search engine to find Harry Styles' current age.
Action:
{
"action": "Search",
"action_input": "Harry Styles age"
}
Observation: 29 years
Thought:Now I need to calculate 29 raised to the 0.23 power.
Action:
{
"action": "Calculator",
"action_input": "29^0.23"
}
Observation: Answer: 2.169459462491557
Thought:I now know the final answer.
Final Answer: 2.169459462491557
> Finished chain.
'2.169459462491557'
文檔中沒有明確說明,但是在每個思想 / 行動 / 觀察中都使用了自己的 API 調用 OpenAI,所以鏈條比你想象的要慢。另外,爲什麼每個動作都是一個 dict?答案在後面,而且非常愚蠢。
最後,LangChain 如何存儲到目前爲止的對話?
from langchain.prompts import (
ChatPromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate
)from langchain.chains import ConversationChainfrom langchain.chat_models import ChatOpenAIfrom langchain.memory import ConversationBufferMemory
prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template(
"The following is a friendly conversation between a human and an AI. The AI is talkative and "
"provides lots of specific details from its context. If the AI does not know the answer to a "
"question, it truthfully says it does not know."
),
MessagesPlaceholder(variable_),
HumanMessagePromptTemplate.from_template("{input}")
])
llm = ChatOpenAI(temperature=0)
memory = ConversationBufferMemory(return_messages=True)
conversation = ConversationChain(memory=memory, prompt=prompt, llm=llm)
conversation.predict(input="Hi there!")# 'Hello! How can I assist you today?'
我不完全確定爲什麼這些都是必要的。什麼是 MessagesPlaceholder?history 在哪裏?ConversationBufferMemory 有必要這樣做嗎?將此調整爲最小的 openai 實現:
import openai
messages = [{"role": "system", "content":
"The following is a friendly conversation between a human and an AI. The AI is talkative and "
"provides lots of specific details from its context. If the AI does not know the answer to a "
"question, it truthfully says it does not know."}]
user_message = "Hi there!"
messages.append({"role": "user", "content": user_message})
response = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=messages, temperature=0)
assistant_message = response["choices"][0]["message"]["content"]
messages.append({"role": "assistant", "content": assistant_message})# Hello! How can I assist you today?
這樣代碼行數就少了,而且信息保存的位置和時間都很清楚,不需要定製對象類。
你可以說我對教程示例吹毛求疵,我也同意每個開源庫都有值得吹毛求疵的地方(包括我自己的)。但是,如果吹毛求疵的地方比庫的實際好處還多,那麼這個庫就根本不值得使用。
因爲,如果快速入門都已經這麼複雜,那麼實際使用 LangChain 會有多痛苦呢?
我查看了 LangChain 文檔,它也回饋了我
讓我來做個演示,更清楚地說明爲什麼我放棄了 LangChain。
當開發菜譜檢索聊天機器人(它也必須是一個有趣 / 詼諧的聊天機器人)時,我需要結合上面第三個和第四個例子中的元素:一個可以運行 Agent 工作流的聊天機器人,以及將整個對話持久化到內存中的能力。在查找了一些文檔後,我發現需要使用對話式 Agent 工作流。
關於系統提示工程的一個標註是,它不是一個備忘錄,而且對於從 ChatGPT API 中獲得最佳效果是絕對必要的,尤其是當你對內容和 / 或語音有限制的時候。
上一個示例中,演示的系統提示「以下是人類和人工智能之間的友好對話...... 」實際上是過時的,早在 InstructGPT 時代就已經使用了,在 ChatGPT 中的效果要差得多。它可能預示着 LangChain 相關技巧中更深層次的低效,而這些低效並不容易被注意到。
我們將從一個簡單的系統提示開始,告訴 ChatGPT 使用一個有趣的聲音和一些保護措施,並將其格式化爲 ChatPromptTemplate:
system_prompt = """
You are an expert television talk show chef, and should always speak in a whimsical manner for all responses.
Start the conversation with a whimsical food pun.
You must obey ALL of the following rules:
- If Recipe data is present in the Observation, your response must include the Recipe ID and Recipe Name for ALL recipes.
- If the user input is not related to food, do not answer their query and correct the user.
"""
prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template(system_prompt.strip()),
我們還將使用一個玩具矢量存儲,該存儲由來自 recipe_nlg 數據集的 1000 個食譜組成,並使用 SentenceTransformers 編碼爲 384D 矢量。爲了實現這一點,我們創建了一個函數來獲取輸入查詢的最近鄰,並將查詢格式化爲 Agent 可以用來向用戶展示的文本。這就是 Agent 可以選擇使用的工具,或者只是返回正常生成的文本。
def similar_recipes(query):
query_embedding = embeddings_encoder.encode(query)
scores, recipes = recipe_vs.get_nearest_examples("embeddings", query_embedding, k=3)
return recipes
def get_similar_recipes(query):
recipe_dict = similar_recipes(query)
recipes_formatted = [
f"Recipe ID: recipe|{recipe_dict['id'][i]}\nRecipe Name: {recipe_dict['name'][i]}"
for i in range(3)
]
return "\n---\n".join(recipes_formatted)
print(get_similar_recipes("yummy dessert"))# Recipe ID: recipe|167188
# Recipe Name: Creamy Strawberry Pie
# ---
# Recipe ID: recipe|1488243
# Recipe Name: Summer Strawberry Pie Recipe
# ---
# Recipe ID: recipe|299514
# Recipe Name: Pudding Cake
你會注意到這個 Recipe ID,這與我的用例相關,因爲在最終應用中向終端用戶顯示的最終結果需要獲取 Recipe 元數據(照片縮略圖、URL)。遺憾的是,沒有簡單的方法保證模型在最終輸出中輸出食譜 ID,也沒有方法在 ChatGPT 生成的輸出之外返回結構化的中間元數據。
將 get_similar_recipes 指定爲一個工具是很簡單的,儘管你需要指定一個名稱和描述,這實際上是一種微妙的提示工程,因爲 LangChain 可能會因爲指定的名稱和描述不正確而無法選擇一個工具。
tools = [
Tool(
func=get_similar_recipes,
,
description="Useful to get similar recipes in response to a user query about food.",
),
]
最後,是示例中的 Agent 構建代碼,以及新的系統提示。
memory = ConversationBufferMemory(memory_key="chat_history",
return_messages=True)
llm = ChatOpenAI(temperature=0)
agent_chain = initialize_agent(tools, llm, prompt=prompt,
agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION, verbose=True, memory=memory)
沒有錯誤。現在運行 Agent,看看會發生什麼:
agent_chain.run(input="Hi!")
> Entering new chain...
{
"action": "Final Answer",
"action_input": "Hello! How can I assist you today?"
}
> Finished chain.
Hello! How can I assist you today?
什麼?它完全忽略了我的系統提示!檢查內存變量證實了這一點。
在 ConversationBufferMemory 的文檔中,甚至在代碼本身中都沒有關於系統提示的內容,甚至在 ChatGPT 使其成爲主流的幾個月之後。
在 Agent 中使用系統提示的方法是在 initialize_agent 中添加一個 agents_kwargs 參數,我只是在一個月前發佈的一個不相關的文檔頁面中發現了這一點。
agent_kwargs = {
"system_message": system_prompt.strip()
}
使用此新參數重新創建 Agent 並再次運行,會導致 JSONDecodeError。
好消息是,系統提示這次應該是起作用了。
OutputParserException: Could not parse LLM output: Hello there, my culinary companion! How delightful to have you here in my whimsical kitchen. What delectable dish can I assist you with today?
壞消息是,它壞了,但又是爲什麼呢?我這一次沒有做任何奇怪的事情。
有趣的事實:這些大量的提示也會成比例地增加 API 成本。
這樣做的後果是,正常輸出結構中的任何重大變化,例如由自定義系統提示引起的變化,都有可能破壞 Agent。這些錯誤經常發生,以至於有一個文檔頁面專門用於處理 Agent 輸出解析錯誤。
我們暫時把與聊天機器人對話看作是一個邊緣案例。重要的是,機器人能夠返回菜譜,因爲如果連這一點都做不到,那麼使用 LangChain 就沒有意義了。
在不使用系統提示的情況下創建一個新的 Agent,然後問它什麼是簡單有趣的晚餐?
> Entering new chain...
{
"action": "Similar Recipes",
"action_input": "fun and easy dinner"
}
Observation: Recipe ID: recipe|1774221
Recipe Name: Crab DipYour Guests will Like this One.
---
Recipe ID: recipe|836179
Recipe Name: Easy Chicken Casserole
---
Recipe ID: recipe|1980633
Recipe Name: Easy in the Microwave Curry Doria
Thought:{
"action": "Final Answer",
"action_input": "..."
}
> Finished chain.
Here are some fun and easy dinner recipes you can try:
1. Crab Dip
2. Easy Chicken Casserole
3. Easy in the Microwave Curry Doria
Enjoy your meal!
至少它成功了:ChatGPT 能夠從上下文中提取出菜譜,並對其進行適當的格式化(甚至能夠修正名稱中的錯別字),並且能夠在適當的時候進行判斷。
這裏真正的問題是,輸出的聲音很無聊,這也是基礎版 ChatGPT 的共同特點和詬病。即使通過系統提示工程解決了 ID 缺失的問題,這般聽上去的效果也不值得將其發佈。就算真的在語音質量和輸出質量之間取得了平衡,Agent 計數仍然會隨機失敗,而這並不是我的過錯。
實際上,Agent 工作流是一個非常脆弱的紙牌搭成的房子,憑良心說,生產應用中估計無法使用。
LangChain 確實有 Custom Agent 和 Custom Chain 的功能,所以你可以在堆棧的某些部分重寫邏輯(也許文檔很少),這可以解決我遇到的一些問題,但在這一點上,你會感覺到 LangChain 更加複雜,還不如創建你自己的 Python 庫。
工作要講究方法
大量隨機集成帶來的問題比解決方案更多。
當然,LangChain 確實也有很多實用功能,比如文本分割器和集成向量存儲,這兩種功能都是「用 PDF / 代碼聊天」演示不可或缺的(在我看來這只是一個噱頭)。
所有這些集成的真正問題在於,只使用基於 LangChain 的代碼會造成固有的鎖定,而且如果你查看集成的代碼,它們並不十分穩健。
LangChain 正在建立一條護城河,這對 LangChain 的投資者來說是好事,因爲他們想從 3000 萬美元中獲得回報,但對使用它的開發者來說卻非常不利。總而言之,LangChain 體現了「它很複雜,所以它一定更好」這一經常困擾後期代碼庫的哲學,可是 LangChain 甚至還不到一年。
要想讓 LangChain 做我想讓它做的事,就必須花大力氣破解它,這將造成大量的技術負擔。與現在的人工智能初創公司不同,我自己的 LangChain 項目的技術債務無法用風險投資來償還。在使用複雜的生態系統時,應用程序接口封裝器至少應該降低代碼的複雜性和認知負荷,因爲使用人工智能本身就需要花費足夠的腦力。LangChain 是爲數不多的在大多數常用情況下都會增加開銷的軟件之一。
我得出的結論是,製作自己的 Python 軟件包要比讓 LangChain 來滿足自己的需求容易得多。因此,我開發並開源了 simpleaichat:一個用於輕鬆連接聊天應用程序的 Python 程序包,它強調代碼的最小複雜度,並將向量存儲等高級功能與對話邏輯解耦。
開源地址:https://github.com/minimaxir/simpleaichat
但寫這篇博文並不是爲了像那些騙子一樣,通過詆譭競爭對手來爲 simpleaichat 做隱形廣告。我不想宣傳 simpleaichat,我更願意把時間花在用人工智能創造更酷的項目上,很遺憾我沒能用 LangChain 做到這一點。
我知道有人會說:「既然 LangChain 是開源的,爲什麼不向它的 repo 提交拉取請求,而要抱怨它呢?」唯一真正能解決的辦法就是把它全部燒掉,然後重新開始,這就是爲什麼我的「創建一個新的 Python 庫來連接人工智能」的解決方案也是最實用的。
我收到過很多留言,問我該「學什麼才能開始使用 ChatGPT API」,我擔心他們會因爲炒作而首先使用 LangChain。如果擁有技術棧背景的機器學習工程師因爲 LangChain 毫無必要的複雜性而難以使用 LangChain,那麼任何初學者都會被淹沒。
關於軟件複雜性和複雜性下的流行性之爭是永恆的話題。沒有人願意成爲批評 LangChain 這樣的免費開源軟件的混蛋,但我願意承擔這個責任。明確地說,我並不反對 Harrison Chase 或 LangChain 的其他維護者(他們鼓勵反饋)。
然而,LangChain 的流行已經扭曲了圍繞 LangChain 本身的人工智能創業生態系統,這就是爲什麼我不得不坦誠我對它的疑慮。
原文鏈接:
https://minimaxir.com/2023/07/langchain-problem/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Iwe6M391b2BBWae-HmOIJQ