cv.cm

AI · 開発者向けAI

LLM API(ChatGPT APIなど)の呼び出し方は?

約15分で読めます

結論

HTTPSのPOSTリクエストを送ります。ヘッダーにAPIキーを入れ、JSONの本文にモデル名とメッセージのリストを書きます。返ってくるのは、生成されたテキストとトークン数を含むJSONです。多くのプロバイダーが OpenAI 形式のチャット補完フォーマットに対応しており、Anthropic と Gemini には独自の形式もあります。キーはサーバー側で管理し、料金はトークン単位で支払います。

JSONフォーマッタを開く

API呼び出し1回で起きること

LLM APIはWebサービスです。プログラムがテキストを送ると、プロバイダーのサーバーがモデルを実行し、結果がJSONで返ってきます。呼び出し間で記憶は引き継がれません。モデルに見せたい会話は、毎回のリクエストにすべて含める必要があります。

必要なのは3つです。APIキーを持つアカウント、エンドポイントのURL、プロバイダーのモデル一覧にあるモデルIDです。以下の例では MODEL_ID がプレースホルダーです。モデル名は頻繁に変わるので、ブログ記事ではなく、プロバイダーの公式ドキュメントから最新のIDをコピーしてください。

curl、Python、JavaScript での最小リクエスト

OpenAI が最初に公開したチャット補完(chat completions)の形式は、最も広く真似されているリクエストの形です。他の多くのプロバイダーやローカルサーバーが、ベースURLを変えるだけで対応していることもあります。最小限で実用的なリクエストは次のとおりです。

curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "MODEL_ID",
    "messages": [
      {"role": "system", "content": "You answer in one short paragraph."},
      {"role": "user", "content": "What is a context window?"}
    ]
  }'

messages のリストが会話を保持します。各メッセージには role と content があります。system メッセージは返答のルールを設定します(OpenAI の新しいモデルでは、このロールに developer という名前も使えます)。user メッセージは依頼、assistant メッセージは過去の返答で、会話を続けるときにそのまま送り返します。レスポンスを短くすると次のようになります。

{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "MODEL_ID",
  "choices": [
    {
      "index": 0,
      "message": {"role": "assistant", "content": "A context window is ..."},
      "finish_reason": "stop"
    }
  ],
  "usage": {"prompt_tokens": 27, "completion_tokens": 64, "total_tokens": 91}
}

テキストは choices[0].message.content にあります。usage オブジェクトは課金対象のトークン数です。finish_reason はモデルが止まった理由を示します。stop は最後まで書き終えたこと、length はトークン上限に達して答えが途中で切れたことを意味します。

Pythonでは、タイムアウトとリトライつきの同じ呼び出しは次のようになります。一般的な requests ライブラリを使っています。各プロバイダーの公式SDKも同じ働きをし、リトライも自動で行います。

import os, random, time
import requests

URL = "https://api.openai.com/v1/chat/completions"
API_KEY = os.environ["OPENAI_API_KEY"]   # read from the server environment

def chat(messages, model="MODEL_ID", max_attempts=5):
    body = {"model": model, "messages": messages,
            "max_completion_tokens": 500}   # many compatible APIs: "max_tokens"
    headers = {"Authorization": f"Bearer {API_KEY}"}
    for attempt in range(max_attempts):
        last = attempt == max_attempts - 1
        try:
            r = requests.post(URL, headers=headers, json=body, timeout=60)
        except (requests.Timeout, requests.ConnectionError):
            if last:
                raise
            time.sleep(2 ** attempt + random.random())   # network error: retry
            continue
        if r.status_code == 429 or r.status_code >= 500:
            if last:
                break
            backoff = 2 ** attempt + random.random()      # exponential + jitter
            try:
                wait = max(float(r.headers.get("Retry-After", 0)), backoff)
            except ValueError:
                wait = backoff
            time.sleep(wait)
            continue
        r.raise_for_status()                # 400, 401, 403: fix the request, do not retry
        data = r.json()
        return data["choices"][0]["message"]["content"], data["usage"]
    raise RuntimeError("API still failing after retries")

text, usage = chat([{"role": "user", "content": "What is a context window?"}])
print(text)
print(usage["prompt_tokens"], "in /", usage["completion_tokens"], "out")

プロバイダーごとの違い:OpenAI互換、Anthropic、Gemini

チャット補完の形は一般的ですが、普遍的ではありません。Anthropic の Messages API と Google の Gemini API には、それぞれ独自のネイティブ形式があります。どちらも互換オプションを公開していますが、全機能が最初に使えるのはネイティブAPIです。主な違いは次のとおりです。

項目OpenAI形式のチャット補完Anthropic Messages APIGemini API(ネイティブ)
エンドポイントPOST /v1/chat/completionsPOST /v1/messagesPOST /v1beta/models/{model}:generateContent
APIキーのヘッダーAuthorization: Bearer <key>x-api-key と、必須の日付ヘッダー anthropic-versionx-goog-api-key
システム指示system または developer メッセージトップレベルの system フィールドトップレベルの systemInstruction フィールド
必須フィールドmodel, messagesmodel, max_tokens, messagescontents(モデルはURLに含まれる)
返答のテキストchoices[0].message.contentcontent 配列内のテキストブロックcandidates[0].content.parts[].text
ストリーミング"stream": true、[DONE] で終わるServer-Sent Events"stream": true、名前付きServer-Sent Eventsalt=sse を付けた streamGenerateContent メソッド

Gemini はOpenAI互換のベースURLを提供しており、DeepSeek や、中国系・オープンモデル系の複数のホスティング事業者を含む多くのプロバイダーが、チャット補完の形式に対応しています。互換性は多くの場合部分的で、構造化出力、ツール呼び出し、画像入力などの高度な機能は異なることがあります。URLを変えるだけで切り替える前に、各プロバイダーの互換性ページを読んでください。

APIキーはサーバーで管理する

APIキーを持つ人は誰でも、あなたのお金を使えます。ブラウザのJavaScript、モバイルアプリ、公開リポジトリ、ページにバンドルされるフロントエンドの環境変数ファイルには、決してキーを入れてはいけません。これらはすべてユーザーが読めます。

  1. キーはサーバーの環境変数かシークレットマネージャーに保存します。
  2. WebアプリやモバイルアプリからはあなたのバックエンドのAPIエンドポイントを呼び出します。
  3. バックエンドでユーザーを確認し(ログイン、ユーザーごとのレート制限、入力の長さ)、そのうえでキーを付けてLLM APIを呼び出します。
  4. クライアントには結果だけを返します。
  5. プロバイダーのダッシュボードで月の利用上限を設定し、プロジェクトごとに別のキーを使い、漏えいしたらすぐにキーをローテーションします。

ストリーミング:書かれた順に回答を表示する

長い回答は数秒以上かかることがあります。"stream": true を指定すると、サーバーが小さな断片をServer-Sent Eventsとして送るので、画面にテキストを逐次表示できます。各イベントの行は data: で始まり、delta を持つチャンクが入ります。OpenAI形式は data: [DONE] で終わります。次の例はNode.jsサーバーで動きます。

// Node.js 18+ (server side). Prints the reply as it arrives.
const res = await fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  },
  body: JSON.stringify({
    model: "MODEL_ID",
    stream: true,
    messages: [{ role: "user", content: "Write a haiku about rain." }],
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);

const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of res.body) {
  buffer += decoder.decode(chunk, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop();                       // keep a partial line for later
  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const data = line.slice(6);
    if (data === "[DONE]") continue;          // end of stream
    const delta = JSON.parse(data).choices[0]?.delta?.content;
    if (delta) process.stdout.write(delta);
  }
}

ブラウザでストリームを表示するには、サーバーからページへチャンクを中継します。キーを付けてブラウザからプロバイダーを直接呼び出してはいけません。OpenAI形式では、stream_options で include_usage を設定しない限り、ストリームのチャンクに使用量は含まれません。課金用にトークン数が必要なら、有効にしてください。

文章ではなくJSONで受け取る

コードが回答を読むなら、JSONを要求します。レベルは2つあります。JSONモード("response_format": {"type": "json_object"})は有効なJSONを保証しますが、どのフィールドが入るかは保証しません。JSON Schemaによる構造化出力は、対応モデルで、指定どおりのフィールドと型にモデルを従わせます。

{
  "model": "MODEL_ID",
  "messages": [
    {"role": "user", "content": "Extract the total and currency: 'Amount due: 1.250,00 EUR'"}
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "invoice_total",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "total": {"type": "number"},
          "currency": {"type": "string"}
        },
        "required": ["total", "currency"],
        "additionalProperties": false
      }
    }
  }
}

スキーマを使っても、パース後の結果はコード側で検証してください。形が正しくても、数値の読み違いなど、値が間違っていることはありえます。Anthropic と Gemini も、それぞれの形式でスキーマとツール定義に対応しています。長いJSONの返答をデバッグするときは、cv.cm の JSON ツールのようなフォーマッターを使うと、入れ子の出力が読みやすくなります。

レート制限、リトライ、エラー

プロバイダーは、1分あたりのリクエスト数(RPM)と1分あたりのトークン数(TPM)の両方を制限し、先に達したほうが適用されます。制限を超えると、HTTPステータス429が返ります。エラーは種類別に処理します。

ステータス意味対処
400不正なリクエスト:フィールドの誤り、トークン数超過リクエストを直す。リトライしない。
401 / 403キーがない、間違っている、権限がないキーと権限を確認する。リトライしない。
429レート制限、または利用枠の使い切り待ってからバックオフ付きでリトライ。利用枠や請求の問題とある場合は、止めてアカウントを直す。
500 / 503サーバーエラーまたは過負荷バックオフ付きでリトライ。
タイムアウト時間内に返答がない1〜2回リトライ。長い回答にはストリーミングを使う。

指数バックオフでリトライします。約1秒、次に2、4、8秒と待ち、多数のクライアントが同じ瞬間にリトライしないよう、小さなランダムな遅延(ジッター)を加えます。レスポンスに Retry-After ヘッダーがあれば、少なくともその時間は待ちます。試行回数には上限を設けましょう。失敗したリクエストも制限に数えられることがあるため、短い間隔のループで即座にリトライすると、状況が悪化します。公式SDKを使う場合は、一部のエラーをすでにリトライしていることを覚えておいてください。

コストはトークンで数える

料金は、入力トークン(システムプロンプトと会話履歴全体を含め、送るものすべて)と、出力トークン(モデルが書くもの)に対して発生します。出力トークンのほうが、入力トークンより高く設定されているのが普通です。計算式は単純です。

cost = input_tokens  / 1,000,000 * input_price_per_million
     + output_tokens / 1,000,000 * output_price_per_million

Example with made-up prices (2.00 in, 8.00 out per million tokens):
  27 input + 64 output tokens
  = 0.000054 + 0.000512 = 0.000566 per call
  x 100,000 calls a month = about 56.60
  • 各レスポンスの usage から実際の数を読み取り、記録します。単語数からの推定はしないでください。
  • 返答ごとの長さに上限を設けます。OpenAI では max_completion_tokens(古い max_tokens は非推奨)、Anthropic や多くの互換APIでは max_tokens です。
  • チャット履歴はターンごとに増えるため、20通目のメッセージは1通目より高くつきます。古いターンは削るか要約しましょう。
  • 分類のような単純なタスクには小さなモデルを使い、大きなモデルは品質が必要な場面だけにします。
  • プロバイダーによっては、繰り返されるプロンプトの先頭部分を割引したり(プロンプトキャッシュ)、待てるジョブ向けにより安いバッチ処理を提供したりします。

よくある質問

LLM APIをブラウザから直接呼び出せますか?

自分のキーでは、安全にはできません。訪問者の誰でもページからキーを読めるからです。キーを付ける自分のサーバーを呼び出してください。例外は、ユーザーが自分のアカウントのキーを入力する場合や、ブラウザ内で完結して動くモデルです。

APIが前に言ったことを忘れるのはなぜですか?

APIはステートレスだからです。各リクエストに、モデルに見せたい過去のメッセージを含める必要があります。チャットアプリは履歴を保存し、毎回それを再送しています。

temperature(温度)は何をするもので、設定すべきですか?

temperature は単語選びのランダムさを制御します。低いほど予測しやすく、高いほど多様になります。プロバイダーや新しいモデルによってはサンプリングパラメータが制限または削除されているので、設定する前にAPIリファレンスを確認してください。

回答が途中で止まるのはなぜですか?

返答が、自分で設定したトークン上限、またはモデルの最大出力に達したためです。finish_reason(Anthropic では stop_reason)を確認してください。上限を上げるか、もっと短い回答を依頼しましょう。

無料のLLM APIはありますか?

レート制限の低い無料枠や試用クレジットを提供するプロバイダーがいくつかあり、オープンなモデルを、OpenAI互換のローカルサーバーの背後で自分のマシンで動かすこともできます。無料枠とその条件は頻繁に変わるので、プロバイダーの最新のページを確認してください。

AIチュートリアル一覧