LangChain を使わない判断基準:素の API/関数呼び出しで十分なケースと見極めポイント
LLM(大規模言語モデル)を活用したアプリケーション開発において、LangChain は非常に強力なフレームワークとして多くの開発者に利用されています。しかし、すべてのケースで LangChain が最適解とは限りません。
実は、シンプルなユースケースでは素の API 呼び出しの方が開発効率が高く、保守性も優れているケースが少なくないのです。本記事では、LangChain を使わない判断をする際の基準や見極めポイントを、具体的なコード例を交えながら詳しく解説していきますね。
背景
LLM アプリケーション開発の現状
近年、OpenAI の GPT シリーズや Anthropic の Claude など、高性能な LLM が次々と登場しています。これらを活用したアプリケーション開発では、API を直接呼び出す方法と、LangChain のようなフレームワークを利用する方法の 2 つの選択肢が存在するでしょう。
LangChain は、LLM を活用したアプリケーション開発を支援するフレームワークとして、チェーン機能、メモリ管理、エージェント機能など、豊富な機能を提供していますね。
LangChain が提供する主な機能
以下の表は、LangChain が提供する代表的な機能をまとめたものです。
| # | 機能カテゴリ | 概要 | 主な用途 |
|---|---|---|---|
| 1 | チェーン | 複数の処理を連鎖的に実行 | 多段階の推論、データ変換 |
| 2 | メモリ | 会話履歴の管理と保持 | チャットボット、対話システム |
| 3 | エージェント | ツールを使った自律的な問題解決 | 動的なタスク実行 |
| 4 | ドキュメントローダー | 様々な形式のデータ読み込み | RAG システム構築 |
| 5 | ベクトルストア連携 | 埋め込みベクトルの保存・検索 | セマンティック検索 |
LangChain を使うことで、これらの機能を簡単に実装できる一方で、プロジェクトの複雑性が増すというトレードオフも存在します。
以下の図は、LLM アプリケーション開発における 2 つのアプローチの関係性を示しています。
mermaidflowchart TB
start["LLM アプリ開発"]
simple["シンプルな要件"]
complex["複雑な要件"]
direct["素の API 呼び出し"]
framework["LangChain 利用"]
start --> simple
start --> complex
simple --> direct
complex --> framework
direct --> benefit1["・コード量が少ない<br/>・学習コストが低い<br/>・デバッグが容易"]
framework --> benefit2["・機能が豊富<br/>・開発が高速化<br/>・ベストプラクティス"]
この図からわかるように、要件の複雑さによって最適なアプローチが異なります。シンプルな要件であれば、素の API で十分な場合が多いのです。
課題
LangChain を安易に採用することのリスク
LangChain は便利なフレームワークですが、すべてのプロジェクトに適しているわけではありません。安易に採用すると、以下のような課題に直面する可能性があるでしょう。
学習コストの増加
LangChain は抽象化レイヤーが厚く、独自の概念や設計パターンを理解する必要があります。シンプルな LLM 呼び出しを実装するだけなのに、Chain、Prompt Template、Output Parser など、多くの概念を学ばなければなりません。
チームメンバー全員が LangChain に習熟していない場合、開発効率が低下するリスクがありますね。
依存関係の肥大化
LangChain をインストールすると、多数の依存パッケージが一緒にインストールされます。これにより、プロジェクトのバンドルサイズが増加し、デプロイ時間やコールドスタート時間に影響を与える可能性があるでしょう。
| # | 項目 | 素の API | LangChain |
|---|---|---|---|
| 1 | パッケージ数 | 1〜2 個 | 20 個以上 |
| 2 | バンドルサイズ | 数 KB | 数 MB |
| 3 | インストール時間 | 数秒 | 数十秒〜数分 |
| 4 | 更新頻度の影響 | 小さい | 大きい |
デバッグの複雑化
LangChain は内部で多くの処理を抽象化しているため、問題が発生した際のデバッグが困難になるケースがあります。エラーメッセージがフレームワーク内部のスタックトレースになり、本質的な問題を見つけにくくなることも。
mermaidsequenceDiagram
participant Dev as 開発者
participant App as アプリコード
participant LC as LangChain
participant API as LLM API
Dev->>App: エラー調査
App->>LC: 内部処理を追跡
Note over LC: 複数の抽象化レイヤー
LC->>API: 実際の API 呼び出し
API-->>LC: エラーレスポンス
LC-->>App: 抽象化されたエラー
App-->>Dev: 元のエラーが不明瞭
この図が示すように、LangChain を経由することで、エラーの原因究明に時間がかかる場合があるのです。
バージョン管理の難しさ
LangChain は活発に開発されており、頻繁にバージョンアップが行われます。これは進化の証でもありますが、破壊的変更が含まれることも多く、アップデート対応に工数がかかる可能性があります。
過剰な抽象化による柔軟性の低下
フレームワークが提供する抽象化は便利ですが、特殊な要件に対応する際に制約となることがあるでしょう。カスタマイズしようとすると、かえって複雑になり、素の API を使った方が簡潔に実装できるケースも存在しますね。
解決策
LangChain を使わない判断基準
LangChain を使わずに素の API で実装すべきケースを見極めるための基準を、以下に整理しました。
基準 1:処理フローがシンプルである
単一の LLM 呼び出しで完結する処理や、2〜3 ステップ程度の簡単な処理フローの場合、LangChain は不要でしょう。
具体的には以下のようなケースです。
- テキストの要約
- 単純な質問応答
- テキストの翻訳
- 感情分析
- カテゴリ分類
これらは OpenAI API や Claude API を直接呼び出すだけで十分に実装できますね。
基準 2:メモリや状態管理が不要である
会話履歴を保持する必要がない、または自前で簡単に管理できる場合は、LangChain のメモリ機能は不要です。
REST API のように、各リクエストが独立している場合や、状態をデータベースで管理する設計の場合は、素の API で十分でしょう。
基準 3:外部ツールとの連携が限定的である
LangChain のエージェント機能は、複数のツールを動的に使い分ける場合に威力を発揮します。しかし、連携するツールが固定されている、または単一のツールのみを使う場合は、自前で実装した方がシンプルになります。
基準 4:パフォーマンスが重要である
レスポンスタイムが厳しい要件や、コールドスタートを最小化したい場合、LangChain の抽象化レイヤーがオーバーヘッドになることがあるでしょう。
以下の表は、パフォーマンス面での比較です。
| # | 指標 | 素の API | LangChain | 差分 |
|---|---|---|---|---|
| 1 | 初期化時間 | 10〜50ms | 100〜300ms | 5〜10 倍 |
| 2 | メモリ使用量 | 5〜10MB | 30〜50MB | 3〜5 倍 |
| 3 | API 呼び出しオーバーヘッド | ほぼなし | 10〜30ms | 追加コスト |
基準 5:チームの習熟度が低い
プロジェクトメンバーが LangChain に不慣れな場合、学習コストを考慮すると素の API から始める方が効率的です。必要になったタイミングで LangChain に移行する戦略も有効でしょう。
判断フローチャート
以下の図は、LangChain を使うべきか素の API を使うべきかを判断するためのフローチャートです。
mermaidflowchart TD
start["LLM 機能を実装したい"]
q1{"処理ステップは<br/>3 つ以下?"}
q2{"会話履歴の<br/>管理が必要?"}
q3{"複数ツールを<br/>動的に使う?"}
q4{"RAG や<br/>ベクトル検索?"}
q5{"パフォーマンス<br/>が最重要?"}
useAPI["素の API を使う"]
useLangChain["LangChain を使う"]
start --> q1
q1 -->|はい| q2
q1 -->|いいえ| q3
q2 -->|いいえ| q5
q2 -->|はい| q3
q3 -->|いいえ| q5
q3 -->|はい| q4
q4 -->|いいえ| useAPI
q4 -->|はい| useLangChain
q5 -->|はい| useAPI
q5 -->|いいえ| useLangChain
この判断フローに従うことで、プロジェクトに最適な実装方法を選択できるでしょう。
見極めポイントの整理
判断基準をさらに整理すると、以下の 3 つの観点から評価すると良いですね。
機能面での見極め
- 必要な機能が LangChain の提供機能と一致しているか
- カスタマイズの必要性はどの程度か
- 将来的な機能拡張の可能性
技術面での見極め
- プロジェクトの技術スタックとの相性
- パフォーマンス要件
- デプロイ環境の制約(サーバーレス、エッジなど)
チーム面での見極め
- チームの LangChain 習熟度
- 学習に割ける時間
- 保守・運用体制
具体例
ケーススタディ 1:テキスト要約機能
シンプルなテキスト要約機能を、素の API と LangChain の両方で実装し、比較してみましょう。
素の API を使った実装
まず、必要なパッケージをインストールします。
bashyarn add openai
次に、OpenAI API を直接呼び出す実装です。
typescript// パッケージのインポート
import OpenAI from 'openai';
typescript// OpenAI クライアントの初期化
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
typescript// テキスト要約関数の定義
async function summarizeText(
text: string
): Promise<string> {
// API リクエストの実行
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content:
'あなたは要約の専門家です。与えられたテキストを簡潔に要約してください。',
},
{
role: 'user',
content: `以下のテキストを要約してください:\n\n${text}`,
},
],
temperature: 0.3, // 要約は一貫性を重視
max_tokens: 500,
});
// レスポンスから要約テキストを抽出
return response.choices[0].message.content || '';
}
typescript// 使用例
const longText = `
LangChain は LLM を活用したアプリケーション開発のためのフレームワークです。
チェーン機能により複数の処理を連鎖させることができ、
メモリ機能で会話履歴を管理できます...
`;
const summary = await summarizeText(longText);
console.log(summary);
この実装は約 30 行で完結し、非常にシンプルですね。OpenAI のドキュメントを読めば誰でも理解できる内容になっています。
LangChain を使った実装
同じ機能を LangChain で実装してみましょう。
bashyarn add langchain @langchain/openai
typescript// 必要なモジュールのインポート
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from 'langchain/prompts';
import { LLMChain } from 'langchain/chains';
typescript// LLM モデルの初期化
const model = new ChatOpenAI({
modelName: 'gpt-4',
temperature: 0.3,
openAIApiKey: process.env.OPENAI_API_KEY,
});
typescript// プロンプトテンプレートの定義
const template = `
あなたは要約の専門家です。
与えられたテキストを簡潔に要約してください。
テキスト:
{text}
要約:
`;
const prompt = PromptTemplate.fromTemplate(template);
typescript// チェーンの作成と実行
const chain = new LLMChain({
llm: model,
prompt: prompt,
});
typescript// 要約関数の定義
async function summarizeTextWithLangChain(
text: string
): Promise<string> {
const result = await chain.call({ text });
return result.text;
}
typescript// 使用例
const summary = await summarizeTextWithLangChain(longText);
console.log(summary);
LangChain を使った場合、約 40 行になり、やや複雑になりました。このシンプルなユースケースでは、LangChain の利点を活かせていないことがわかるでしょう。
比較結果
| # | 項目 | 素の API | LangChain | 推奨 |
|---|---|---|---|---|
| 1 | コード行数 | 約 30 行 | 約 40 行 | 素の API |
| 2 | 依存パッケージ | 1 個 | 2 個以上 | 素の API |
| 3 | 理解しやすさ | ★★★★★ | ★★★☆☆ | 素の API |
| 4 | デバッグ容易性 | ★★★★★ | ★★★☆☆ | 素の API |
| 5 | 拡張性 | ★★★☆☆ | ★★★★★ | LangChain |
このケースでは、明らかに素の API の方が適していますね。
ケーススタディ 2:エラーハンドリングの実装
実際のプロダクション環境では、エラーハンドリングとリトライ処理が重要です。これも両方の方法で実装してみましょう。
素の API でのエラーハンドリング
typescript// エラータイプの定義
class LLMError extends Error {
constructor(
message: string,
public code: string,
public statusCode?: number
) {
super(message);
this.name = 'LLMError';
}
}
typescript// リトライ処理を含む要約関数
async function summarizeWithRetry(
text: string,
maxRetries: number = 3
): Promise<string> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// API 呼び出しの実行
const response = await openai.chat.completions.create(
{
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'テキストを要約してください。',
},
{ role: 'user', content: text },
],
}
);
return response.choices[0].message.content || '';
} catch (error: any) {
lastError = error;
// エラーコードの判定
if (error.status === 429) {
// Rate limit エラー:待機してリトライ
const waitTime = attempt * 1000; // 1秒、2秒、3秒...
await new Promise((resolve) =>
setTimeout(resolve, waitTime)
);
continue;
} else if (error.status >= 500) {
// サーバーエラー:リトライ可能
continue;
} else {
// その他のエラー:即座に失敗
throw new LLMError(
error.message,
'API_ERROR',
error.status
);
}
}
}
// すべてのリトライが失敗
throw new LLMError(
`Failed after ${maxRetries} attempts: ${lastError?.message}`,
'MAX_RETRIES_EXCEEDED'
);
}
この実装では、エラーコードに応じた適切な処理を行っています。Error 429: Rate Limit Exceeded の場合は待機してリトライし、Error 500: Internal Server Error などのサーバーエラーもリトライ対象としていますね。
typescript// 使用例とエラーハンドリング
try {
const result = await summarizeWithRetry(longText);
console.log('要約成功:', result);
} catch (error) {
if (error instanceof LLMError) {
console.error(`エラーコード: ${error.code}`);
console.error(`ステータス: ${error.statusCode}`);
console.error(`メッセージ: ${error.message}`);
// エラーコードに応じた処理
switch (error.code) {
case 'API_ERROR':
// API エラー時の処理
break;
case 'MAX_RETRIES_EXCEEDED':
// リトライ上限到達時の処理
break;
}
}
}
エラー情報には具体的なエラーコードを含めることで、問題の切り分けとデバッグが容易になります。
ケーススタディ 3:ストリーミングレスポンス
リアルタイムで LLM の応答を表示したい場合、ストリーミング API を使用します。
素の API でのストリーミング実装
typescript// ストリーミング対応の要約関数
async function* streamingSummarize(
text: string
): AsyncGenerator<string> {
// ストリーミングモードで API を呼び出し
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'テキストを要約してください。',
},
{ role: 'user', content: text },
],
stream: true, // ストリーミングを有効化
});
// チャンクごとにデータを yield
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
}
typescript// Next.js API Route での使用例
// app/api/summarize/route.ts
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { text } = await request.json();
// ストリーミングレスポンスの作成
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
try {
// ストリーミング要約を実行
for await (const chunk of streamingSummarize(
text
)) {
// クライアントにチャンクを送信
controller.enqueue(encoder.encode(chunk));
}
} catch (error) {
controller.error(error);
} finally {
controller.close();
}
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
}
この実装により、ユーザーはリアルタイムで要約結果を見ることができます。素の API を使うことで、ストリーミングの制御を細かく行えるのです。
ケーススタディ 4:関数呼び出し(Function Calling)
OpenAI の Function Calling 機能を使った実装例です。
typescript// 利用可能な関数の定義
const functions = [
{
name: 'get_weather',
description: '指定された都市の天気情報を取得します',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '都市名(例:東京、大阪)',
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: '温度の単位',
},
},
required: ['city'],
},
},
];
typescript// 実際に天気を取得する関数
async function getWeather(
city: string,
unit: string = 'celsius'
): Promise<string> {
// 実際の API 呼び出しをシミュレート
return `${city}の天気は晴れ、気温は25${
unit === 'celsius' ? '℃' : '°F'
}です。`;
}
typescript// Function Calling を使った会話
async function chatWithFunctionCalling(
userMessage: string
): Promise<string> {
// 最初の API 呼び出し
const response = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: userMessage }],
functions: functions,
function_call: 'auto', // 自動的に関数を呼び出し
});
const message = response.choices[0].message;
// 関数呼び出しが必要な場合
if (message.function_call) {
const functionName = message.function_call.name;
const functionArgs = JSON.parse(
message.function_call.arguments
);
// 関数を実行
let functionResult = '';
if (functionName === 'get_weather') {
functionResult = await getWeather(
functionArgs.city,
functionArgs.unit
);
}
// 関数の結果を含めて再度 API 呼び出し
const secondResponse =
await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'user', content: userMessage },
message,
{
role: 'function',
name: functionName,
content: functionResult,
},
],
});
return secondResponse.choices[0].message.content || '';
}
return message.content || '';
}
typescript// 使用例
const result = await chatWithFunctionCalling(
'東京の天気を教えて'
);
console.log(result);
// 出力: 東京の天気は晴れで、気温は 25℃です。
このように、素の API でも Function Calling を使った高度な機能を実装できます。コードの流れが明確で、デバッグもしやすいですね。
実装パターンの整理
以下の図は、素の API を使った実装パターンの全体像を示しています。
mermaidflowchart LR
subgraph "基本パターン"
simple["シンプルな<br/>API 呼び出し"]
end
subgraph "高度なパターン"
retry["リトライ処理"]
stream["ストリーミング"]
func["Function<br/>Calling"]
end
subgraph "共通機能"
error["エラー<br/>ハンドリング"]
log["ロギング"]
cache["キャッシング"]
end
simple --> retry
simple --> stream
simple --> func
retry --> error
stream --> error
func --> error
error --> log
error --> cache
図で理解できる要点:
- 基本的なシンプルな呼び出しから始めて、必要に応じて高度な機能を追加できる
- エラーハンドリングやロギングなどの共通機能は横断的に適用可能
- 段階的に機能を拡張しやすい設計になっている
まとめ
本記事では、LangChain を使わない判断基準と、素の API で実装する際のポイントを詳しく解説しました。
重要なポイントを整理すると、以下のようになります。
素の API が適しているケース
- 処理フローが 3 ステップ以下のシンプルな要件
- 会話履歴の管理が不要、または自前で簡単に実装可能
- 外部ツールとの連携が固定的で限定的
- レスポンスタイムやコールドスタートなどパフォーマンスが重要
- チームの LangChain 習熟度が低く、学習コストを避けたい
LangChain が適しているケース
- 複雑な多段階処理が必要(5 ステップ以上)
- エージェント機能で動的にツールを選択したい
- RAG システムでベクトル検索と組み合わせる
- 様々な LLM プロバイダーを切り替えて使いたい
- チームが LangChain に習熟しており、生産性向上が見込める
実装のベストプラクティス
素の API で実装する際は、以下の点に注意すると良いでしょう。
- エラーハンドリングを適切に実装する - エラーコードを含めた詳細なエラー情報を記録し、検索しやすくする
- リトライロジックを組み込む - Rate Limit やサーバーエラーに対応できるようにする
- 型安全性を確保する - TypeScript の型定義を活用して、実行時エラーを防ぐ
- ストリーミングを活用する - ユーザー体験向上のため、長い応答はストリーミングで返す
- 段階的に機能を追加する - 最初はシンプルに実装し、必要に応じて機能を拡張していく
最終的に、LangChain を使うか素の API を使うかは、プロジェクトの要件、チームのスキルセット、パフォーマンス要求などを総合的に判断することが大切です。
「とりあえず LangChain」という選択ではなく、本当に必要な機能は何かを見極めることで、保守性が高く、パフォーマンスに優れたアプリケーションを構築できるでしょう。シンプルなケースでは、素の API の明快さと制御性の高さが大きな武器になりますね。
関連リンク
articleLangChain を使わない判断基準:素の API/関数呼び出しで十分なケースと見極めポイント
articleLangChain プロンプトのバージョニング運用:リリース・ロールバック・A/B テスト
articleLangChain JSON 出力が壊れる問題を解く:ストリーミング整形・スキーマ強制・再試行設計
articleLlamaIndex と LangChain を徹底比較:開発速度・可観測性・精度ベンチ
articleLangChain 再ランキング手法の実測:Cohere/OpenAI ReRank/Cross-Encoder の効果
articleLangChain マルチエージェント設計パターン:役割分担/階層化/仲裁/投票
articlegpt-oss 推論パラメータ早見表:temperature・top_p・repetition_penalty...その他まとめ
articleLangChain を使わない判断基準:素の API/関数呼び出しで十分なケースと見極めポイント
articleJotai エコシステム最前線:公式&コミュニティ拡張の地図と選び方
articleGPT-5 監査可能な生成系:プロンプト/ツール実行/出力のトレーサビリティ設計
articleFlutter の描画性能を検証:リスト 1 万件・画像大量・アニメ多用の実測レポート
articleJest が得意/不得意な領域を整理:単体・契約・統合・E2E の住み分け最新指針
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来