生成AIのAPIを選ぶとき、「どのモデルが一番賢いか」だけで判断すると、実際のシステムでは期待と異なる結果になりがちです。回答品質はプロンプトとの相性に左右され、速度はネットワークや出力トークン数に影響され、料金は入力・出力トークンの比率で変わるためです。
私が3つのAPIを比較するときは、同じ質問を投げるだけでなく、モデル、プロンプト、生成条件、測定回数、採点基準を固定します。結論として、単純な総合順位を作るよりも、「品質」「レイテンシ」「1回答あたりの料金」を分けて測定し、用途ごとに判断するのが再現性の高い方法です。
1. まず比較条件を固定する
API比較の準備から結果の集計までの流れを図にすると次の通りです。
最初に比較対象のモデルIDを明示します。「最新モデル」のような曖昧な指定は避け、実行時点のモデルIDを記録します。モデルのエイリアスは更新される可能性があるため、検証日も一緒に保存します。
比較用の入力は、JSON Lines形式で管理すると扱いやすくなります。
{"id":"summary_001","prompt":"次の文章を100文字以内で要約してください。対象読者はソフトウェア開発者です。"}
{"id":"code_001","prompt":"PythonでCSVを読み込み、空行を除外して別ファイルに保存するコードを書いてください。"}
{"id":"reasoning_001","prompt":"在庫管理APIの設計で、同時更新による在庫数の不整合を防ぐ方法を説明してください。"}
最低限、次の条件を統一します。
- システムプロンプト
- temperature
- 最大出力トークン数
- ツール、Web検索、ファイル検索の有無
- 入力データと文字コード
- 実行回数
- タイムアウトとリトライ方針
今回は外部ツールを使わず、temperatureを0、最大出力トークン数を512にしました。ただし、temperatureを0にしても完全に同じ回答になるとは限りません。各問題を複数回実行し、中央値を見る必要があります。
2. 3つのAPIを同じスクリプトから呼び出す
SDKをインストールします。
pip install openai anthropic google-genai
APIキーとモデルIDは環境変数から渡します。モデルIDをコードに直接書かないことで、同じベンチマークを別のモデルにも流用できます。
$env:OPENAI_API_KEY = "..."
$env:ANTHROPIC_API_KEY = "..."
$env:GEMINI_API_KEY = "..."
$env:OPENAI_MODEL = "比較対象のOpenAIモデルID"
$env:ANTHROPIC_MODEL = "比較対象のClaudeモデルID"
$env:GEMINI_MODEL = "比較対象のGeminiモデルID"
python benchmark.py
benchmark.pyの最小例です。
import csv
import json
import os
import time
from openai import OpenAI
import anthropic
from google import genai
from google.genai import types
openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
anthropic_client = anthropic.Anthropic(
api_key=os.environ["ANTHROPIC_API_KEY"]
)
gemini_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
def call_openai(prompt):
started = time.perf_counter()
response = openai_client.chat.completions.create(
model=os.environ["OPENAI_MODEL"],
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=512,
)
elapsed = (time.perf_counter() - started) * 1000
usage = response.usage
return (
response.choices[0].message.content or "",
usage.prompt_tokens,
usage.completion_tokens,
elapsed,
)
def call_anthropic(prompt):
started = time.perf_counter()
response = anthropic_client.messages.create(
model=os.environ["ANTHROPIC_MODEL"],
max_tokens=512,
temperature=0,
messages=[{"role": "user", "content": prompt}],
)
elapsed = (time.perf_counter() - started) * 1000
return (
response.content[0].text,
response.usage.input_tokens,
response.usage.output_tokens,
elapsed,
)
def call_gemini(prompt):
started = time.perf_counter()
response = gemini_client.models.generate_content(
model=os.environ["GEMINI_MODEL"],
contents=prompt,
config=types.GenerateContentConfig(
temperature=0,
max_output_tokens=512,
),
)
elapsed = (time.perf_counter() - started) * 1000
usage = response.usage_metadata
return (
response.text or "",
usage.prompt_token_count,
usage.candidates_token_count,
elapsed,
)
providers = {
"openai": call_openai,
"anthropic": call_anthropic,
"gemini": call_gemini,
}
with open("cases.jsonl", encoding="utf-8") as source, \
open("results.csv", "w", newline="", encoding="utf-8") as output:
writer = csv.writer(output)
writer.writerow([
"case_id", "provider", "input_tokens",
"output_tokens", "elapsed_ms", "answer"
])
for line in source:
case = json.loads(line)
for name, call in providers.items():
answer, input_tokens, output_tokens, elapsed = call(
case["prompt"]
)
writer.writerow([
case["id"],
name,
input_tokens,
output_tokens,
round(elapsed, 1),
answer,
])
実運用では、429や5xxに対する指数バックオフを追加します。ただし、リトライ時間まで通常の応答速度に含めると、モデル性能とサービス混雑が混ざります。通常リクエストのレイテンシと、リトライ発生率は別々に記録するのが安全です。
3. 速度は平均値だけで判断しない
APIの速度は、少なくとも次の2種類に分けて測定します。
- TTFT(Time To First Token):最初のトークンが届くまでの時間
- E2Eレイテンシ:リクエストから全文が返るまでの時間
ストリーミングを使わない場合、測定できるのは基本的にE2Eレイテンシです。チャットUIのように体感速度を重視するなら、ストリーミングAPIでTTFTも測ります。
平均値だけでは、一時的な遅延に引っ張られます。各問題を5回以上実行し、ウォームアップの1回を除外したうえで、p50とp95を集計します。
import statistics
latencies = [820, 790, 910, 1400, 860]
print("p50:", statistics.median(latencies))
print("min:", min(latencies))
print("max:", max(latencies))
長い回答を返すモデルは、最初のトークンが速くてもE2Eでは遅く見えます。そのため、速度比較では出力トークン数も必ず併記します。
4. 料金はトークン数から計算する
料金は「1回いくら」ではなく、入力トークンと出力トークンを分けて計算します。
1回の料金 =
入力トークン数 / 1,000,000 × 入力単価
+
出力トークン数 / 1,000,000 × 出力単価
例えば入力8,000トークン、出力1,000トークンの場合は、次のように計算できます。
def estimate_cost(input_tokens, output_tokens,
input_price, output_price):
return (
input_tokens / 1_000_000 * input_price
+ output_tokens / 1_000_000 * output_price
)
cost = estimate_cost(
input_tokens=8000,
output_tokens=1000,
input_price=1.0,
output_price=5.0,
)
print(f"${cost:.6f}")
単価は検証日ごとに設定ファイルへ保存します。OpenAIは公式料金表、Claudeは公式の料金ドキュメント、Geminiは公式料金表を参照しました。
キャッシュ入力、長いコンテキスト、Batch API、推論トークン、Web検索などを使う場合は、通常の入力・出力単価だけでは計算できません。請求額とスクリプトの推定額が一致しないときは、まずキャッシュや追加ツールの利用を確認します。
5. 回答品質は盲検で採点する
品質比較では、回答を見た瞬間の印象を点数にしないようにします。回答にプロバイダー名を付けず、順番もランダムにして採点します。
私がよく使う採点項目は次の4つです。
- 正確性:事実やコードに誤りがないか
- 指示遵守:文字数、形式、対象読者などを守ったか
- 完全性:必要な観点を漏らしていないか
- 可読性:日本語として自然で理解しやすいか
各項目を0〜2点で採点し、合計点だけでなく項目別の点数も保存します。コード生成では、文章の印象よりもテストを優先します。生成コードを実行し、構文エラー、境界値、例外処理を確認します。
LLMに採点させる方法もありますが、採点者自身の好みに影響されます。まず人手の基準回答を作り、LLM判定を使う場合もサンプルを人間が再確認します。
まとめ
OpenAI、Claude、Geminiを公平に比較するには、同じプロンプトを送るだけでは不十分です。モデルID、生成条件、入力データ、実行回数を固定し、品質・速度・料金を別の指標として記録します。
実際に測定してみると、品質が高いモデルほど常に高価とは限らず、短い回答では出力トークン数の差が料金に大きく影響します。また、平均レイテンシだけではユーザー体験を説明できません。
比較結果には検証日、モデルID、単価、入力データ、集計方法を添えます。そうすれば、モデルや料金が更新された後も同じ条件で再測定でき、用途に合ったAPIを継続的に選べます。