Spring AI Alibaba 实战:从手写HTTP到标准化大模型接入

发布时间:2026/10/18 15:42:36
Spring AI Alibaba 实战:从手写HTTP到标准化大模型接入
1. 为什么我从“手写 HTTP”转向 Spring AI Alibaba1.1 传统接入方式到底痛在哪先说说我之前是怎么接大模型的吧。服务端要调用模型最直接的做法就是 JDK 自带的 HttpClient 发 POST自己拼 JSON、自己解析返回。单接一个模型的时候倒还能凑合可一旦项目里要对比两个甚至更多模型的效果问题就全冒出来了各家模型的请求体结构不一样有的把 system prompt 放在顶层有的必须在 messages 数组里区分角色返回结果的位置五花八门有的在 choices[0].message.content有的在 output.text还有的在 result 字段里嵌套取流式返回的协议各自为政SSE 格式、事件名、消息边界都不统一鉴权方式不同有的传 Header Key有的还要额外签名。这些差异逼着我写了一个越来越厚的适配层最后光是模型对接代码就攒了两千多行。更难受的是每换一个模型供应商适配层就要动一遍业务代码跟着一起被牵连。这个痛点凡是做 AI 服务集成的后端应该都有体会。所以当我看到 Spring AI 的核心抽象时第一反应就是这不就是我一直想要的 JDBC 式模型接入方案吗。1.2 ChatModel 抽象的核心价值Spring AI 的核心思路其实特别朴素把“模型调用”这个动作收敛成一个接口这个接口就是 ChatModel。调用方不管底层是哪个模型拿到手的都是一个可以对话、可以返回文本的对象。它内部做的事情说穿了就三件把 Java 对象翻译成目标模型要求的请求格式把模型原始的 JSON 响应翻译成统一的 AssistantMessage 或结构化对象把超时、重试、流式协议这些非业务逻辑挡在外面。这个抽象的价值类比一下就是 JDBC 之于数据库。写 SQL 的人不需要关心底层是 MySQL 还是 Oracle只要换驱动、换连接串业务代码基本不动。Spring AI 对模型就是这个思路接口是“方言”实现类是“驱动”。一旦你习惯了 ChatModel后面再引入一个模型成本就从“重写对接层”降到了“改配置”这是本质区别。1.3 Spring AI Alibaba 解决了“最后一公里”抽象归抽象能不能落地还得看有没有好用的实现。Spring AI Alibaba 做的事情就是把 Spring AI 的标准接口对准国内大模型的调用协议做了一套完整实现。我理解它的价值主要在四点对接了 DashScope 服务协议顺手把通义系 qwen 全家桶的调用格式都处理好了提供了 starter 自动配置只要引入依赖、填上 API Key容器里就有一个可用的 ChatModel Bean完整支持 Spring AI 官方的 ChatClient、ChatOptions、Advisor、ToolCalling 等高层 API说直白点你在 Spring AI 教程里学到的写法在这里基本都能用面向中文场景做了大量示例和文档补齐比如中文知识问答、中文信息抽取这些高频场景都有现成参考。但如果你只是需要一个能跑的通义大模型调用自己写 HTTP 也能做到这个项目真正的意义在于标准化。选它是一个“长期收益”的选择业务代码和底层模型实现松耦合将来无论是换模型、加模型、还是做灰度对比都只需要在配置和依赖层操作不用再重写业务。2. 环境准备与最小项目搭建2.1 依赖引入与版本选择我用的是一个内部模拟项目 X 来验证整套流程最终的稳定组合是JDK 17、Spring Boot 3.3.x、Spring AI 1.0.0、spring-ai-alibaba-starter 1.0.0。我特别提醒一句不要直接照抄网上随意给的版本号先去翻当前版本的官方 Release 说明确认它支持的 Spring Boot 主版本版本踩错是这类 starter 项目最常见的第一道坎。pom.xml 核心依赖长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0/version /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement两个细节需要多说一句。第一dependencyManagement里引入 BOM 是为了统一 Spring AI 系列库的版本否则 starter 和核心库版本不一致运行时会出现抽象的类方法对不上第二JDK 版本至少要 17Spring Boot 3.x 已经把这一点写死在要求里了我用 Java 21 实测也是能跑的但 17 是兼容性最好的分界线。2.2 配置文件的关键参数依赖放进去之后接着就是配置。Spring AI Alibaba 的自动配置识别的是spring.ai.dashscope这组属性我用的配置是这样的spring: application: name: ai-chat-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/api/v1 chat: options: model: qwen-plus temperature: 0.7 max-tokens: 1024这些字段别照着抄完就完事背后有几个讲究api-key我从不在 yml 里写死都是走环境变量 DASHSCOPE_API_KEY。一是防止误提交到代码仓库二是方便在多环境之间切换。如果本地测试想快速跑临时写进配置没问题但代码一旦要共享或提交请立刻改成环境变量或配置中心base-url默认值就是官方服务地址一般不用改。只有当你要接入兼容协议的网关、私有化部署或代理服务时才需要调整。我项目里曾通过改 base-url 指向了一个内网网关从而统一做限流和审计这比改代码优雅得多model决定你实际调用哪个模型。qwen-turbo 响应最快、价格最低qwen-plus 是性价比平衡点qwen-max 效果最好但单价高。日常联调我优先用 turbo 或 plus等 prompt 调整得差不多了再换 max 做效果验收max-tokens这是最大输出 token 数不是输入。中文场景下一个汉字大概对应 1.5 到 2 个 token需要模型输出长文时1024 只是一个保守安全的默认值真要它写完整报告我会调到 2048 或更高。2.3 第一个最小可运行 Demo配置完之后我习惯先写一个极小的 Controller 把链路打通RestController public class ChatController { private final ChatModel chatModel; public ChatController(ChatModel chatModel) { this.chatModel chatModel; } GetMapping(/chat) public String chat(RequestParam String message) { return chatModel.call(new UserMessage(message)).getContent(); } }启动服务后访问/chat?message介绍一下Spring AI Alibaba如果 key、配置都对几秒内就能看到一句完整回复。之所以这里用最原始的 ChatModel 而不是更高层的 ChatClient是因为我第一步只验证自动配置是否生效、链路是否通畅。等这条路跑通再上高层 API 会顺很多。注意如果启动时 ChatModel 注入失败最常见的原因是依赖版本不一致或者 yml 前缀配置错误。可以先在启动日志里搜索 dashscope 相关自动配置日志来定位问题别急着改代码。3. ChatModel 核心 API 的实战拆解3.1 ChatClient日常开发真正的主力入口ChatModel.call()是最底层的用法真实业务我基本不这么写。Spring AI 提供了更高层的ChatClient构建方式是流式链式调用阅读顺序就像在说话RestController public class ChatClientController { private final ChatClient chatClient; public ChatClientController(ChatClient.Builder chatClientBuilder) { this.chatClient chatClientBuilder.build(); } GetMapping(/ask) public String ask(RequestParam String question) { return chatClient.prompt(请用一句话回答 question) .call() .content(); } }ChatClient.Builder由自动配置注入内部把之前那个 ChatModel 封装好了你完全不用关心它的实现类是什么。这里我想强调一句能注入ChatClient.Builder就尽量别手动 newChatModel实现类。Builder 上挂着很多全局的 Advisor顾问、拦截器和默认参数配置手动 new 对象会把这些能力全部丢掉而且代码也不利于测试。除了最简单的prompt(String)ChatClient 还支持更结构化的消息构建。我实际项目里最常用的模式是把 system 消息做成模板在方法里动态拼入角色设定和约束而不是让用户输入决定模型的角色String result chatClient.prompt() .system(你是一个严谨的技术文档助手回答必须分点列出且不编造不存在的功能。) .user(帮我整理一个Spring AI入门清单) .call() .content();system 和 user 分开写对模型输出质量的影响非常直接。同一个问题如果我把这些约束夹在用户消息里模型经常“忘记”单独放到 system 里就稳定得多。这也是我第一次从“手写 HTTP”时代转过来时感受最明显的一个差异。3.2 流式输出别再用“等完整结果”的思路大模型生成回复要几秒到十几秒接口如果同步阻塞等待用户那边就是白屏转圈体验很糟糕。所以生产级项目基本都要走流式返回。ChatClient 的写法和同步区别很小把.call().content()换成.stream().content()就行GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String question) { return chatClient.prompt(question) .stream() .content(); }返回类型不再是 String而是 Reactor 的FluxString。前端接 SSEServer-Sent Events就能一段一段地往页面打印配合打字机效果体验很好。因为返回的是 Flux 而不是阻塞线程去等待完整结果所以即使是长文本生成服务端线程也不会一直占用。实际写流式时有几个点特别容易踩别在map之类的操作符里做耗时任务Flux 的每个元素都会经过它你放个数据库查询进去整个流就被拖慢成串行Controller 的produces一定要设置成text/event-stream我自己就吃过亏不设置时浏览器会等全部内容结束后一次性渲染流式完全失效如果需要在流结束时做清理应该订阅时使用doOnComplete/doOnError不要试图在.content()后面同步等返回值。我最初犯的错就是把流式响应当成同步响应去理解以为返回的 Flux 会被框架“等完再给你”结果前端照样是白屏。直到我意识到流式本身就是异步的代码必须围绕 Flux 的惰性执行特性来写才算真正用对。3.3 结构化输出让模型返回你想要的 Java 对象聊天类应用给 String 没问题但很多需求是“让模型抽取出结构化数据”。比如从一段简历里提取姓名、技能和工作年限这就不适合让调用方自己去解析模型返回的长文本。Spring AI 提供了 OutputConverterChatClient 写起来非常顺手public record ResumeInfo(String name, String skills, Integer workYears) { } ResumeInfo info chatClient.prompt(提取以下简历信息 resumeText) .call() .entity(ResumeInfo.class);不需要手写 JSON 解析也不需要专门在 prompt 里强调“只输出 JSON”。它的原理是框架会把你的目标类型转换成一份 JSON Schema 描述通过 system 消息传给模型模型按这个 schema 输出之后框架再把结果反序列化成你要的对象。用的时候我有几个习惯目标类型推荐用 record字段语义清晰反序列化也稳定字段名尽量和业务语义对齐比如workYears就比wy这类缩写好用得多如果模型偶尔返回格式错误导致反序列化抛异常可以在.options(...)里显式指定 JSON 响应格式让模型强制按 JSON 输出如果抽取结果不稳定优先检查有没有在 system 消息里告诉模型“只输出 JSON 对象不要解释”。结构化输出我在实际项目里用得非常频繁信息抽取、内容审核、工单分类这类场景全靠它把模型能力揉进现有业务链路。这一步做好AI 就不是一个孤立的聊天玩具而是能真正参与业务逻辑的组件。3.4 参数背后的大模型语义很多同学对temperature、top_p的理解停留在“知道设置但不知道怎么设置”。我结合自己的调参经验说明一下temperature控制输出随机性。0 附近模型基本只挑最高概率词结果稳定适合信息抽取、分类、规则问答0.8 以上更适合创意文案和头脑风暴。我的经验是业务规则明确的场景设 0.2 到 0.3生成类场景设 0.7 到 0.9不固定必须按场景微调top_p核采样控制候选词范围。官方建议是不要同时调 temperature 和 top_p二选一更可控。我习惯固定 top_p 默认值只调 temperature这样变量少出问题也好排查max-tokens前面说过输出上限中文场景留足余量stop停止词。指定模型生成到某个位置就停做代码补全、固定格式输出时很有用。之前我遇到过一个挺隐蔽的问题在某个实现类里直接 new 了一个 ChatOptions 覆盖默认配置结果导致全局设置失效。后来才想明白局部 options 的优先级高于全局配置是框架的预期行为但如果你的业务代码里某个方法不小心传了一个不完整的 options很多默认参数就会被“局部覆盖”成空值。尽量避免在业务方法里随手 new 局部 options在真正需要覆盖的地方再显式设置能省事很多。4. 进阶玩法Function Calling 与 RAG4.1 让模型学会“调用你的方法”模型的知识有截止时间也没有权限访问你的系统内部数据。比如你问它“当前库存是多少”它只能瞎猜。Function Calling工具调用解决的就是这个问题让模型认识到某些问题它回答不了但可以通过调用你注册的函数来获得答案。Spring AI 里注册一个工具方法很简单用Tool注解标记普通 Bean 方法即可Service public class StockService { Tool(description 查询指定SKU的当前库存数量) public int getStock(String sku) { // 这里可以走真实库存服务 return 26; } }然后把这些工具类注册到 ChatClient 上var chatClient chatClientBuilder .defaultTools(new StockService()) .build();当你问“SKU-10086 还有多少货”时模型自己会判断需要调用 getStock 方法把参数填好然后框架负责回调 Java 方法、把返回结果重新交给模型最后模型才给你组织出完整回答。这里有个很容易误解的点整个“决定调用工具”的过程是模型在判断框架只是执行。所以工具的描述写得越清楚模型判断就越准确。Tool注解上的 description 别看长了偷懒不写它对模型选择工具起着决定性作用我体会极深同一个工具描述从一句话改成三句话之后选对工具的准确率肉眼可见地提升。另一个容易踩坑的点是工具方法参数命名。模型是按照参数名来填值的如果你把参数写成a、b这种无意义字符模型根本不知道每个参数该传入什么。我建议把所有工具方法的参数名都写成有意义的名字比如sku、userName哪怕代码里要多打几个字母也值。4.2 向量库检索与知识问答Function Calling 解决的是“工具型问题”知识问答类需求则要依靠 RAG检索增强生成。简单理解RAG 就是把你的私有文档切碎后向量化存进向量数据库用户提问时先把最相关的片段找出来塞进 prompt模型再基于这些片段做回答而不是凭空编造。Spring AI 里这种能力抽象为 Advisor写在业务代码里非常干净。我用过一个最小化的链路大致是这样先用 EmbeddingModel 把文档切块后的内容向量化存入 VectorStore在 ChatClient 上配置.advisors(QuestionAnswerAdvisor)用户提问时Advisor 自动完成相关性检索并把召回结果拼接进上下文。这个过程一旦搭好后面的知识库维护就只剩“上传文档 重新向量化”。但我要提醒的是RAG 的效果瓶颈往往不在 Spring AI而在你的切块策略和检索质量。切块太大召回不精准切块太小上下文碎片化。我踩过几次坑之后的经验是先按标题和段落切每块控制在三四百字左右效果通常比粗暴按字符切好很多。如果只是想低门槛体验 RAG也可以用内存实现的 SimpleVectorStore 做本地验证先把流程跑通再切换到正式的向量数据库避免一开始就被基建细节拖住。4.3 多模态扩展ChatModel 这套抽象不止支持文本视觉模型相关的输入也有对应接口。我在一个图像理解的小项目里试过把图片描述能力接进来传入的 Prompt 类型可以带着图片内容一起发送模型返回的是对图片的文字理解。这对做图文审核、拍照识别类的业务场景来说基本是开箱即用。不过多模态对 token 消耗明显更高尤其是图片这类视觉输入计费模式和纯文本完全不同。建议对图片做预压缩、只传必要的区域能省不少成本。我在项目里曾因为传了原图单次请求的 token 消耗比文本翻了好几倍后来改成压缩图和局部裁剪费用立刻降下来了。5. 踩坑实录这些问题我排查了很久5.1 鉴权、配额与账号相关异常先说一个很基础但容易懵的问题请求报 401。我遇到过三种情况表现一模一样排查路径却完全不同API Key 配置错误环境变量没生效或者 yml 里写了个假 key。排查方式是先确认配置中心里是否真的下发了对应值再看自动配置有没有读取到账号没有开通对应模型服务在 DashScope 控制台里确认目标模型已经开通。有些模型默认不开通直接调就会报权限错误网关或代理层把 Header 里的鉴权字段过滤掉了如果你像我一样在前面挂了网关做了统一出口一定要确认透传了鉴权请求头。这个是排起来最隐蔽的我那次查了半天最后发现是网关白名单漏了字段。建议统一把这些异常映射成业务层可读的错误码不要直接透传给用户。大模型服务偶发 429限流是很正常的事情一定要做重试和熔断。5.2 参数配了但完全不生效这个问题我在 3.4 里提过这里展开说。我遇到过的是全局 yml 里配了temperature: 0.2结果某个接口返回的内容随机性还是很强后来定位到是代码某处new了局部ChatOptions把全局参数覆盖了。Spring AI 的参数优先级是方法局部 options ChatClient 默认 options 全局配置。这不是 bug是设计但如果你没意识到就会觉得“配置失灵”。排查思路很简单检查所有显式创建 options 的地方确认是不是每个字段都按预期设置。如果只是覆盖一个字段其他字段通常会回落默认值而不是继承全局这是最容易造成困惑的坑。5.3 流式响应超时与中断流式接口比同步接口更容易碰到网络层问题。生成的响应时间长如果底层 HTTP 客户端超时设得短流就被截断了前端看到的是说了一半的话。排查方向有三个服务端到模型服务之间的连接超时和读取超时要把读取超时设得足够长。默认值往往只有几秒长文本生成根本不够如果用了网关或负载均衡要检查它们对 SSE 长连接的 idle timeout有些网关默认 30 秒就掐断空闲连接这个非常坑如果服务本身是容器化部署还要看容器或平台的网络策略有没有限制长连接。另外流式重试也要小心已经吐出的一部分内容无法“回滚”所以重试策略要设计成“用户可手动重新发起”而不是框架自动重试导致内容重复。5.4 版本兼容性这块我总结成一句话Spring AI 版本迭代非常快依赖别乱追。某个版本里ChatClient.Builder的方法是defaultSystem在后续版本可能就改名或废弃了Tool注解在不同版本之间也有过行为调整。我发现最有效的办法是锁定三个版本Spring Boot 主版本、Spring AI BOM 版本、Alibaba starter 版本并且在每次升级时看官方升级日志而不是只看有没有编译报错。编译能过、运行报错才是这类框架升级里最折磨人的情况。因为很多 API 变化在编译期看不出来但运行时行为完全不同。所以我对版本升级的态度是生产环境不追新等新版本发布一两个月后再评估升级。6. 一点个人体会我在整理这份学习记录的过程中回头看最早那个手写 HTTP 的项目最大的感触不是“Spring AI 省了多少代码”而是它改变了我和模型交互时的思考方式。以前我在想的是“怎么把这个请求发出去、怎么把返回解析出来”现在我想的是“我想让它扮演什么角色、给出什么结构、调哪个工具”这是两个层面的事情。如果你也准备在项目里接入国产大模型我的建议路径是这样的先用ChatModel把链路跑通然后立刻换到ChatClient写业务能流式就流式别同步等待需要结构化结果时就上.entity()别让调用方去解析文本等业务稳定了再去研究 Function Calling 和 RAG把模型的能力边界一步步撑开。最后分享一个小的操作习惯凡是和模型相关的请求我都会在日志里记录一份入参和出参的摘要但不记完整内容只记 token 数量、模型、耗时和返回状态。调试模型效果时这些日志比任何分析工具都直接。而且时间长了你会发现真正要优化的往往不是模型本身而是你喂给它的上下文和工具描述。Spring AI Alibaba 这套东西还在快速演进我这份记录也只能算某个时间段内的使用心得。但它带给我的一个判断是清晰的大模型接入这件事正在从“谁的对接代码写得好”走向“谁的抽象和生态更稳”。对于做 Java 后端的人来说跟随 Spring AI 这套标准走短期内是一个性价比很高的选择。