LangChainをやめてAnthropic SDKの直接呼び出しに移行した理由を技術的根拠とともに記録する。デバッグ困難・バージョン破壊・トークン肥大という3つの地雷を踏んだ経験から、Raw API移行の判断基準を整理した。
LangChainを使い始めた理由、やめた理由
最初にLangChainを使い始めたのは、「とにかく早くRAGを動かしたい」という事情からだった。2023年末、複数のLLMを切り替えながら試せる抽象化層は魅力的に映った。ドキュメントも豊富で、Chain・Agentの概念は直感的だった。
ところが、本番環境に載せた瞬間から問題が連続した。デバッグできない、バージョンが壊れる、トークンが爆発する。この3つの地雷を半年かけて踏み抜いた結果、Anthropic SDKの直接呼び出しに全面移行した。その記録を残しておく。
理由1:デバッグがほぼ不可能だった
LangChainの問題として最も痛かったのが、ブラックボックス化によるデバッグの困難さだ。
あるエージェントの挙動がおかしいと気づいたのは、本番リリースから2週間後。APIコストが想定の10倍近くに達していた。原因を追うため、LangChainのAgentExecutorのソースを読み始めたが、Chain・Memory・Callbackが多重に絡み合い、「どこで何が起きているか」を掴むまでに丸2日を費やした。
結論は単純だった。LangChainのConversationBufferMemoryが全会話履歴をそのままシステムプロンプトに追記し続けていた。セッションが長くなるほどトークンが積み上がり、10ターンの会話で最初の10倍のコンテキストを送り続けていた。
Anthropic SDK直接呼び出しに書き直したのは1日かからなかった。毎回渡すメッセージ配列を自分で管理し、不要な履歴を捨てる設計にしたところ、トークン消費は約80%削減された。
LangChainの抽象化はプロトタイプには便利だが、本番チューニングの段階では障害になる。何が渡されているかを完全に制御できないフレームワークは、コスト最適化の場面で詰む。
理由2:バージョンアップのたびに本番が壊れた
LangChainのリリースサイクルは速い。そして、破壊的変更が黙って入る。
2024年初頭、langchain 0.0.x系から0.1系へのアップグレード時に経験した障害が典型だ。ステージング環境では問題なく通ったのに、本番の並列処理環境でのみデッドロックが発生した。原因は内部の非同期処理クラスが静かに削除されていたことで、カスタムToolを継承していた部分が想定外の挙動をしていた。
切り戻しは即座にできたが、原因特定に3時間かかった。LangChainのCHANGELOGはあるが、カスタム継承箇所まで影響を追うのは現実的ではなかった。
Anthropic SDKのAnthropicクライアントは、APIの変更があれば明示的にバージョンを指定すれば済む。送るJSONの構造が変わるときはSDKのリリースノートに書いてある。何が変わったかが自分の目に見えるというのは、本番運用においては極めて重要な性質だ。
理由3:Claude固有の機能を活かせなかった
LangChainがOpenAIを主軸に設計されていることは、Claude APIを使う上での制約になった。
最も困ったのはプロンプトキャッシュへの非対応だ。AnthropicのClaude APIには、システムプロンプトや大量のコンテキストをキャッシュし、2回目以降のリクエストコストを最大90%削減できる機能がある。実際に使ったプロジェクトでは、月次のAPI費用が7割近く下がった。
しかし2024年時点でLangChainのChatAnthropicラッパーはこの機能に対応していなかった。extra_headersやbetasパラメータを直接渡すには、ラッパーの内部実装をフォークするか、SDKを直接呼ぶかの二択になった。
マルチモーダル処理でも同様の問題があった。ツール呼び出し結果に画像(base64)を含めてLLMに返す処理が、LangChainの文字列前提の設計では素直に書けなかった。SDK直接呼び出しならtool_resultブロックに画像コンテンツをそのまま渡せる。
新機能が出るたびにLangChainの対応を待つより、SDKを直接使う方が常に最新の機能を試せる。
移行後のコード構成
移行後の基本構成はシンプルだ。anthropicパッケージを直接使い、メッセージ配列を自前で管理する。
import anthropic
client = anthropic.Anthropic()
def call_claude(
messages: list[dict],
system: str,
model: str = "claude-opus-4-7",
max_tokens: int = 4096,
cache_system: bool = True,
) -> str:
system_block = [{"type": "text", "text": system}]
if cache_system:
system_block[0]["cache_control"] = {"type": "ephemeral"}
response = client.messages.create(
model=model,
max_tokens=max_tokens,
system=system_block,
messages=messages,
)
return response.content[0].text
これだけで、キャッシュ・ツール呼び出し・マルチモーダルすべてを制御できる。行数はLangChainのChain定義より少ない。
それでもLangChainを使う場面
LangChainを全否定するつもりはない。以下のケースでは今でも選択肢に入る。
- 複数LLMプロバイダーを同一コードで切り替えたいプロトタイプ段階
- LangGraphを使った複雑なステートマシンが必要なエージェント設計
- チーム内にLLMアプリ未経験者が多いスタートアップ初期フェーズ
要するに「早く動かす」フェーズには向いている。「安く・安定して動かす」フェーズには向いていない。その切り替えのタイミングを見誤ると、本番で余計なコストを払うことになる。
判断の基準
移行タイミングの判断基準として、自分が使っているのは以下のチェックリストだ。
- LLMへの入力をバイト単位で制御したいか
- API新機能(キャッシュ・ツール仕様更新)を即日使いたいか
- 月のAPIコストが5万円を超えているか
- 本番障害の原因をLangChain側のコードまで追えているか
ひとつでも「はい」が出たら、Raw API移行を検討する価値がある。移行コスト自体は思ったより低い。LangChainのChainは結局「メッセージを組み立ててAPIを叩く」だけなので、その処理を自前に書き直すだけだ。
LangChainを使い続けることの問題は、「使えている」という感覚のまま、コストとデバッグの問題が静かに積み上がる点にある。気づいたときには本番が複雑に絡まっている。早めに気づいた方が移行は楽だ。
🌟 お知らせ
この記事が役に立ったら、ぜひフォローやいいねをお願いします!
🐦 X: @nabe_AI_dev
AI開発の最新情報や技術Tips、開発の進捗などを定期的にツイートしています。
📝 ブログ: AI Developer Blog
AIツール開発に関する詳細な記事や実装事例を公開中です。