1. 老项目接 Spring AI 为什么总卡在依赖和超时上如果你手上有一个跑了三五年的 Spring Boot 老系统JDK 8、Spring Boot 2.7.x、MyBatis 加 Oracle 的组合现在产品突然说要加个智能问答或者智能客服你第一反应大概率是「接个大模型 API 能有多难」。我一开始也是这么想的结果从加依赖那一刻起就开始踩坑启动直接NoSuchMethodError好不容易跑起来了流式输出又在前端卡到 15 秒超时日志里全是Connection reset by peer。这篇就聚焦两件事Spring AI 接入存量老项目时的依赖冲突怎么排以及流式响应超时怎么从根上解决。中间会给出可直接复制的pom依赖树处理方式、SSE 超时参数配置、线程池隔离方案最后用 TaoToken 统一 Key/API 通道做一次连通性验证确认你的改造链路是通的。先说清楚适合谁看你正在维护一个 Spring Boot 2.x 的老项目想引入 Spring AI 但被版本断层卡住或者你已经接上了但流式接口在网关或前端频繁超时。这两种场景我都会给到具体命令和配置不是泛泛而谈。核心检索词先摆出来Spring AI 依赖冲突排查和流式响应超时配置这两个是整篇的主线。依赖地狱的本质是 Spring AI 1.x 要求 Spring 6.1而老项目锁在 Spring 5.3传递依赖一交叉就炸。流式超时则是网络层、应用层、中间件三层超时没对齐任何一层先断前端就显示超时。我试过的顺序是先解决依赖能不能启动再解决流式能不能稳定输出最后才是 Prompt 和监控这些上层的东西。顺序反了会浪费大量时间因为依赖没理顺之前你连一个正常的请求都发不出去根本没法验证超时问题。下面按这个顺序展开每一步都给可复制的配置和验证方式。2. TaoToken 统一 Key 通道的前置准备在动老项目代码之前建议先把模型通道这件事独立出来验证。原因很简单如果依赖冲突和网络问题混在一起排查你分不清是 Spring 版本的问题还是 Key 通道的问题。用一个统一的 Key/API 通道先把「模型能不能通」这件事确认掉后面改代码时心里有底。TaoToken 在这里的角色就是统一通道你不需要在项目里散落多个厂商的 Key也不用为每个模型单独配一套 Base URL。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end API 地址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置的时候别把查询串带进去。前置准备分三步都是几分钟能完成的第一步拿到 Key。进入控制台创建 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建后复制保存后面配置里要用。Key 的权限和额度在控制台能看到建议先建一个测试用的 Key别直接上生产 Key。第二步确认你要用的模型 ID。不同模型在请求体里的model字段值不一样这个值要和你后面 Spring AI 配置里的模型名对齐。可以先在模型对话页面手动发一条消息验证通道地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 发一句「你好」看有没有正常返回。这一步能排除掉 Key 错误、额度不足、模型名写错这类低级问题。第三步记下接入文档的位置后面配 Spring AI 的 Base URL 和路径时对照着来文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。文档里会说明兼容的接口路径Spring AI 的 OpenAI 兼容模式需要把 Base URL 指到对应位置。这里有个关键点Spring AI 的 OpenAI starter 默认会拼接/v1/chat/completions这类路径所以你的 Base URL 要配成https://taotoken.net/api让它去拼后面的部分。如果你配成了带/v1的地址很可能出现 404。这个坑我在验证阶段踩过日志里是 404 而不是 401很容易误判成路径问题。把这三步做完你手上就有了一个可用的 Key、一个确认能返回的模型 ID、一份路径对照文档。接下来再回到老项目里改依赖和配置出问题时就能快速区分是通道问题还是代码问题。3. 可复制的依赖对齐与超时配置这一节是整篇的核心给的都是能直接粘进项目的片段。先解决依赖地狱再解决流式超时。3.1 依赖树分析与 BOM 对齐老项目引入 Spring AI 后启动报NoSuchMethodError: org.springframework.core.io.Resource.getContentAsString根因是 Spring AI 1.x 传递进来了 Spring 6.1 的spring-core而你的项目锁在 Spring 5.3。两个版本的类同时存在类加载器加载到旧版本就炸。先看依赖树把冲突暴露出来mvn dependency:tree -Dincludesorg.springframework -Dverbose输出里你会看到同一个groupId:artifactId出现多个版本比如spring-core:5.3.30和spring-core:6.1.x同时被引。处理原则是老项目不要强行升 Spring 大版本而是把 Spring AI 的传递依赖排除掉让它复用你现有的 Spring 5.3。在pom.xml里对 Spring AI 的 starter 做排除dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0-M4/version exclusions exclusion groupIdorg.springframework/groupId artifactIdspring-core/artifactId /exclusion exclusion groupIdorg.springframework/groupId artifactIdspring-context/artifactId /exclusion exclusion groupIdorg.springframework/groupId artifactIdspring-web/artifactId /exclusion /exclusions /dependency然后用dependencyManagement统一锁定 Spring 版本确保全项目只有一个版本dependencyManagement dependencies dependency groupIdorg.springframework/groupId artifactIdspring-framework-bom/artifactId version5.3.30/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement注意 Spring AI 的某些版本对 Spring 6 有硬依赖如果排除后编译报错找不到类说明这个版本不兼容 Spring 5.3需要换一个更早的 milestone 版本或者把 Spring AI 的调用层单独抽成一个模块用独立的类加载器隔离。模块隔离的配置如下Configuration EnableAutoConfiguration(exclude { MybatisAutoConfiguration.class, DataSourceAutoConfiguration.class }) ComponentScan(excludeFilters ComponentScan.Filter( type FilterType.REGEX, pattern com\\.company\\.legacy\\..* )) public class AiIsolationConfig { Bean public Module aiModule() { return new SimpleModule(ai-module) .setClassLoader(AiIsolationConfig.class.getClassLoader()); } }3.2 流式超时的三层配置依赖理顺后流式接口的典型故障是前端 15 秒显示超时后台日志显示模型 8 秒就返回完了但 TCP 连接在第 12 秒被断开。这是三层超时没对齐Netty 的 readTimeout、WebClient 的 timeout、Nginx 的proxy_read_timeout。先配应用层的 WebClient 超时注意流式场景要按 chunk 设超时而不是整个响应设一个总超时Bean public WebClient aiWebClient() { return WebClient.builder() .baseUrl(https://taotoken.net/api) .defaultHeader(Authorization, Bearer ${TAOTOKEN_API_KEY}) .clientConnector(new ReactorClientHttpConnector( HttpClient.create() .responseTimeout(Duration.ofSeconds(60)) .keepAlive(true) )) .build(); }流式请求单独设 chunk 超时和心跳public FluxString stream(String prompt) { return webClient.post() .uri(/v1/chat/completions) .bodyValue(buildRequest(prompt)) .retrieve() .bodyToFlux(String.class) .timeout(Duration.ofSeconds(30), Flux.just({\type\:\timeout\})) .onBackpressureBuffer(1000) .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))); }application.yml里把 WebFlux 和 Tomcat 的超时一起对齐spring: webflux: timeout: response: 60s codec: max-in-memory-size: 10MB server: tomcat: connection-timeout: 60s max-keep-alive-requests: 100Nginx 侧对应调整proxy_read_timeout要大于你的 chunk 超时location /api/ai/ { proxy_pass http://backend; proxy_read_timeout 90s; proxy_send_timeout 90s; proxy_buffering off; proxy_cache off; }proxy_buffering off对流式很关键开着缓冲会导致 SSE 数据被攒着一起发前端看起来就像卡住了。3.3 线程池隔离老项目的 Tomcat 线程池是给普通 HTTP 请求用的流式请求会长时间占用线程容易把线程池打满导致整个服务不可用。给 AI 调用单独开一个线程池Configuration public class AiExecutorConfig { Bean(aiExecutor) public ThreadPoolTaskExecutor aiExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(200); executor.setThreadNamePrefix(ai-exec-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }调用时指定这个线程池别用默认的Async(aiExecutor) public CompletableFutureString chatAsync(String prompt) { return CompletableFuture.completedFuture(aiClient.chat(prompt)); }到这里依赖、超时、线程池三块配置就齐了。下一节验证请求能不能通。4. 验证请求与成功结果配置写完先别急着接前端用最小请求验证通道和流式是否正常。这一步能帮你把「代码问题」和「通道问题」分开。4.1 用 curl 验证通道先用命令行确认 TaoToken 通道本身是通的把 Key 换成你自己的curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-3-5-sonnet, messages: [{role: user, content: 你好}], stream: false }预期结果是返回一个 JSONchoices[0].message.content里有模型的回复。如果这里就报 401说明 Key 有问题报 404说明路径拼错了检查 Base URL 是不是多了或少了/v1。4.2 验证流式输出把stream改成true观察是否逐块返回curl -N -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-3-5-sonnet, messages: [{role: user, content: 写一段100字的介绍}], stream: true }-N参数关闭 curl 的缓冲你能看到data:开头的行一块块刷出来。如果全部内容一次性出现说明中间有缓冲没关掉回去检查 Nginx 的proxy_buffering。4.3 Spring 侧验证写一个测试接口确认 Spring AI 的调用链路是通的RestController RequestMapping(/api/ai) public class AiTestController { private final ChatClient chatClient; public AiTestController(ChatClient.Builder builder) { this.chatClient builder .baseUrl(https://taotoken.net/api) .apiKey(System.getenv(TAOTOKEN_API_KEY)) .build(); } GetMapping(/ping) public String ping() { return chatClient.prompt() .user(回复pong) .call() .content(); } GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String q) { return chatClient.prompt() .user(q) .stream() .content(); } }启动后访问/api/ai/ping预期返回包含pong的字符串。访问/api/ai/stream?q你好预期在浏览器里看到 SSE 逐块输出而不是等很久一次性出来。成功结果的特征ping接口 3 秒内返回stream接口首字节在 1 秒左右出现后续 chunk 间隔稳定。如果首字节超过 5 秒回去看线程池和连接超时配置。5. 本篇常见错误排查这一节按真实报错来对照都是我在改造过程中实际遇到的。401 UnauthorizedKey 没配或配错。检查Authorization头是不是Bearer加 Key中间有空格。如果是环境变量注入确认变量名没写错System.getenv拿不到会返回 null拼出来就是Bearer null。另外确认 Key 没有多余的空格或换行从控制台复制时容易带上。404 Not FoundBase URL 路径拼错。Spring AI 的 OpenAI 兼容模式会自己拼/v1/chat/completions所以 Base URL 应该是https://taotoken.net/api不要再加/v1。如果你用的是自定义的 RestTemplate 直接调那 URL 要写全https://taotoken.net/api/v1/chat/completions。两种方式别混。local proxy failed / Connection refused这类报错通常是本地网络或代理配置问题。检查你的application.yml里有没有残留的proxy配置或者系统环境变量里有没有HTTP_PROXY。老项目经常在启动脚本里设了代理AI 请求走代理就会失败。把代理配置清掉直连通道地址。reading choices 相关报错一般是响应体解析失败。流式场景下如果服务端返回的不是标准 SSE 格式Spring AI 解析choices字段时会报错。先用 curl 确认返回格式再检查你的produces是不是text/event-stream。另外max-in-memory-size太小也会导致大响应被截断调到 10MB 以上。OAuth / token 过期如果你用的是需要 OAuth 的通道token 过期会报这个。TaoToken 用的是 API Key 方式正常不会出现 OAuth 报错。如果出现了检查是不是误配了别的认证方式把认证头统一成Authorization: Bearer key。流式超时但后台显示已完成这是最典型的。后台日志显示模型 8 秒返回完前端 15 秒超时。按第 3.2 节的三层超时逐层检查WebClient 的responseTimeout、Nginx 的proxy_read_timeout、前端的AbortController超时时间。三层里最小的那个决定实际超时。前端如果设了 15 秒而 Nginx 设了 90 秒那前端先断后台还在发日志就会显示连接被重置。线程池打满导致整个服务不可用流式请求长时间占用 Tomcat 线程普通接口也拿不到线程。按 3.3 节单独开线程池并且给 AI 线程池设CallerRunsPolicy或者直接拒绝别让它拖垮主线程池。排查顺序建议先 curl 确认通道再看 Spring 日志确认请求发出去了最后看前端和网关的超时。从外到内别一上来就改代码。6. 长期编码与 Agent 场景的通道选择如果你只是偶尔调一下模型做验证用按量的 API Key 就够了。但如果你的老项目改造后要长期跑智能客服、代码辅助或者 Agent 类的任务调用量会持续上来这时候通道的稳定性和成本结构就值得单独考虑。TaoToken 的 Coding Plan 适合长期编码和 Agent 场景入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它的定位是给持续调用的场景提供一个稳定的通道不用每次请求都去关心额度波动。对于已经改造完、准备上生产的项目可以先把测试流量切过去跑一段时间观察成功率和延迟再决定要不要全量。回到技术本身老项目接 Spring AI 这件事难点从来不在「调 API」这个动作而在存量系统的兼容性和流式链路的稳定性。依赖地狱靠 BOM 对齐和排除传递依赖解决流式超时靠三层超时对齐和线程池隔离解决。这两块理顺了剩下的 Prompt 工程和监控都是增量工作。最后给一个实用建议改造过程中保留一个「最小可复现」的测试接口就是第 4.3 节那个ping和stream。每次改完依赖或超时配置先跑这两个接口确认没退化再去跑完整的业务流程。这样出问题时能快速定位是配置改坏了还是业务代码的问题。