AIエンジニアがLangChainを推奨しない理由
はじめに
先日、あるスタートアップがシステムを作り直すという文脈の中で「LangChain を採用」という記述があり、X で疑問を呈したところ「LangChain v1 で大幅に改善された」「批判は古い印象に基づいている」「根拠のない批判だ」という反論がありました。
では実際にソースコードを読んで確認してみよう、というのが本記事の趣旨です。
私自身、2年前は LangChain をメインで使っていました。
当時は生成 AI の最適解がまだ見えておらず、各社 SDK のインターフェースも頻繁に変わっていた時代です。そんな中で LangChain のようなラッパーは、コンテキストスイッチを減らし、プロトタイプを素早く回すには一定の価値がありました。
しかし 2026 年、状況は変わっています。
- 各社 SDK のインターフェースは安定してきた
- AI がどの SDK のコードでもすぐ生成できる
- 実務で効くのは抽象の綺麗さより、障害時の追跡容易性
この前提で、LangChain のような厚いラッパーを入れる合理性はどの程度あるのか。
本記事ではソースコードと uv による実測値をもとに検証します。
本記事は、医師→AIエンジニアに転向した株式会社GENSHI AI代表の長嶋が執筆しました。
要するに
RAG を組むことを想定して、LangChain v1 のソースを実際に読んでみました。
問題は3つに集約されます。
-
ラッパーが厚すぎる
- Embedding: 5行→3行の短縮のために 773行のラッパーと 30個の設定パラメータ
- VectorStore: 1,296行のラッパー + 1,112行の基底クラス(非推奨が多数残る)
- 裏で何をやっているか分からず、エラー時の原因特定が遅くなる
-
肝心な部分は結局カバーされない
- PDF ローダーは pypdf 等への丸投げ(日本語対応なし)
- Chunking は品質を詰めると自前実装になる
- v1 本体に document loader がなく、community 依存
-
不要な依存が強制される
-
langsmith(モニタリング SaaS)がlangchain-coreの必須依存 -
langgraph(Agent フレームワーク)がlangchainv1 の必須依存 - 依存パッケージ数 16→34、サイズ 16MB→31MB
-
LangChain の実装を追ったりドキュメントを読む時間があるなら、公式 SDK のドキュメントをコピペして AI に書かせたほうが圧倒的に早いと思います。
SDK 直利用なら「何が起きているか」が見通しが良いので。
結論: LangChain は「初手で入れる標準」ではない。
過度な依存や抽象化が開発に与える悪影響が大きい
検証スコープ
- 検証日: 2026-02-02
- 対象バージョン
langchain==1.2.7langchain-core==1.2.7langchain-openai==1.1.7- (比較)
openai==2.16.0
- 検証方法
- LangChain モノレポのソース読解(
libs/*) -
uvで隔離 venv を作り、依存数・サイズ・import 時間を実測
- LangChain モノレポのソース読解(
1. Embeddings: 5行→3行の短縮、その裏側
まずはよく使う OpenAI Embeddings です。
SDK 直利用
from openai import OpenAI
client = OpenAI()
response = client.embeddings.create(
model="text-embedding-3-small",
input="ベクトル化したいテキスト",
)
vector = response.data[0].embedding
LangChain 経由
from langchain_openai import OpenAIEmbeddings
emb = OpenAIEmbeddings(model="text-embedding-3-small")
vector = emb.embed_query("ベクトル化したいテキスト")
見た目は 5行→3行。短くなったように見えます。
裏で動いている実装
libs/partners/openai/langchain_openai/embeddings/base.py は 773行 あります。
内訳を見ると:
| コンポーネント | 概算行数 | 内容 |
|---|---|---|
| docstring・型定義 | ~300行 | ドキュメント |
| Pydantic フィールド | ~130行 | 30個 の設定パラメータ |
validate_environment() |
~80行 | クライアント初期化、proxy 設定 |
_tokenize() |
~100行 | tiktoken/transformers でトークン分割 |
_get_len_safe_embeddings() |
~68行 | length-safe なバッチ処理 + API コール |
_aget_len_safe_embeddings() |
~74行 | 上の async 版 |
_process_batched_chunked_embeddings() |
~58行 | チャンクの加重平均 |
| エントリポイント群 | ~90行 | sync/async の embed_documents / embed_query
|
ファイル内の .create() 呼び出しは 6箇所(base.py:576, 591, 650, 667, 698, 733)。sync/async × 通常経路/length-safe経路/空文字列フォールバックの組み合わせです。
30個のフィールド vs SDK の 12 パラメータ
OpenAIEmbeddings には 30個の設定フィールドがあります。
client, async_client, model, dimensions, deployment, openai_api_version,
openai_api_base, openai_api_type, openai_proxy, embedding_ctx_length,
openai_api_key, openai_organization, allowed_special, disallowed_special,
chunk_size, max_retries, request_timeout, headers, tiktoken_enabled,
tiktoken_model_name, show_progress_bar, model_kwargs, skip_empty,
default_headers, default_query, retry_min_seconds, retry_max_seconds,
http_client, http_async_client, check_embedding_ctx_length
一方、OpenAI SDK の OpenAI() コンストラクタは 12個のキーワード引数です。
api_key, organization, project, webhook_secret, base_url,
websocket_base_url, timeout, max_retries, default_headers,
default_query, http_client, _strict_response_validation
12個でも決して少なくはありませんが、LangChain 側はその 2.5倍。Azure 対応パラメータ(openai_api_version, openai_api_type, deployment)やトークン制御パラメータ(tiktoken_enabled, embedding_ctx_length, check_embedding_ctx_length)が OpenAI クラスに混在しています。
length-safe embedding は必要か
773行の大部分を占めるのは「長いテキストを自動的にトークン分割して embedding し、加重平均で再結合する」ロジックです。
check_embedding_ctx_length=True がデフォルトのため、embed_documents() は常にこの経路を通ります(base.py:706-707 に最適化余地ありのコメントあり)。
しかし、embedding に渡す時点でテキストは通常チャンク済みです。
LangChain 自身が RAG チュートリアルで Text Splitter でのチャンキングを推奨しているにもかかわらず、embedding API 側にも分割ロジックを持っている(check_embedding_ctx_length=True がデフォルト)。
実際、ソースコード内にも以下のコメントがあります(base.py:706-707):
This could be optimized to avoid double work when all texts are short enough.
(短いテキストの場合、二重処理を避けるよう最適化できる余地がある)
開発者自身が「二重処理」の可能性を認識しているわけです。
2行の短縮のために 773行の内部挙動を引き受けるかどうか。
LangChainの内部実装やパラメータを考えながらコードを書くのは正直苦痛です。
2. Chunking は結局、自前実装になる
「LangChain が chunking してくれる」は、実務ではほぼ価値になりません。
理由は単純で、chunking 戦略はデータ特性で毎回変わるからです。
- 規約文書: 見出し境界を優先
- 技術文書: code block を壊さない
- 日本語 PDF: 改行崩れを補正してから分割
- テーブル中心: 行列構造を保持
品質を詰めると、最低でもこの程度の前処理は自前で書くことになります。
import re
def normalize_japanese_pdf_text(text: str) -> str:
text = text.replace("\u3000", " ")
text = re.sub(r"[ \t]+", " ", text)
text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r"([。!?])\n", r"\1", text)
return text.strip()
def split_by_sentence_window(text: str, max_chars: int = 900) -> list[str]:
sentences = re.split(r"(?<=[。!?])", text)
chunks: list[str] = []
buf = ""
for sentence in sentences:
if not sentence:
continue
if len(buf) + len(sentence) <= max_chars:
buf += sentence
continue
if buf:
chunks.append(buf)
buf = sentence
if buf:
chunks.append(buf)
return [c.strip() for c in chunks if c.strip()]
この時点で「ラッパーが全部やってくれる」は成立しません。
重要なロジックは自前になる。なら最初から自前でいいはず。
3. v1 の RAG 構造: 本体外に分散している
v1 本体のディレクトリ
libs/langchain_v1/langchain/ は以下の構成です。
libs/langchain_v1/langchain/
├── agents/
├── chat_models/
├── embeddings/
├── messages/
├── rate_limiters/
└── tools/
document_loaders/ は存在しません。
PDF ローダーの所在
PDF ローダーは langchain_classic(レガシー)側にありますが、実体ではなく import を転送する shim(中継スタブ)です。
# libs/langchain/langchain_classic/document_loaders/pdf.py (65行)
DEPRECATED_LOOKUP = {
"UnstructuredPDFLoader": "langchain_community.document_loaders",
"PDFPlumberLoader": "langchain_community.document_loaders",
"PyMuPDFLoader": "langchain_community.document_loaders",
"PDFMinerLoader": "langchain_community.document_loaders",
}
def __getattr__(name: str) -> Any:
return _import_attribute(name)
parsers/pdf.py も同様に 50行の redirect です。
実装本体は langchain-community パッケージにあります。
つまり、v1 だけでは RAG の基本部品が完結せず、実運用では langchain-community への依存が事実上必要です。
日本語 PDF に対するラッパーの価値
LangChain の PDF ラッパー層は日本語に関して何もしていません。
CJK フォントの処理、改行処理、レイアウト解析は全て下位ライブラリ(pypdf, pdfminer, pdfplumber)に丸投げです。
日本語 PDF の品質は「どの LangChain ローダーを選ぶか」ではなく「どの下位ライブラリを使うか」で決まります。であれば、下位ライブラリを直接使って、そのドキュメントだけを読めばよい。
結局LangChain を挟んでしまうと、LangChain のドキュメントと下位ライブラリのドキュメントの両方が必要になります。
4. VectorStore: ラッパー層の厚さ
Qdrant SDK 直利用
from openai import OpenAI
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, PointStruct, VectorParams
openai_client = OpenAI()
qdrant = QdrantClient(":memory:")
qdrant.create_collection(
collection_name="docs",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
texts = ["LangChainは初手で要らない", "SDK直利用で十分だ"]
embed_res = openai_client.embeddings.create(
model="text-embedding-3-small",
input=texts,
)
vectors = [item.embedding for item in embed_res.data]
points = [
PointStruct(id=i, vector=vectors[i], payload={"text": texts[i]})
for i in range(len(texts))
]
qdrant.upsert(collection_name="docs", points=points)
query_vector = openai_client.embeddings.create(
model="text-embedding-3-small",
input="LangChain",
).data[0].embedding
hits = qdrant.search(
collection_name="docs",
query_vector=query_vector,
limit=3,
)
for hit in hits:
print(hit.payload["text"], hit.score)
embedding の呼び出しが明示的で、デバッグしやすい。
LangChain 経由
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams
qdrant_client = QdrantClient(":memory:")
qdrant_client.create_collection(
collection_name="docs",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
store = QdrantVectorStore(
client=qdrant_client,
collection_name="docs",
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
)
docs = [
Document(page_content="LangChainは初手で要らない", metadata={"id": 1}),
Document(page_content="SDK直利用で十分", metadata={"id": 2}),
]
store.add_documents(docs)
results = store.similarity_search("LangChain", k=3)
for doc in results:
print(doc.page_content, doc.metadata)
見た目の行数差は小さいですが、背後では:
| ファイル | 行数 |
|---|---|
langchain_core/vectorstores/base.py |
1,112行 |
langchain_qdrant/qdrant.py |
1,296行 |
langchain_qdrant/vectorstores.py(deprecated Qdrant クラス) |
2,332行 |
が読み込まれます。
なお、deprecated(非推奨) の Qdrant クラス(2,332行)は @deprecated(非推奨) デコレータ付きですが、__init__.py で export されており、integration test でも使用されています。
完全な dead code ではありませんが、新規利用は非推奨で、QdrantVectorStore(1,296行)に移行が促されています。
「薄いラッパー」ではなくかなり厚い抽象層を追加、かつ非推奨の実装がそのまま残されているのが実態です。
5. 依存・サイズ・import 時間(実測)
依存とサイズ
uv でクリーン環境を作って計測した結果です。
| パッケージ | 依存パッケージ数 | サイズ |
|---|---|---|
openai==2.16.0 |
16 | 16MB |
langchain-openai==1.1.7 |
34 (+18) | 31MB (+15MB) |
anthropic==0.77.0 |
16 | 12MB |
langchain-anthropic==1.3.1 |
32 (+16) | 23MB (+11MB) |
langchain==1.2.7 |
32 | 22MB |
pypdf |
1 | 1.5MB |
langchain-openai は openai 直利用に比べて 依存 2.1倍、サイズ 1.9倍 です。
追加される主な依存パッケージ
openai SDK 直利用に比べて、langchain-openai は直接依存として langchain-core と tiktoken を追加し、langchain-core 経由で langsmith(= requests 系の依存)まで入ります。
※ 依存の段階は各 pyproject.toml と uv.lock で確認。
langsmith 依存の実態
langsmith は LangChain Inc. が提供する LLM モニタリング SaaS のクライアントライブラリです。
これが langchain-core の必須依存に入っています:
# libs/core/pyproject.toml
dependencies = [
"langsmith>=0.3.45,<1.0.0",
...
]
さらに、langchain_core 内の 7ファイルで直接 import されています(try/except ImportError なし):
-
callbacks/manager.py—from langsmith.run_helpers import get_tracing_context -
runnables/config.py—from langsmith.run_helpers import _set_tracing_context, get_tracing_context -
tracers/langchain.py,tracers/context.py,tracers/schemas.py,tracers/evaluation.py document_loaders/langsmith.py
import langchain_core 自体は成功しますが、langchain_core.runnables.config や langchain_core.callbacks.manager など主要モジュールは langsmith がないと ModuleNotFoundError になります。実質的に切り離せない依存です。
langgraph の強制依存
# libs/langchain_v1/pyproject.toml
dependencies = [
"langchain-core>=1.2.7,<2.0.0",
"langgraph>=1.0.7,<1.1.0",
]
Agent を使わないユーザーにも langgraph, langgraph-checkpoint, langgraph-prebuilt, langgraph-sdk が入ります。
HTTP クライアントの重複
langsmith は httpx と requests の両方に依存しています。
openai SDK は httpx のみなので、LangChain を挟むことで requests 系(requests, urllib3, requests-toolbelt, charset-normalizer)が追加されます。
import 時間
同一環境で 12回測定(外れ値除外):
| モジュール | 平均 import 時間 |
|---|---|
import openai |
0.213秒 |
import langchain_openai |
0.377秒 |
約 1.77倍。1回あたりは小さく見えますが、CI や短命プロセスでは積み上がります。
6. なぜLangChainはおすすめできないのか
ここまでの事実を実務判断に落とすと、こうなります。
LangChain を挟むコスト
- 依存の増加: 16→34パッケージ、16MB→31MB
- 中間層の追加: エラー時に「LangChain → SDK → 外部サービス」と層が増え、原因切り分けが遅い
- 設定の過多: 30個のフィールドは AI にも人間にも読ませるコストになる
- 暗黙の動作: embedding の自動分割・再結合など、意図しない処理が隠れる
- v1 の不完全性: document loader が本体に無く、community 依存
- langsmith の混入: 監視 SaaS クライアントが core に直接依存
LangChain を挟む便益
- コード短縮: 5行→3行(2行の短縮)
- プロバイダ切り替え: 1社だけなら不要、2社以上なら検討に値する
- langsmith: 必要なら有用だが、全員が必要なわけではない
判断基準
次の条件に多く当てはまるなら、SDK 直利用を推奨します。
- 利用するモデル提供元が 1〜2社程度
- 失敗時に最短で原因追跡したい
- 依存サイズを抑えたい
- SDK の新機能を即日使いたい
- chunking や前処理はプロダクト固有のロジックで行う
まとめ
LangChain v1 は整理されました。これは事実です。
しかしソースと実測を見る限り、ラッパーは依然として厚く、依存は重く、しかもv1 本体だけでは完結しない領域が残ります。
したがって、2026年時点ではあまり推奨できないと感じます。
AI がコードを書いてくれる時代に、「5行を3行にする」ためだけに 18パッケージと 773行のラッパーを背負う理由は薄いはずです。
無理な抽象化のために、コンテキストの読み込みコストをいたずらに増やしたり、追跡を複雑にする必要はないと考えます。
Discussion
たくさんの証拠をご提示いただきありがとうございます。
私も LangChain は採用すべきではないと感じていたので、整理いただき助かりました。
理由 (1) OpenAIなど最新のAPIを試したいのに LangChain が追随していないとフラストレーション
理由 (2) LangSmith などのトレーシングはPJの最初の方にしか使わず、自前で実装すれば事足りる
あとは仰られている通りで「邪魔な抽象レイヤー」というイメージしか持てなかったです。
記事を拝読しました。LangChain v1の依存管理について詳細な調査をされており、大変興味深く読ませていただきました。いくつか技術的な観点から気になる点がございましたので、コメントさせていただきます。
1. 因果関係について
この表現ですが、因果関係が逆ではないでしょうか。18パッケージの主目的は、マルチプロバイダー対応(OpenAI/Anthropic/Google/AWS/Azure)、統一インターフェース、リトライ機構、ストリーミング、langsmith統合といった機能拡張かと思われます。コード短縮は副次的な結果に過ぎません。
記事では「1-2プロバイダー」という単一プロバイダー環境を想定されているようですが、この前提条件を冒頭で明示されていないため、あたかも「コード短縮のため"だけ"に依存が増えている」という誤解を招く表現になっているように見受けられます。
2. 測定環境の再現性について
import時間の測定(0.213秒→0.377秒、1.77倍)について、macOS/CPython 3.12という情報は記載されていますが、以下の情報が欠けているため、再現性の観点で懸念があります:
__pycache__の有無、コールドスタート vs ウォームスタート)openai先測定→キャッシュウォーム状態でlangchain_openai測定の可能性)0.164秒という差は、18個の追加
.pyファイルのディスクI/Oやファイルシステムキャッシュの影響を強く受けるため、上記の情報がないと他環境での再現が困難ではないかと思います。3. 測定対象について
「抽象化のコスト」を評価されるのであれば、import時間(開発体験の指標)だけでなく、実行時のパフォーマンス指標も測定されるべきではないでしょうか:
現状の記事では、開発体験(import時間、依存数)のみを測定されており、実運用環境でのパフォーマンスオーバーヘッドが未測定のように見受けられます。これでは「依存管理の複雑性批判」にはなっても、「技術的な抽象化コスト分析」としては不十分ではないでしょうか。
4. LangChain v1の本質的価値について
記事では触れられていませんが、LangChain v1の最重要な功績は**「v1→v2まで破壊的変更なし」を保証する初の安定版**である点です。プロダクション環境では以下が決定的な価値を持ちます:
import時間や依存数は開発体験の問題ですが、プロダクション環境では「API安定性」「保守性」「将来対応力」が支配的指標になります。これらの観点からの評価が欠けているのは残念に感じました。
まとめ
記事の主張「単一プロバイダー環境ではLangChainは過剰」という結論自体は妥当かもしれませんが、以下の点で技術的な分析としては不十分と思われます:
もしよろしければ、上記の観点も含めて追加の検証をされると、より説得力のある記事になるのではないでしょうか。
まずマークダウンの**が残っていたのはZennの書式の問題であり、AI生成の根拠にはなりません。内容で判断していただきたい。
本題に入ります。記事では「抽象化のコスト」を論じていますが、測定しているのはimport時間(0.164秒差)のみです。これは開発環境のコールドスタートの話であり、実行時オーバーヘッドとは全く別の指標です。LLMのAPI呼び出しは1回あたり数百ms〜数秒かかります。その前で0.164秒のimport差は本番環境では無意味です。
「抽象化のコスト」を評価するなら実行時レイテンシ(SDK直 vs LangChain経由でのAPI呼び出し時間差)、メモリオーバーヘッド、スループットを測定すべきです。これは計測方法論の基本でありLLMの受け売りではありません。
773行のラッパーについても、マルチプロバイダー対応・リトライ機構・ストリーミング対応を含む設計と「コード短縮のため"だけ"」を混同されています。記事の前提が「1-2プロバイダー環境」であるなら、それを冒頭で明示すべきです。前提を隠して結論だけ述べるのはミスリーディングです。
「コードを読んで人間の言葉で殴ってくれ」とのことでしたので申し上げます。import時間と実行時オーバーヘッドの区別は基礎中のド基礎です。コードを読む前にまず技術的知見が不足しているので勉強し直してください。
個人的には、LangChainよりもVercel AI SDKの方が良いんじゃない?程度で、書かれていない要件などがあり得るでしょうから否定まではすべきではないと考えます。
そもそも、この記事がPython版のLangChainありきでの話である時点で微妙な話ですね。
今のLangChainは、Python版もTypeScript版のどちらも LangChain と呼ばれるプロダクトとなっている(以前はLangChain.js と区別されていたが、今は単に LangChain と呼ぶ場合はどちらを指すかは前提を置かないと判断が付かない)のです。
故に、件のスタートアップ企業がPython版のLangChainを使用するという前提は、根拠に欠く話です。
Next.jsに組み込んで使うのであれば、TypeScript版を使用する可能性も大いにあります。
他の方もご指摘されているようにモデルプロバイダーのSDK直接ではなく抽象化する必要性という部分をご理解なさられていないものかと。
特定のモデルプロバイダーに依存していて、そのプロバイダーのモデルの性能劣化が著しいとする。
即座にプロバイダーごとモデルを切り替える際に、特定のプロバイダーの機能に依存させていては迅速な切り替えが難しくなります。
故に、TypeScript であれば LangChain でなくても Vercel AI SDK などの抽象化層を挟むでしょう。
そのほうが、システムプロンプトと使用するモデルだけを変更すれば、他のコードを書き換える必要がありません。
いくら、コーディングエージェントを使って変更するとしても、既に安定して動作しているコードを意味もなく書き換える必要性がなく、書き換えてしまったが故に要らぬエンバグさせる可能性すらあるわけです。
そのための、モデルプロバイダーのSDKの隠蔽が重要となります。
勿論、絶対にモデルプロバイダーを切り替えることなどしないという前提であれば、そういうスタイルに限っては抽象化は不要でしょう。
でも、貴方の仰られるような画一的な使い方だけとは限らないのです。
Retriever の抽象化もそう。VectorStore は Retriever の一種でしかなくて、その先が GraphDB かも知れませんし、Amazon Bedrock Knowledgebase かも知れません。
Bedrock Knowledgebaseなら、チャンキング周りはわざわざ LangChain を使ったコードを書かずにVectorStoreなりに書き込ませられますし、チャンキング戦略もある程度は選択できます。(まぁ、LangChainでもできたはずですけど)
Qdrantを例に挙げられていますが、Retrieverが検索する先のデータソースに何を使うかが書かれていないため決めつけに近い。
仮に抽象化が要らないという例だとしたら、Retriever を OpenSearch Service から Bedrock Knowledgebase に移行するのに、わざわざ Retriever を呼び出している部分まで変更しないとならないのか?となります。
また、LangChain の v1 では、LangChain の createAgent() の checkpointer に LangGraph の CheckPointer API を使います。
チャット履歴の保存を必要としないならば確かに不要となりますが、チャットエージェントとして提供する場合は同一スレッド(セッション)において、チャット履歴を元に会話を成り立たせる必要が生じることが多いはずです。
そのため、不要なLangGraphに依存しているという指摘自体が誤りです。
LangSmithではなく、他の Observability プラットフォームを使用したいのに、LangSmith への依存が必須なのは良くないというのは確かにわかる話ですが、可視化・監視を開発時にしか使用しないという指摘ですので、「本当に本番運用されている方なのか?」と疑問すら抱きます。
例えば、Agentic RAG を構築して提供している場合に、本番環境では想定よりもレスポンスが遅いとします。
どこが遅いか?などはトレースを確認して切り分けを行うはずです。