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 頂同一個限額。
