LangChain 的问题所在
AI魔法学院
2023-10-12
分享海报

摆脱繁琐,追求高效。是开发者永远追求的目标。LangChain,虽号称多功能,但集成过多引发问题,逼人只用其代码。LangChain 给人带来的是,令人沮丧的声音,脆弱的 Agent 工作流,技术债务增加。简而言之,做自己的 Python 包比强行改造 LangChain 更好。本文作者开发了 simpleaichat,轻松与聊天应用交互,摆脱复杂,避免锁定。别误解,本文并不是攻击 LangChain,但更实际的解决方案是重新开始。技术复杂性与流行性之争是永恒的,早年是 React,今日是 ReAct

如果你在过去几个月里一直关注人工智能的爆炸式炒作,你可能已经听说过 LangChain。由 Harrison Chase 开发的 LangChain 是一个用于与 OpenAI GPT API(后来扩展到更多模型)进行人工智能文本生成交互的 Python JavaScript 库。更具体地说,它是 2022 10 月发布的论文《ReAct:在语言模型中协同推理和行动》(ReAct: Synergizing Reasoning and Acting in Language Models)的实现,俗称为 ReAct 论文,该论文演示了一种提示技术,使模型能够通过 “思维链” 进行 “推理”(reason),并能够通过使用预定义工具集中的工具(如能够搜索互联网)来 “行动”(act)。实验证明,这种组合极大地提高了输出文本质量,并赋予大型语言模型正确解决问题的能力。

IMG_256

(1d) ReActReason+Act

思考 1:我需要搜索 “Apple Remote” 并找到它最初设计用来互动的程序。

行动 1:搜索 [Apple Remote]

观察 1Apple Remote 是由苹果于 2005 10 月推出的遥控器。最初设计用于控制 Front Row 媒体中心程序。

思考 2Apple Remote 最初设计用于控制 Front Row 媒体中心程序。我需要搜索下一个 Front Row 并找出哪个其他设备可以控制它。

行动 2:搜索 [Front Row]

观察 2:未找到 [Front Row]。类似项:[‘Front Row Seat to Earth’‘Front Row Motorsports’‘Front Row (software)’…]

思考 3:未找到 Front Row。我需要搜索 Front Row(软件)。

行动 3:搜索 [Front Row(软件)]

观察 3Front Row 是一款已停用的媒体中心软件。

思考 4Front Row(软件)由 Apple Remote 或键盘功能键控制。所以答案是键盘功能键。

行动 4:完成 [键盘功能键]

React 论文中的 ReAct 流程示例

基于 LangChain 推广的 ReAct 工作流程在 InstructGPT/text-davinci-003 方面表现尤为出色,尽管在小型项目中使用代价高昂且不易操作。在 2023 3 月,随着 ChatGPT API 的使用因其极为便宜的 API 而广受欢迎,正如我准确预测的那样,LangChain 的使用也迅速扩大,以至于 LangChain 能够在没有任何收入或任何明显的收入生成计划的情况下,成功筹集到了 1000 万美元的种子轮融资,以及在估值 2 亿美元的 A 轮融资中又融资了 2000 万至 2500 万美元。

而这正是我与 LangChain 的个人经历开始的地方。在我在 BuzzFeed 的工作中,我被要求为 Tasty 品牌创建一个基于 ChatGPT 的聊天机器人(后来作为 Tasty iOS 应用程序中的 Botatouille 发布),该机器人可以与用户交流并提供相关的食谱。源食谱被转换为嵌入式表示并保存在向量存储中:例如,如果用户询问“健康食品”,则查询会被转换为嵌入式表示,并执行近似最近邻搜索,以找到与嵌入式查询类似的食谱,然后将其作为附加上下文提供给 ChatGPT,随后可显示给用户。这种方法通常被称为检索增强生成。

IMG_257

使用检索增强生成的聊天机器人的示例架构。来源:Joseph Haaga

LangChain 显然是 RAG 的首选工具,所以我认为现在是学习它的绝佳时机。我花了一些时间阅读 LangChain 相当详尽的文档,以更好地理解如何最好地利用它:经过一周的研究,我一无所获。运行 LangChain 示例演示确实有效,但是任何试图调整它们以适应食谱聊天机器人约束的尝试都会导致它们崩溃。在解决了错误后,聊天对话的整体质量很差且无趣,经过激烈的调试,我没有找到解决方案。最终,我陷入了存在危机:当很多其他机器学习工程师都能理解 LangChain,而我却不能,我是否是一个毫无价值的机器学习工程师呢?我们回到了较低层次的 ReAct 流程,这立即在对话质量和准确性方面胜过了我在 LangChain 中的实现。

总之,我浪费了一个月的时间学习和测试 LangChain,最大的收获是热门的人工智能应用可能并不一定值得炒作。在一个 Hacker News 的帖子中,我看到有人用 100 行代码重新实现了 LangChain,大多数评论都在抱怨 LangChain

loveparade

难道我是唯一一个对 LangChain 的价值主张持怀疑态度的人吗?其中 99% 都是外部工具的接口定义和实现,其中大多数都非常直观。我可以在不到一个小时内为我的应用编写集成。为什么要引入一个充满主观看法的外部框架呢?这对我来说有点像 npm “left-pad”。每个人都在使用它,因为它似乎很受欢迎,而不是因为他们需要它。

crazyedgar

对我们来说,LangChain 实际上引发了比解决的问题更多的问题。我们的生产系统在运行良好的几周后突然开始频繁失败(超过 30% 的请求)。经过调查,似乎 LangChain 为每个请求设置了默认的 60 秒超时。而这种行为没有记录在文档中!LangChain 所做出的这些不明智决策无处不在,并且最终都会给你带来麻烦。最后,我们用普通的请求客户端替换了所有内容。绝对不建议在一个提供非常有限价值同时又从你那里隐藏了大量细节和决策的库上构建系统。

Spivak

然而,LangChain 绝对是完美的,它糟糕到会让你纯粹出于沮丧而写出更好的东西,但它又给了你足够好的想法和线索来真正做到这一点。它可能是实际使用 llms”的最佳入口,因为它刚好满足了开发者的需求。

LangChain 的问题在于它使得本来简单的事情相对复杂化,由此带来的不必要复杂性导致了一种部落主义,这对整个新兴的人工智能生态系统造成了伤害。如果你是一个想要学习如何与 ChatGPT 进行交互的新手,绝对不要从 LangChain 开始。

LangChain 中的 “Hello World”(或更准确地说,“Hell World”

LangChain 的快速入门指南始于一个关于如何简单地从 Python LLMs/ChatGPT 进行交互的迷你教程。例如,要创建一个能够从英文翻译成法文的机器人:

from langchain.chat_models import ChatOpenAI
from 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 官方的 Python 库进行 ChatGPT 的等效代码:

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 来做同样的事情呢?

但我们真正想要知道的是如何创建 Agents,它们包含了我们迫切需要的 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'

文档并没有说明得很清楚,但在每个 Thought/Action/Observation 中,都使用了自己的 API 调用到 OpenAI,因此这个链条比你想象的要慢。另外,为什么每个动作都是一个 dist?这个问题的答案稍后会解释,而且相当幼稚可笑。

最后,LangChain 如何存储到目前为止的对话?

from langchain.prompts import (
ChatPromptTemplate,
MessagesPlaceholder,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate
)
from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
from 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_name="history"),
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?'

我并不完全确定为什么需要这些。什么是 MessagesPlaceholderhistory 在哪里?这对 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。当我在开发检索食谱的聊天机器人(它还必须是一个有趣 / 机智的聊天机器人)时,我需要结合前面所提到的第三个和第四个示例中的元素:一个可以运行代理工作流的聊天机器人,以及将整个对话持久保存到内存中的能力。经过一些文档的查找,我发现我需要利用 Conversational Agent 工作流。

关于系统提示工程的一个小侧记:这不是一个模因,绝对有必要从 ChatGPT API 中获得最佳结果,特别是如果你对内容和 / 或语气有限制。在最后一个示例中演示的以下系统提示 The following is a friendly conversation between a human and an AI…(“这是一个人类与人工智能之间友好对话……”)实际上是一个过时的提示,它是在 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 数据集的 1,000 个食谱,这些食谱使用 SentenceTransformers 编码成 384 维的向量。为了实现这一点,我们创建了一个函数,用于获取输入查询的最近邻,以及一个将其格式化为 Agent 可以呈现给用户的文本的查询。这可以作为 Agent 可以在适当的情况下选择使用的 Tool,或者只是返回普通生成的文本。

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,对于我的用例来说,这是相关的,因为需要获取食谱元数据(照片缩略图、URL)用于在最终应用程序中向最终用户展示。不幸的是,没有简单的方法来确保模型在最终输出中输出 Recipe ID,也没有办法在 ChatGPT 生成的输出之外返回结构化的中间元数据。

get_similar_recipes 指定为一个工具是直接的,虽然你需要指定一个 name description,这实际上是一种微妙的提示工程,因为如果两者都没有很好地指定,LangChain 可能无法选择一个工具。

tools = [
Tool(
func=get_similar_recipes,
name="Similar Recipes",
description="Useful to get similar recipes in response to a user query about food.",
),
]

最后,Agent 构建代码,这是从示例中延续过来的,加上了新的系统 prompt

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?

等一下,它完全忽略了我的 system 提示!该死。检查 memory 变量确认了这一点。查看 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?

好消息是这次系统提示绝对起作用了!坏消息是它出错了,但是为什么?我这次什么怪事都没干。

问题的根源可能是 LangChain 代理实际上是如何进行 Tool 选择的。还记得我说过在链条中 Agent 输出一个 dict 是奇怪的吗?当查看 LangChain 代码时,结果发现工具选择是通过要求输出通过提示工程是有效的 JSON 来完成的,然后希望一切都会顺利。

FORMAT_INSTRUCTIONS RESPONSE FORMAT INSTRUCTIONS
When responding to me,please output a response in one of two formats:
**Option1:**
Use this if you want the human to use a tool.
Markdown code snippet formatted in the following schema:
json
{{{{
"action":string,\\The action to take.Must be one of {tool_names}
"action_input":string\\The input to the action
}}}}

**Option2: **
Use this if you want to respond directly to the human.Markdown code snippet formatted in the following schema:
json
{{{{
"action":"Final Answer",
"action_input":string \You should put what you want to return to use here
}}

有趣的事实:这些大量的提示也会成比例地增加 API 成本!

这个结果的后果是,任何正常输出结构的显著更改,比如由自定义系统提示引起的更改,都有一定的随机机会来破坏 Agent!这些错误经常发生,以至于有一个专门处理 Agent 输出解析错误的文档页面!

无论如何,互联网上的人们都是些令人讨厌的家伙,所以我们可以将与聊天机器人进行对话视为一种边缘情况。重要的是,聊天机器人能够返回食谱,因为如果它甚至不能做到这一点,使用 LangChain 就没有意义。在创建一个新的 Agent,不使用系统提示的情况下,然后问它 Whats a fun and easy dinner?(“什么是有趣又简单的晚餐?”):

> 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:

Crab Dip
Easy Chicken Casserole
Easy in the Microwave Curry Doria

Enjoy your meal!

至少这个部分是成功的:ChatGPT 能够从上下文中提取出食谱,并适当地进行格式化(甚至修正了名称中的拼写错误!),并且能够决定何时适合呈现这些内容。

真正的问题在于输出的语气实在太无聊了,这也是基本 ChatGPT 的一个共同特点和批评。即使我通过系统提示工程来解决了缺失 ID 的问题,输出听起来也不值得投入任何东西。即使我在声音质量和输出质量之间取得了平衡,代理的数量仍然会在没有我的任何过错的情况下随机失败。这个 Agent 工作流程就像一个非常脆弱的纸牌屋,我良心无法在生产应用程序中使用。

LangChain 确实具有 Custom Agent Custom Chain 的功能,因此你可以在堆栈的某些部分(也许?那里的文档很简单)覆盖逻辑,以解决我遇到的一些问题,但在这一点上,你正在使 LangChain 变得更加复杂,最好创建你自己的 Python 库,这个想法不错!

工作更聪明,而不是更努力

IMG_258

大量的随机集成引发的问题比解决方案更多。来源:LangChain 文档

LangChain 还有许多实用函数,比如文本分割器和集成的向量存储,这两者都是“与 PDF/ 你的代码聊天”的演示的重要组成部分(在我看来只是一种花招)。所有这些集成的真正问题在于它创建了一种固有的锁定,只能使用基于 LangChain 的代码,而且如果你查看这些集成的代码,它们并不是非常强大。LangChain 正在建立一堵壁垒,这对于试图获得他们 3000 万美元回报的 LangChain 投资者来说是件好事,但对于使用它的开发人员来说却是非常糟糕的。

总的来说,LangChain 体现了“它很复杂,所以一定更好!”的哲学,这困扰着后期的代码库,只是 LangChain 甚至还不到一年。将 LangChain 改造成满足我的需求所需的努力将会产生极大的技术债务。与现今的人工智能初创公司不同,对于我自己使用 LangChain 的项目来说,技术债务不能通过风险投资来偿还。API 封装应该至少在操作复杂生态系统时减少代码复杂性和认知负荷,因为处理人工智能本身已经需要足够多的脑力。LangChain 是为数不多在大多数热门用例中增加开销的软件之一。

我得出结论,制作自己的 Python 包比将 LangChain 改造以适应我的需求要容易得多。因此,我开发并开源了 simpleaichat:一个用于轻松与聊天应用进行交互的 Python 包,强调最小的代码复杂性,并将高级功能(如向量存储)与会话逻辑解耦,以避免 LangChain 的锁定,以及许多其他功能,需要一个单独的博客文章来详细介绍。

但这篇博文并不是为了通过批评竞争对手来暗中宣传 simpleaichat,就像那些搞怪的人一样。我并不想制作 simpleaichat:我宁愿把时间花在与人工智能一起创造更多酷项目上,遗憾的是,我不能用 LangChain 做到这一点。我知道有人会说:“既然 LangChain 是开源的,为什么不提交一个拉取请求到 LangChain 仓库,而不是抱怨呢?”但我的大部分抱怨都是 LangChain 库的基本问题,不能在不破坏现有用户的情况下进行更改。唯一的真正解决办法是将其全部销毁并重新开始,这就是为什么我的“创建一个用于与人工智能交互的新 Python 库”的解决方案也是最实际的。

我收到了许多消息,询问我“我应该从何开始学习 ChatGPT API”,我担心他们会因为炒作而首先去用 LangChain。如果具有技术栈背景的机器学习工程师由于不必要的复杂性而难以使用 LangChain,那么任何初学者都将被淹没。

没有人想成为像 LangChain 这样的自由开源软件的批评家,但我愿意担这个责任。清楚地说,我对 Harrison Chase 或其他 LangChain 维护者(他们鼓励反馈!)没有任何偏见。然而,LangChain 的流行已经使人工智能初创公司的生态系统围绕着 LangChain 本身以及“天哪,AGI,我创造了天网”的希望发生了扭曲,这就是我不得不对它的疑虑坦诚相待的原因。

不管软件的复杂性和流行性引发了怎样的争议,它们都是永恒的循环。在 2010 年代,是关于 React 的争论;而在 2023 年,现在则是关于 ReAct 的。

作者简介:

Max Woolf@minimaxir 是旧金山 BuzzFeed 的数据科学家,他在人工智能 / 机器学习工具和开源项目领域工作。Max 的项目由他的 Patreon 资助。

出自:https://mp.weixin.qq.com/s/y88G5e483vF83XfgVFaHkQ

© THE END

转载请联系本网站获得授权

投稿或版权问题请加微信:skillupvip