Agentic Research
首頁/工具/第一次呼叫 LLM API:Token、計費與常見錯誤

第一次呼叫 LLM API:Token、計費與常見錯誤

2026/09/3021 分鐘君澤智庫最後更新 2026/09/30
這篇屬於工具主題LLMAPITutorial

上一篇你用 curl 打通了 API 鏈路,這篇開始正式寫程式。目標有三個:用 Python 的 openai SDK 發出第一個請求並看懂回應;理解 token 與計費的運作方式——這決定了你的程式會花多少錢;遇到 401、429 這類錯誤時知道怎麼排查。前置條件只有一個:完成 五分鐘設置你的 LLM 開發環境。

開始之前:關於 openai SDK 的三件事

第一,openai SDK 不等於只能呼叫 OpenAI。 業界大量供應商(DeepSeek、以及多數本地推理框架)提供「OpenAI 相容端點」——同樣的請求格式與介面約定。同一套程式碼,換掉 base_url 與 api_key 就能換供應商。本文範例以 DeepSeek 端點為主,換成 OpenAI 只需改兩行。

第二,你需要三樣資訊。 API key(上一篇已放進環境變數)、base_url(端點網址)、model 名稱。模型名稱各家不同、也會隨時間新增淘汰,一律以供應商官網當下公布的清單為準。

第三,安裝。 在激活的虛擬環境裡執行 pip install openai 即可。

第一個請求

完整程式碼,每行都有註解。存成 first_call.py,在專案資料夾執行 python first_call.py(Windows 用 py first_call.py):

import os
from openai import OpenAI

# 建立客戶端。api_key 從環境變數讀取,絕不寫死在程式碼裡。
# base_url 指向 DeepSeek 的 OpenAI 相容端點;換供應商就是換這一行。
client = OpenAI(
    api_key=os.environ["DEEPSEEK_API_KEY"],
    base_url="https://api.deepseek.com/v1",
)

# 發送一次 chat completions 請求
resp = client.chat.completions.create(
    model="deepseek-chat",  # 模型名以供應商官網清單為準
    messages=[
        {"role": "system", "content": "你是一個簡潔的助手,回答不超過三句話。"},
        {"role": "user", "content": "用一句話解釋什麼是 API。"},
    ],
)

# 回應結構:choices 是結果清單(可以要求多組),usage 是本次用量
print("回覆:", resp.choices[0].message.content)
print("用量:", resp.usage)

跑成功後你會看到模型的回覆,以及類似 prompt_tokens=xx, completion_tokens=xx 的用量資訊——xx 是這次請求實際消耗的 token 數,它是計費的依據,本節後面會展開。

回應物件的結構值得記熟,後面所有程式都圍繞它:

欄位內容
choices[0].message.content模型回覆的文字
choices[0].finish_reason為什麼停止:stop 正常講完、length 被長度上限截斷
usage.prompt_tokens輸入消耗的 token 數
usage.completion_tokens輸出消耗的 token 數

訊息角色:system、user、assistant

messages 是一個訊息清單,每則訊息都有一個 role(角色)。三種角色分工如下:

  • system:給模型定規矩與身分,例如「你是客服助手,只回答與訂單相關的問題」。它通常在清單最前面、只有一則,對模型行為的約束力最強——但注意是「最強」不是「絕對」,寫得含糊的 system 訊息約束不住任何東西。
  • user:使用者說的話。
  • assistant:模型自己先前的回覆。

這裡藏著新手最容易誤解的一件事:API 是無狀態的。 伺服器不會替你記住上一輪對話。聊天軟體看起來有記憶,是因為它每次都把完整歷史重新發送。多輪對話要自己維護:

messages = [{"role": "system", "content": "你是一個簡潔的助手。"}]

def chat(user_input: str) -> str:
    """發送一輪對話,並把雙方發言都記進 messages 清單。"""
    messages.append({"role": "user", "content": user_input})
    resp = client.chat.completions.create(
        model="deepseek-chat",
        messages=messages,
    )
    reply = resp.choices[0].message.content
    # 關鍵:把模型回覆以 assistant 角色放回歷史,
    # 下一輪請求模型才「記得」自己說過什麼
    messages.append({"role": "assistant", "content": reply})
    return reply

print(chat("我叫小君,我在學習呼叫 LLM API。"))
print(chat("我剛剛說我叫什麼名字?"))
# 第二輪能答對,不是伺服器有記憶,而是歷史被整包重發了

這個設計有一個直接後果:對話越長,每次請求的輸入 token 越多,費用越高、也越接近模型的上下文上限。 這不是 bug,是 LLM API 的基本計費邏輯。

三個最常用參數

temperature:隨機性旋鈕

LLM 生成文字時,每一步其實是在一大堆候選詞上算出一個機率分佈,然後抽一個出來。temperature 控制抽選的隨機程度:越低,模型越傾向選機率最高的詞,輸出保守、穩定、可重現性高;越高,低機率詞也有機會出線,輸出更多樣也更容易跑偏。

多數供應商的取值範圍是 0 到 2 之間的一段(確切範圍與預設值以各家文件為準)。實務原則:分類、抽取、程式碼生成這類「有標準答案」的任務用低值;文案發想、命名這類「要創意」的任務用高值。

max_tokens:輸出長度上限

限制模型這一次最多生成多少 token。撞到上限時回應會被硬生生截斷,finish_reason 顯示 length——如果你發現回覆總是「說一半」,先檢查這個參數。

兩個易混淆點:一,它限制的是輸出,輸入長度由模型的上下文視窗(context window,模型單次能看到的輸入加輸出總量)決定,是兩件事;二,部分較新的 OpenAI 模型把參數改名為 max_completion_tokens,OpenAI 相容端點則普遍仍接受 max_tokens,以供應商文件為準。

stream:串流輸出

預設情況下,API 等模型全部生成完才一次回傳,長回覆會讓使用者盯著空白畫面等。stream=True 改成邊生成邊回傳:

stream = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "用三句話介紹什麼是 token。"}],
    stream=True,
)

# 串流模式下回應是一個個 chunk(片段),逐段印出
for chunk in stream:
    delta = chunk.choices[0].delta.content  # 這一片段新增的文字
    if delta:
        print(delta, end="", flush=True)
print()

ChatGPT 網頁版那種「文字一個個冒出來」的效果,底層就是它。互動式產品幾乎必用串流;批次處理的腳本則不需要。

一個實務細節:串流模式下,usage 用量資訊通常要等最後一個 chunk 才拿得到,而且部分供應商需要你明確傳入額外選項才會回傳(例如 OpenAI 的 stream_options)。如果你要做成本統計,先確認你的供應商在串流模式下怎麼給 usage,以各家文件為準——否則你會發現日誌裡全是空的用量欄位。

Token 與計費:錢是怎麼算的

token 是什麼

token 是模型處理文字的最小單位。模型不吃「字」,吃 token——文字先被 tokenizer(分詞器)切成一片片的 token,模型再逐個處理與生成。英文一個 token 大約是幾個字元;中文的切法依各家 tokenizer 而異,同樣一句話在不同模型下的 token 數可能不同。OpenAI 官方文件給過「英文約四個字元 ≈ 1 token」這類粗略經驗值,但經驗值只能用來抓感覺,不能用來對帳。

想知道確切數字只有兩條路:事後看 API 回應裡的 usage 欄位(最準,供應商按它計費);事前估算用對應的 tokenizer 工具(OpenAI 模型可用 tiktoken 這個 Python 套件,其他供應商看自家文件是否提供)。

輸入與輸出分開計價

所有主流供應商都採「按 token 計費」,而且輸入(prompt tokens)與輸出(completion tokens)單價不同——多數情況下輸出單價高於輸入。這不是任意的定價遊戲,背後有技術原因:讀輸入時模型可以一次性平行處理整段文字,生成輸出時卻必須逐 token 接續計算,每一步都佔用算力。這個結構對成本有直接含意:「長輸入、短輸出」的任務(例如文件摘要、資訊抽取)與「短輸入、長輸出」的任務(例如長文生成),即使總 token 數相同,帳單也完全不同。部分供應商還對重複的輸入前綴提供快取折扣。這些都是計費結構上的通則,具體單價一律以供應商官網定價頁為準,本文不寫任何金額:價格隨時在變,寫進文章就是過時的錯誤資訊。

由此推出兩條省錢原則,比背價格有用得多:

  1. 輸入端:別把用不到的內容塞進 prompt。整個對話歷史每輪重發、把萬行文件整包丟進去,都是按 token 收錢的。
  2. 輸出端:用 system 訊息與 max_tokens 約束回覆長度。「請簡短回答」四個字常常比你想像中省錢。

怎麼算自己的用量

公式很簡單:花費 = 輸入 token 數 × 輸入單價 + 輸出 token 數 × 輸出單價。單價去官網查,token 數從每次回應的 usage 拿。

讀定價頁時有個容易算錯的細節:各家標價的計量單位不一定相同——有的按每一千 token 標價、有的按每一百萬 token 標價,比較之前先把單位對齊,否則帳面數字會差一千倍。務實做法是在你的程式裡把每次呼叫的 usage 寫進日誌:

import json

def log_usage(resp, task: str):
    """把每次請求的用量追加寫入 usage.log,方便月底統計。"""
    record = {
        "task": task,
        "prompt_tokens": resp.usage.prompt_tokens,
        "completion_tokens": resp.usage.completion_tokens,
    }
    with open("usage.log", "a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")

跑了幾百次之後把日誌加總,乘以官網單價,就是你的成本。另外兩件事要主動做:到供應商控制台看用量儀表板並設定花費上限或告警(多數平台支援,位置各異);開發與測試階段先用較小較便宜的模型,上線前才換大模型。本站對多家供應商定價的整理可參考 LLM API 定價整理,但請把它當「去哪查、怎麼比」的地圖,下決定前仍以官網即時價格為準。

常見錯誤與排查

401:身分驗證失敗

AuthenticationError。可能原因依機率排序:key 複製時混入空格或換行;用了 A 家的 key 打 B 家的 base_url(最常見於混用 DeepSeek 與 OpenAI);key 已被撤銷;帳號未開通 API 權限或未充值。先確認 key 與 base_url 是同一家供應商的,再逐項排查。

429:請求太多

RateLimitError。這個狀態碼底下其實有兩種完全不同的狀況,要看錯誤訊息本文:

  • 限流(rate limit):你在短時間內發了太多請求,或併發數超過帳號等級的上限。解法:降低頻率、加入重試並讓重試間隔逐次拉長(稱為指數退避),或到控制台提升額度。
  • 餘額不足 / 配額用盡:部分供應商餘額耗盡時也回 429。解法:充值。錯誤訊息裡通常寫得很明白。

context length exceeded:上下文超長

通常以 400 BadRequestError 回來,訊息中會出現 context length、maximum tokens 之類的字眼。原因是「輸入 token + 你要求的輸出上限」超過了模型的上下文視窗。最常見於多輪對話:歷史越滾越長,某一輪突然爆掉。解法依序考慮:裁掉最舊的對話(保留 system 與最近幾輪);把舊對話摘要成一段文字再放回;換上下文視窗更大的模型(單價通常不同,回到官網查)。

JSON 解析失敗

你叫模型輸出 JSON,json.loads() 卻報錯——因為模型有時會在 JSON 外面包一層「好的,以下是結果:」之類的廢話,或把它放進 Markdown 程式碼區塊。三層解法:

import json

resp = client.chat.completions.create(
    model="deepseek-chat",
    messages=[
        {"role": "system", "content": "你只輸出合法的 JSON,不要輸出任何其他文字。"},
        {"role": "user", "content": '列出三種程式語言,JSON 格式:{"languages": [...]}'},
    ],
    # 結構化輸出模式:要求端點回傳合法 JSON(支援度以各家文件為準,
    # 且多數實作要求 messages 中必須出現「JSON」字樣)
    response_format={"type": "json_object"},
)

text = resp.choices[0].message.content
try:
    data = json.loads(text)
except json.JSONDecodeError:
    # 保底:剝掉可能包著的 Markdown 程式碼圍欄再試一次
    cleaned = text.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
    data = json.loads(cleaned)  # 再失敗就記錄原文並告警,別讓程式無聲崩掉

逾時與連線錯誤

APITimeoutError / APIConnectionError:網路不穩、代理設定、或供應商短暫故障。建立 client 時可傳入 timeout 與 max_retries 參數讓 SDK 自動處理基本重試。

一個統一的錯誤處理骨架:

from openai import APIStatusError, APIConnectionError, APITimeoutError

try:
    resp = client.chat.completions.create(
        model="deepseek-chat",
        messages=[{"role": "user", "content": "你好"}],
    )
except APIStatusError as e:          # 伺服器回了 4xx/5xx 狀態碼
    print(f"HTTP {e.status_code}: {e.message}")
except (APITimeoutError, APIConnectionError) as e:  # 根本沒連上
    print(f"網路問題: {e}")

排查的總原則:先讀錯誤訊息本文,再猜。 供應商的錯誤訊息通常已經直接說出原因,跳過它去搜尋「為什麼我的 API 不能用」是浪費時間。如果錯誤訊息看不出來,按這個順序做系統性排查,每一步都在縮小範圍:

  1. 最小重現:用最短的一句話 prompt 直接呼叫一次。成功,代表問題出在你原本請求的內容(太長、格式錯、參數錯);失敗,代表問題出在帳號、key 或網路層。
  2. 隔離變數:一次只換一個東西——換一把確定有效的 key、換一個確定可用的模型名、換回官方預設參數。同時改三樣東西等於什麼都沒測。
  3. 看控制台:登入供應商控制台,確認帳號狀態、餘額、用量曲線與速率限制等級。很多「程式問題」其實是帳務問題。
  4. 看狀態頁:供應商都有 status page,先確認不是對方在故障,再回去改自己的程式碼。

下一步