3. 最初のストリーミングCLIチャットの構築
第1章では、最初のLLM呼び出しを行い、完全なレスポンスが一度に表示されるのを確認しました。第2章では、エージェンティックAIの概念的基礎とLangChainが存在する理由を学びました。今度は実用的なものを構築する時です:レスポンシブでプロフェッショナルな印象を与えるストリーミングチャットアプリケーションです。
ストリーミング(streaming)が重要な理由: LLMに複雑な質問をすると、完全なレスポンスを10〜30秒待つのは壊れているように感じます。ストリーミングを使用すると、トークン(token)が生成されるたびに表示され、自然な会話の流れが生まれます。この章では、ストリーミング出力、適切な設定管理、デバッグ機能、堅牢なエラーハンドリングを備えたCLIチャットアプリケーションを構築します。
構築するもの: この章の終わりまでに、以下の機能を持つ動作する chat.py スクリプトが完成します:
- LLMレスポンスをトークンごとにターミナルにストリーミング
- 環境変数からAPIキーを安全に読み込み
- 異なるモデルタイプ(チャットモデルvs推論モデル)を適切なパラメータで処理
- LLMに実際に送信される内容を検査するデバッグツールを提供
- 一般的なエラー(APIキーの欠落、ネットワーク障害、無効な入力)を適切に処理
3.1) 作業フォルダの作成とパッケージのインストール
コードを書く前に、クリーンなプロジェクト構造と適切な依存関係が必要です。このセクションでは、保守可能なPythonプロジェクトの基盤を確立します。
プロジェクト構造
チャットアプリケーション用の新しいディレクトリを作成します:
mkdir langchain-chat
cd langchain-chatPython環境のセットアップ
依存関係を分離するために仮想環境を作成します:
# 仮想環境を作成
python -m venv venv
# アクティベート (macOS/Linux)
source venv/bin/activate
# アクティベート (Windows)
venv\Scripts\activate仮想環境を使う理由は? LangChainには多くの依存関係(例:OpenAI SDK、Pydantic、非同期ライブラリ)があります。仮想環境により以下が保証されます:
- システムのPythonがクリーンな状態を保つ
- 異なるプロジェクトが異なるLangChainバージョンを使用できる
- 依存関係が再現可能(
requirements.txt経由)
アクティベートされると、ターミナルプロンプトに (venv) が表示されます。
LangChainのインストール
コアLangChainパッケージをインストールします:
pip install langchain-core==1.2.7 langchain-openai==1.1.7 python-dotenvパッケージの内訳:
langchain-core: コア抽象化(メッセージ、プロンプト、チェーン、ランナブル)langchain-openai: OpenAI固有の実装(ChatOpenAI、埋め込み)python-dotenv:.envファイルから環境変数を読み込み
バージョンに関する注意: この本は2026年1月時点でLangChain 1.2.xを使用しています。将来これを読んでいる場合は、最新バージョンについてLangChainドキュメントを確認してください。
インストールの確認
すべてが動作することを確認するための簡単なテストを作成します:
# test_install.py
try:
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
print("✓ langchain-core: OK")
print("✓ langchain-openai: OK")
print("\nインストール成功!")
except ImportError as e:
print(f"✗ インポート失敗: {e}")
print("仮想環境がアクティベートされていることを確認してください。")実行します:
python test_install.py期待される出力:
✓ langchain-core: OK
✓ langchain-openai: OK
インストール成功!「インストール成功!」と表示されれば、次に進む準備ができています。インポートエラーが発生した場合は、以下を再確認してください:
- 仮想環境がアクティベートされている(プロンプトに
(venv)が表示されているか確認) - パッケージが正常にインストールされた(
pip listを実行してみる)
requirements.txtの作成
pip installコマンドでパッケージをインストールしました。これは学習には有効ですが、より良い方法があります:requirements.txtファイルです。これはPythonプロジェクトの標準的な慣行で、いくつかの理由があります:
requirements.txtを使う理由は?
- 再現性: 他の人(または6ヶ月後の自分)が全く同じパッケージバージョンをインストールできる
- 明確な依存関係管理: プロジェクトに必要なパッケージを一目で確認できる
- チームコラボレーション: チームメンバーが同一のバージョンを使用し、「私のマシンでは動く」問題を回避
- 自動化: サーバーやCI/CDパイプラインが1行で環境をセットアップできる:
pip install -r requirements.txt
プロジェクトルートに requirements.txt ファイルを作成します:
# requirements.txt
langchain-core==1.2.7
langchain-openai==1.1.7
python-dotenv構文に注意:
==1.2.7は正確なバージョンを固定(再現性のため推奨)- バージョン指定なし(
python-dotenvのように)は最新の安定版をインストール #で始まる行はコメント
これで誰でも1つのコマンドですべての依存関係をインストールできます:
pip install -r requirements.txtこれは各パッケージを個別に入力するよりもはるかに優れています。チームメイトがプロジェクトをクローンした場合、必要なのは:
- 仮想環境を作成
pip install -r requirements.txtを実行
パッケージ名やバージョンを覚える必要はありません—すべてファイルに記載されています。
プロジェクト構造
このセクションを完了すると、フォルダは次のようになります:
langchain-chat/
├── venv/ # 仮想環境 (gitにコミットしない)
├── requirements.txt # 依存関係リスト
└── test_install.py # インストール確認スクリプト次: セクション3.2では、.envファイルを使用してAPIキーを安全に読み込む方法を示します。
3.2) .envを使った環境変数
APIキーは秘密情報です。コードにハードコーディングすることはセキュリティリスクです(特にgitにコミットする場合)。このセクションでは、標準的なアプローチを示します:.envファイルから読み込まれる環境変数です。
環境変数を使う理由は?
ハードコードされたキーの問題:
# ❌ 絶対にこれをしないでください
llm = ChatOpenAI(api_key="sk-proj-abc123...")このコードをGitHubにコミットすると、APIキーが公開されます。誰でもそれを使用し、アカウントに料金を発生させたり、キーを取り消されたりする可能性があります。
解決策: 秘密情報を環境変数に保存し、実行時に読み込みます。
.envファイルの作成
プロジェクトルートに .env ファイルを作成します:
# .env
OPENAI_API_KEY=sk-proj-your-actual-key-hereAPIキーを取得:
- platform.openai.com/api-keysにアクセス
- 新しいシークレットキーを作成
- すぐにコピー(再度表示できません)
.envファイルに貼り付け、sk-proj-your-actual-key-hereを置き換え
重要なセキュリティステップ: 他のことをする前に、APIキーがgitにコミットされないように保護します。
プロジェクトルートに .gitignore ファイルを作成し、以下の行を追加します:
# .gitignore
venv/
__pycache__/
*.pyc
.env.env行はgitにAPIキーファイルを無視するように指示します。これにより、秘密情報を誤ってバージョン管理にコミットすることを防ぎます。
現在のプロジェクト構造:
langchain-chat/
├── venv/
├── .env # APIキー (gitで無視される)
├── .gitignore # 内容: .env, venv/, など
├── requirements.txt
└── test_install.py環境変数の読み込み
python-dotenvパッケージは.envファイルをos.environに読み込みます:
# chat.py
import os
from dotenv import load_dotenv
# .envファイルを読み込み
load_dotenv()
# 環境変数にアクセス
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEYが環境に見つかりません")
print(f"APIキーが読み込まれました: {api_key[:8]}...") # 最初の8文字のみ表示load_dotenv()の動作:
- スクリプトを実行する場所から
.envファイルを検索 KEY=value形式の各行を読み取り- 各変数を
os.environに追加 - 変数がすでに設定されている場合(例:ホスティングプラットフォームによって)、上書きされません—既存の値が保持されます
LangChainでのAPIキーの使用
LangChainのOpenAI実装(ChatOpenAIなど)は、自動的にos.environのOPENAI_API_KEYを探します:
from langchain_openai import ChatOpenAI
load_dotenv()
# これは自動的にos.environ["OPENAI_API_KEY"]を使用します
llm = ChatOpenAI(model="gpt-4o-mini")LangChainの規約: api_keyパラメータなしでChatOpenAI()を作成すると、自動的に環境内のOPENAI_API_KEYを探します。これはLangChain統合全体で標準的なパターンです。
明示的なAPIキー(テストまたは複数のキー用):
llm = ChatOpenAI(
model="gpt-4o-mini",
api_key=os.environ.get("OPENAI_API_KEY")
)これは、複数のAPIキー(開発vs本番)がある場合や、どのキーが使用されているかを明示したい場合に便利です。
本番環境での環境変数
本番環境(クラウドプラットフォーム、Dockerコンテナ)では、.envファイルを使用しません。代わりに、プラットフォームの設定を通じて環境変数を構成します:
- Docker: コンテナ実行時に
-eフラグを使用 - クラウドプラットフォーム: 設定ダッシュボードで環境変数を設定
- CI/CD: シークレット管理ツールを使用
重要な点:コードは変更されません。os.environ.get("OPENAI_API_KEY")は、変数が.envファイルから来るかクラウドプラットフォームから来るかに関係なく、同じように動作します。デプロイについては後の章で詳しく説明します。
セットアップの確認
すべてが動作していることを確認するために、先ほど示した環境変数読み込みコードをテストできます。.envファイルが適切に構成されていれば、os.environ.get("OPENAI_API_KEY")はAPIキーを返します。
os.environ.get("OPENAI_API_KEY")がNoneを返す場合は、以下を確認してください:
- 環境変数にアクセスする前に
load_dotenv()を呼び出した .envがプロジェクトルートに存在するOPENAI_API_KEY=sk-proj-...が.envに正しく記述されている- プロジェクトルートディレクトリから実行している
次: セクション3.3では、ストリーミング出力を使用した実際のチャットループを実装します。
3.3) ストリーミング出力を使用したチャットループの実装
これからコアチャットループを構築します。このセクションではストリーミングを紹介します - 遅いチャットボットとレスポンシブなチャットボットの主な違いです。
ストリーミングの理解
ストリーミングなし(第1章のアプローチ):
response = llm.invoke("AIについて500語のエッセイを書いてください")
print(response.content) # 20秒待って、エッセイ全体が表示されるストリーミングあり:
for chunk in llm.stream("AIについて500語のエッセイを書いてください"):
print(chunk.content, end="", flush=True) # トークンが生成されるたびに表示されるストリーミングが重要な理由:
- 即座のフィードバック: 20秒間空白の画面を見つめる代わりに、すぐに単語が表示され始める
- 自然な会話の感覚: 人と話すのと同じように - レスポンスが一度にではなく、徐々に来る
- 時間とお金の節約: LLMが間違った答えを出し始めた場合、完全な(役に立たない)レスポンスを待つ代わりに、早期に停止できる
- より良いデバッグ: アプリケーションを構築する際、長い待ち時間の後ではなく、問題(フォーマットエラーなど)が発生したときにすぐに発見できる
ストリーミングとは実際には何か: ストリーミングは同じレスポンステキストの段階的配信です。隠れた推論や内部モデルプロセスを公開するものではありません - APIから利用可能になった部分的な出力を表示するだけです。ファイルのダウンロードのように考えてください:チャンクが到着するにつれて進行状況が表示されますが、ファイルの内容は一度にダウンロードしても断片的にダウンロードしても同じです。
チャンク境界に関する注意: チャンクは単語や文に揃うことが保証されていません。APIは効率のためにトークンを小さなバッチで送信するため、チャンクは「こん」、「にちは! どの」、「ように」、「お手伝い」、「できますか」、「?」のようになる可能性があります。これは正常で予想される動作です - 個々のチャンクから意味を解析しようとしないでください。
基本的なチャットループ
最小限のストリーミングチャットループは次のとおりです:
# chat.py
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
def main():
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
print("チャットを開始しました。'quit'または'exit'と入力して終了します。\n")
while True:
user_input = input("あなた: ")
if user_input.lower() in ["quit", "exit"]:
print("さようなら!")
break
print("アシスタント: ", end="", flush=True)
for chunk in llm.stream([HumanMessage(content=user_input)]):
print(chunk.content, end="", flush=True)
print("\n")
if __name__ == "__main__":
main()動作の仕組み:
while True:: 継続的な会話のための無限ループinput("あなた: "): ターミナルからユーザー入力を取得llm.stream([HumanMessage(...)]): LLMレスポンスをストリーミング- 特殊なパラメータを使用したストリーミング出力:
end="": 各チャンクの後に改行を追加しない(出力を同じ行に保つ)flush=True: バッファリングせずにターミナルへの即座の出力を強制
なぜ[HumanMessage(content=user_input)]なのか?
LangChainのチャットモデルは、生の文字列ではなくメッセージのリストを期待します。各メッセージには役割があります:
- HumanMessage: ユーザー入力
- AIMessage: LLMレスポンス
- SystemMessage: LLMへの指示(第4章で説明)
単一のユーザーメッセージでも、リストを渡します:[HumanMessage(content="こんにちは")]。
主な制限 - 単一ターンの会話: このチャットループは意図的にステートレスです。各リクエストは現在のメッセージのみを送信し、以前の会話履歴は送信しません。これは以下を意味します:
- LLMは以前に尋ねたことを覚えていない
- 「フランスの首都は?」と尋ねた後の「その人口は?」のようなフォローアップ質問は機能しない
- これはLLMの基本的な特性です - 明示的にコンテキストを提供しない限り、メモリはありません
制限の例:
あなた: フランスの首都は何ですか?
アシスタント: パリです。
あなた: その人口は?
アシスタント: 十分なコンテキストがありません。どの都市について尋ねていますか?while TrueループはUXの継続性を提供します(チャットを続けられます)が、各ターンは独立しています。第8章で登場: メッセージ履歴を保存し、各リクエストで再送信することで会話メモリを実装します。
チャットループの実行
python chat.py対話の例:
チャットを開始しました。'quit'または'exit'と入力して終了します。
あなた: LangChainとは何ですか?
アシスタント: LangChainは言語モデルを活用したアプリケーションを開発するためのフレームワークです。プロンプト管理、チェーン、エージェント、メモリのためのツールを提供します。
あなた: 簡単な例を教えてください
アシスタント: 基本的な例は次のとおりです: ...
あなた: quit
さようなら!ストリーミングAPIの理解
「チャンク」とは何か?
各チャンクは以下を持つAIMessageChunkオブジェクトです:
content: 生成されたテキストトークンresponse_metadata: モデル情報、トークン数など
for chunk in llm.stream([HumanMessage(content="こんにちは")]):
print(f"チャンク: {chunk}")
print(f"コンテンツ: {chunk.content}")
print(f"タイプ: {type(chunk)}")出力:
チャンク: content='こんにちは' response_metadata={'model_provider': 'openai', ...}
コンテンツ: こんにちは
タイプ: <class 'langchain_core.messages.ai.AIMessageChunk'>
チャンク: content='!' response_metadata={...}
コンテンツ: !
タイプ: <class 'langchain_core.messages.ai.AIMessageChunk'>
チャンク: content=' どの' response_metadata={...}
コンテンツ: どの
タイプ: <class 'langchain_core.messages.ai.AIMessageChunk'>完全なレスポンスの蓄積
完全なレスポンスが必要な場合があります(ログ記録、テスト、またはさらなる処理のため):
def chat_with_accumulation():
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
user_input = input("あなた: ")
full_response = ""
print("アシスタント: ", end="", flush=True)
for chunk in llm.stream([HumanMessage(content=user_input)]):
print(chunk.content, end="", flush=True)
full_response += chunk.content
print("\n")
# これで完全なレスポンスが得られます
print(f"[DEBUG] 完全なレスポンスの長さ: {len(full_response)} 文字")
return full_responseこのパターンは以下が必要な場合に一般的です:
- 会話をデータベースに保存
- 構造化データのためにレスポンスを解析
- トークン使用量やコストを計算
このセクション後のプロジェクト構造:
langchain-chat/
├── venv/
├── .env
├── .gitignore
├── requirements.txt
├── test_install.py
└── chat.py # ストリーミングチャットループ (新規!)次: セクション3.4では、スマートなパラメータ設定で異なるモデルタイプを処理する方法を示します。
3.4) スマート設定: 推論モデルvsチャットモデルのパラメータ処理
OpenAIは異なる機能と制御メカニズムを持つ2種類のモデルを提供しています:
チャットモデル(gpt-4o、gpt-4o-mini):
- 高速で会話的
- ランダム性と創造性を制御するための
temperatureをサポート - 一般的なタスク、クリエイティブライティング、日常的なコーディングに最適
推論モデル(o1、o3、GPT-5):
- 遅いがより論理的で一貫性がある
temperatureをサポートしない(代わりに内部推論を使用)- 複雑な数学、複数ステップの計画、形式的分析に最適
主な違い: チャットモデルは確率的サンプリングを使用し(ランダム性を制御)、推論モデルは決定論的な内部ロジックを使用します(モデルが独自の推論プロセスを制御)。
Temperature(チャットモデルのみ)の理解
Temperatureとは何か?
Temperatureは0.0から2.0の間の数値で、モデルのレスポンスがどれだけクリエイティブかを制御します。低い値(0に近い)では、一貫性のある予測可能な答えが得られます。高い値(2.0に近い)では、クリエイティブで多様なレスポンスが得られます。「創造性ダイヤル」のように考えてください。
動作の仕組み: 各単語を生成する際、モデルは異なる確率を持つ多くの可能な次の単語を見ます。Temperatureはモデルの選択方法に影響します:
- 低いtemperature(0.0): ほぼ常に最も確率の高い単語を選択 → 一貫性のある焦点を絞ったレスポンス
- 高いtemperature(2.0): より確率の低い単語を選択する可能性が高い → 多様でクリエイティブなレスポンス
重要: Temperatureはチャットモデル(gpt-4o、gpt-4o-mini)でのみ機能します。 確率的サンプリングの代わりに内部ロジックを使用する推論モデル(GPT-5、o1、o3)には適用されません。
Temperature値ガイド:
-
0.0: 高度に決定論的、焦点を絞った、一貫性のある
- 使用例: 事実に基づくQ&A、日常的なコード生成、構造化された出力
- 同じ入力 → ほぼ同一の出力が毎回
- 例: 「2+2は何ですか?」 → 常に「4」
-
0.7–1.0: 標準的なサンプリング動作(デフォルトは1.0)
- 使用例: 一般的な会話、説明、バランスの取れたレスポンス
- 表現や例に適度な変動
- 例: 「光合成を説明してください」 → 毎回異なる表現、同じコア情報
-
1.2–2.0: よりクリエイティブで多様、予測不可能
- 使用例: クリエイティブライティング、ブレインストーミング、アイデア出し
- トーン、構造、表現に高い変動
- 例: 「月についての詩を書いてください」 → 毎回非常に異なるスタイル
注意: 1.0を超える値は創造性を高めますが、事実の正確性と一貫性を低下させる可能性があります。最大値は2.0です。
例: Temperatureの影響(チャットモデルのみ)
# Temperature 0.0 - 決定論的、毎回同じ答え
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
response = llm.invoke([HumanMessage(content="2+2は何ですか?")])
print(response.content) # 出力: 4
# Temperature 1.0 - デフォルト動作、わずかな変動の可能性
llm = ChatOpenAI(model="gpt-4o-mini", temperature=1.0)
response = llm.invoke([HumanMessage(content="2+2は何ですか?")])
print(response.content) # 出力: 4 (簡単な説明が含まれる場合がある)閉じた事実に基づく質問の場合、temperatureは正確性にほとんど影響しません。
オープンエンドまたはクリエイティブなタスクの場合、temperatureは多様性、トーン、スタイルに大きく影響します。
推論モデルでチャットモデルパラメータを使用するとどうなるか?
モデルによって異なります - 拒否するものもあれば、黙って無視するものもあります:
# ❌ これはo3モデルで失敗します
llm = ChatOpenAI(model="o3-mini", temperature=0.7)エラー:
BadRequestError: Temperature is not supported with this model異なるモデル、異なるポリシー:
- o1 / o3モデル: サポートされていないパラメータを明示的に拒否します。temperatureが含まれている場合、APIはすぐに400 BadRequestエラーを返します。
- GPT-5モデル: より寛容 - パラメータは受け入れられますが、黙って無視されます。リクエストは成功しますが、temperatureは効果がありません。
これが重要な理由: 使用しているモデルを常に確認し、それに応じてパラメータを設定してください。間違ったパラメータを使用すると、エラーが発生するか、黙って失敗し、デバッグ時間を無駄にする可能性があります。
推論モデルの動作を制御する方法
チャットモデルがtemperatureを使用し、推論モデルが使用しないことがわかりました。では、推論モデルをどのように制御するのでしょうか?
推論モデルはパラメータではなくプロンプト設計を通じて調整されます:
- 推論モデルは
temperatureや類似の制御を公開しません - 代わりに、プロンプトの書き方によって動作をガイドします:
- 明示的な指示: 「ステップバイステップで考えてください」、「作業を示してください」
- ルールとしての制約: 「...を仮定してはいけません」、「常に検証してください...」
- 構造化された要件: 「JSON形式で出力」、「答えの前に推論を含める」
- 決定ロジック: 「条件Aの場合、Xを実行、そうでなければYを実行」
例: チャットパラメータvs推論プロンプト
# ❌ チャットアプローチ - 推論モデルでは機能しません
llm = ChatOpenAI(model="o3-mini", temperature=0.5)
# エラー: BadRequestError: Temperature is not supported
# ✅ 推論アプローチ - プロンプト構造を通じてガイド
prompt = """
この問題をステップバイステップで解決してください:
1. 知っていることを述べる
2. 計算を示す
3. 答えを検証する
問題: x + 5 = 12の場合、xは何ですか?
"""
llm = ChatOpenAI(model="o3-mini")
response = llm.invoke([HumanMessage(content=prompt)])
print(response.content)出力:
1. 知っていること: x + 5 = 12
2. 計算: x = 12 - 5 = 7
3. 検証: 7 + 5 = 12 ✓
答え: x = 7重要な洞察: チャットモデルはパラメータによって制御され、推論モデルはプロンプトによって制御されます。
モデル選択決定表
両方のタイプのモデルを制御する方法を理解したので、それぞれをいつ使用するかを示します:
| タスクタイプ | 推奨モデル | 理由 |
|---|---|---|
| 一般的な会話 | gpt-4o-mini | 高速、低コスト、会話的 |
| 簡単なQ&A | gpt-4o-mini | 事実検索に十分 |
| クリエイティブライティング | gpt-4o-mini (temp 0.8–1.0) | Temperatureが創造性を可能にする |
| コード生成 | GPT-5 | より良い論理的計画 |
| 複雑な推論 | GPT-5 | 複数ステップのロジックに最適化 |
| 数学問題 | o3 / o1 | 専用推論モデル |
| 複数ステップの計画 | GPT-5 | 長期的な計画に強い |
| 形式的分析(法律/政策) | o3 | 厳密に決定論的 |
コストとレイテンシのトレードオフ
実用的なトレードオフを理解することで、ユースケースに適したモデルを選択できます:
| モデルタイプ | 速度(典型的なレイテンシ) | コスト(相対的) | 最適な用途 |
|---|---|---|---|
| gpt-4o-mini | 非常に高速(<2秒) | 非常に低い | 一般的な会話、簡単なタスク |
| gpt-4o | 高速(1–4秒) | 中程度 | より高品質なチャット、マルチモーダルタスク |
| GPT-5 | 中程度(3–8秒) | 高い | 複雑な推論、計画 |
| o1 / o3 | 最も遅い(5–15秒以上) | 最も高い | 決定論的推論、形式的ロジック |
注意:
- 速度は典型的なレスポンスレイテンシを反映(プロンプトの長さと複雑さによって変動)
- コストは相対的な比較 - 現在の価格はOpenAIのウェブサイトで確認
- 推論モデルは速度とコストを一貫性と正確性とトレードオフ
- チャットモデルはレスポンシブ性と効率を優先
推論モデルを使用するタイミング(GPT-5、o1、o3):
- 正しい中間ステップを必要とする複数ステップの数学とSTEM(科学、技術、工学、数学)問題
- 依存関係と制約を持つ複雑な論理分析
- 複数の相互作用する原因を持つコードデバッグ
- 多くのルール、エッジケース、またはトレードオフを持つ計画タスク
- 一貫性と長期的思考を必要とするエージェントワークフロー
チャットモデルを使用するタイミング(gpt-4o、gpt-4o-mini):
- 一般的な会話とインタラクティブチャット
- 限られた推論深度を持つ簡単なQ&A
- コンテンツ生成(ブログ、要約、クリエイティブライティング)
- 日常的なコード生成とボイラープレートタスク
- 速度とコストが深い推論よりも重要なアプリケーション
次: セクション3.5では、LLMに実際に送信される内容を検査するデバッグ技術を示します。
3.5) デバッグ: レスポンスとトークン使用量の検査
LLMが予期しない動作をする場合、正確に何が送信され、受信されたかを確認する必要があります。このセクションでは、LLM呼び出しを検査し、問題をデバッグする方法を示します。
デバッグが重要な理由
一般的なデバッグシナリオ:
- 「なぜLLMはこの答えを出したのか?」 → 正確なプロンプトを確認
- 「このリクエストにいくらかかったか?」 → トークン使用量を確認
- 「なぜこれほど遅いのか?」 → レイテンシを測定
- 「メッセージフォーマットは正しいか?」 → メッセージ構造を検査
課題: llm.invoke()を呼び出すと、レスポンスオブジェクトが得られます。しかし、実際にその中には何がありますか?デバッグに利用できる情報は何ですか?
レスポンスオブジェクトの理解
デバッグする前に、llm.invoke()が何を返すかを理解する必要があります。
基本構造:
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="こんにちは")])
# レスポンスには何が含まれているか?
print(type(response)) # AIMessage
print(response.content) # 実際のテキスト
print(response.response_metadata) # トークン使用量、モデル情報など出力:
<class 'langchain_core.messages.ai.AIMessage'>
こんにちは!今日はどのようにお手伝いできますか?
{
'token_usage': {
'completion_tokens': 9,
'prompt_tokens': 8,
'total_tokens': 17
},
'model_name': 'gpt-4o-mini-2024-07-18',
'finish_reason': 'stop',
...
}レスポンスの主要部分:
response.content: LLMが生成したテキストresponse.response_metadata: 以下を含む辞書:token_usage: 使用されたトークン数(コスト計算用)model_name: レスポンスした正確なモデルバージョンfinish_reason: 生成が停止した理由(詳細はデバッグモードセクションを参照)
トークン使用量へのアクセス:
token_usage = response.response_metadata['token_usage']
print(f"プロンプトトークン: {token_usage['prompt_tokens']}")
print(f"レスポンストークン: {token_usage['completion_tokens']}")
print(f"合計: {token_usage['total_tokens']}")出力:
プロンプトトークン: 8
レスポンストークン: 9
合計: 17これが重要な理由: デバッグ、コスト追跡、プロンプトの最適化にこれらの値が必要です。
トークン使用量からのコスト計算
トークン使用量がコストを決定します。各モデルには異なる価格設定があります:
GPT-4o-mini(2026年1月時点):
- 入力: 100万トークンあたり$0.15
- 出力: 100万トークンあたり$0.60
GPT-4o:
- 入力: 100万トークンあたり$2.50
- 出力: 100万トークンあたり$10.00
コスト計算関数:
def calculate_cost(token_usage, model_name):
"""トークン使用量に基づいてコストを計算します。"""
prompt_tokens = token_usage.get('prompt_tokens', 0)
completion_tokens = token_usage.get('completion_tokens', 0)
# 100万トークンあたりの価格(2026年1月時点)
pricing = {
'gpt-4o-mini': {'input': 0.15, 'output': 0.60},
'gpt-4o': {'input': 2.50, 'output': 10.00},
'gpt-5': {'input': 1.25, 'output': 10.00},
}
if model_name not in pricing:
return None
input_cost = (prompt_tokens / 1_000_000) * pricing[model_name]['input']
output_cost = (completion_tokens / 1_000_000) * pricing[model_name]['output']
return input_cost + output_cost
# 例
response = llm.invoke([HumanMessage(content="量子コンピューティングを説明してください")])
token_usage = response.response_metadata['token_usage']
cost = calculate_cost(token_usage, "gpt-4o-mini")
print(f"コスト: ${cost:.6f}")出力:
コスト: $0.000123これが重要な理由: 本番アプリは1日あたり50,000以上のリクエストを処理できます。リクエストあたり$0.002で、月額$3,000です。間違ったモデルや肥大化したプロンプトを使用すると、コストは月額$30,000に跳ね上がります。リトライループのバグは一晩で数千ドルを消費する可能性があります。初日からトークン使用量を追跡してください。
デバッグモードの有効化(生のAPI詳細が必要な場合)
レスポンスオブジェクトとカスタムラッパーはほとんどのデバッグニーズに対応します。しかし、時にはLangChainがOpenAIに送信する正確な内容 - 生のJSONリクエストとレスポンスを確認する必要があります。
これが必要になる可能性がある場合:
- LangChainのメッセージフォーマットのデバッグ
- APIパラメータが正しく設定されていることの確認
- 予期しないAPIエラーの調査
- 正確なAPIペイロードの理解
LangChainにはlangchain_core.globals経由の組み込みデバッグログがあります:
from langchain_core.globals import set_debug
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
set_debug(True)
# これですべてのLLM呼び出しがデバッグ情報を出力します
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="こんにちは")])出力:
[llm/start] [llm:ChatOpenAI] Entering LLM run with input:
{
"prompts": [
"Human: こんにちは"
]
}
[llm/end] [llm:ChatOpenAI] [1.45s] Exiting LLM run with output:
{
"generations": [
[
{
"text": "こんにちは!今日はどのようにお手伝いできますか?",
"generation_info": {
"finish_reason": "stop",
"logprobs": null
},
"type": "ChatGeneration",
...
}
]
],
"llm_output": {
"token_usage": {
"completion_tokens": 9,
"prompt_tokens": 8,
"total_tokens": 17,
...
},
"model_provider": "openai",
"model_name": "gpt-4o-mini-2024-07-18",
...
},
}注意: 出力形式はLLMプロバイダーによって異なります。この例はOpenAIの構造を示しています。
デバッグ出力が明らかにすること:
デバッグモードは完全なLangChain → OpenAI通信フローを示します:
1. メッセージフォーマット変換:
# あなたのコード
[HumanMessage(content="こんにちは")]
# デバッグ出力で表示される内容
{
"prompts": ["Human: こんにちは"]
}デバッグモードは、LLMに送信する前にLangChainがメッセージを内部的にどのように表現するかを示します。
2. 生成完了ステータス:
"finish_reason": "stop"生成が終了した理由:
"stop": モデルがレスポンスを自然に完了した"length": max_tokens制限に達したためレスポンスが切り捨てられた"tool_calls": モデルが最終的なテキストレスポンスの代わりにツール呼び出し指示を生成して生成を終了した(第12章)"content_filter": 安全性またはコンテンツモデレーションルールによりレスポンスがブロックまたは抑制された
"length"が表示された場合、完全なレスポンスを取得するためにmax_tokensを増やしてください。
3. トークン使用量の内訳:
"token_usage": {
"completion_tokens": 9,
"prompt_tokens": 8,
"total_tokens": 17,
"completion_tokens_details": {
"reasoning_tokens": 0 # 推論モデル用(o1/o3など)
},
"prompt_tokens_details": {
"cached_tokens": 0 # プロンプトキャッシング(コスト削減)
}
}基本的なカウントを超えて、以下を確認できます:
- reasoning_tokens: 内部推論ステップ(推論モデルのみ)
- cached_tokens: キャッシュから提供されたプロンプトトークンの数(コスト削減)
4. モデルバージョンとフィンガープリント:
"model_name": "gpt-4o-mini-2024-07-18",
"system_fingerprint": "fp_8bbc38b4db"- model_name: 正確なスナップショットバージョン(時間の経過とともにレスポンスが変わる理由を説明)
- system_fingerprint: OpenAIのバックエンド構成ID(システム更新時に変更)
5. リクエストタイミング:
[llm/end] [llm:ChatOpenAI] [1.56s][1.45s]は合計リクエスト時間を示します—遅いクエリを特定するのに便利です。
次: セクション3.6では、一般的なエラーを適切に処理する方法を示します。
3.6) 障害の処理(一般的なエラーのシミュレーションと修正)
本番環境のLLMアプリケーションは予測可能な障害モードに直面します:資格情報の欠落、ネットワークタイムアウト、レート制限、無効な入力。このセクションでは、これらのエラーを適切に処理し、初日から堅牢なアプリケーションを構築する方法を示します。
6つの一般的なエラー
1. APIキーの欠落
発生するタイミング: ChatOpenAIインスタンスを作成しようとしますが、OPENAI_API_KEYが環境に設定されていません。
例:
# .envファイルが存在しない、またはOPENAI_API_KEYが定義されていない
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="こんにちは")])表示されるエラー:
OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable修正方法:
.envファイルがプロジェクトルートに存在することを確認- キー名が正確に
OPENAI_API_KEYであることを確認(よくあるタイプミス:OPENAPI_KEY) - LLMを作成する前に
load_dotenv()が呼び出されていることを確認
2. 間違ったAPIキー
発生するタイミング: .envファイルに無効、期限切れ、または誤ってコピーされたAPIキーが含まれています。
例:
# .envに: OPENAI_API_KEY=sk-invalid-key-12345
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke([HumanMessage(content="こんにちは")])表示されるエラー:
AuthenticationError: Incorrect API key provided修正方法:
- https://platform.openai.com/api-keysにアクセス
- キーがまだアクティブであることを確認(取り消されていないか期限切れでないか)
- 必要に応じて新しいキーを生成
- キー全体を注意深くコピー(よくある間違い:最初/最後の文字が欠落)
- 余分なスペースなしで
.envに貼り付け:
OPENAI_API_KEY=sk-proj-exactkeyhere3. ネットワーク障害
発生するタイミング: インターネット接続が切断されるか、リクエスト中にOpenAIのサーバーが一時的に到達不能になります。
例:
# リクエスト中にWiFiが切断される、またはOpenAI APIがダウンしている
response = llm.invoke([HumanMessage(content="こんにちは")])表示されるエラー:
APIConnectionError: Connection error修正方法:
- インターネット接続を確認
- https://status.openai.comでOpenAIのステータスを確認
4. レート制限
発生するタイミング: 短時間に多くのリクエストを送信し、APIクォータを超えます。
例:
# 1000件のリクエストを瞬時に送信
for i in range(1000):
llm.invoke([HumanMessage(content=f"リクエスト {i}")])表示されるエラー:
RateLimitError: Rate limit reached for requests修正方法:
- https://platform.openai.com/account/limitsでレート制限を確認
- より高い制限が必要な場合はプランをアップグレード
- 大規模なワークロードにはバッチ処理を使用(第6章で説明)
5. 無効なモデル名
発生するタイミング: 存在しないモデル名、またはプランで利用できないモデル名を指定します。
例:
llm = ChatOpenAI(model="gpt-99-ultra") # 存在しない
response = llm.invoke([HumanMessage(content="こんにちは")])表示されるエラー:
NotFoundError: The model `gpt-99-ultra` does not exist or you do not have access to it修正方法:
6. トークン制限超過
発生するタイミング: プロンプトが長すぎて、モデルの最大コンテキストウィンドウを超えます。
例:
# 100万文字のプロンプトを作成
huge_prompt = "x" * 1_000_000
response = llm.invoke([HumanMessage(content=huge_prompt)])表示されるエラー:
BadRequestError: This model's maximum context length is 128000 tokens. However, your messages resulted in 250000 tokens.修正方法:
- 送信前に入力長を確認
- モデルの制限を把握:
- gpt-4o-mini: 128Kトークン
- gpt-4o: 128Kトークン
- gpt-5: 400Kトークン
- 長いドキュメントの場合、チャンキングまたは要約を使用(第9章で説明)
次のステップ: 第4章では、プロンプトエンジニアリングをアプリケーションコードから分離する再利用可能なプロンプトテンプレートの設計方法を示します。