Spring AI 降级链延迟叠加?让走 TaoToken 的 Codex 对着熔断配置查
这篇不重写 Spring AI 的降级链业务逻辑,而是用 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=spring-ai-fallback-codex)给 Codex 开一条排查通道,让它对着MultiModelChatService、CircuitBreakerConfig和isRetryable白名单查三件事:降级链延迟到底叠在哪、熔断判定有没有提前生效、429/503/504 之外的异常是不是被误判成可重试。TaoToken 只提供 Key 和 Base URL,不接管 Spring AI 的ChatModel调用链,改熔断参数、加日志、调整超时仍然是读者本地工程里的事。
这个场景的痛点很典型:MODEL_FALLBACK_CHAIN把 openai、claude、qwen 串成一条链,CircuitBreakerConfig设置了failureRateThreshold(50.0f)、waitDurationInOpenState(30s)、slidingWindowSize(10),同时用isRetryable判断 429、503、504 和超时。表面上看,主模型失败后会自动降级,可用性提高了;但真实用户感知到的可能是“主模型先等超时,再重试,再降级,备用模型又等超时”,延迟一层层叠加。再加上三家供应商错误码不统一,监控分散在不同异常类型里,排查时很难一眼看出问题在超时配置、重试次数,还是熔断判定。
一、原问题与场景:MultiModelChatService 的降级链延迟叠加
MultiModelChatService的核心逻辑是:先按 preferredModel 构建降级链,然后依次遍历模型,对每个模型包一层 Resilience4j 的 CircuitBreaker 和 Retry。只要当前模型没有熔断,就尝试调用;如果抛CallNotPermittedException,说明熔断器已打开,直接跳到下一个模型;如果是其他异常,记录llm.call.failure后也继续下一个模型。
问题出在“继续下一个模型”之前,请求已经在当前模型上消耗了太多时间。特别是当主模型没有单独设置 3 到 5 秒的短超时,而是依赖底层 HTTP 客户端默认超时或较长的 ReadTimeout 时,一次调用可能先等满超时,再触发maxAttempts(2)的第二次尝试,第二次又等满超时,最后才进入 catch 分支降级。用户看到的是首字延迟和总耗时同时升高,而不是快速切换到备用模型。
从CircuitBreakerConfig看,failureRateThreshold(50.0f)、waitDurationInOpenState(30s)、slidingWindowSize(10)是一组典型的熔断参数。它们控制的是“什么时候打开熔断器”和“打开后多久再放量”,并不直接控制单次请求的等待时间。真正影响用户延迟的是:
- 主模型单次调用超时是多少;
maxAttempts(2)会让单次模型调用最多等待几次超时;- 降级链上 openai、claude、qwen 是否都会经历同样的重试;
isRetryable是否把不该重试的异常也放进了重试路径。
更麻烦的是错误码不统一。isRetryable当前只判断HttpStatusCodeException的 429、503、504,以及TimeoutException、SocketTimeoutException。但不同供应商 SDK 可能把限流包装成其他异常,或者在 200 响应体里返回错误码。这样会出现两种极端:该重试的没有重试,直接降级;不该重试的却被反复重试,把延迟进一步拉长。
所以排查目标不是推翻MultiModelChatService,而是把这段降级链代码、熔断参数、重试白名单和实际超时设置交给会消耗 Token 的排查工具,让 Codex 还原调用链,重点核对三件事:主模型是否该在 3 到 5 秒超时即降级,而不是等熔断器打开;slidingWindowSize(10)与maxAttempts(2)的叠加会不会把单次请求拖长;429、503、504 之外的异常是否被误判为可重试。
二、TaoToken 前置:给 Codex 的 config.toml 配 Key 和 Base URL
先打开 TaoToken 官网注册账号:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=spring-ai-fallback-codex 。注册完成后,在控制台创建 API Key。这个 Key 只用于 Codex 走 TaoToken 通道,不放进 Spring AI 的application.yml,也不参与OpenAiChatModel、ClaudeChatModel、QwenChatModel的调用。
Codex 的配置文件是config.toml。把模型提供方指向 TaoToken,Base URL 填https://taotoken.net/api,注意不要带/v1。Key 通过环境变量注入,避免把明文写进配置文件。
model = "MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"然后在终端里设置环境变量。Linux 或 macOS:
export TAOTOKEN_API_KEY="YOUR_API_KEY"Windows PowerShell:
$env:TAOTOKEN_API_KEY="YOUR_API_KEY"这里要强调边界:TaoToken 在这条排查链路里只负责给 Codex 提供 Key 和 Base URL。Spring AI 的降级链仍然是本地工程在跑,熔断参数、超时时间、isRetryable白名单、MeterRegistry 计数器都在读者自己的代码里。Codex 的作用是读代码、读配置、给结论,不是替代 Spring AI 的运行时容错。
三、可复制配置:CircuitBreakerConfig 与 isRetryable 排查上下文
配置好 Codex 后,不要只丢一句“帮我看看为什么慢”。排查效果取决于上下文是否完整。建议在工程根目录下把这几类文件或片段整理到一个临时目录,或者直接让 Codex 读取当前工程:
MultiModelChatService.java,重点包含MODEL_FALLBACK_CHAIN、buildFallbackChain、chatWithFallback;CircuitBreakerConfig相关配置类,包含failureRateThreshold(50.0f)、waitDurationInOpenState(Duration.ofSeconds(30))、slidingWindowSize(10);RetryConfig片段,包含maxAttempts(2)、waitDuration(Duration.ofMillis(500))、retryOnException(this::isRetryable);isRetryable方法完整实现;application.yml或application.properties中所有与 openai、claude、qwen 相关的超时配置,例如 connectTimeout、readTimeout、responseTimeout;- 如果有网关或 HTTP 客户端配置,也一并提供,例如 WebClient、RestTemplate、OkHttp 的超时。
让 Codex 执行排查时,Prompt 要限定范围,避免它重写整个业务类。可以用下面这段:
请阅读当前工程中的以下内容: 1. MultiModelChatService.java 中的降级链逻辑; 2. CircuitBreakerConfig 中 failureRateThreshold、waitDurationInOpenState、slidingWindowSize 的配置; 3. RetryConfig 中 maxAttempts、waitDuration、retryOnException 的配置; 4. isRetryable 方法完整实现; 5. application.yml 中 openai、claude、qwen 相关的 connectTimeout、readTimeout、responseTimeout。 只做三件事: 第一,指出主模型调用是否会在 3 到 5 秒超时后立即降级,还是必须等到 CircuitBreaker 打开后才降级。 第二,计算 slidingWindowSize(10) 与 maxAttempts(2) 叠加后,单次请求在最坏情况下可能增加多少等待时间。 第三,检查 isRetryable 白名单是否只覆盖 429、503、504 和超时异常,列出当前代码中可能被误判为可重试或不可重试的异常类型。 不要重写 MultiModelChatService,不要新增业务代码,只输出结论、证据行号和最小参数修改建议。这段 Prompt 的关键是“只输出结论、证据行号和最小参数修改建议”。如果直接让 Codex 改代码,它可能引入新的重试逻辑或改变降级顺序,反而让排查失焦。
四、验证请求与成功结果:MeterRegistry 三个计数器是否按模型对上号
先验证 Codex 到 TaoToken 的通道是否可用。可以用 curl 请求模型列表接口,具体路径以接入文档为准:
curl -s https://taotoken.net/api/v1/models \ -H "Authorization: Bearer $TAOTOKEN_API_KEY"如果返回模型列表 JSON,说明 Key 和 Base URL 基本可用。接着在工程目录启动 Codex,让它读取上面的文件并执行排查 Prompt。
一个合理的成功结果应该包含三类结论。
第一,主模型超时是否独立设置。如果application.yml里 openai 的 readTimeout 是 10 秒或更长,而CircuitBreakerConfig的slidingWindowSize(10)需要累积统计窗口,那么主模型在熔断器打开前会先经历多次超时。Codex 应该指出:熔断器打开是“统计结果”,不是“单次请求的快速失败机制”。要让用户少等,应该给主模型单独设置 3 到 5 秒超时,超时后直接进入降级链下一跳,而不是等failureRateThreshold(50.0f)被满足。
第二,slidingWindowSize(10)与maxAttempts(2)的叠加关系。slidingWindowSize(10)是熔断器统计最近 10 次调用的窗口,maxAttempts(2)是单次模型调用的重试次数。二者不在同一层:前者影响熔断器何时打开,后者直接影响单次请求耗时。如果单次超时是 5 秒,maxAttempts(2)最坏会让一个模型消耗约 10 秒;降级到 claude 后如果又重试,用户感知可能继续叠加。Codex 应该给出最坏耗时估算,并指出waitDurationInOpenState(30s)只保证已打开的熔断器在 30 秒内快速跳过,不能解决熔断器打开前的超时叠加。
第三,isRetryable白名单是否完整。当前实现只认HttpStatusCodeException的 429、503、504 和TimeoutException、SocketTimeoutException。Codex 应该检查实际 SDK 抛出的异常类型,例如WebClientResponseException、ResourceAccessException、RestClientException,以及供应商是否在 200 响应体里返回错误码。如果这些异常没有进入重试路径,llm.call.failure会增长,但重试次数不增长;如果被 catch(Exception) 直接降级,llm.circuitbreaker.open又可能对不上。
验证阶段仍然沿用原文的 MeterRegistry 三个计数器:
meterRegistry.counter("llm.call.success", "model", modelName).increment(); meterRegistry.counter("llm.circuitbreaker.open", "model", modelName).increment(); meterRegistry.counter("llm.call.failure", "model", modelName).increment();在 Prometheus 或 Actuator 端点里,它们通常会转成:
llm_call_success_total{model="openai"} llm_circuitbreaker_open_total{model="claude"} llm_call_failure_total{model="qwen"}重点不是看总量,而是看三个计数器是否按模型维度对上号。比如 openai 的llm.call.failure很高,但llm.circuitbreaker.open很低,说明失败没有进入熔断统计,可能是异常类型没被 Resilience4j 记录,或者重试把异常吞掉了。再比如 claude 的llm.call.success增长,但用户总延迟仍然很高,说明降级虽然成功,但主模型超时时间太长,降级发生得太晚。
五、本篇常见错排查:Base URL、超时、slidingWindowSize(10) 与 maxAttempts(2)
第一个常见错:Codex 的 Base URL 写成https://taotoken.net/api/v1。TaoToken 的 API 地址是https://taotoken.net/api,不带/v1。如果 Codex 或 SDK 自己再拼/v1,就会变成/v1/v1/...,表现为 404 或模型不可用。遇到这种报错,先检查config.toml里的base_url。
第二个常见错:把 TaoToken 当成 Spring AI 的模型供应商。TaoToken 在这条链路里只给 Codex 提供 Key 和 Base URL,不参与MultiModelChatService的ChatModel调用。Spring AI 的 openai、claude、qwen 仍然走各自原生配置。不要为了排查延迟去改 Spring AI 的 base-url,否则可能引入新的变量。
第三个常见错:只贴MultiModelChatService,不贴CircuitBreakerConfig和application.yml超时。Codex 看不到failureRateThreshold(50.0f)、waitDurationInOpenState(30s)、slidingWindowSize(10),也看不到 readTimeout,就无法判断 3 到 5 秒超时应该加在哪里。排查降级链延迟,超时配置和熔断配置必须一起给。
第四个常见错:让 Codex 直接重写MultiModelChatService。一旦它改了降级顺序或重试逻辑,原来的问题可能被掩盖。正确做法是限定它做参数核对和调用链还原,输出最小修改建议,由读者本地决定是否采纳。
第五个常见错:只改failureRateThreshold,不改超时。熔断器打开前,单次请求已经可能经历“超时 + 重试 + 超时”。降低失败率阈值只会让熔断器更早打开,但如果单次超时本身是 10 秒,第一次请求仍然要等很久。主模型设置 3 到 5 秒超时,才是减少用户感知延迟的关键。
第六个常见错:混淆slidingWindowSize(10)和maxAttempts(2)。前者是熔断统计窗口大小,后者是单次调用的重试次数。真正拉长单次请求的是“单次超时 × maxAttempts × 降级链上实际尝试的模型数”。排查时让 Codex 分别列出这三个乘数,不要只盯熔断器参数。
第七个常见错:监控只看llm.call.failure总量,不按 model tag 拆。三家供应商错误码不统一,失败可能散落在不同异常类型里。按模型维度看llm.call.success、llm.circuitbreaker.open、llm.call.failure,才能判断是 openai 限流、claude 超时,还是 qwen 返回了非标准错误。
第八个常见错:429 没有区分是主模型限流还是备用模型限流。主模型 429 后降级到备用模型,备用模型也可能被瞬时流量打满,继续 429。isRetryable如果对 429 做固定重试,可能让备用模型也进入重试等待。建议在日志里带上 model 和 attempt,Codex 排查时才能还原“哪个模型在第几次尝试上耗时”。
六、语义一致 CTA:把结论落回工程侧按模型成本告警
排查完成后,建议把 Codex 给出的结论落回三个工程位置:主模型独立超时配置、isRetryable白名单、按模型维度的 MeterRegistry 计数器。降级链延迟叠加通常不是单点问题,而是超时、重试、熔断统计三者叠加出来的结果。改完参数后,再跑一轮llm.call.success、llm.circuitbreaker.open、llm.call.failure,确认三个计数器按模型对上号。
如果你还要继续用 Codex 做这类配置排查,可以在 TaoToken 控制台管理 Key 和查看接入方式:
- 管理 API Key:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=spring-ai-fallback-codex
- 对照 Base URL 与 Codex 配置:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=spring-ai-fallback-codex
- 长期在 Coding Agent 场景使用:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=spring-ai-fallback-codex
- 验证模型是否可用:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=spring-ai-fallback-codex
跑通后,如果还要按模型拆 Token 消耗,可以在同一个创建 Key 的账号里查看调用是否成功,再把结论落回工程侧的按模型成本告警。这样,降级链的可用性、延迟和成本才能放在同一张监控视图里判断。