cv.cm

AI · 開發者看 AI

如何呼叫 LLM API?

閱讀約 11 分鐘

簡答

發送一個 HTTPS POST 請求,在標頭放入 API 金鑰,並在 JSON 主體中指定模型和訊息清單。回應是包含生成文字與 token 用量的 JSON。許多供應商接受 OpenAI 風格的 chat completions 格式,Anthropic 與 Gemini 則另有各自的格式。金鑰請放在伺服器端,並依 token 計費。

開啟JSON 格式化

一次 API 呼叫發生了什麼事

LLM API 是一種網路服務。你的程式送出文字,供應商的伺服器執行模型,結果以 JSON 傳回。呼叫之間沒有記憶:每個請求都必須帶上模型該看到的完整對話。

你需要三樣東西:一個有 API 金鑰的帳號、端點的網址,以及供應商模型清單中的模型 ID。下面的範例中,MODEL_ID 只是佔位符。模型名稱經常更換,請從供應商的文件複製目前的 ID,而不是從部落格文章。

用 curl、Python 與 JavaScript 寫出最簡單的請求

chat completions 格式最早由 OpenAI 發布,是被最廣泛仿效的請求形式。許多其他供應商和本機伺服器都接受它,有時只需要換一個基礎網址。以下是最小而實用的請求:

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 物件記錄你被計費的 token 數。finish_reason 說明模型為什麼停下來:stop 表示正常結束,length 表示達到你設定的 token 上限,答案被截斷了。

在 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

chat completions 的形式很常見,但並非通用。Anthropic 的 Messages API 和 Google 的 Gemini API 都有自己的原生格式。兩家供應商也都提供相容選項,但原生 API 會優先開放所有功能。主要差異如下:

項目OpenAI 風格 chat completionsAnthropic Messages APIGemini API(原生)
端點POST /v1/chat/completionsPOST /v1/messagesPOST /v1beta/models/{model}:generateContent
API 金鑰標頭Authorization: Bearer <key>x-api-key 加上必要的 anthropic-version 日期標頭x-goog-api-key
系統指示system 或 developer 訊息頂層的 system 欄位頂層的 systemInstruction 欄位
必要欄位model、messagesmodel、max_tokens、messagescontents(模型在網址中)
回覆文字choices[0].message.contentcontent 陣列中的文字區塊candidates[0].content.parts[].text
串流"stream": true,以 [DONE] 結尾的 server-sent events"stream": true,具名的 server-sent events搭配 alt=sse 的 streamGenerateContent 方法

Gemini 提供相容 OpenAI 的基礎網址,包括 DeepSeek 以及多家中國與開源模型託管商在內的許多供應商,也接受 chat completions 格式。相容性通常只是部分的:結構化輸出、工具呼叫或圖片輸入等進階功能可能有差異。只換網址就切換供應商之前,請先閱讀各家的相容性說明頁。

API 金鑰要留在伺服器端

任何拿到你 API 金鑰的人,都能花你的錢。千萬不要把它放在瀏覽器 JavaScript、行動應用程式、公開儲存庫,或會被打包進頁面的前端環境檔案裡,這些地方使用者都讀得到。

  1. 把金鑰存放在伺服器上的環境變數或密鑰管理工具中。
  2. 讓你的網頁或行動應用程式呼叫你自己的後端端點。
  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,否則串流區塊不包含用量,所以如果需要 token 數來計費,請開啟它。

取得 JSON 而不是散文

當程式要讀取答案時,就要求輸出 JSON。有兩個層級。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
      }
    }
  }
}

即使有 schema,也要在程式中驗證解析後的結果。就算結構正確,值仍然可能出錯,例如看錯數字。Anthropic 和 Gemini 以各自的格式支援 schema 與工具定義。除錯很長的 JSON 回覆時,用 cv.cm 的 JSON 工具這類格式化工具,巢狀輸出會更好讀。

速率限制、重試與錯誤

供應商同時限制每分鐘請求數(RPM)與每分鐘 token 數(TPM),先碰到哪個就適用哪個。超過限制時,你會收到 HTTP 狀態碼 429。請依錯誤類型處理:

狀態碼意義處理方式
400錯誤的請求:欄位有誤、token 太多修正請求,不要重試。
401 / 403金鑰遺失、錯誤或未授權檢查金鑰與權限,不要重試。
429速率限制,有時是額度用完等待後以退避方式重試。如果訊息提到額度或帳務,就停下來修正帳戶。
500 / 503伺服器錯誤或過載以退避方式重試。
逾時沒有及時回應重試一兩次。長篇回答請使用串流。

以指數退避重試:先等約 1 秒,再等 2、4、8 秒,並加上一點隨機延遲(抖動,jitter),避免許多用戶端在同一時刻重試。如果回應有 Retry-After 標頭,至少要等那麼久。限制重試次數的上限。失敗的請求也可能算進你的額度,所以在緊密迴圈中立即重試只會更糟。如果你使用官方 SDK,請記得它已經會重試部分錯誤。

費用以 token 計算

你要付輸入 token(你送出的一切,包括系統提示詞和整段對話歷史)與輸出 token(模型寫出的內容)的費用。輸出 token 的單價通常高於輸入 token。公式很簡單:

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 則訊息比第一則花費更多。請裁剪或摘要舊的輪次。
  • 簡單任務(例如分類)用較小的模型,只有在品質需要時才用較大的模型。
  • 部分供應商對重複的提示詞前綴提供折扣(提示詞快取),或對可以等待的工作提供更便宜的批次處理。

常見問題

可以直接從瀏覽器呼叫 LLM API 嗎?

用你自己的金鑰並不安全,因為任何訪客都能從頁面中讀到它。請呼叫你自己的伺服器,由它加上金鑰。例外是使用者為自己帳戶輸入的金鑰,或完全在瀏覽器中執行的模型。

為什麼 API 會忘記我先前說過的話?

API 是無狀態的。每個請求都必須包含你希望模型看到的先前訊息。聊天應用程式會儲存歷史紀錄,並在每次請求時重新送出。

temperature 有什麼作用?我該設定嗎?

temperature 控制用詞選擇的隨機程度:越低越可預測,越高越多樣。部分供應商和較新的模型限制或移除了取樣參數,所以設定之前請先查看 API 參考文件。

為什麼我的回答在中途停了?

回覆達到了你設定的 token 上限,或模型的最大輸出長度。請檢查 finish_reason(在 Anthropic 是 stop_reason)。調高上限,或要求更簡短的答案。

有免費的 LLM API 嗎?

有幾家供應商提供免費方案或試用額度,但速率限制很低;開放模型也可以在你自己的電腦上,透過相容 OpenAI 的本機伺服器執行。免費方案及其條款經常變動,請查看供應商目前的頁面。

全部 AI 教學