StackTraces
Spring AI已驗證

Spring AI 調 OpenAI 報 429 Too Many Requests

@debug_master32 天前更新閱讀 6 min00

429 不是「重試幾次就好」這麼簡單。同一句 429 背後有 rate_limit、insufficient_quota 等不同 error.code,前者才該重試,後者重試沒用。而且 Spring AI 預設對 4xx 不重試,直接拋 NonTransientAiException,很多人看到「重試沒生效」其實是設定問題。

Issue & Env(案發現場與環境)

  • 業務場景:併發或高頻調 OpenAI,請求彈 429,被 Spring AI 包成例外拋出,沒按預期重試。
  • 環境:Spring AI 1.x / 2.0,OpenAI 官方 API。重試相關設定在 spring.ai.retry 前綴下,預設 on-client-errors=false。

StackTrace(堆疊日誌)

底層 HTTP 層拋的是 TooManyRequests,對應的 OpenAI 錯誤響應原文長這樣:

{
    "error": {
        "message": "Rate limit reached for requests",
        "type": "rate_limit_error",
        "param": null,
        "code": "rate_limit_exceeded"
    }
}

Spring AI 預設不重試 4xx,把它包成不可重試例外往外拋,報錯原文:

org.springframework.ai.retry.NonTransientAiException: 429 - {
    "error": {
        "message": "Rate limit reached for requests",
        "type": "rate_limit_error"
    }
}

完整堆疊,從 Spring AI 的重試判斷一路回到業務調用:

org.springframework.ai.retry.NonTransientAiException: 429 - {
    "error": {
        "message": "Rate limit reached for requests",
        "type": "rate_limit_error"
    }
}
    at org.springframework.ai.retry.RetryUtils.handleResponseError(RetryUtils.java:163)
    at org.springframework.ai.chat.model.ChatModel.call(ChatModel.java:204)
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$InternalChatClientRequest.call(DefaultChatClient.java:686)
    at com.example.demo.chat.OpenAiService.summarize(OpenAiService.java:36)
    at com.example.demo.chat.ChatController.summarize(ChatController.java:23)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
Caused by: org.springframework.web.client.HttpClientErrorException$TooManyRequests: 429 Too Many Requests
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:123)
    at org.springframework.web.client.RestClient$DefaultResponseSpec$DefaultBodySpec.evaluate(RestClient.java:1187)

Reproduce(復現程式碼)

最小依賴:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

配好 key 後,用一個循環把短時間內的請求量打上去:

for (int i = 0; i < 100; i++) {
    chatClient.prompt().user("hi").call().content();
}

一旦撞上賬戶的 RPM(每分鐘請求數)或 TPM(每分鐘 token 數)限制,就拋 429。報錯是否重試,取決於 spring.ai.retry 的設定。

Root Cause(根因分析)

兩件事容易混,得拆開看。

第一件,429 背後的原因不止一種。OpenAI 返回 429 時,error.code 可能是 rate_limit(臨時限流),也可能是 credit_balance_exhausted、organization_spend_limit_exceeded 這類額度、餘額、限額耗盡。前者等一會兒再試能過,後者重試多少次都不會恢復訪問——官方文檔明確說,billing 或 quota 類錯誤重試無用,得去充值或調整 limit。

第二件,Spring AI 的預設重試策略。spring.ai.retry.on-client-errors 預設 false,意思是 4xx 客戶端錯誤一律不重試,直接拋 NonTransientAiException。429 屬於 4xx,所以預設情況下你根本看不到重試。想讓 429 走重試,要麼開 on-client-errors=true,要麼精準一點,把 429 加進 spring.ai.retry.on-http-codes。

還有一個坑:有些上游(尤其 Azure)的 429 會帶一個超大 Retry-After 頭,比如 24 小時。客戶端要是盲從,會把執行緒掛一整天(issue #2078)。所以重試最好帶退避上限,別無條件尊重 Retry-After。

The Fix(解決方案)

第一步:看 error.code 分流

拿到 429 先看 error.code,別急著加重試。

  • rate_limit:臨時限流,降頻或重試。
  • credit_balance_exhausted、organization_spend_limit_exceeded 等:額度類,去充值或調 limit,重試沒用。

第二步:臨時限流才配重試

只對 429 開重試,其他 4xx 保持不重試:

spring:
  ai:
    retry:
      max-attempts: 5
      backoff:
        initial-interval: 1s
        multiplier: 2
        max-interval: 10s
      on-http-codes:
        - 429

on-http-codes 只放你確定要重試的狀態碼;exclude-on-http-codes 放你明確不想重試的碼。兩者都別開太寬,避免把額度類 429 也捲進重試。

第三步:控併發、拆 key

重試只是兜底,根子上得降併發、控制頻率,或給不同服務分配獨立的 key,別多個服務共用一個 key 頂同一個限額。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法