
说实话Spring AI 这个框架出来之前Java 后端想接大模型是一件相当“原始”的事情。你得自己用 WebClient 对着各家模型的 HTTP 接口发请求手写 JSON 解析自己处理超时重试还要为 prompt 拼字符串拼到头秃。后来 Spring 官方把这一整套东西抽象成了一层标准化的“AI 接入层”也就是 Spring AI情况才彻底变了统一的 ChatModel 抽象、一套 Prompt/Message 消息模型、能直接挂进 Spring IoC 容器的工具调用和 RAG 管线甚至 MCP 这类新协议也给你封装好了。这篇是教程的上篇我从“这框架到底解决了什么问题”讲起一路拆到 ChatClient、结构化输出、RAG、MCP、观测治理每段都给可直接抄走的代码和配置。适合两类人一类是刚想把 Spring Boot 项目接上大模型的后端开发另一类是已经写过几个 Demo、但始终没搞懂 ChatClient 和 RAG、MCP 内部关系的人。下篇我会重点讲多模态、模型评估、Agent 编排这些进阶东西先把基础盘稳了再说。1. Spring AI 到底是什么先别急着写代码1.1 没有 Spring AI 之前我们是怎么调大模型的我印象特别深2023 年那会儿要给项目接入 GPT常规操作是这样的写一个 HttpClient 封装类拿着 API Key 拼 Authorization 头把消息数组塞进 JSON body发完请求再写一个 DTO 去接 response然后还得单独处理 token 超限、连接超时、流式返回的 SSE 解析。最烦的是换模型厂商OpenAI 的接口字段和国内厂商的字段不完全一样prompt 风格也不同一旦换供应商业务代码里全是 if else。这套流程不是说不能用而是每个项目都在重复造轮子而且造出来的轮子参数调教、错误处理、日志埋点各不一样维护成本极高。Spring AI 做的就是把这层“和模型打交道”的脏活收敛起来给你一个稳定、依赖注入友好的门面你核心只写业务。1.2 Spring AI 的定位模型之上的抽象层你可以把它理解成 JDBC 之于数据库你写 JDBC 代码不需要关心底层是 MySQL 还是 PostgreSQL换驱动就能换库。Spring AI 同理你面向 ChatModel 编程底层接的是 OpenAI、通义千问、GLM 还是 DeepSeek只是配置差异代码基本不动。这套抽象覆盖了这几大块对话补全ChatModel、向量化EmbeddingModel、向量存储VectorStore、消息组装Prompt/Message、工具调用Function Calling、RAG 检索增强生成以及 2.0 时代重点强化的 MCP 客户端支持和观测Observation能力。每一块都可以单独使用组合起来又是一套完整的 AI 应用底座。Spring AI 的迭代速度非常快1.0.0 GA 刚出不久社区就已经在讨论 2.0 的 API 调整。所以我的建议是先抓稳核心概念和设计思路版本层面的细节差异完全可以通过官方文档补齐思路不变形代码怎么改都不慌。2. 十分钟跑通一个 Spring AI 项目2.1 依赖引入选官方 Starter 还是 Spring AI Alibaba动手前先回答一个很多新手纠结的问题Spring Boot 项目到底该接哪个 AI我的看法是别把“选模型”当成“选框架”框架层面直接用 Spring AI 官方 Starter模型层面再根据你的部署环境和成本来定。如果你主要用 OpenAI 系模型就引入spring-ai-starter-model-openai。如果你在国内部署更推荐用 Spring AI Alibaba 生态它基于千问模型做了深度适配接入的是 DashScope百炼平台也可以用它去接兼容 OpenAI 协议的第三方网关灵活性很高。依赖示例dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0/version /dependencydependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0/version /dependency注意Spring AI 的依赖版本号更新很频繁我这里写的是写作时的稳定版本你实际引入时建议去 Maven Central 或官方 BOM 确认一下最新版本。2.2 配置项与密钥管理application.yml 到底填什么依赖引入只是第一步真正的关键在配置。以 OpenAI 为例一个最小的对话配置长这样spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: https://api.openai.com chat: options: model: gpt-4o-mini temperature: 0.7用 Spring AI Alibaba 接千问的配置则是spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7这里的 api-key 我强烈建议用环境变量或者配置中心注入不要硬编码进代码或配置仓库。密钥泄露后被人刷接口的费用足够让你肉疼好几个月。temperature默认 0.7 是个比较稳妥的起始值做代码生成或结构化输出的时候可以调到 0.2做文案创意再调到 0.9 左右这些都是我反复实测出来的经验区间。2.3 写第一个对话接口ChatClient 的王牌体验配置好后直接注入ChatClient就能干活。Spring AI 最惊艳的设计就是这个流式 API它把“发消息、拿回复”压缩成了一行链式调用RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder.build(); } GetMapping(/chat) public String chat(RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }从这段代码能看到 Spring AI 的核心套路prompt()表示开始组装一次对话请求.user()往里面塞用户消息.call()表示同步阻塞等待结果.content()取出模型返回文本。整套 API 非常像你在跟框架说“帮我把这段用户消息发给当前配置的模型然后给我结果”没有任何多余的抽象负担。3. 把核心概念啃下来Message、Prompt 和 ChatClient3.1 对话的本质是一串消息SystemMessage 与 UserMessage很多新手上来就把 ChatClient 当成“一个发字符串的接口”这其实是误解。大模型的对话本质是无状态的你想让它记住上下文就必须把历史消息每次都一起发过去。Spring AI 用Message接口统一了不同类型消息SystemMessage定义模型角色和系统约束UserMessage表示用户输入AssistantMessage是模型之前生成的回复。举个例子做一个多轮数学辅导ListMessage messages new ArrayList(); messages.add(new SystemMessage(你是一个很有耐心的小学数学老师每次只解答一道题并给出详细步骤)); messages.add(new UserMessage(3加5等于多少)); messages.add(new AssistantMessage(等于8。3和5相加个位上358没有进位所以结果是8。)); messages.add(new UserMessage(那如果再加7呢)); String answer chatClient.prompt(new Prompt(messages)).call().content();把历史消息全部传给模型模型才知道前面聊到哪了。这解释了为什么一定要用消息列表来组织对话直接拼字符串的“伪多轮”不仅难以维护而且 token 浪费严重。我在实际项目里习惯封装一个ConversationHistory工具类负责滑动窗口截断和 token 数估算避免消息无限膨胀。3.2 Prompt 的完整结构不止是消息还有生成参数Prompt在 Spring AI 里不只是“消息集合”它同时携带生成参数。默认配置里的temperature、maxTokens、topP都可以在构造 Prompt 时动态覆盖。比如某次请求你需要严格的 JSON 输出就希望模型更保守Prompt prompt new Prompt( List.of(new UserMessage(请把这句话转成JSON我今天很开心)), OpenAiChatOptions.builder() .withTemperature(0.1) .withMaxTokens(200) .build() );这种方式特别适合同一个接口内部做不同场景的差异化控制不需要为每种场景单独建配置文件和 Bean。总的来说记住一个公式Prompt 多条 Message 生成参数你就能理解 Spring AI 几乎所有上层设计。3.3 ChatClient 的三种调用方式同步、流式与响应式call()是最直观的同步方式适合接口响应快的场景比如内部工具调用、服务间调用。用户感知强的对话页面则建议用stream()FluxString chunks chatClient.prompt() .user(用三句话介绍杭州) .stream() .content();stream()返回一个 Reactor 的 Flux前端通过 WebFlux 的 SSE 就能实现打字机效果。用流式主要有两个好处首字延迟大幅降低用户不用干等一个完整响应对超长文本也更友好不会因为响应过大导致网关超时。还有一个.call().entity()的用法就是下一节要讲的结构化输出。这里提前点一句ChatClient 的设计虽然看起来简单但它统一了同步、流式、结构化、工具调用四种主要形态学会这一个类基本等于学会了大半 Spring AI。4. 进阶玩法结构化输出、Function Calling 与 RAG4.1 让模型直接返回 JSON结构化输出的价值大模型返回的是一段自然语言但业务系统要的是 Java 对象。以前你得让模型“像 JSON 一样输出”然后自己用正则或 JSON 解析器去剥开代码块标记遇到模型偶尔夹带一句解释就解析报错非常难受。Spring AI 直接用.entity()把这件事收敛了record MovieInfo(String title, String director, String releaseYear) {} MovieInfo info chatClient.prompt() .user(推荐一部诺兰的科幻电影返回电影名、导演和上映年份) .call() .entity(MovieInfo.class); System.out.println(info.title());背后实现是把MovieInfo的 JSON Schema 塞进提示词要求模型严格按结构返回再把返回文本反序列化成对象。我自己踩过的坑是字段名别用无意义缩写模型理解力再强也猜不出dir是导演还是方向。另外 record 类型比传统 POJO 更省心序列化反序列化天然友好。结构化输出是所有 AI 业务落地的必备能力比如信息抽取、商品属性补全、工单自动分类都靠它把“人话”转成系统能消费的数据。4.2 给模型加一双手Function Calling 的实战用法模型本身不会查数据库、不会调外部接口Function Calling 就是给模型提供“可调用的工具”。流程是你把 Java 方法注册成 Function模型在生成回答时判断需要调用这个函数框架自动把参数传进来执行再把结果带回模型让它基于结果生成最终回答。Spring AI 里最简单的方式是直接注册一个 BeanBean Description(根据城市名查询当前天气) public FunctionWeatherRequest, WeatherResponse currentWeather() { return req - weatherService.getWeather(req.city()); }record WeatherRequest(String city) {} record WeatherResponse(String city, String temperature, String condition) {}注册之后只要在 ChatClient 里稍微配置一下模型就知道有currentWeather这个工具可用。注意这里的Description非常关键它相当于给模型看的“函数说明书”写清楚参数含义和返回结构模型才懂得什么时候调用、该传什么参数。我见过不少翻车案例Function 没反应或参数乱传十有八九就是 Description 写得太敷衍。4.3 RAG 实战让模型“读”你业务文档的正确姿势RAG检索增强生成解决的核心问题是知识时效和私有知识分三步理解就通了先把业务文档切片、向量化存到向量数据库用户提问时把问题也向量化在库里做相似度检索捞最相关的几个片段把片段塞进 prompt 一起发给模型让它基于这些原文回答。Spring AI 提供的抽象让这套流程异常简洁。向量化Bean VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); } vectorStore.add(List.of( new Document(公司年假制度入职满一年享有5天年假满三年享有10天当年未休完不结转。) ));检索并回答ListDocument docs vectorStore.similaritySearch(我入职两年能休几天年假); String answer chatClient.prompt() .system(你只依据提供的资料回答问题资料不足时明确说明不知道) .user(docs.stream().map(Document::getText).collect(Collectors.joining(\n))) .user(根据以上资料回答我入职两年能休几天年假) .call() .content();生产环境建议把 SimpleVectorStore 换成 PostgreSQL pgvector或者独立的向量数据库同时把文档切片长度、重叠比例这些参数做调优。切太碎语义会丢切太长又容易塞进无关内容我一般控制在 500 字左右、重叠 50 字具体还得看文档类型。Spring AI 2.0 的 RAG 也在持续演进上篇先把这三步串起来后面再细讲切分策略和 re-ranking。4.4 顺带聊句 NL2SQL热搜词里有人搜 NL2SQL就是让模型把中文问题转成 SQL再执行查询返回结果。这个业务场景本质上就是“结构化输出 Function Calling”的组合让模型输出 SQL 字符串项目里拦截校验后执行。注意别让模型直接执行任意 SQL最好只允许白名单表名和只读查询否则一条注入式 prompt 就能让你整个数据库裸奔。Spring AI Alibaba 在这个方向上也有成熟方案你可以把它当作一个延伸案例去深入研究。5. 聊一聊 MCP怎么直接使用别人提供的 MCP 服务5.1 MCP 到底解决了什么问题MCPModel Context Protocol是这两年 AI 圈最受关注的协议之一你可以把它理解成大模型世界的 USB-C 接口过去每个 AI 应用要对接一个外部工具就得单独写一套集成代码MCP 把这个过程标准化了。服务方通过 MCP 暴露工具和数据AI 应用通过统一的 MCP 客户端去发现和调用它们双方不再互相绑定。放到 Spring AI 的场景里价值非常具体很多团队已经把内部能力包成了 MCP 服务比如查订单、查库存、提交工单你不需要知道对方内部实现只要知道 MCP 服务地址一个配置就能把这些工具注册给模型相当于“直接使用别人提供的 MCP 服务”。这跟 Function Calling 是一脉相承的只是工具来源从本地方法变成了远端标准服务。5.2 对接第三方 MCP 服务的配置与使用Spring AI 从 1.0 开始就提供了 MCP 客户端支持集成思路非常简单。第一步引入依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId version1.0.0/version /dependency第二步在配置里声明要连接的 MCP 服务。常见的连接方式有两种基于 SSE 的远端服务和基于 stdio 的本地进程服务。SSE 方式适合连接部署在服务器上的第三方服务配置大致长这样spring: ai: mcp: client: sse: connections: order-service: url: http://order-server:8080/sse配好后Spring AI 的 MCP 客户端会自动向远端服务发起能力发现把对方暴露的工具注册成模型可调用的 Function。也就是说你只要在 ChatClient 里正常发提示词模型在需要时就会自动调用这个外部 MCP 工具整个调用链是框架替你拼好的。我实测下来的感受是接入成本确实低但要注意 MCP 服务的鉴权和限流生产环境务必确认服务方是否支持认证头配置别裸奔上线。6. 观测与治理ObservationHandler 的价值6.1 为什么必须给 AI 调用做观测模型调用是典型的黑盒你不知道模型为什么答成这样也不知道一次调用到底慢在哪。尤其在生产环境用户投诉“AI 变笨了”你需要能回答三个问题这次请求发了什么 prompt、模型返回了什么、耗时和 token 消耗是多少。Spring AI 基于 Micrometer Observation 提供了标准的观测扩展点这就是热搜里 ObservationHandler 相关内容的实际用途。你可以简单地把 Observation 理解成一条贯穿“请求进入 → 模型调用 → 工具调用 → 响应返回”的埋点链路通过注册不同的 Handler 把观测数据导出到日志、Metrics 或 Trace 系统。一个最小的日志观测 Handler 长这样Bean public ObservationHandlerObservationContext loggerObservationHandler() { return new LoggerObservationHandler(); }配上之后控制台就能看到一次模型调用从开始到结束的完整过程记录包括耗时和 token 数。想上更完整的可观测体系时可以接入 Micrometer Tracing 实现链路追踪或者对接到任何一个支持 Micrometer 的监控面板。6.2 观测能帮你发现哪些实际问题观测不只是“日志多打一行”它能直接帮我快速定位两类高发问题。一类是慢响应在观测数据里一旦发现某个工具调用耗时奇高马上能拆出来是模型本身慢还是下游服务慢。另一类是 token 浪费观测记录了每次调用的输入输出 token连续几轮对话后 token 消耗暴涨说明历史消息没有做窗口裁剪观测数据直接把泄漏点暴露出来了。我自己给项目接观测之后的体会是这玩意儿平时不起眼一旦上线出问题就是救命稻草。没有观测的时候排查 AI 问题等于盲人摸象加了观测后你至少能确定“模型收到了什么、返回了什么、卡在哪一步”再把排查范围压缩到业务代码或者模型行为效率完全不同。7. 常见问题与排查技巧实录7.1 高频问题速查表下面这张表整理了我接到咨询或被问到最多的几类问题都是实操现场可复现的现象可能原因解决办法报 401 Unauthorizedapi-key 没配或环境变量没生效检查 application.yml 与启动环境变量是否一致确认没有额外空格请求超时模型响应太长或网络不稳使用流式调用或调高 WebClient 超时时间设置合理的 maxTokens返回内容被截断超出模型上下文长度或 maxTokens 太小裁剪历史消息按滑动窗口保留最近几轮必要时启用摘要归档模型回答和文档不符RAG 检索结果不相关或切片太碎调整切片长度、增加 re-ranking、完善 prompt system 中的约束中文回答乱码接口返回编码或控制台编码问题确认读取流用 UTF-8IDEA 控制台设置 UTF-8 文件编码Function 工具一直不被调用Description 不清晰、参数 Schema 不明确重写工具描述把触发场景、参数含义、返回字段都写清楚切换模型后输出风格变化大各模型对同一参数响应不同按模型单独维护默认 temperature 和 prompt 模板7.2 我踩过的几个大坑第一个坑是版本不匹配。Spring AI 的 Starter 和核心依赖强绑定 Spring Boot 版本新旧混搭经常出现 NoSuchMethodError。我的实践是优先用 Spring Initializr 生成的工程直接选择对应的 Spring AI 版本不要手动东拼西凑。第二个坑是密钥写在配置仓库里。有一段时间我图省事把 api-key 写在本地 yml 里结果代码提交到共享仓库后差点泄露。现在我的原则是所有密钥一律环境变量或配置中心注入本地开发用.env文件加载Git 仓库用 ignore 规则排除。第三个坑是流式接口没有做好客户端断开处理。用户在下行流式响应时关掉页面后端如果没做取消订阅一次 token 浪费可能就发生在你察觉不到的角落。现在我在所有流式接口里都加了.takeUntilOther或者请求取消的监听成本低但收益非常实实在在。第四个坑是把“结构化输出”完全交给模型自觉。在早期版本里.entity()偶尔会碰到模型返回额外解释导致解析失败后来我是先做重试再做降级解析失败自动重试一次并把 temperature 降到 0再不行就返回兜底对象行为可预期很多。写在最后的一点个人体会做 Spring AI 集成跟做普通 CRUD 最大的区别在于你要同时搞定“系统确定性”和“模型不确定性”这两件事。系统侧要做好依赖管理、超时重试、观测埋点模型侧要不断调 prompt、校准参数、设计结构化输出。你别指望一次写对我几乎每个项目都要经历“上线后被用户问法教做人”的阶段好在 Spring AI 把可观测和调试这件事做得足够好让这个迭代过程快了很多。上篇把基础盘梳理完之后我个人强烈建议你亲自动手跑一遍先跑通 ChatClient再实现一个 RAG 问答最后把一个本地工具注册成 Function Calling再把一个外部 MCP 服务接进来。这四个动作做完你对 Spring AI 的理解会从“好像懂了”变成“确实能干活”。下一篇我会聊多模态、模型路由、Agent 编排以及 Spring AI Alibaba 在生产环境的具体落地姿势到时候再接着填坑。