🙃

LangChain は LLM アプリケーションの開発に採用すべきではない

2023/08/24に公開

TL;DR

個人の意見ですが LangChain は不必要にコードが複雑で、設計が悪いので production での採用をおすすめしません

LangChain とは

LangChain とは、大規模言語モデル(LLM) に対して簡単なインターフェイスを提供するライブラリです。またその機能は多岐に渡り、Index (ベクタ DB を通して PDF などの外部データを用いるための機能) や Chains (一連の処理を連続実行する機能)などのモジュールを含みます。
LangChain とは - Hakky
長所として、以下が挙げられます。

  • OpenAI などの LLM Provider を使ってプログラムから回答を生成するのには良い
    • チュートリアルに沿って動かすのは高速に終わる
  • 複数の LLM Provider やベクタ DB に対して統一されたインターフェイスを提供するため、差異を吸収できる
  • プロトタイプの作成にも用途によっては適切な選択肢となりえる

お気持ち

LangChain の最も大きな問題として設計が悪いということが挙げられます。複数の LLM Provider やの処理を吸収するために過度な抽象化を行っていて、不必要な複雑化を産んでいます。実際にソースコードを見てみると処理を追うのに労力を費やすことになります。
またドキュメントも不十分であり、実際の処理や使い方を把握するためにソースコードを追うと前述の複雑化の問題が顕在化します。
加えて、PR Review があまり機能しておらず、各 LLM Provider やベクタ DB をラップするためのコードが肥大化していくなどの問題もあります。

そもそも OpenAI を使いだけであれば Completion や Embedding API を呼ぶぐらいで済むことが多く、公式ライブラリや直接 API を叩いて困ることはありません。
複数の LLM Provider やベクタ DB を吸収するラッパーとしては有用かもしれませんが、それをすることはあるでしょうか?その考慮は YAGNI 原則に反してはいないでしょうか?

LangChain の設計の悪さは Hackernews や Reddit、Twitter を見ていても痛烈に批判されていて、LangChain を不採用にしたり剥がしたりした例は散見されます。実際筆者のプロジェクトでも LangChain は不採用としました。

(訳) 残念ながら、Langchain は設計が非常に不十分であり、重複する抽象化で満たされており、多くの混乱を引き起こしています。ドキュメントの構成も不十分です

Langchain is unfortunately very poorly designed and is filled with overlapping abstractions which leads to a lot of confusion. The documentation suffers from poor organisation too
LangChain is pointless

(訳) 率直に言って、コードを見ただけで LangChain が garbage software だとわかりました。
それでも、物事がどう動くことになっているのかを理解するために、早く仕事を終わらせるのに役立っている。AI のレシピ本のようなものだ。アプローチが絞れたら、langchain がラップしていると思われるものの上にすべてを書き直すつもりだ。今のところ、個々のライブラリを探し出したり、api を学んだりするよりもその方が早い。基本的にはノートブックに残すつもりだ

Frankly I could see langchain is garbage software just by looking at the code.
It still helps me get shit done fast to figure out how things are supposed to work. Sort of a cookbook of AI recepies. Once I have an approach narrowed down I'll rewrite everything on top of stuff langchain is supposedly wrapping. For now it's faster than tracking down individual libraries and learning the apis. It will stay in notebooks basically.
The Problem With LangChain - Hacker News

具体例: ChatCompletion

具体例として OpenAI 公式ライブラリと LangChain で ChatCompletion を行う例を見て、LangChain の方を深掘りしていきます。

OpenAI 公式ライブラリで ChatCompletion を呼ぶ例

import os
import openai

openai.api_key = os.getenv("OPENAI_API_KEY")

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "user", "content": "Hello ChatGPT!"},
    ],
)

print(response["choices"][0]["message"]["content"])  # Hello! How can I assist you today?

LangChain で ChatCompletion を呼ぶ例

from langchain.chat_models import ChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage,
)

chat = ChatOpenAI()

response = chat([HumanMessage(content="Hello ChatGPT!")])

print(response.content)  # Hello! How can I assist you today?

どちらも簡単ですね。

LangChain のリクエストを送る処理

では LangChain 具体的にリクエストを送る部分を追っていきます。

  • LangChain のコードを見ると ChatOpenAI クラスの __call__ メソッドを参照すると良さそうです。

https://github.com/langchain-ai/langchain/blob/34088107481a029b60bc6dd166770e80ff72fadd/libs/langchain/langchain/chat_models/openai.py#L119

  • ChatOpenAI__call__ メソッドは親クラスの BaseChatModel で定義されています。

https://github.com/langchain-ai/langchain/blob/34088107481a029b60bc6dd166770e80ff72fadd/libs/langchain/langchain/chat_models/base.py#L544-L557

  • generate メソッドも同様に BaseChatModel で定義されています。
    • _generate_with_cache が本質的な処理に見えます

https://github.com/langchain-ai/langchain/blob/34088107481a029b60bc6dd166770e80ff72fadd/libs/langchain/langchain/chat_models/base.py#L269-L323

  • _generate_with_cacheBaseChatModel で定義されています。
    • キャッシュに関する処理を除けば _generate で処理してそうです。

https://github.com/langchain-ai/langchain/blob/34088107481a029b60bc6dd166770e80ff72fadd/libs/langchain/langchain/chat_models/base.py#L428-L465

  • _generateBaseChatModel で定義されておらず、継承元の ChatOpenAI で定義されています。
    • 今回は streaming ではないので _completion_with_retry で API を叩いてそうです

https://github.com/langchain-ai/langchain/blob/34088107481a029b60bc6dd166770e80ff72fadd/libs/langchain/langchain/chat_models/openai.py#L317-L342

  • completion_with_retryChatOpenAI で定義されています
    • 本質的な処理は self.client.create のようです

https://github.com/langchain-ai/langchain/blob/34088107481a029b60bc6dd166770e80ff72fadd/libs/langchain/langchain/chat_models/openai.py#L268-L278

  • ChatOpenAI__init__ メソッドは親クラスを見てみても見当たりません。self.client に代入している部分を探すと ChatOpenAI に以下のような処理が見当たります
    @root_validator()
    def validate_environment(cls, values: Dict) -> Dict:
	    # 省略
	    try:
		    values["client"] = openai.ChatCompletion

https://github.com/langchain-ai/langchain/blob/3c7cc4d4402a60bb43adf70f4f2037d3d784d24c/libs/langchain/langchain/chat_models/openai.py#L209-L242

  • self.clientopenai.ChatCompletion らしいので結局 openai.ChatCompletion.create を呼んでいることがわかりました
  • root_validator デコレータがついているので Pydantic の BaseModel を継承していれば validate_environment が初期化時に実行されそうです
`ChatOpenAI` が `BaseModel` を継承していることの確認
  • @root_validator デコレータのついたメソッドは Pydantic (v1) の BaseModel を継承していれば初期化時に実行されます
    • コードを追うと以下のような継承関係がわかります
      • ChatOpenAI -> BaseChatModel -> BaseLanguageModel -> Serializable -> BaseModel

https://github.com/langchain-ai/langchain/blob/3c7cc4d4402a60bb43adf70f4f2037d3d784d24c/libs/langchain/langchain/chat_models/base.py#L52

https://github.com/langchain-ai/langchain/blob/3c7cc4d4402a60bb43adf70f4f2037d3d784d24c/libs/langchain/langchain/schema/language_model.py#L49-L51

https://github.com/langchain-ai/langchain/blob/3c7cc4d4402a60bb43adf70f4f2037d3d784d24c/libs/langchain/langchain/load/serializable.py#L33

結論

以下のような流れを追うと reddit などで批判されている理由がわかるのではないでしょうか。これが恐ろしいのはこのコードは最も簡単な例ということです。streaming を使った場合の処理や、他のよりブラックボックス化されたモジュールを使うとより酷いことになりそうです。
LangChain のバグを踏んでエラートラッキングをしたり、LLM のパフォーマンスチューニングや表示の変更で細かなカスタマイズをする際にこのような悲しい体験をすることになります。そういった理由で私はアプリケーション開発時に LangChain を採用しないことに決めました。

一応フォローしておくと、継続的に開発するアプリケーションではない場合や、複数の LLM Provider を比較したい場合は選択肢の一つとして出てくるかと思っています。メリット・デメリットを比較しつつ快適な LLM ライフを送りましょう。

参考

Discussion