☰
Spring AI实战:从统一抽象到餐饮SaaS集成
2026/9/28 15:04:40 网站建设 项目流程

如果你最近在写 Java 后端,应该已经感受到身边的同事开始讨论 Spring AI 了。这个项目从 0.x 预览版一路走到 1.0,终于把“Java 调用大模型”这件事做成了一套标准姿势。不再是各家模型 SDK 各自为政,也不是自己拿 RestTemplate 去拼 /chat/completions 接口。Spring AI 提供了一套和 Spring Boot 深度绑定的抽象:模型接入、聊天客户端、流式响应、结构化输出、Tool Calling、多轮记忆管理,都有不笨重的落地方案。

这篇文章不是官方文档的复读,而是我实际把 Spring AI 用在 Spring Boot 服务里、对接国产模型、再做餐饮 SaaS 场景 AI 集成之后的一份沉淀。适合谁?如果你是第一次接触 Spring AI,想在三十分钟内跑通第一个对话接口;或者你已经会写 ChatClient,但想在真实业务里把 Tool Calling 和多轮记忆用好,这里应该都有能直接照抄的东西。我尽量把每一步背后“为什么这么做”也写清楚,毕竟只抄代码不理解原理,下次换模型换版本还是会踩坑。

1. 为什么选择 Spring AI:先从“翻译器”说起

1.1 从 API 拼接软件到统一抽象

以前在 Java 项目里接大模型,典型流程是这样的:注册账号、拿 API Key、翻出官方 SDK 的 README,然后在 Service 层写一个封装类,内部处理 HTTP Client、超时重试、JSON 解析、错误码。这些代码好像不难写,但一旦换了模型提供商,或者同一个项目要接两个模型做对比,封装类的代码就要成倍膨胀。

Spring AI 做的事情,类似 Spring 对数据库访问的封装。JDBC 时代,不同数据库方言多、驱动差异大,Spring 的 JdbcTemplate 和后来 JPA 让上层开发不用关心底层是 MySQL 还是 PostgreSQL。Spring AI 也是同一个思路:把“模型提供商”当作可替换的底层实现,对外暴露统一的ChatClient接口。你在业务代码里只面对Prompt、Message、Tool这些东西,不理会请求到底发给了 OpenAI、智谱AI 还是本地 Ollama。

用一句话给第一次接触的人解释:ChatClient在大模型应用里的地位,好比RestTemplate在 HTTP 调用里的地位。所有模型提供商都会帮你适配成同一个门面,切换模型时,只需要改配置和换依赖,业务代码基本不动。这个抽象对中小团队尤其友好,因为大模型的迭代速度很快,今天用的模型不一定是最优解,抽象层给了你随时换路的底气。

1.2 Spring AI 与 LangChain4j:谁更适合你

聊 Java AI 框架,绕不开 LangChain4j。它也是一套优秀的库,Agent、RAG、Memory 这些概念都有,社区活跃度也不错。我身边不少同事用过,反馈是“功能很全,但要自己拼装的东西也多”,尤其是当你只想写一个简单的对话接口,它默认生成的工程结构可能比你想要的复杂。

Spring AI 更贴合 Spring Boot 项目的组织方式。它把自动配置玩得很透,引入一个 starter,配置几行 yml,就能在类里注入ChatClient。Spring Boot 之外的观测体系、Actuator 端点、配置加密方案,也都能直接复用。如果项目本来就是 Spring Boot,选 Spring AI 可以减少框架之间的隔阂。LangChain4j 更适合那些需要大量自研 Agent 编排、不太依赖 Spring 体系的项目。选择没有对错,我自己的判断标准很简单:团队文化是“Spring 重度用户”,就优先 Spring AI;是“想从零搭一套 AI 中间件”,LangChain4j 值得研究。

2. 环境准备与 Maven 依赖:别在版本上翻车

2.1 基础环境与脚手架

在动手之前,先确认你的 Java 环境。Spring AI 1.0 要求 Java 17 以上,Spring Boot 建议用 3.4 系列,Maven 用 3.9 就够。如果你还在用 JDK 8,那得先给项目迁移一下运行时,否则后面会碰到很多奇怪的类加载问题。

创建项目最简单的方式是去 start.spring.io,选 Spring Web、Lombok、Validation 这几个基础依赖,Java 版本选 17。不过我更习惯直接改 pom.xml,因为 AI 相关 starter 的版本统一管理很重要,可视化界面默认生成的依赖可能不带 Spring AI 的 BOM。一个小建议:先在本地准备好一个干净的空项目,再用下面的依赖片段往下堆,避免网上复制来的配置互相冲突。

2.2 Maven 依赖的两种玩法

Spring AI 的 Maven 依赖看起来简单,但版本管理有个关键节点:Spring AI 官方推荐你先导入它的 BOM,再使用各个 starter,这样所有模块的版本能保持一致,不会出现spring-ai-core是 1.0.0、spring-ai-openai还是 1.0.0-M1 这种混乱。

在你的 pom.xml 里加入:

<properties> <java.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

这样后面加的每一个 starter 都不需要再写版本号。接下来,添加最常用的 OpenAI 协议 starter:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency>

注意,这里引入的是 OpenAI 协议模型的 starter,并不是只能连 OpenAI 官方。很多国产大模型提供了 OpenAI 兼容接口,我们后面接入智谱AI 就是用这个 starter,只是把base-url指向智谱的服务地址。

另外,如果你的网络环境访问 Maven Central 不够顺利,或者碰到某些 Spring AI 模块没有及时同步到中央仓库,需要在 pom 里加两个 Spring 仓库:

<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> </repository> </repositories>

我建议正式项目只引入spring-milestones,不要在生产环境用 snapshot 版本,否则不定哪天依赖就变成了一个不可控的中间版本。

2.3 用 OpenAI 兼容接口接入智谱AI

现在很多国产模型都提供了 OpenAI 兼容的 API 端点,好处是客户端生态可以直接复用。以智谱AI 为例,在 application.yml 里配置:

spring: application: name: spring-ai-demo ai: openai: base-url: https://open.bigmodel.cn/api/paas/v4 api-key: ${ZHIPUAI_API_KEY} chat: options: model: glm-4-flash temperature: 0.7

重点说三个小细节:

第一,api-key不要直接写死在 yml 里,用环境变量传递。你永远不知道代码什么时候会被推到公共仓库,一个泄漏的 key 可能几天内就被刷爆。

第二,glm-4-flash是智谱AI 提供的免费模型,拿来跑通整个流程性价比很高。想用更高质量的glm-4-plus或者多模态模型,再去控制台申请相应权限。

第三,这种接入方式本质上是让 Spring AI 按照 OpenAI 协议去请求智谱的服务,所以请求参数里和 OpenAI 不兼容的扩展字段没法直接用,但只要只是做 Chat、Function Calling,体验差别不大。

3. 第一个“能聊天”的后端接口

3.1 ChatClient 是怎么工作的

把依赖和配置准备好后,启动项目,Spring Boot 的自动配置会基于你的模型 starter 创建一个ChatClient.BuilderBean。这个 Builder 是后续所有业务调用的入口。理解它的工作流程,只需要记住一条链路:

prompt()创建一个 Prompt 构建器,我们往里面塞用户消息、系统消息或者其他配置,构建出Prompt。call()或者stream()方法把 Prompt 交给ChatModel,ChatModel负责真正发送请求给大模型。返回值是ChatResponse,里面包含了模型生成的文本和 token 用量等信息。

日常开发中,绝大多数情况你只需要操作ChatClient,不会直接碰ChatModel,除非要定制非常底层的请求行为。这也是 Spring AI 做得好的地方:把灵活的部分放在 Builder 里,把复杂的部分藏在自动配置后面。

3.2 实现一个 Web 接口

最基础的例子,写一个 GET 接口,接收参数并返回大模型回答:

@RestController @RequestMapping("/ai") public class AIController { private final ChatClient chatClient; public AIController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @GetMapping("/chat") public String chat(@RequestParam(defaultValue = "你好,请用一句话介绍你自己") String message) { return chatClient.prompt() .user(message) .call() .content(); } }

这段代码注意一个细节:我注入的是ChatClient.Builder,然后在构造器里build()出来。如果你直接注入ChatClient,Spring Boot 在只有一个模型实现时也能自动装配,但我觉得使用 Builder 更清晰,以后要加defaultAdvisors、defaultTools都很方便。

启动项目后,浏览器或命令行访问:

curl "http://localhost:8080/ai/chat?message=用一句诗形容程序员加班"

正常情况下会返回一句符合意境的诗。到这里,一个 Spring Boot + 大模型的最小闭环已经跑通了。整个过程没有写过一行 HTTP 调用代码,也没有解析过一次 JSON,这就是 Spring AI 带来的直接价值。

3.3 流式输出与结构化输出

对话接口只是最基础的功能,实际业务里更常用的是流式输出和结构化输出。

流式输出适合聊天机器人,字是一个个蹦出来的,体验比等了三四秒直接出一大段文字好很多。实现方式很直接:

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

返回类型变成Flux<String>,Spring Boot 会把它包装成 SSE。前端用EventSource或者fetch读取流式响应。我这里有个实际经验:测试流式接口时,命令行用 curl 看输出经常感觉是乱序的,但这不是程序的问题,是 curl 缓冲导致的。用浏览器或者 Postman 的 SSE 视图测试更直观。

结构化输出是让大模型返回一个 Java 对象,而不是一串自由文本。Spring AI 内部会构建 JSON Schema,让模型按照这个约束输出,再反序列化成对象。例如餐饮场景里,把一段菜品的自由描述解析成结构化数据:

public record DishInfo( String dishName, String taste, Integer price, List<String> ingredients ) {}
@PostMapping("/parse-dish") public DishInfo parseDish(@RequestBody String description) { return chatClient.prompt() .user("请从菜品描述中提取结构化信息,菜品描述:" + description + "。如果描述中不存在某个字段,用空值代替,不要编造。") .call() .entity(DishInfo.class); }

这个能力很有用。做餐饮 SaaS 时,商家员工录入菜品往往就是一句话:“招牌红烧肉,精选五花肉,酱香微甜,炖两小时,一份 68 元”。用上面的接口,这条文本就能被拆成dishName=红烧肉、taste=酱香微甜、price=68、ingredients=[五花肉, 冰糖],直接入库做结构化检索和标签化运营。

4. 在餐饮 SaaS 里做 AI 集成:一次实战拆解

4.1 需求切分:哪些功能适合 AI 介入

餐饮 SaaS 是一个典型的多租户业务系统,每家商户都有自己的菜品、订单和评价数据。做 AI 集成之前,第一步不是写代码,而是把需求切分清楚。以我实际做过的项目为例,商家后台最常提的三个需求分别是:自然语言查经营数据、菜品自动归类、差评自动回复。

自然语言查数据听着高级,但并不是任何场景都适合 AI。如果只是写死的“今日营业额”,一个 SQL 查询比调用大模型又快又准。真正适合 AI 的是模糊问题,比如“上月销量前五的招牌菜是哪些”、“这周和上周比退菜率有没有变化”,用户输入千变万化,没法穷举按钮。所以我把这类需求定位为:AI 负责任务理解,工具负责执行数据查询,最后再由 AI 组织答案。

菜品自动归类则完全是结构化抽取的舞台。商家批量导入菜品图片或文字描述,AI 抽取口味、食材、烹饪方式,输出统一的标签体系。这种任务即使某个菜品猜错了,人工改一下成本也不高,属于“AI 处理 80% 常规场景”的好选择。

差评自动回复要更谨慎,因为涉及客户体验。我的做法是只生成“草稿”,不直接自动发布,并且提示词里要求模型把道歉、解释、解决方案三段控制在合理范围内,避免过度承诺。

4.2 Tool Calling 让 AI 学会查订单

真正让 AI 融入业务系统的关键,是让模型能够调用我们已有的 Java 方法。Spring AI 把它叫做 Tool Calling,实现方式也很 Spring。

先定义一个工具类,用一个注解标注可被模型调用的方法:

@Component public class RestaurantSalesTool { @Tool(description = "根据菜品名称和月份查询销量,月份格式为 yyyy-MM,返回结果是销量数字") public String getMonthlySales(String dishName, String month) { // 这里实际应该注入 Mapper 查询数据库 // 为了演示,返回模拟数据 return "红烧肉在 " + month + " 的销量是 1280 单"; } }

然后在构建ChatClient时,把工具挂上去:

@Bean ChatClient chatClient(ChatClient.Builder builder, RestaurantSalesTool salesTool) { return builder .defaultSystem("你是餐饮SaaS平台的商家运营助手。根据用户问题,分析是否需要调用工具。" + "如果需要查询具体销量数据,调用 getMonthlySales 工具,不要凭空编造数字。") .defaultTools(salesTool) .build(); }

这样当用户问“我这个月红烧肉卖得怎么样”时,模型会判断需要调用getMonthlySales,自动把参数dishName=红烧肉、month=2025-06传进去,拿到方法返回结果后,再组织语言回答给用户。

Tool Calling 的体验感很强,但有一个特别容易被忽略的坑:@Tool的description必须清晰,因为模型不是看你的方法名猜功能,它读取的是这个描述。描述里最好说清楚参数格式、返回内容、什么情况下用,描述越精准,调用准确率越高。

4.3 用 Advisor 管理多轮记忆

单轮工具调用只是 AI 助手的第一步,真正聊天过程中,用户会追问“那上个月呢”,如果模型不记得上文,这句追问就是无效信息。Spring AI 提供了Advisor机制来处理这个问题。

最简单的做法是用MessageChatMemoryAdvisor和内存版InMemoryChatMemory:

ChatMemory chatMemory = new InMemoryChatMemory(); ChatClient chatClient = ChatClient.builder(chatClientBuilder) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();

实际业务里要注意多租户隔离。餐饮 SaaS 有几十上百个商户,如果所有商家的对话都放在同一个记忆空间,A 商家的上下文就可能泄露到 B 商家的会话里。所以设置会话 ID 时,一定要带上租户 ID:

String conversationId = tenantId + ":" + userId; return chatClient.prompt() .advisors(advisor -> advisor .param(ChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)) .user(userMessage) .call() .content();

InMemoryChatMemory适合单机开发环境,生产环境建议换成 Redis 实现。Spring AI 目前有相应的扩展点,你可以根据自己的缓存体系实现ChatMemory接口,把历史消息存储对接到底层 Redis 集群。

5. 常见问题与排查技巧实录

5.1 依赖与版本问题速查

我自己在学习和落地过程中,遇到最多的问题都集中在 Maven 依赖和版本不匹配上,很多现象看起来很像代码错误,其实根因就是版本混乱。下面这张表是我整理过的问题速查:

现象原因解决方案
启动报错:No qualifying bean of type 'ChatClient.Builder'没有真正引入可用的模型 starter检查是否引入了spring-ai-starter-model-openai或对应模型 starter
明明加了依赖,代码里还是找不到ChatClient类Spring AI 模块版本和 Spring Boot 版本不匹配使用 Spring AI BOM 管理版本,并确认 Boot 版本在 3.4 系列
Maven 下载依赖时 502 或连接超时Spring AI 里程碑版本在中央仓库同步不及时在 pom 中增加 Spring 官方 milestones 仓库
同一个类出现多个不同版本子模块分别指定了不同版本号统一使用 BOM,不要单个依赖写<version>
项目能启动,但调用接口时返回 404base-url 配置把请求路径拼错了检查spring.ai.openai.base-url是否包含完整前缀,智谱一般是https://open.bigmodel.cn/api/paas/v4

版本问题是 Spring AI 新手最容易被绊倒的一关。我建议拿到任何一份网上代码,先看它的spring-ai.version是什么,再看是否和你的 Boot 版本兼容,不要无脑复制最新的版本号。

5.2 接口调用与输出问题速查

依赖解决了,接口调用阶段也有几个高频问题:

现象原因解决方案
返回 401 UnauthorizedAPI Key 错误或未正确读取环境变量确认ZHIPUAI_API_KEY已设置,不要在代码里硬编码
请求一直卡住,最后超时模型响应慢,或网络到模型服务的链路存在问题调整 Spring AI 的超时和重试参数,或者先用免费模型验证
返回的文本带着 Markdown 符号模型默认自由输出在 system prompt 里明确“只返回 JSON,不要返回 Markdown”
使用.entity()时抛 JSON 解析异常结构化输出约束不够,模型返回了多余内容强化记录字段描述,并在提示词里增加“缺少的字段不要编造”
流式输出中文变乱码客户端没有正确解析 SSE 编码确保 Controller 的produces = MediaType.TEXT_EVENT_STREAM_VALUE,前端设置 UTF-8
Tool Calling 没有被触发工具描述不清晰,或模型本身不支持 function calling检查工具方法注释,尝试换更智能的模型

记忆特别深的一次,是我在餐饮 SaaS 项目里用.entity()解析菜品信息,模型偶尔会多输出一句“根据您提供的描述”,因为 prompt 里没有强调只输出 JSON。后来我在系统消息里加了一句话:“你是一个 JSON 输出器,任何解释、寒暄、Markdown 标注都不允许出现”,问题立刻消失。这个技巧对于所有结构化输出场景都适用。

5.3 实测建议与避坑心得

根据我在这类项目里的实测,有几个经验想重点分享给你:

第一,先用免费模型跑通全链路,再切换高配模型。很多团队一上来就申请了高精度商业模型,调试阶段对话不多,看起来没多少费用,等联调时才发现提示词有问题,高配模型也救不回来。还不如先用便宜的模型把流程走顺,最后在关键任务上换优质模型。

第二,Tool Calling 的调试要独立进行。写一个测试用例,直接调用工具类方法,确认返回结果。然后再通过 ChatClient 让模型调用工具,否则出了问题很难定位是模型没调用对,还是工具代码本身有 bug。

第三,生产环境一定要加审计日志。AI 生成的回复和调用的工具参数,建议都记录到日志或数据库。餐饮商户面向消费者,一旦商家误用了 AI 生成的错误数据,你需要能追溯上下文。

第四,不要把租户 ID 交给模型去猜。正确的做法是,从当前登录上下文或者请求头里拿到租户 ID,通过系统提示词注入给模型,或者在工具调用上下文中传入。系统提示词可以这样构造:

String systemPrompt = "你是【XX餐饮云】的商家运营助手。当前商户ID:" + tenantId + "。查询任何数据都必须限制在当前商户ID内,不允许跨商户使用其他ID。";

这个思路对任何多租户 SaaS 都成立。AI 再聪明,也比不上在源头把数据隔离做好。

6. 从小接口到智能体:一条务实的演进路线

很多人在了解 Spring AI 之后,会立刻想做一个全能的 Agent。我的建议是,不要一步到位,而是沿着一条务实路线往上走:先做单轮对话,再做流式输出,接着接入 Tool Calling,最后才引入多轮记忆和 Agent 编排。每一步都是上一步的自然延伸,出现问题也知道是哪个环节。

在实际项目里,你可以先从一个小接口开始,例如给菜单识别分类,让商家觉得“这东西有点用”。然后在这个接口上叠加 Tool Calling,让 AI 能查菜品销量和库存,商家的问题就从“帮我分类”变成“帮我看看这道菜要不要补货”。最后再加上多轮记忆,允许商家连续追问,才真正像一个 AI 助理。

Spring AI 给了我一个很好的支点,它把模型接入这个最脏最累的活收掉了,让我可以更专注于业务本身。如果你也在做餐饮 SaaS,或者任何类似的行业 SaaS 想集成 AI,我的建议都是:不要急着炫技,先找出一个最痛、最适合 AI 的结构化或查询场景,跑通之后,再谈放大。这个打法,比我见过的很多“先搭 AI 中台再找场景”的项目要稳妥得多。

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

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

立即咨询