為每個請求選擇 Auto、TokenLab Verified 或 Official,並預先顯示價格。 查看最新動態

在不重複輸出的情況下重試串流 LLM 回應

CryptoCrypto
·2026年9月28日·約 8 分鐘閱讀·更新 2026年9月28日·37 次瀏覽
#串流#回應 API#可靠性#WebSocket
在不重複輸出的情況下重試串流 LLM 回應

只有在同時滿足以下三個條件時,串流請求才能安全地重試:沒有任何內容到達您的客戶端、沒有任何可觀察到的計量行為,且請求不帶有任何伺服器端狀態。在第一個輸出事件發生後,正確的做法是回報失敗,而不是重新發送請求。

TokenLab 在其 Responses API 串流的閘道器上應用了此規則,涵蓋 HTTP 和 WebSocket。WebSocket 路徑已於 2026 年 9 月 28 日變更,以與 HTTP 一致。

為什麼串流與一般請求不同

非串流呼叫會回傳主體或錯誤。您可以重試該錯誤,因為您沒有收到任何內容。

串流會在請求完成前就將輸出傳送給您。第一個輸出事件是「不歸路」。如果在此之後連線中斷,您將持有部分文字。重新發送請求意味著再次產生相同的答案並為此付費。您甚至可能重複執行代理程式已經執行過的工具呼叫。

《TokenLab 串流指南》直接說明了這一點:

在第一個事件到達後,中斷的串流即為不完整,不會自動重新啟動。

因此,您的客戶端需要一個本地狀態位元:saw_output。當任何輸出到達您的程式碼時,它會變更為 true。每次重試決策都會先讀取該位元。

以非 response.completed 結束的串流即為失敗。請勿假設您擁有的文字是完整的。請處理 response.failed、response.incomplete 和 error 事件。

重試決策的逐點分析

當滿足以下所有條件時,TokenLab 會在另一個可用路由上重試一次請求:請求是無狀態的;沒有任何內容到達客戶端;失敗的嘗試沒有觀察到任何結果或用量;且失敗屬於可重試的輸出前事件,或是第一個事件之前的上游讀取錯誤。每個請求最多重試一次。如果替換請求在輸出前也失敗,則該失敗不會再次重試。

來源:TokenLab 串流指南與閘道器行為,觀察於 2026 年 9 月 28 日。

失敗點 TokenLab 是否重試? 原因
可重試的輸出前事件(response.failed,或標記為可重試的 error 事件,例如過載或內部上游錯誤) 是,一次,若請求為無狀態 沒有內容到達客戶端且未觀察到用量,因此第二次執行是不可見的。
第一個事件前的上游串流中斷(讀取錯誤) 是,一次,若請求為無狀態 同上。客戶端未持有任何輸出且未產生費用。
重試一次後,在輸出前再次失敗 否 每個請求的預算為一次重試。
輸出到達客戶端後的任何失敗 否 客戶端已持有部分文字。重試會導致輸出重複並產生額外費用。
儲存的回應 (store)、接續 (previous_response_id) 或綁定來源的請求 否 第二次執行可能會建立第二個儲存的回應或導致對話狀態分歧。
第一個事件逾時 否 上游可能仍在產生內容。重試可能會在第一次嘗試繼續進行的同時執行兩次相同的工作。
輸出前緩衝區溢位 否 限制是閘道器本地的。相同的過大前綴很可能會在下一個路由上再次觸發限制。
客戶端中斷連線 否 客戶端已停止監聽。
確定性失敗,例如無效請求 否 重試無法改變結果。按原樣傳遞。
已產生用量的失敗 否 該嘗試已被計量。按原樣傳遞。
沒有其他剩餘路由 否 沒有地方可以傳送。客戶端會收到帶有其自身代碼的失敗。

當失敗未被重試,或沒有其他剩餘路由時,您會收到帶有其特定錯誤代碼的失敗。公開範例:上游串流中斷時的 stream_read_error,以及緩衝區溢位時的 upstream_stream_buffer_limit。如果路由選擇本身在重試決策後失敗,WebSocket 轉向將以 websocket_response_failed(狀態 500)結束,並退還預留費用。

計費遵循相同的原則。您僅為已交付的嘗試付費。重試的請求可能在上游執行了兩次,但該額外的上游成本由 TokenLab 承擔,因為第一次嘗試沒有任何內容到達您手中。未交付任何內容的失敗轉向將被退款。

一個關於錯誤處理的時序細節:在輸出開始前,閘道器會保留 response.created 和 response.in_progress,直到第一個輸出事件或失敗到達為止(最多 10 秒)。這些保留的事件隨後會與第一個輸出或終止事件一起到達。順序和內容不變,您只是稍微晚一點看到它們。這 10 秒是最大值,而非典型延遲。

2026 年 9 月 28 日 WebSocket 的變更內容

TokenLab 透過 HTTP 串流("stream": true,伺服器發送事件)以及 wss://api.tokenlab.sh/v1/responses 的 WebSocket 提供 Responses API,客戶端在該處發送 response.create 事件。WebSocket 回應一律為串流。它們不支援 background 或 response.cancel。每個連線一次處理一個活動回應,最長可達 60 分鐘。

在變更之前,兩條路徑的行為不一致。HTTP 會保留生命週期事件並重試無狀態的輸出前失敗。WebSocket 則立即轉發 response.created 並將輸出前失敗傳遞給客戶端,並進行退款。同樣的上游故障在 HTTP 上會產生乾淨的答案,但在 WebSocket 上卻產生錯誤。

WebSocket 路徑現在遵循 HTTP 規則,包括在任何事件到達前中斷的串流重試。在內部,WebSocket 轉向上看到的大多數上游失敗都發生在任何輸出之前。這正是重試安全的窗口。

閘道器改善了輸出前失敗的情況,但不保證串流一定會完成。

變更如何在不破壞其他行為的情況下發布

這項工作遵循了一套旨在捕捉靜默行為變更的流程。

  • 行為鎖定。在變更之前,每個 WebSocket 轉向場景都被記錄為一個測試案例(fixture):客戶端接收的訊框、進行的上游呼叫以及計費結果。在此期間,測試套件增加到 63 個記錄的場景。行為變更必須事先宣告。只有該宣告中命名的測試案例可以變更,其他所有測試案例必須保持位元組相同。
  • 變異檢查。每個新的決策規則都透過刻意翻轉來測試,例如重試第一個事件逾時或不重試讀取器失敗,並確認鎖定失敗。
  • 審查捕捉。第一個版本也使緩衝區溢位情況可重試,理由是與 HTTP 一致。審查顯示 HTTP 從不重試該情況(原因如表格所示)。後續版本恢復了舊行為並增加了邊界場景:第二次讀取器失敗不重試、無路由剩餘、在保留 response.created 後的失敗,以及隨後中斷的替換串流。

在重試後成功的轉向請求日誌,現在也會記錄先前失敗的嘗試,如同 HTTP 原本的做法。

由您掌控重試決策的客戶端程式碼

將 SDK 自動重試設定為 0 以進行串流呼叫。這將決策權保留在您的程式碼中。將重試決策保持在一個地方,而不是分散在各個處理器中。對於 HTTP 錯誤,請遵守《錯誤處理指南》中描述的 retryable 和 retry_after,並保留請求 ID。

SSE over HTTP

import os

from openai import OpenAI

with OpenAI(
    api_key=os.environ["TOKENLAB_API_KEY"],
    base_url="https://api.tokenlab.sh/v1",
    timeout=30.0,
    max_retries=0,  # 自行掌控重試決策,而不是重新發送半讀取的串流
) as client:
    completed, saw_output = False, False
    with client.responses.create(
        model="gpt-5.6-terra",
        input="Reply with one short sentence about retries.",
        stream=True,
    ) as stream:
        for event in stream:
            if event.type == "response.output_text.delta":
                saw_output = True
                print(event.delta, end="", flush=True)
            elif event.type == "response.completed":
                completed = True
            elif event.type in {"response.failed", "response.incomplete", "error"}:
                raise RuntimeError(f"{event.type} after_output={saw_output}")
    if not completed:
        raise RuntimeError(f"stream closed before response.completed, after_output={saw_output}")
    print()

此範例使用 OpenAI SDK 2.15.0 對接 https://api.tokenlab.sh/v1,並設定 max_retries=0。它追蹤 saw_output,並在 response.failed、response.incomplete 和 error 事件,以及在 response.completed 之前關閉的串流上引發錯誤。已於 2026 年 9 月 28 日使用 gpt-5.6-terra 在生產環境中驗證。

如果失敗到達時 saw_output == false 且請求符合資格(無狀態,且具有可重試的失敗),TokenLab 已經重試過一次;儲存的回應、接續和第一個事件逾時則完全未重試。請在應用程式層級決定是否接受新的請求,因為新的請求意味著新的產生。如果 saw_output == true,請回報失敗並顯示您擁有的內容,或刻意丟棄部分文字。

WebSocket

import asyncio
import json
import os

import websockets

URL = "wss://api.tokenlab.sh/v1/responses"
TERMINAL = {"response.completed", "response.failed", "response.incomplete", "error"}


async def run_turn(prompt: str) -> str:
    headers = {"Authorization": f"Bearer {os.environ['TOKENLAB_API_KEY']}"}
    async with websockets.connect(URL, additional_headers=headers, max_size=None) as ws:
        await ws.send(json.dumps({
            "type": "response.create",
            "model": "gpt-5.6-terra",
            "input": prompt,
            "store": False,
        }))
        text, saw_output = [], False
        async for raw in ws:
            event = json.loads(raw)
            kind = event.get("type")
            if kind == "response.output_text.delta":
                saw_output = True
                text.append(event["delta"])
            elif kind in TERMINAL:
                if kind != "response.completed":
                    # 輸出開始後,失敗對此轉向而言是最終結果。
                    # 僅在您的應用程式可以丟棄部分文字時才重新發送。
                    raise RuntimeError(f"{kind} after_output={saw_output}: {json.dumps(event)[:300]}")
                return "".join(text)
        raise RuntimeError(f"socket closed before a terminal event, after_output={saw_output}")


print(asyncio.run(run_turn("Reply with one short sentence about retries.")))

此範例使用 websockets 16.0,連線至 wss://api.tokenlab.sh/v1/responses 並帶有 Bearer 標頭,發送一個 store: false 的 response.create,並收集 response.output_text.delta。它會在任何非完成的終止事件或提早關閉時引發帶有 after_output 的錯誤。已於 2026 年 9 月 28 日使用 gpt-5.6-terra 在生產環境中驗證。

after_output 旗標與 saw_output 的概念相同。它告訴您的呼叫程式碼是否可以在不重複副作用的情況下進行新的轉向。

您的重試邏輯檢查清單

  • 每次都將未以 response.completed 結束的串流視為失敗。
  • 追蹤一個布林值,記錄輸出是否到達您的程式碼。在第一個輸出事件時翻轉它,而不是在第一個生命週期事件時。
  • 符合資格的請求的輸出前失敗已經過閘道器的一次重試;是否進行進一步嘗試由您決定。
  • 在部分輸出後,僅在您的應用程式可以丟棄部分文字並接受為兩次產生付費的情況下才重新發送。
  • 在代理程式迴圈中,檢查部分串流是否已包含您的程式碼已執行過的工具呼叫。不要重試您無法撤銷副作用的轉向。
  • 對於儲存的回應和 previous_response_id 接續,請在重新發送任何內容之前檢查現有的狀態。
  • 在您的 SDK 中將串流重試設定為 0,並將重試決策保留在一個函式中。
  • 記錄請求 ID,以便您可以將交付的答案與其背後的嘗試進行比對。

常見問題

TokenLab 是否會在部分輸出後重新啟動串流?

不會。一旦輸出到達您的客戶端,失敗就會被回報且永不重試。您持有部分文字,因此重新啟動會導致輸出和成本重複。您的應用程式決定是否顯示、截斷或丟棄已有的內容。

如果閘道器重試了我的請求,我會被收費兩次嗎?

不會。您僅為已交付的嘗試付費。重試的請求可能在上游執行了兩次,但第一次嘗試沒有任何內容到達您手中,該額外的上游成本由 TokenLab 承擔。未交付任何內容的失敗轉向將被退款。

為什麼第一個事件逾時不重試?

因為上游可能仍在產生內容。重試可能會在第一次嘗試繼續進行的同時執行兩次相同的工作。第一個事件逾時的處理方式與在第一個事件前中斷串流的讀取錯誤不同。

我可以重試儲存的回應或 previous_response_id 接續嗎?

無法自動重試。TokenLab 從不重試儲存的回應、接續或綁定來源的請求,因為第二次執行可能會建立第二個儲存的回應或導致對話狀態分歧。在重新發送任何內容前請檢查現有狀態,且僅在您的應用程式可以協調該狀態時才重新發送。

如果您想自行觀察原始事件串流,請建立 API 金鑰並記錄您的客戶端接收到的每個事件類型。《串流指南》和《錯誤處理指南》涵蓋了完整的事件集。關於閘道器如何路由和恢復的背景資訊,請參閱《TokenLab AI API 可靠性基礎設施》和《Responses API 與 Chat Completions 對代理程式的比較》。

來源

相關模型

最近發布的模型

用本文涉及的模型開始構建

比較價格、測試路由,把文章研究直接變成可執行的 API 呼叫。