豆包 API は火山方舟が提供しており、インターフェースの形は OpenAI に近いものの、エラーの意味には独自の特徴があります。最も典型的なのは 404 です。サービスが落ちているのではなく、問題は推論エンドポイント ID、モデル名、リージョン、パスのいずれかにあることが多いです。429 も誤判定されやすく、レート制限、同時実行、割当、滞納、リソースパックの枯渇のいずれかで、対処法はまったく異なります。

切り分けの順序は五段階に固定することをおすすめします。HTTP ステータスコードを見る、レスポンス本文の code と message を読む、リクエスト ID を取得する、最小リクエストで再現する、最後にパラメータやモデルを変更する。本記事では 401、404、400、429、タイムアウト、5xx に分けて説明し、エラー処理コードとログ項目を掲載します。モデルと課金はモデル価格を基準とします。

注記:豆包、火山方舟、火山エンジンは字節跳動(ByteDance)の商標です。本記事は技術的な説明のみを目的としており、ByteDance との資本関係、代理関係、提携関係はありません。エラーコード、レート制限の基準、インターフェースの挙動はバージョンによって変わる場合があるため、最終的には公式ドキュメントと実際のレスポンスに従ってください。

一、まず二層のエラーを分ける

HTTP ステータスコードはプロトコル層の結果を示し、レスポンス本文の code、type、message は具体的な原因を示します。同じ 401 でも、Key を付け忘れた、Key が誤っている、失効した、プロジェクトに権限がない、といった原因があります。同じ 429 でも、リクエストが速すぎる、同時実行が多すぎる、割当が尽きた、滞納している、といった原因があります。ステータスコードだけを記録すると、切り分けは推測に頼るしかありません。

HTTP 一般的な意味 豆包での場面 再試行
401 認証失敗 Key の欠落、形式エラー、失効、リージョン不一致 不可。まず認証情報を修正
403 権限なし Key に該当モデルまたはエンドポイントの権限がない 不可。権限を確認
404 リソース不在 エンドポイント ID、モデル名、パス、リージョンが誤り 不可。まず照合
400 パラメータエラー コンテキスト超過、max_tokens が不正、画像形式が非対応 不可。リクエストを修正
429 レート制限または割当 RPM、TPM、同時実行の超過、滞納、リソースパック枯渇 原因による
500/502/503 サーバーエラー 上流の異常、ゲートウェイの揺らぎ、一時的な不調 可。バックオフして再試行
タイムアウト 接続または読み取りのタイムアウト ネットワーク、待ち行列、入力過多、出力過多 回数を制限して再試行

フィールド名は実際の返却値に従ってください。error.code にある場合も、最上位にある場合もあります。コードは一つの構造だけを前提にしないでください。

二、401:認証失敗の四つの原因

401 はモデルの能力や残高とは関係ありません。まず認証情報の経路を確認します。

  • リクエストヘッダーが Authorization: Bearer <key> で、コロンの後に半角スペースが一つあるか。
  • Key が環境変数から正しく読み込まれているか、前後に引用符、空白、改行がないか。
  • Key が削除、ローテーション、または有効期限切れになっていないか。
  • Key の所属プロジェクト、リージョン、Base URL が一致しているか。

チャット画面からコピーした Key は、不可視文字を含みやすいです。まず一語だけを出力する最小リクエストを送れば、ほとんどの初歩的なミスを排除できます。同じ Key がコンソールでは使えるのにコードでは 401 になる場合は、環境変数とリバースプロキシが認証ヘッダーを破棄していないか確認してください。

三、404:エンドポイント ID とモデル名が最も混同しやすい

火山方舟ではモデルの指定方法が主に二つあります。コンソールで作成する推論エンドポイント(通常 ep- で始まる)と、一部モデルが対応するモデル ID の直接指定です。どちらも model フィールドに入れますが、権限、レート制限、適用範囲は異なる場合があります。エンドポイント ID とモデル名の混用、他プロジェクトのエンドポイントのコピーは、いずれも 404 または同様の「リソース不在」を招きます。

切り分けの順序は次のとおりです。

  1. Base URL が https://ark.cn-beijing.volces.com/api/v3 のようなリージョンアドレスで、/api/v3 の過不足がないか確認する。
  2. コンソールでエンドポイントが存在し、現在のプロジェクトに属し、停止されていないか照合する。
  3. 現在の呼び出し方式がエンドポイント ID とモデル ID のどちらを要求するか確認し、記憶で混在させない。
  4. curl で最小リクエストを送り、SDK の自動パス結合とプロキシの書き換えの影響を排除する。
  5. レスポンスの内容種別を見る。HTML のエラーページが返る場合、リクエストが方舟に届かず、ゲートウェイやリバースプロキシで止められている可能性がある。

404 に HTML が付く場合はパスまたはプロキシの問題、JSON のエラー本文が付く場合はモデルまたはエンドポイントの問題である可能性が高いです。

四、400:コンテキスト超過とパラメータエラー

400 の範囲は広く、最も多いのはコンテキスト超過です。コンテキストウィンドウは通常、入力トークンと出力トークンの合計で計算され、max_tokens が大きすぎると、本来通るはずのリクエストが上限を超えます。画像、ツール定義、履歴メッセージもトークンを消費します。

対処は上限を大きくすることではなく、入力に余裕を残すことです。

  • システムプロンプトを圧縮または分割し、重複する規則を削除する。
  • 過去の会話を要約し、直近の数ターンと重要事実だけを残す。
  • 長い文書は先に検索してから結合し、全文をそのまま詰め込まない。
  • 画像は先に圧縮し、形式とサイズが対応範囲内か確認する。
  • temperature、top_p が範囲外でないか、メッセージの role が正しいか確認する。

トークン数は推定値です。上限に近い場合は 10% から 20% の余裕を残すほうが安定します。

五、429:レート制限、同時実行、割当を分けて処理する

429 の原因は四種類あります。

  • レート制限:毎分リクエスト数またはトークン数がプランの上限を超えた。バックオフ後は通常回復する。
  • 同時実行制限:同時に飛んでいるリクエストが多すぎる。同時実行を下げるか待ち行列に入れる必要があり、単純な再試行は混雑を悪化させる。
  • 割当またはリソースパックの枯渇:枠を使い切った。再試行は無意味で、チャージかプラン変更が必要。
  • アカウント状態の異常:滞納、未実名、権限変更。コンソールの案内に従う。

レスポンスに Retry-After がある場合はそれに従って待ち、なければ指数バックオフにランダムジッターを加えます。割当系と滞納系の 429 は再試行しないでください。障害時間が延びるだけです。

ストリーミングの 429 は接続確立時に起きることも、ストリーム内のエラーイベントとして現れることもあります。クライアントはステータスコードとストリーム内エラーの両方を処理し、最初の断片だけを見て判断しないでください。

六、タイムアウトとストリーム中断:三段階に分けて特定する

タイムアウトは三段階に分けます。

  • 接続タイムアウト:DNS、TLS、出口プロキシ、ネットワークへの到達不能。通常は数秒で失敗する。
  • 初回トークンタイムアウト:リクエストは上流に届いたが、待ち行列または長い入力の処理中で、最初の断片が長時間来ない。
  • 読み取りタイムアウト:二つの断片の間隔が長すぎる。出力が長い、推論時間が長い、中間機器に接続を切られた、などが考えられる。

三段階には異なるタイムアウト設定を使います。全体のタイムアウトが大きすぎると、故障したリクエストが接続プールを長く占有します。小さすぎると、正常な長い出力を誤って打ち切ります。接続タイムアウトは 5 秒程度、読み取りタイムアウトは業務の P95 に合わせ、アプリケーション層で総予算を設定することをおすすめします。

ストリーミングではリバースプロキシのバッファリングも確認してください。バッファリングは断片の配信を遅らせ、「止まった」ように見せます。切断後は、途中までの内容を完全な結果としてデータベースに入れないでください。

七、リクエスト ID とログ:トラブルシューティングの証拠

火山方舟のレスポンスには通常、リクエスト識別子が付きます。一般的なフィールドは X-Request-Id で、一部のインターフェースは X-Tt-Logid のようなログ ID も返します。フィールド名は実際のレスポンスに従い、一つのフィールドだけを抜き出さず、レスポンスヘッダー全体を保存してください。

クライアント側でも独自の trace_id を生成し、同じ業務リクエストのすべての再試行で変えないようにします。ログには少なくとも、時刻、trace_id、Base URL、モデルまたはエンドポイント、ステータスコード、エラーコードと message、所要時間、再試行回数、ストリーミングかどうか、トークン使用量を記録します。

Key は先頭数文字またはハッシュ値のみを記録し、ユーザーの原文と完全なレスポンスを丸ごと保存しないでください。エラーレスポンス本文は先頭 500 文字を残せば十分です。

八、再試行とバックオフ:実用的なコード

次のコードは SDK の自動再試行を無効にし、ステータスコードとエラー内容で再試行可否を判断し、Retry-After を尊重し、論理リクエストごとに trace_id を生成します。

import os
import random
import time
import uuid

import httpx
from openai import OpenAI, APIConnectionError, APIStatusError, APITimeoutError

client = OpenAI(
    api_key=os.environ["ARK_API_KEY"],
    base_url=os.environ.get(
        "ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"
    ),
    timeout=httpx.Timeout(connect=5.0, read=180.0, write=30.0, pool=10.0),
    max_retries=0,  # SDK の自動再試行を無効化し、下のロジックで一元管理する
)

def parse_error(exc: APIStatusError) -> tuple[str, str]:
    try:
        body = exc.response.json()
    except Exception:
        return "", exc.response.text[:200]
    err = body.get("error", body)
    return str(err.get("code") or err.get("type") or ""), str(err.get("message") or "")

def is_retryable(exc: Exception) -> bool:
    if isinstance(exc, (APITimeoutError, APIConnectionError)):
        return True
    if not isinstance(exc, APIStatusError):
        return False
    if exc.status_code >= 500:
        return True
    if exc.status_code != 429:
        return False
    code, message = parse_error(exc)
    text = f"{code} {message}".lower()
    # 割当、滞納、権限系の 429 は再試行しても無意味。実際の文言に合わせてキーワードを追加する
    return not any(k in text for k in ("quota", "arrears", "balance", "permission"))

def ask(prompt: str, max_attempts: int = 4) -> str:
    trace_id = uuid.uuid4().hex[:12]
    for attempt in range(1, max_attempts + 1):
        try:
            resp = client.chat.completions.create(
                model=os.environ["ARK_MODEL"],  # エンドポイント ID またはモデル ID
                messages=[{"role": "user", "content": prompt}],
                max_tokens=1024,
            )
            return resp.choices[0].message.content
        except Exception as exc:
            status = getattr(exc, "status_code", "")
            code, message = parse_error(exc) if isinstance(exc, APIStatusError) else ("", "")
            print(
                f"trace={trace_id} attempt={attempt} status={status} "
                f"code={code} message={message[:200]}"
            )
            if attempt == max_attempts or not is_retryable(exc):
                raise
            retry_after = getattr(
                getattr(exc, "response", None), "headers", {}
            ).get("retry-after")
            try:
                wait = float(retry_after) if retry_after else min(2 ** (attempt - 1), 20)
            except ValueError:
                wait = min(2 ** (attempt - 1), 20)
            time.sleep(wait + random.uniform(0, 1))
    raise RuntimeError("再試行回数を使い切りました")

補足の規則が三つあります。副作用のあるリクエストは冪等キーで状態を先に確認する。再試行の予算は全体で管理する。429 の比率と P95 遅延を監視し、急増時はまず同時実行を下げる。

九、サポート連絡前のチェックリスト

  1. 障害の発生日時とタイムゾーン、継続的か断続的か。
  2. Base URL、完全なパス、モデル名またはエンドポイント ID。
  3. HTTP ステータスコード、エラーコード、message、リクエスト ID。
  4. 再現可能な最小 curl リクエスト。Key はマスキングする。
  5. ストリーミングか、長コンテキストか、画像やツール呼び出しを含むか。
  6. 直近に Key、エンドポイント、割当、プロキシを変更したか。
  7. 影響範囲:単一 Key、単一プロジェクト、単一リージョン、それとも全サービスか。

これらの情報が揃っていれば、サポートチームは上流ログと直接照合できます。障害のたびに監視またはアラートを一つ追加してください。

十、よくある質問(FAQ)

1. 404 は必ずモデルの提供終了ですか?

いいえ。より多いのは、エンドポイント ID やモデル名の誤り、エンドポイントの削除、リージョン不一致、パスに /api/v3 が欠けている、といった原因です。まずレスポンスが JSON か HTML のエラーページかを見て、それから項目ごとに照合してください。

2. 429 は何回再試行すべきですか?

レート制限と一時的な混雑には、2 から 3 回、指数バックオフ付きで再試行します。割当枯渇、滞納、権限不足による 429 は再試行せず、先にアカウントやプランを処理してください。Retry-After を尊重します。

3. コンテキスト長は結局どう計算されますか?

通常は入力トークンと出力トークンの合計で計算され、max_tokens も総枠を消費します。画像、ツール定義、履歴メッセージはすべて入力に計上されるため、上限に近い場合は余裕を残してください。

4. ストリーミングのタイムアウトはどう設定しますか?

読み取りタイムアウトは通常、二つの断片の間隔を制限するもので、総時間ではありません。接続タイムアウトは別に設定し、アプリケーション層で総予算とアイドルタイムアウトを設定します。プロキシのバッファリングは断片の配信を遅らせます。

5. ゲートウェイを経由したらエラーが増えた場合は?

まずエラーがゲートウェイ由来か上流由来かを確認します。レスポンス本文の形式、ステータスコード、リクエスト ID を見ます。trace ID を保持し、直結とゲートウェイでそれぞれ最小リクエストを送って比較してください。多くの問題はパスの結合、認証ヘッダーの転送、タイムアウトにあります。

次のステップ

まずZSQH AI Gatewayでテスト用 Key を作成し、401、404、429、タイムアウトをそれぞれ一度ずつ演習してから、本番監視に接続してください。利用可能なモデルと課金方式はモデル価格を参照してください。

関連記事