Spring AI实战:Java后端集成大模型的工程化指南

发布时间:2026/10/18 2:41:05
Spring AI实战:Java后端集成大模型的工程化指南 说实话第一次把 Spring AI 引入我们项目时我是带着怀疑的。当时的局面很典型一个跑了好几年的 Java 后端服务技术栈以 Spring Boot 为主团队突然接到需求要在内部工具里接入大模型对话能力。大家的条件反射是——又要写 HttpURLConnection、拼 JSON、解析 JSON、处理流式返回再自己维护一套密钥和重试逻辑。结果做了两三个 Demo 之后我们发现真正耗时的根本不是调通模型而是那些跟业务无关、但必须反复重写的粘合代码。Spring AI 解决的核心问题恰恰是让 Java 开发者不需要跨到 Python 生态也不需要自己搭一套模型接入框架就能把人工智能集成这件事嵌进原本熟悉的 Spring 工程里。这篇文章写给两类人一类是 Java 后端开发者想搞清楚 Spring AI 到底是什么、能不能用在生产环境另一类是团队里负责技术选型的人想找一个更工程化、不押注单一模型厂商的接入方案。我会从组件结构、核心 API、结构化输出、函数调用、RAG 检索增强、多模型切换再到生产环境真实遇到的坑一层一层拆开讲清楚。1. Java集成AI的老三难与Spring AI的破局思路1.1 过去我们是怎么接一个大模型的先回溯一下没有 Spring AI 之前Java 项目接大模型是什么体验。假设我们要做一个客服工单分类助手最朴素的实现长这样先准备好 API Key组装一段 system prompt 和 user message发送 HTTP POST 请求到模型服务商的接口拿到返回的 JSON剥出 content 字段再展示给前端。听起来不复杂对吧一旦进入真实业务事情立刻变味。模型接口偶尔超时、偶尔返回 5xx需要写重试策略流式输出要按 SSE 格式解析一个字节一个字节地拼有些模型返回的 JSON 里还夹杂着 Markdown 代码块你得自己清洗。更麻烦的是不同厂商的接口风格不同有的用messages数组有的用input字段换一家模型几乎等于重写一个 client。我们当时一个 200 行的小工具至少三分之一代码跟业务无关全是基础设施。这不是某个团队管理混乱而是 Java 生态里一直缺少一个标准层。Python 那边有各种 AI 编排框架Java 这边长期停留在谁家的 SDK 好用就用谁家的状态。Spring AI 的出现本质上是在试图结束这种混乱。1.2 Spring AI 想解决的不是算法是工程化很多人一听到 AI 框架下意识以为里面有什么高深的模型推理逻辑。但实际上Spring AI 的定位很接地气它不训练模型不优化模型它负责把模型接入这件事做成 Spring 风格的标准工程组件。你可以把它理解成 Spring Boot 对数据库做的事情——过去连不同的数据库都要写不同的 JDBC 驱动代码Spring Boot 统一了数据源配置、事务管理、异常转换。Spring AI 也一样它抽象出ChatModel、EmbeddingModel这些统一接口把各家模型的差异收敛到实现类里。开发者写业务代码时面对的是Prompt、Message、ChatClient这种和具体厂商无关的概念。这个设计带来的实际收益等到你要换模型、或者同一套代码要同时接两三个模型时才会真正体会到。我在后面的可移植性部分会专门展开这里先记住一个结论Spring AI 的破局点不在AI 多聪明而在接入多轻松。1.3 和 Python 系框架比它的优势在哪里团队里如果同时有 Python 和 Java 两种背景的人一定会争论直接用 LangChain 之类的 Python 框架不香吗答案取决于你的系统底座。如果新服务是从零开始、没有历史包袱Python 生态的选择确实更丰富。但大多数企业级系统不是这样用户系统、订单系统、权限体系全在 Java 服务里合规审计要求调用链可追踪运维体系基于 Spring Boot Actuator 和现有日志平台。这时候强行引入一个 Python 微服务就要解决跨语言调用、链路追踪打通、两套部署流程等一堆额外问题。Spring AI 的思路是在 Java 服务内部直接开一个AI 接入层和现有业务代码共用事务管理、配置中心、监控体系。尤其是函数调用Function Calling这个能力Spring AI 直接让你的普通 Bean 方法被模型调用相当于把一个 Java 方法变成模型可以操作的工具这种融合度是跨语言方案很难做到的。2. 从零接入核心组件与你的第一段对话代码2.1 依赖与配置先把环境跑起来实战之前先把项目依赖配好。我用的是 Spring Boot 3.x 项目Spring AI 的版本迭代比较快建议参照官方当前使用的版本Maven 坐标大概是这样dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency如果你的模型接的是国内服务商或者自建网关还可以选对应的 starter比如spring-ai-starter-model-qwen或spring-ai-starter-model-ollama。选 starter 的核心原则是你需要的是模型协议适配不一定是厂商绑定后面第七部分我再细说。配置文件里最核心的是模型端点、密钥和默认参数。我当时的application.yml长这样spring: ai: model: provider: openai openai: api-key: ${AI_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.7 max-tokens: 1024这里有个容易踩的小坑有些版本的配置前缀是spring.ai.openai.chat.options有些版本兼容旧的spring.ai.openai.chat.completions。如果启动后日志提示配置项无法识别优先检查版本号对应的文档不要凭经验乱改。2.2 核心概念速查ChatModel、Prompt、Message、ChatClient在写第一段代码前先把 Spring AI 里的高频概念理清楚。刚开始接触的人容易把ChatModel和ChatClient搞混实际上它们职责完全不同核心组件职责类比ChatModel底层模型调用抽象负责和具体模型通信类似 JDBC 中的 ConnectionPrompt一次完整的请求输入包含若干 Message 和参数类似 HTTP 请求体Message单条消息分为 System、User、Assistant 等类型类似一段对话记录ChatClient面向开发者的高层 API提供链式调用类似 JdbcTemplateChatResponse模型返回结果包含文本和 token 统计信息类似 HTTP 响应体日常开发中你打交道最多的是ChatClient它的设计参照了 Spring 生态里经典的RestTemplate/WebClient风格用链式调用来组装请求。第一次用它时我有一个非常直观的感受终于不用手动造一堆Map再序列化成 JSON 了。2.3 最小示便注入 ChatClient 完成第一次对话Spring AI 的自动配置会帮你创建好ChatClient.Builder你只需要注入并构建自己的客户端即可Service public class AiAssistantService { private final ChatClient chatClient; public AiAssistantService(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个严谨的Java架构师回答技术问题时要给出具体方案。) .build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }这段代码虽然简单但已经可以覆盖一个基础的对话接口。注意到我用defaultSystem()预设了 system prompt这样后续所有对话都会带上这一层背景信息不需要每个请求重复传。第一次跑通时项目里同事说了一句非常真实的话原来接大模型就这么点代码 是的框架的价值就在这里——把复杂留给自己把简单留给开发者。3. ChatClient 的日常姿态同步、流式、结构化一次讲透3.1 同步调用大部分后端接口的第一选择同步调用是所有调用方式里最容易理解的调用方发一个请求等到完整响应返回再继续。在内部工具、离线任务、异步消息处理这些场景里同步调用完全够用代码也最好维护。上面那段示例就是同步调用call()是同步方法content()直接拿到完整文本。同步调用的一个隐含问题是如果模型生成时间很长HTTP 连接会一直占用。所以实际项目中我会给这个接口配一个合理的超时时间。Spring AI 底层用的是 RestClient 或 WebClient超时配置可以在spring.ai.model.timeout或者对应的RestClient.Builder里设置不能用默认值硬扛。3.2 流式调用用 Flux 实现打字机效果如果前端要求像 ChatGPT 那样一个字一个字蹦出来就必须用流式接口。Spring AI 的stream()方法返回的是 Reactor 的FluxString天然适配事件流场景GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(String message) { return chatClient.prompt() .user(message) .stream() .content(); }我第一次用FluxString时最大的困惑是为什么不能像IteratorString那样同步遍历后来想了想用响应式流是有道理的——模型生成内容的时间不确定Flux 可以天然处理背压和取消。当前端断开连接时订阅者取消底层 HTTP 请求也会被终止不会浪费模型端的 token 和带宽。流式场景里有个细节容易忽略TEXT_EVENT_STREAM_VALUE这个 Content-Type 是给前端 EventSource 用的。如果前后端之间有网关记得确认网关不会缓存整个响应否则前端看到的还是一个整体字符串流式效果直接消失。3.3 可观测性别让调模型成为黑盒Spring AI 提供了和 Spring Boot Actuator 的集成能力。打开 Actuator 的 metrics 端点后你至少能看到以下几类信息每次请求消耗的 token 数、耗时、调用的模型名称。这个能力在排查问题时特别有用——比如某天模型服务变慢到底是网络问题、参数问题还是模型端限流通过指标数据能快速定位。我在生产环境里做了一个简单的调用记录把每次请求的消息数、token 数、耗时写进日志表业务方问为什么这个回答这么慢的时候可以直接翻数据说话而不是靠猜。4. 结构化输出从 JSON 拉锯战到类型安全的实体映射4.1 痛点让模型好好说话有多难把模型接入对话只是第一步真正让我觉得 Spring AI 值得深入用的是它的结构化输出能力。以前让模型返回 JSON最怕的不是它不会而是它总爱自由发挥——有时在 JSON 前后加 Markdown 代码块有时把字段名从orderId变成order_id心情好还会多给你几个字段。这些随机性对展示型对话无所谓但对下游系统是灾难。我们曾做过一个自动生成 SQL 的工具模型返回的 JSON 里多了一个reason字段导致反序列化直接报错。当时团队的做法是写正则表达式去清洗模型输出每次模型升级都要重新测试痛苦到怀疑人生。4.2 BeanOutputConverter原理与用法Spring AI 提供了BeanOutputConverter核心思路是让模型按照我们定义的 Java 类型来生成结构化的响应。它的底层机制是生成 JSON Schema塞进 prompt 里告诉模型你只能按这个格式输出然后把模型返回的文本用 Jackson 反序列化回 Java 对象。用法也很简单直接看代码public record OrderInfo(String orderId, String customerName, BigDecimal amount) { } public OrderInfo extractOrder(String text) { BeanOutputConverterOrderInfo converter new BeanOutputConverter(OrderInfo.class); String formatInstruction converter.getFormatInstruction(); String json chatClient.prompt() .system(从文本中提取订单信息严格按照给定格式输出。) .user(text) .call() .content(); return converter.convert(json); }getFormatInstruction()会生成一段类似你的输出必须是如下 JSON 结构...的提示词模型的输出被限制在这个 Schema 内。相比自己写正则这个方案最直接的价值是你定义了契约模型按契约输出。4.3 直接返回对象entity() 的高级用法如果你熟悉 Spring Data JPA 的查询方法应该很容易接受entity()这个方法。它把转换器 调用 反序列化一步封装了public OrderInfo extractOrderWithEntity(String text) { return chatClient.prompt() .user(text) .call() .entity(OrderInfo.class); }内部逻辑相当于自动构造了一个BeanOutputConverter并且把生成格式说明、调用模型、解析响应三步合并。这里有个重要前提模型返回的 JSON 必须能反序列化成目标类型。所以实体字段不要设计得过于复杂尽量用基本类型、String、常用日期类型如果字段名和模型输出不一致用JsonProperty映射一下即可。我在实际项目中用entity()做的第一个功能是客服对话情感分类。输入一段用户反馈返回情感标签、置信度、关键词列表。整个接口没有一行 JSON 手工解析而且模型升级之后输出结构依然稳定。5. 让模型动手Function Calling 的业务接入实战5.1 为什么需要 Function Calling对话模型最大的局限是只会说不会做。用户问帮我查一下订单号 A10086 的物流状态模型即使知道怎么查也没有权限访问你的订单数据库。以前的做法是让模型生成一段 SQL 或一段代码再去执行中间存在严重的幻觉风险——生成的 SQL 可能是错的。Function Calling 的思路完全不同你告诉模型系统里有哪几个函数、每个函数的参数是什么、什么时候该调用它模型根据用户意图自行决定是否调用、传什么参数。Spring AI 对 Function Calling 的支持是把某个普通 Java 方法注册成 Tool然后模型的调用请求自动路由到这个方法上方法返回值再作为模型生成回答的参考。5.2 Tool 注解把 Bean 方法变成模型工具Spring AI 的注解风格很直观你不需要写复杂的 JSON Schema直接在方法上加点元数据就行Component public class OrderTools { Tool(name queryOrderStatus, description 根据订单号查询订单物流状态) public String queryOrderStatus(ToolParam(description 完整的订单号例如 A10086) String orderId) { if (A10086.equals(orderId)) { return 订单已发货当前在武汉中转站预计三天后送达; } return 未找到该订单; } }注册这个 Tool 之后在构建ChatClient时指定它chatClient builder .defaultSystem(你是一个电商客服助手。查询订单时务必使用工具不要编造状态。) .defaultTools(queryOrderTools) .build();当我问订单 A10086 到哪了模型内部实际发生了一次工具调用模型先判断这个请求需要调用queryOrderStatus方法然后把orderIdA10086传给 Spring AI框架通过反射调用我们的 Java 方法拿到返回的物流信息再让模型把这段信息组织成自然语言回答用户。整个过程对我这个调用方来说几乎是无感的——我得到的就是一句自然的回复。5.3 实操中的三个重要提醒第一Tool的方法名和描述必须极其明确。模型是通过描述来判断何时调用工具的如果描述写得模糊模型会为了保险起见调用一堆无关工具浪费 token 还增加延迟。第二方法返回值不要过长模型收到工具结果后还会把它放进上下文。我们曾有一个工具把整个查询结果集返回给模型结果上下文窗口被大量占满后续对话的连贯性明显下降。第三重要数据操作类工具要有权限兜底——Function Calling 帮你打开了从模型到业务系统的通路这条路上必须有鉴权和风控不能靠模型的自律。6. 把知识库搬进对话Spring AI 的 RAG 链路搭建6.1 企业场景模型回答不了内部问题对话模型训练数据是有截止时间的也不包含企业内部的规章制度、产品文档、历史沟通记录。直接让模型回答离职流程是什么它大概率会编一个看起来很像真事的流程。RAG检索增强生成是目前这个场景最稳妥的解法先把你自己的知识文档切块、编码、存进向量库每次用户提问时先检索最相关的片段再把这些片段作为参考信息交给模型让模型基于给定材料作答。Spring AI 对 RAG 的支持是全链路的从文档读取到向量存储再到检索增强都有对应组件。没有 Spring AI 的时候这一步又要自己写 Embedding 调用、自己管理向量库客户端、自己拼检索结果的 prompt——又是一堆与业务无关的工作。6.2 离线索引文档切块、向量化、写入存储RAG 分成两个阶段离线索引建好知识库在线问答时做检索增强。离线的核心操作是读取文档、按一定粒度切块、用 Embedding 模型把每个文本块变成向量、写入向量数据库。我用 Spring AI 实现的索引代码核心思路如下var reader new PagePdfDocumentReader(new ClassPathResource(hr-policy.pdf)); var documents reader.get(); // 对文档切块默认约 300-500 token 一块 TextSplitter splitter TokenTextSplitter.builder().build(); var chunks splitter.split(documents); // 向量化并写入向量存储 vectorStore.write(chunks);PagePdfDocumentReader是 Spring AI 内置的多种文档读取器之一类似的还有TxtDocumentReader、MarkdownDocumentReader等。切块大小没有绝对标准如果业务文档偏技术手册我一般控制在 300 到 500 token太短会丢失上下文太长则检索噪声大。6.3 在线问答检索 合成回答在线问答时用户问题先进过同样的 Embedding 模型做向量化然后到向量库里找最相似的几段文本最后把原文片段拼进 system prompt。Spring AI 中可以直接用QuestionAnswerAdvisorChatClient ragClient ChatClient.builder(chatModel) .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .defaultSystem(回答时只能使用提供的文档内容不得编造。) .build();加了这行 Advisor 之后每次对话都会自动检索向量库并把检索到的内容附加到 prompt 里。业务代码依旧干净不需要手动管理检索结果和 prompt 拼接。6.4 RAG 的工程经验不是搭起来就完事RAG 的效果好不好关键往往不在模型而在检索环节。我遇到过的几个典型问题一是切片太死板一段文字被切断导致语义不完整检索到的片段答非所问二是没有元数据过滤所有部门的文档混在一起问技术问题却检索到行政规范。建议在写入向量库时给文档加上部门、文档类型、更新时间等元数据检索时带上过滤条件效果会有质的提升。7. 模型可移植性一套代码对接不同大模型的细节7.1 为什么要在意可移植性商业模型 API 的价格、稳定性、合规性都是变化的。我们还遇到过一种情况客户要求整个系统部署在内网只能使用本地化的开源模型。如果代码紧绑某一家的 SDK这种需求基本等于重构。Spring AI 的可移植性设计让你大部分业务代码不感知具体模型厂商切换时只需要改配置和依赖。7.2 接口抽象层依赖接口而不是 SDKSpring AI 的核心接口是ChatModel所有模型的实现类都实现这个接口业务代码依赖接口。切换模型的时候通常只改 starter 依赖和配置。比如从云服务提供商 A 切换到本地 Ollama业务层代码完全不变# 方案一云服务商A spring: ai: openai: api-key: ${AI_API_KEY} chat: options: model: gpt-4o-mini # 方案二本地 Ollama spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: qwen2.5这种切换背后Spring AI 的自动配置会根据你已经引入的 starter 自动创建对应的ChatModelBean。业务代码里的ChatClient.Builder拿到的实现类变了但上层调用不变。7.3 不同模型的实测对比别指望完全一致不过可移植不等于零修改。不同模型在同样的提示词下表现差异很大。我做了个小规模对比供选型参考场景小型商业模型大型商业模型本地开源模型日常对话够用偶有逻辑漏洞稳定指令理解强中规中矩依赖提示词长文总结会丢细节较好控制摘要比例上下文有限时效果下降结构化输出偶尔格式错误基本稳定需要多加约束语函数调用偶发不调用工具调用准确率高对复杂参数理解偏弱延迟低中高取决于硬件我的建议是不要在多个模型之间追求 100% 一致输出而是把关键能力用自动化评估用例固定下来。换模型后跑一遍评估集看准确率、格式合规率、调用工具的成功率。这样即使模型换了你能快速知道哪些提示词需要调整而不是上线后才发现回答质量断崖式下跌。7.4 提示词的可移植性一份 Prompt 走天下的幻觉很多团队会犯一个错误认为抽象了接口Prompt 就可以跨模型复用。实际上不同模型对 system prompt 的敏感度差异很大。某些开源模型对不要编造这类负向指令理解不足需要改成更具体的正向说明。所以模型切换时建议把 Prompt 也纳入版本管理和模型版本绑定一起做回归测试。8. 生产环境实测性能、成本和那些文档里没有的坑8.1 流式超时与重试模型一慢全链路遭殃上线第一个月我们遇到最多的就是超时问题。模型生成速度受服务端负载影响高峰期一个流式请求可能持续 30 秒以上。如果网关超时设成 10 秒用户会看到回答断到一半体验极差。解决方案是分级设置网关对 SSE 连接不设整体超时只设空闲超时比如 60 秒内没有新数据就断开应用层重试要格外小心不是所有错误都适合重试。认证失败、请求参数错误这类 4xx 错误重试只会让事情更糟网络超时和 5xx 才值得重试。重试策略一定要加退避和最大次数限制否则模型服务端抖动时你的应用会瞬间发起大量重复请求。8.2 Token 成本与上下文膨胀预算是怎么烧掉的没有成本管控就上生产月底账单会让你清醒。最大也是最隐蔽的成本黑洞是上下文膨胀——每次对话都把完整历史消息传给模型加上工具返回的结果、检索到的文档片段很快就触及上下文窗口费用随着对话轮数几何级增长。常规做法是限制单次会话最大消息数、对历史消息做摘要压缩、把不必要的历史内容从请求里移除。另外如果同一个问题被多人反复问把相似问题的回答做一层缓存能省下不少钱。8.3 幻觉与安全边界给 AI 的口无遮拦上锁RAG 能大幅降低幻觉但无法根除。模型有时会从检索到的多段文档里脑补出不存在的内容尤其是文档之间存在冲突时。生产级方案需要三层防护检索层通过相关性阈值过滤掉低置信度的片段生成层在 system prompt 中明确要求如果没有答案就直说不知道业务层对关键结论做兜底校验比如涉及金额、日期、用户信息的字段需要走规则引擎校验而不是全盘相信模型。8.4 版本迭代跟着它的脚步但别追得太急Spring AI 目前迭代速度很快API 偶有调整。这意味着两点一是不要用快照版本上生产锁定正式版本二是升级前读一下 release notes重点看ChatClient、Tool相关 API 是否有 breaking change。我们项目里把 Spring AI 版本和模型版本一起固定每次升级都跑一遍自动化评估集这样即使行为有变化也能第一时间发现。常见问题症状解决方向流式断流前端输出中途停止网关空闲超时调大区分连接超时和读取超时响应解析失败JSON 解析异常、字段缺失用entity() 固定 DTO清洗 prompt 兜底Token 超预算账户费用飙升限制上下文长度缓存重复问题启用 token 用量监控回答问题不专业结果看着对实际错RAG 元数据过滤 人工兜底校验切换模型后质量下降语义理解、指令遵循变差自动化评估集逐条对比输出调整 Prompt最后说点个人体会把 Spring AI 这套体系从零搭到生产我的整体感受是它不会替你把业务逻辑设计好它真正擅长的是把大模型接入从脏活累活变成配置项 接口调用。说得直白一点Spring AI 最大的价值甚至不是省了几百行代码而是让你不再被任何一个模型厂商绑定你可以根据成本和效果随时换底下的引擎而业务代码纹丝不动。如果让我给正在评估的团队一个建议先在评估环境做一个结构化输出的小功能把数据格式、调用链、指标监控全部打通再用同样一套代码去接第二个模型。这个过程会让你快速理解 Spring AI 哪些地方是白拿的福利哪些地方需要你额外兜底。等你对它的能力边界心里有数了再上 RAG 和函数调用风险会小得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询