Spring AI 1.1.3接入实践:在Spring Boot 3项目中集成多家AI供应商的真实经历 我们公司做的是一套企业级低代码平台技术栈是 Spring Boot 3.4 Java 21。去年Q4产品经理甩过来一个需求平台要支持13家AI供应商让企业客户自己选模型——OpenAI、Azure OpenAI、智谱、DeepSeek、通义千问、文心一言、讯飞星火、Gemini、Ollama、豆包、MiniMax、Moonshot、Claude一个都不能少。需求评审会上我问了一句为啥要接这么多PM回我客户有的用OpenAI有的只能用国产模型有的要私有部署Ollama你能让客户只选一个吗。我想了想确实没法反驳。于是这个活儿就落到了我头上前前后后折腾了将近三周这篇文章把选型、踩坑、架构设计都记录下来给后面要做类似事情的同行一个参考。一、选型纠结Spring AI 还是 LangChain4j接手这个需求第一周没写一行代码全在选型。2025年下半年Java生态做AI集成主流就两个框架Spring AI 和 LangChain4j。我当时两个都试了说说我的判断。LangChain4j 的优势是功能全Agent、Tool、Memory、RAG 一整套都封装好了上手快社区也活跃。但我看了眼它的模型适配方式每家供应商一个XxxChatModel类配置散在代码里或者YAML里想做运行时动态切换供应商这件事得自己包一层。而且它的 Spring Boot Starter 集成感比较弱更像是一个独立的库硬塞进 Spring。Spring AI 不一样它是 Spring 亲儿子从设计上就是按 Spring 的套路来的——ChatClient是核心接口自动配置、Configuration、Bean 注入这套东西和 Spring Boot 无缝衔接。我们平台本来就是 Spring Boot 3选 Spring AI 等于顺着生态走团队学习成本最低。还有一点很关键Spring AI 的ChatClient是流式APIBuilder 模式构造请求调用方式统一不管底下是 OpenAI 还是 Ollama上层代码长得一样。这一点对我们13家供应商统一接入的需求来说太重要了。最后定 Spring AI 1.1.3。选这个版本而不是 1.0 GA是因为 1.1.x 修了不少自动配置的 bug对国产模型的 OpenAI 兼容接口支持也更好。当时 1.1.3 是最新 patch 版本发布说明里修了一个OpenAiChatAutoConfiguration的 Bean 冲突问题这个后面我会提到。Maven 依赖Spring AI BOM 多供应商 StarterdependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-bom/artifactIdversion1.1.3/versiontypepom/typescopeimport/scope/dependency/dependencies/dependencyManagementdependenciesdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-openai-spring-boot-starter/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-ollama-spring-boot-starter/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-vertex-ai-gemini-spring-boot-starter/artifactId/dependency/dependencies二、ChatClient 基本用法和第一个坑Spring AI 的用法本身不复杂核心就是ChatClient。通过ChatClient.Builder注入调用.prompt().user(问题).call().content()就能拿到结果。听起来很美好实际接的时候坑不少。第一个坑是 starter 的版本冲突。我们项目里同时引入了spring-ai-openai-spring-boot-starter、spring-ai-azure-openai-spring-boot-starter、spring-ai-ollama-spring-boot-starter、spring-ai-vertex-ai-gemini-spring-boot-starter编译期没事一启动就报NoSuchBeanDefinitionException。排查了半天才发现spring-ai-core在不同 starter 里被传递依赖的版本不一致有的带 1.1.2有的带 1.1.3Maven 的依赖调解机制挑了个错版本。解决方式很土但有效在根pom.xml里用dependencyManagement把spring-ai-bom整个锁死到 1.1.3所有 starter 都从 BOM 继承版本不允许子模块自己指定。这一步做完编译和启动都正常了。第二个坑更隐蔽是自动配置类的覆盖问题。Spring AI 的每个 starter 都会注册一个XxxChatClientAutoConfiguration这些配置类里会往容器里塞ChatClient.Builder的 Bean。问题在于当你同时引入 OpenAI 和 Azure OpenAI 两个 starter 时容器里会有两个ChatClient.BuilderSpring 不知道该用哪个直接抛NoUniqueBeanDefinitionException。官方文档给的解法是给每个 Builder 加Qualifier但这套方案在我们这种运行时动态选供应商的场景下根本用不上——你不可能在调用前就知道客户要选哪家。我最后的做法是绕开自动配置自己写了一个ChatClientConfig手动为每家供应商构造ChatClient按providerCode放进一个MapString, ChatClient要用的时候按 code 取。这样自动配置类虽然还在但我不依赖它注入的 Bean等于把 Spring AI 当成了一个模型调用的 SDK来用而不是Spring Boot 集成套件。这个取舍我想了很久等于放弃了 Spring AI 自动配置的便利换来了对 Bean 生命周期的完全控制。事后看这个决定是对的因为我们后面要做数据库驱动配置所有参数都从数据库读自动配置那套基于application.yml的机制本来就用不上。三、Spring AI 覆盖不了的供应商怎么办13家供应商里Spring AI 有官方 starter 的就那么几家OpenAI、Azure OpenAI、Ollama、Gemini。剩下的怎么办智谱、DeepSeek、通义千问、文心一言、讯飞星火这几家我都走 OpenAI 兼容接口。这是个取巧的办法——这几家国产模型为了生态兼容都提供了和 OpenAI 一样的/v1/chat/completions接口请求格式、响应格式几乎一模一样。Spring AI 的OpenAiChatModel支持自定义base-url把base-url指向智谱的https://open.bigmodel.cn/api/paas/v4把api-key换成智谱的 key模型名改成glm-4就能跑起来。但这里有个坑DeepSeek 的兼容接口在流式响应上和 OpenAI 有细微差异finish_reason字段在某些情况下返回的是null而不是stopSpring AI 的OpenAiChatResponse解析时会 NPE。我在spring-ai-openai的 issue 区翻到了同样的问题官方说要等 1.2.0 修我等不及自己 fork 了一版响应解析逻辑用 OkHttp 单独包了一个 DeepSeek 的适配器绕过去。真正没法用 Spring AI 接的是豆包、MiniMax、Moonshot、Claude 这四家。豆包的 API 签名机制是火山引擎那一套要算 HMAC-SHA256请求头格式和 OpenAI 完全不一样MiniMax 的接口要传group_id响应结构是自定义的Moonshot 虽然兼容 OpenAI 协议但它有个is_regression字段处理比较特殊Claude 我们走的是第三方代理直连 Anthropic 官方在国内有网络问题代理的协议和 Spring AI 的 Anthropic starter 对不上。13家供应商适配方式对比供应商适配方式协议类型是否流式难度OpenAISpring AI 官方 StarterOpenAI 原生✅⭐Azure OpenAISpring AI 官方 StarterOpenAI 兼容✅⭐⭐OllamaSpring AI 官方 StarterOllama 原生✅⭐GeminiSpring AI 官方 StarterGemini 原生✅⭐⭐智谱 GLMOpenAI 兼容接口 base-urlOpenAI 兼容✅⭐DeepSeekOpenAI 兼容接口 base-urlOpenAI 兼容有差异✅⭐⭐通义千问OpenAI 兼容接口 base-urlOpenAI 兼容✅⭐文心一言OpenAI 兼容接口 base-urlOpenAI 兼容✅⭐讯飞星火OpenAI 兼容接口 base-urlOpenAI 兼容✅⭐豆包OkHttp 手写适配器火山引擎 HMAC-SHA256✅⭐⭐⭐MiniMaxOkHttp 手写适配器自定义协议✅⭐⭐⭐MoonshotOkHttp 手写适配器OpenAI 兼容有差异✅⭐⭐ClaudeOkHttp 手写适配器第三方代理代理协议✅⭐⭐⭐这四家我全部用 OkHttp 手写适配器。说实话手写 HTTP 调用没什么技术含量就是体力活但有个好处你对请求和响应有完全的控制权遇到协议差异不需要和框架斗智斗勇。每个适配器大概200行代码构造请求体、发请求、解析响应、异常处理一个供应商半天搞完。四、适配器架构策略模式 工厂模式别用 if-else13家供应商如果用 if-else 写调用入口会变成一个几百行的开关地狱if (openai.equals(provider)) {...} else if (zhipu.equals(provider)) {...}。这种代码我见过维护起来是灾难——加一家供应商要改这个方法改那个方法漏一个地方就出 bug。我用的是策略模式 工厂模式。定义了一个AIAdapter接口里面就一个核心方法chat(AIRequest request): AIResponse。所有供应商都实现这个接口OpenAIAdapter、ZhipuAdapter、DoubaoAdapter、ClaudeAdapter……各管各的。AIAdapter 接口定义publicinterfaceAIAdapter{/** * 执行 AI 对话 * param request 包含providerCode、model、messages等 * return AIResponse 包含回答内容、token用量、耗时等 */AIResponsechat(AIRequestrequest);/** 获取供应商编码 */StringgetProviderCode();/** 流式对话 */defaultFluxStringchatStream(AIRequestrequest){thrownewUnsupportedOperationException(流式对话未实现);}}application.yml 中 AI 相关配置ys:ai:retry:max-attempts:3backoff-ms:1000max-backoff-ms:8000timeout:connect:10000read:60000openai:base-url:${OPENAI_BASE_URL:https://api.openai.com}zhipu:base-url:https://open.bigmodel.cn/api/paas/v4deepseek:base-url:https://api.deepseek.com然后抽了一个AbstractAIAdapter抽象类做模板方法。这个抽象类把公共逻辑全收拢了参数校验AIRequest里的providerCode、model、messages不能为空、SM4 解密 API Key、3次指数退避重试、调用日志写入。子类只需要实现一个doChat方法专注处理自己供应商的协议差异。工厂这边有两个类AIAdapterFactory负责按providerCode创建适配器实例AIAdapterRegistry负责缓存。为什么要分两个因为创建适配器要读数据库配置、要解密 API Key、要构造 OkHttp 客户端是个重操作不能每次调用都重建但又不能在启动时全部初始化因为有些供应商的客户可能根本没启用。所以用Registry做懒加载 缓存第一次调用时创建之后复用配置变更时清缓存重建。这个架构最大的好处是扩展性。上个月产品又提了要加阶跃星辰Step我写了一个StepAdapter实现AIAdapter注册到Registry前后不到两小时没动一行已有代码。这就是设计模式的价值——不是为了装逼是为了少改代码。五、API Key 安全SM4 加密存储运行时解密13家供应商每家都有 API Key有的客户一个供应商还配了好几把 key 做负载均衡。这些 key 怎么存最偷懒的做法是放application.yml里明文。但我们的平台是私有化部署给企业客户的数据库客户自己管明文存 key 等于把客户的钱包敞开——谁拿到数据库谁就能拿 key 去刷账单。这种事出一次就是事故不能干。我们用的是国密 SM4 加密。数据库里有一张ai_provider_config表api_key字段存的是 SM4 加密后的密文加密密钥放在配置中心Nacos不在数据库里。运行时AbstractAIAdapter调用前会先把密文读出来用 SM4 解密成明文 key传给具体的适配器调用完之后明文 key 所在的局部变量随方法栈一起销毁不落日志、不落缓存、不落任何持久化介质。有人可能问为啥不用 AES 而用 SM4。原因是部分客户是政企单位有等保要求国密算法是硬性指标。SM4 的性能和 AES 差不多代码上多引一个hutool-crypto依赖就行SmUtil.sm4Decrypt(key, ciphertext)一行搞定没什么额外成本。还有个细节调用日志里能不能打请求头不能。我们在AbstractAIAdapter的日志切面里做了脱敏Authorization头打出来就是***请求体里的api_key字段也过滤掉。这个不做好日志系统一旦泄露加密就形同虚设。六、重试机制3次指数退避为什么不用 Spring RetryAI 供应商的 API 不稳定是常态限流、网关超时、偶发 500 都会遇到。我们的策略是3次指数退避重试第一次失败等 1 秒第二次等 2 秒第三次等 4 秒还不行就抛异常给上层。有人会问Spring 不是有Retryable注解吗Spring Retry 一行注解搞定为什么要自己写我用过 Spring Retry但在这种场景下它有几个不舒服的地方。第一Retryable基于AOP代理只能作用在 Spring Bean 的公开方法上而我们的重试逻辑在AbstractAIAdapter的模板方法里子类的doChat是被父类调用的AOP 代理在这种自调用场景下不生效得用RetryTemplate编程式调用。第二Spring Retry 的退避策略配置不够灵活我想对不同异常做不同处理——429 Too Many Requests要重试401 Unauthorized不用重试重试也是错500要重试这种细粒度控制用注解表达不了。自己写一个重试循环也就30行代码for (int i 0; i 3; i)配合Thread.sleep和异常判断清清楚楚。我还在重试逻辑里加了抖动jitter每次退避时间随机加 0~200 毫秒避免多个实例同时重试造成惊群。这种小细节 Spring Retry 也能做但配置起来比写代码还麻烦。指数退避重试机制代码// AbstractAIAdapter 中的重试机制protectedAIResponseexecuteWithRetry(AIRequestrequest){intmaxAttempts3;longbaseBackoff1000L;for(intattempt0;attemptmaxAttempts;attempt){try{returndoChat(request);}catch(Exceptione){// 401 不重试重试也是错if(einstanceofUnauthorizedException)throwe;if(attemptmaxAttempts-1)thrownewAIException(重试maxAttempts次后仍失败,e);// 指数退避 抖动: 1s, 2s, 4s 0~200ms 随机longdelaybaseBackoff*(1Lattempt)ThreadLocalRandom.current().nextLong(200);log.warn(AI调用失败第{}次重试等待{}ms,attempt1,delay);Thread.sleep(delay);}}thrownewAIException(不可达);}重试的时候还要注意一点只有幂等请求才能重试。AI 对话请求本身是幂等的同样的输入给出同样的输出不改变服务端状态所以可以放心重试。但如果哪天要接创建微调任务这种非幂等接口就不能无脑重试了得在适配器层面禁用。七、全数据库驱动配置不用 YAML支持热更新最后说一个架构上的决定所有 AI 配置全部放数据库不放application.yml。这个决定一开始有争议。团队里有同事说YAML 多清晰改完重启就行。我反驳的是客户不会接受改个模型名就要重启服务。我们平台是给企业用的客户可能在白天高峰期想从 GPT-4 切到 DeepSeek 降成本你不能告诉他等晚上重启。数据库表设计了两张ai_provider存供应商级别配置provider_code、provider_name、base_url、enabledai_model存模型级别配置model_code、provider_code、max_tokens、temperature、enabled。API Key 放在ai_provider_config表里按租户隔离。热更新的实现不复杂。AIAdapterRegistry里维护了一个MapString, AIAdapter缓存配置变更时通过后台管理界面改的发一个AIConfigChangedEventRegistry监听到事件后清掉对应providerCode的缓存下次调用时按新配置重建适配器。整个过程不停机、不重启对上层调用方完全透明。有个坑要提缓存重建时要保证线程安全。我们用的是ConcurrentHashMap的computeIfAbsent但重建过程中如果有请求正在用旧的适配器会出现新旧适配器并存的窗口期。我的处理方式是旧适配器不主动销毁等它自然 GC新请求走新适配器旧请求走完就结束。AI 调用本身是短任务几秒到几十秒这个窗口期对业务无感知。写在最后三周接完13家供应商回头看的几个关键决定选 Spring AI 而不是 LangChain4j是因为生态契合度手写 OkHttp 适配器接那4家Spring AI覆盖不了的供应商是因为协议差异摆在那策略模式 工厂模式是为了后面加供应商不动老代码SM4 加密 数据库驱动配置是企业级场景的硬要求不是锦上添花。如果你也在做类似的多供应商接入我的建议是别迷信框架的全家桶Spring AI 解决了70%的问题剩下30%自己写反而更可控。框架是工具不是信仰。