はじめに
あるAIエージェント開発において、約 3 ヶ月間 LangChain をコアの LLM 呼び出し基盤として利用していました。その後、約 2 週間の段階的な移行を経て、LangChain/LangGraph 関連の全パッケージを排除し、OpenAI SDK の直接呼出に切り替えました。
この記事では、なぜ移行を決断したのか、実際にどう移行したのか、そして移行後にどうなったか を記載します。
注意: この記事は LangChain を否定するものではありません
1. LangChain を採用した理由
当初 LangChain を採用した理由は明確でした
- プロバイダ切替の容易さ: OpenAI 以外の LLM(Claude、Gemini 等)への切替を将来容易にしたかった
- 豊富な便利機能: ツール定義、エージェントループ、コールバック、トークンカウント等が最初から揃っている
- エコシステム: LangGraph によるワークフロー制御、LangSmith によるトレース、コミュニティの統合パッケージ群
実際、LangChain のおかげでプロトタイプを素早く立ち上げることができました。@tool デコレーターでツールを定義し、create_react_agent でエージェントを構築し、ChatOpenAI でモデルを呼び出す。少ないコード量で動くものができたのは LangChain の大きな利点でした。
2. 何が問題になったのか
開発が進むにつれ LangChain の抽象化が足かせになる場面が増えてきました
2.1 マルチモーダル入力の壁
AIエージェントを作るとき、途中で画面のスクリーンショットを取得し、その画像を LLM に渡して「この画面の状態を分析してほしい」といった処理を行いたくなることがあります。
しかし LangChain を経由すると、ツールで取得した画像をそのまま LLM の入力メッセージに画像として渡すことが意外と簡単にはできません。
マルチモーダルな LLM に画像を渡す場合、通常はメッセージの中に「content block」と呼ばれる構造を使い、テキストと画像を次のように並べて渡します。
{
"role": "user",
"content": [
{"type": "text", "text": "この画面を分析してください"},
{"type": "image", "image_url": "..."}
]
}
LLM は「テキスト」と「画像」のブロックを含むメッセージを受け取ることで、画像を見ながら推論します。
ところが LangChain では、ツールの実行結果は 文字列 しか想定していないため、AIエージェントが連続してツールを自動で呼び出す処理中に、スクリーンショットツールが base64 の画像データを返しても、それを画像としてLLMに送信する方法が見つけられませんでした。
2.2 新 API 機能へのアクセス制限
OpenAI は急速に API を進化させています。以下のような機能を使いたくても、LangChain の抽象化が追いついていない、あるいは抽象化の外にあるケースがありました。
| 機能 | OpenAI SDK | LangChain 経由 |
|---|---|---|
developer ロール(system より強い指示) |
そのまま使える | 抽象化の外 |
Responses API の instructions パラメータ |
そのまま使える | 未対応の時期があった |
| Structured Outputs(strict JSON schema) |
response_format で直接指定 |
部分対応・挙動差あり |
| レスポンスのメタデータ(usage 詳細等) | 全フィールド取得可能 | 非標準項目は保持されないことがある |
LangChain の公式ドキュメントにも、「マルチモーダル API はまだプロバイダ間で十分標準化されていない」「非標準のレスポンス項目は保持されないことがある」 というニュアンスが明確にあります。これは LangChain の問題というより、共通化しようとする対象そのものが、まだ十分に共通化できる段階にないということでしょう。
2.3 抽象化の内側がブラックボックスになる
create_react_agent の内部で何が起きているのか。トークンがどう消費されているのか。会話履歴がどう累積しているのか。LangChain の抽象化が厚くなるほど、性能チューニングやデバッグが困難になりました。
たとえば、あるエージェントのトークン消費が想定の 10 倍以上になっていることに気づいたとき、原因は LangChain のエージェントループが会話履歴を累積し続けていたことでした。SDK 直接呼出に切り替え、毎回必要最小限のコンテキストだけを渡す設計にしたところ、トークン消費を推定 80% 以上削減できました。
3. LangChain が向いている場合・向かない場合
ざっくりと LangChain を利用することを考えた方が良いケースと、使わない方が良いケースをまとめてみました。
LangChain が向いているケース
| ユースケース | 理由 |
|---|---|
| RAG(検索拡張生成) | ドキュメントローダ、テキスト分割、ベクトルストア統合が充実 |
| 基本的なチャットボット | プロバイダ切替が容易、コールバック機構が便利 |
| プロトタイピング | 少ないコードで動くものが素早く作れる |
| 複数プロバイダの A/B テスト | 共通インターフェースの恩恵が大きい |
| 周辺ツールとの統合 | LangSmith(トレース)、700+ の統合パッケージ |
LangChain が窮屈になりやすいケース
| ユースケース | 理由 |
|---|---|
| マルチモーダル(画像・音声)の細かい制御 | プロバイダ固有の入出力形式が抽象化しきれない |
| 最新 API 機能の即時利用 | LangChain 側の対応を待つ必要がある |
| トークン消費の精密な制御 | 抽象化の内側で何が起きているか把握しにくい |
| プロバイダ固有の組み込みツール利用 | 共通化の範囲外になりやすい |
| 高頻度のツール呼び出しループ | エージェントの内部動作を細かく制御したい |
LangChain を使うべきかの判断基準
LLM を「テキストを入れてテキストを返す箱」として扱えるなら LangChain の利用はおすすめできる。LLM を「画像を見せてツールを呼ばせて結果を判断させるAIエージェント」を扱うなら、SDK 直接呼出の方が自然に実装できる。
4. 移行の実際
4.1 段階的移行の戦略
一括置換ではなく、モジュール単位で段階的に移行しました。
Week 1:
├── spec_review モジュール: LangChain → OpenAI SDK (Chat Completions)
├── spec_review モジュール: Chat Completions → Responses API
├── StepExecutor: 新バージョンを Responses API で新規作成
└── DialogAgent: ReAct エージェント → Responses API 直接ループ
Week 2:
├── Core エージェント群: LangChain agent を全置換
├── 旧バージョンのファイルを削除
└── 残存する LangChain import を全排除(最終一括クリーンアップ)
各フェーズで既存テスト(ユニットテスト + リプレイテスト + E2E テスト)の通過を確認してから次に進めました。これにより、移行中のデグレードリスクを最小化できました。
4.2 自前で作り直したもの
LangChain が担っていた機能のうち、実際に自前で作り直す必要があったものは想像より少なかったです。
| LangChain の機能 | 自前の代替 | 規模感 |
|---|---|---|
ChatOpenAI |
OpenAI SDK の薄いラッパー | 約 280 行 |
@tool デコレーター |
Pydantic + inspect ベースの自前デコレーター | 約 190 行 |
LangGraph StateGraph |
単純なループ + 状態辞書 | 約 130 行 |
langchain-mcp-adapters |
公式 mcp SDK の stdio_client
|
既存コードの書き換え |
BaseCallbackHandler |
自前のフック関数 | 継承を除去するだけ |
合計で約 600 行の自前コードで、LangChain が担っていた中核機能を置換できました。
4.3 概念的なコード例: ツールループの自前実装
LangChain の create_react_agent が内部で行っていることを、SDK 直接呼出で書くとこうなります。
async def run_agent_loop(client, model, instructions, tools, input_messages):
"""OpenAI Responses API を使った自前のエージェントループ"""
response = await client.responses.create(
model=model,
instructions=instructions,
input=input_messages,
tools=[to_openai_tool(t) for t in tools],
)
# ツール呼び出しがなくなるまでループ
while response.output and any(
item.type == "function_call" for item in response.output
):
tool_results = []
for item in response.output:
if item.type == "function_call":
result = await execute_tool(item.name, item.arguments)
tool_results.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(result),
})
# 前回のレスポンスを入力に含めて継続
response = await client.responses.create(
model=model,
instructions=instructions,
input=[*input_messages, *response.output, *tool_results],
tools=[to_openai_tool(t) for t in tools],
)
return response
見てのとおり、エージェントループの本質は 「ツール呼び出しがなくなるまで繰り返す」だけ です。LangChain はこれに多くの便利機能(コールバック、メモリ、ストリーミング等)を載せていますが、それらが不要な場合は、素の SDK で十分にシンプルに書けました。
5. 「LLM 切替の容易さ」について
LangChain 採用の最大の理由だった「プロバイダ切替の容易さ」についての考えを述べます。
5.1 プロンプト差し替えだけでは切り替えられない
LLM の切替は、プロンプトの差し替えだけでは吸収できません。理由は 3 つあります。
理由 1: モデルごとに指示の効き方が違う
同じ「JSON を返して」「必要ならツールを呼んで」でも、指示の粒度、few-shot の効き方、長い制約文への耐性がモデルごとに違います。OpenAI の Structured Outputs のように、API 機能としてJSON スキーマ準拠を保証する仕組みはプロバイダ固有です。
理由 2: 差はプロンプトではなく API 機能にある
画像入力の形式、ツール呼び出し仕様、ストリーミング粒度、構造化出力、組み込みツール — これらの差は文面の調整では埋まりません。
理由 3: 評価なしの「切り替え容易」は幻想
実運用では、成功率、ツール選択ミス率、JSON 破損率、レイテンシ、コスト、幻覚率で比較する必要があります。「動くか」ではなく「同等の品質で動くか」が重要です。OpenAIのgpt-4.1とgpt-4.1-miniでは「できること」が異なりますし、gpt-4.1とgpt-5.2では挙動があまりに違うため、単純なプロンプトの修正で同等の品質で動かすのは簡単ではありません。
5.2 LangChain なしでも切り替えは可能
ここで重要な視点があります。
AI によるコード生成が当たり前になった今、LLM プロバイダ切替のための抽象化レイヤーを人手で書く必要は薄れつつあるのではないか? という視点です。
以前であれば、プロバイダ切替のためのアダプタ層を「手作業で」書くのは膨大な作業でした。だからこそ LangChain のような共通フレームワークに頼る合理性がありました。
しかし現在は、AI コーディングアシスタントに「このモジュールを Claude API 用に書き換えて」と指示すれば、SDK の差分を吸収したコードを生成してくれます。
- 従来: 人手でアダプタ層を書くのは大変 → LangChain に共通化を任せる
- 現在: AI にアダプタ層を書かせる or モジュールごと書き換えさせることが現実的
もちろん、AI 生成コードの品質検証は必要ですし、すべてのケースでこのアプローチが最適とは限りません。しかし、「将来の切り替えに備えて今から抽象化しておく」必要性は、以前ほど高くない のかもしれません。もちろん、モジュール化をしておき、あとでモジュール取り替えられる設計をしておくのは自分の仕事になります。
5.3 現実的な設計
採用したアプローチは以下のとおりです。
# 自分のユースケースに合わせた薄いアダプタ
class LLMAdapter(ABC):
"""LLM を抽象化するのではなく、自分のユースケースを抽象化する"""
@abstractmethod
async def generate_structured(self, prompt, schema, images=None): ...
@abstractmethod
async def run_tool_loop(self, instructions, messages, tools): ...
@abstractmethod
async def analyze_image(self, image_b64, prompt): ...
class OpenAIAdapter(LLMAdapter):
"""OpenAI 固有機能(Structured Outputs, Responses API 等)を素直に使う"""
...
ポイントは以下の 3 つです。
- 抽象化の単位を下げる: 「LLM を抽象化」ではなく「自分のアプリに必要な能力を抽象化」
- プロバイダ固有機能を隠しすぎない: OpenAI の Structured Outputs や画像入力は素直に使う
- プロンプトは共通部分と個別部分に分ける: 1 つの万能プロンプトで全プロバイダに対応しようとしない
6. 移行の効果
定量的な効果
| 指標 | Before (LangChain) | After (SDK 直接) |
|---|---|---|
| LLM 関連依存パッケージ | 6 パッケージ + 推移的依存 39パッケージ | 2 パッケージ (openai, mcp) |
| エージェントのトークン消費 | 会話履歴累積で肥大化 | 毎回最小コンテキスト渡しで大幅削減 |
| 新 API 機能の利用 | LangChain 対応待ち | 即日利用可能 |
| 移行期間 | — | 約3日(完全排除に2週間) |
| 自前コード追加量 | — | 約 600 行 |
定性的な効果
- デバッグが容易に: LLM への入出力がすべてコントロール可能で、トークン消費やツール呼び出しの問題を直接特定できる
- 設計の自由度: 画像入力、Structured Outputs、カスタムツールループを自然な形で実装できる
- 依存関係の単純化: パッケージ更新時の互換性問題が大幅に減少
- 学習コストの移転: LangChain 固有の概念(Chain, Runnable, Callback 等)の学習が不要に。代わりに OpenAI API の理解が深まる
7. LangChain との付き合い方: 提言
LangChain の利用はやめるべきか?
開発するアプリーケーション次第です。しかし、以下の兆候があれば「OpenAI SDK 中心設計 + LangChain 補助利用」への移行を検討して良いでしょう。
- LangChain の抽象化を迂回するコードが増えている
- プロバイダ固有の新機能を使いたいが、LangChain の対応を待っている
- エージェントの内部動作を細かく制御・チューニングしたい
- マルチモーダル入出力(画像・音声)を多用している
LangChain を部品として使う
移行後も、LangChain のエコシステムの一部は引き続き有用です。
- ドキュメントローダ: PDF、Web ページ、各種フォーマットの読み込み
- テキスト分割: チャンク分割のアルゴリズム群
- ベクトルストア統合: 各種ベクトル DB への統一的なアクセス
- LangSmith: トレースと評価の基盤(SDK 直接呼出のコードとも併用可能)
段階的移行のすすめ
一括移行はリスクが高いです。以下のような段階的アプローチを推奨します。
- 独立性の高いモジュールから始める(例: データ前処理、レポート生成)
- 各フェーズで既存テストの通過を確認してから次に進む
- 旧コードは即座に削除せず、新旧並行稼働の期間を設ける
- 最後に一括クリーンアップで LangChain 依存を完全排除
8. まとめ
| 観点 | 結論 |
|---|---|
| LangChain の価値 | プロトタイピング、RAG、エコシステム連携では引き続き強力 |
| LangChain の限界 | 最新 API 機能・マルチモーダル・精密制御では窮屈になりやすい |
| 移行の現実性 | 約 600 行の自前コードで中核機能を置換可能。2 週間で完了 |
| LLM 切替の考え方 | LangChain の抽象化に頼らずとも、薄い自前アダプタ + AI コード生成で対応可能 |
| 推奨アプローチ | SDK 中心設計 + LangChain は部品として必要な箇所だけ利用 |
LLM アプリ開発のベストプラクティスは、LLM 自体の進化と同じ速度で変わり続けています。大切なのは、フレームワークに依存しすぎず、自分のユースケースに最適な抽象化の粒度を見極めること です。
そして、AI コーディングアシスタントの進化により、「将来の変更に備えて今から抽象化を厚くしておく」よりも「必要になったときに AI の力を借りて素早く書き換える」 方が、多くの場合で現実的なアプローチになるかもしれません。
本記事は 2026 年 3 月時点の情報に基づいています。LangChain および各 LLM プロバイダの API は急速に進化しており、ここで述べた課題の一部は将来改善される可能性があります。