☰
Spring AI实战:Java后端集成大模型的工程化指南
2026/10/18 2:40:21 网站建设 项目流程

说实话,第一次把 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> <groupId>org.springframework.ai</groupId> <artifactId>spring-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 中的 Connection
Prompt一次完整的请求输入,包含若干 Message 和参数类似 HTTP 请求体
Message单条消息,分为 System、User、Assistant 等类型类似一段对话记录
ChatClient面向开发者的高层 API,提供链式调用类似 JdbcTemplate
ChatResponse模型返回结果,包含文本和 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 的Flux<String>,天然适配事件流场景:

@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> chatStream(String message) { return chatClient.prompt() .user(message) .stream() .content(); }

我第一次用Flux<String>时,最大的困惑是:为什么不能像Iterator<String>那样同步遍历?后来想了想,用响应式流是有道理的——模型生成内容的时间不确定,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) { BeanOutputConverter<OrderInfo> 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方法",然后把orderId=A10086传给 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 中可以直接用QuestionAnswerAdvisor:

ChatClient 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 接口抽象层:依赖接口而不是 SDK

Spring 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 和函数调用,风险会小得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询