Spring AI 实战:从一次可运行的模型调用到 RAG 体系
面向有 Java 与 Spring 基础的开发者。前半篇跑通一个最小可运行案例,把一次对话调用的完整链路拆开看;后半篇梳理 Spring AI 的概念体系、核心能力、以及"什么情况下该用它"的判断依据。
一、先跑通:一个最小可运行案例
1.1 版本口径先对齐
Spring AI 目前有三条维护线,选哪条首先取决于你项目的 Spring Boot 版本。三条线的 Boot 版本不重叠,跨线升级等于跨 Boot 大版本。
| 版本线 | 最新发布 | 适配 Spring Boot | 定位 |
|---|---|---|---|
| 2.0.x | 2.0.1 | 4.1.x | 已 GA,编排能力有破坏性变更,新项目可评估 |
| 1.1.x | 1.1.8 | 3.5.x | 生产主流选择,生态适配最全 |
| 1.0.x | 1.0.9 | 3.4.x | 维护线,只修问题不加特性 |
本文示例以 Spring Boot 3.5.x + Spring AI 1.1.x 为基线。文中会标注 2.0 线的主要差异,避免你抄过去跑不通。
1.2 依赖
工程结构很小,四个文件就能跑起来:
pom.xml 只需要三样东西:Boot 父工程、Spring AI BOM、一个模型 starter。
两个容易踩的点先说清楚:
- starter 命名在 1.0 GA 换过一轮 。老的
spring-ai-openai-spring-boot-starter已经不存在,现在的规则是spring-ai-starter-model-<厂商标识>。照着两三年前的博客抄依赖,会直接解析失败。
- 1.0 之后 Spring AI 已在 Maven 中央仓库。加 BOM 就够了,不需要再挂 Spring 的 milestone 仓库。只有追 2.0 里程碑版本时才需要。
1.3 配置
密钥一律走环境变量或配置中心,${OPENAI_API_KEY} 由 Boot 的宽松绑定从环境变量读入。把 key 写进 application.yml 再提交仓库,是这类项目最常见的事故来源。
国内环境通常没法直连 OpenAI。Spring AI 的 OpenAI 实现走的是兼容协议,改 base-url 就能指向任意兼容服务:
阿里云百炼(通义千问)走社区项目 Spring AI Alibaba,依赖换成 spring-ai-alibaba-starter-dashscope,配置前缀是 spring.ai.dashscope。本地调试则用 Ollama,依赖 spring-ai-starter-model-ollama,配置只要一行 spring.ai.ollama.base-url,模型不用付钱也不用联网。
1.4 三行核心代码
主类就是普通的 @SpringBootApplication,不需要任何额外注解。启动后:
一句话回答就回来了。整条链路的形状和 restClient.get().uri(...).retrieve().body(...) 完全一致:构造请求 → 指定输入 → 发起调用 → 取出结果。写过 Spring 的人不需要重新学一套思维模型。
1.5 这条链路里到底发生了什么
看起来只有三行,实际经过了两段过程。
启动期(自动装配)
spring-ai-starter-model-openai引入自动配置模块,读取spring.ai.openai.*配置项;
- 自动配置构造出 HTTP 客户端与
OpenAiChatModel(ChatModel接口的实现),注册为 Bean;
- 同时注册一个
ChatClient.Builder原型 Bean,交给你的业务类构造ChatClient。
调用期(一次请求)
prompt()返回一个请求规格对象(ChatClientRequestSpec),user()往里追加一条UserMessage,system()追加SystemMessage;
call()进入 Advisor 拦截链。记忆 Advisor 从会话存储里读出历史消息拼进消息列表,检索 Advisor 去向量库捞相关文档片段,日志 Advisor 打印请求;
- 组装成
Prompt(消息列表 + 调用参数),交给ChatModel.call();
- 具体厂商实现把
Prompt序列化成 HTTP 请求,发到/v1/chat/completions;
- 响应反序列化成
ChatResponse,其中包含生成的候选结果(generations)和 token 用量(usage);Advisor 链的后置逻辑再把本轮问答写回记忆;
content()取第一条 generation 的文本。若用entity(),则再走一遍输出转换器把 JSON 反序列化成对象。
理解第 5 步是关键。Advisor 是 Spring AI 真正的扩展点,记忆、RAG、日志、内容安全全部挂在这一层,后面每一节的能力都绕不开它。
1.6 关键代码要点
一、ChatClient 要做成单例
很多人图省事在方法里 builder.build(),每次请求重建一遍 Advisor 链。正确写法是在配置类里建 Bean:
ChatClient 本身是线程安全的不可变对象,构造一次全局复用。
二、参数分两层:全局默认 + 单次覆盖
defaultSystem、defaultOptions、defaultAdvisors 属于全局默认值;单次调用要调参,在链式调用里覆盖即可:
用 ChatOptions 这个通用接口,换厂商时这行代码不用动。需要调厂商私有参数(比如 OpenAI 的 reasoningEffort),换成对应的 OpenAiChatOptions,代价是这一处与厂商绑定。
三、流式输出只是换一个终结方法
call() 换成 stream(),返回类型从 String 变成 Flux<String>。注意网关和 Nginx 的响应缓冲要关掉,否则前端看到的仍是一次性输出。
四、结构化输出靠 entity() 一行解决
Spring AI 会自动往提示词里注入 JSON Schema 格式说明,拿到响应后再反序列化。字段优先用 record + 简单类型(String / int / boolean / List),嵌套过深或用了复杂泛型时,模型输出格式跑偏的概率显著上升。
五、异常分两类处理
重试策略统一在 spring.ai.retry.* 下配置,默认最大 10 次、指数退避。生产环境记得按厂商的实际配额把次数和间隔调小,否则一次限流会被重试放大成雪崩。
二、Spring AI 的定位
一句话概括:Spring AI 把 AI 应用开发里反复出现的套路,抽象成 Spring 风格的 API 与自动配置。
这个"套路"具体指六件事:调模型、写提示词、存对话历史、让模型调你的方法、把私有知识喂给模型、观测调用过程。每件事单独实现都不难,难的是换个模型厂商、换个向量库、加一个租户隔离时不用重写一遍。Spring AI 做的就是把这六件事各抽出一个接口。
理解它的设计哲学,看三组对应关系最快:
| Spring AI | Spring 生态里的同类 | 抽象的是什么 |
|---|---|---|
ChatClient / ChatModel |
RestClient / 具体 HTTP 实现 |
模型调用协议 |
VectorStore |
JdbcTemplate |
向量库的读写 |
ChatMemory |
SessionRepository |
会话状态存储 |
Advisor |
HandlerInterceptor / Filter |
调用链上的横切逻辑 |
@Tool |
@RequestMapping |
把方法暴露成模型可调用的能力 |
这套抽象带来的直接结果:业务代码里几乎不出现厂商名。换模型是改一行依赖加几行配置,代码不用动。
它不做的事也要说清楚,避免预期错位:
- 它不做模型训练、微调、推理优化,那是模型层的事;
- 它不提供提示词的"最佳实践模板库",提示词怎么写仍然靠你的业务理解;
- 它对 Agent 编排的支持在 1.x 里偏基础,复杂多智能体编排需要看 Spring AI Alibaba 的 Graph 或 2.0 线的编排能力;
- 它不解决数据合规、内容审核、成本控制,这些要在架构层自己做。
三、核心概念
概念不多,按三层记最省力。
模型层------怎么和模型说话
| 概念 | 说明 |
|---|---|
ChatModel |
单轮对话的底层接口,call(Prompt) 返回 ChatResponse。OpenAiChatModel、OllamaChatModel 都是它的实现 |
ChatClient |
面向业务的高层门面,链式 API,内置 Advisor、记忆、工具、结构化输出。日常写业务只用它 |
Prompt |
一次请求的完整描述:消息列表 + 调用参数 |
Message |
SystemMessage / UserMessage / AssistantMessage / ToolResponseMessage,对应 OpenAI 协议里的 role |
ChatOptions |
温度、最大 token、top-p、停止词等采样参数 |
EmbeddingModel |
文本转向量,RAG 的底座。换模型等于换向量空间,历史向量必须重建 |
编排层------调用前后能插什么
| 概念 | 说明 |
|---|---|
Advisor |
包裹在 ChatModel.call() 外层的拦截器,有前置和后置两个切入点。记忆、RAG、日志、安全护栏都在这里实现 |
ChatMemory |
对话历史容器,MessageWindowChatMemory 按条数滑动窗口,底层可换 JDBC / Redis / Cassandra 仓储 |
ToolCallback |
工具调用的统一表示,@Tool 注解的方法会被包装成它 |
OutputConverter |
把模型返回的文本转成对象,entity() 内部就是它 |
数据层------私有知识怎么进来
| 概念 | 说明 |
|---|---|
Document |
文本 + 元数据的载体,RAG 的最小单位 |
DocumentReader |
从 PDF / Word / Markdown / 网页读出 Document |
DocumentTransformer |
切分、清洗、加元数据 |
VectorStore |
向量库的统一接口:add / delete / similaritySearch |
SearchRequest |
检索参数:topK、相似度阈值、元数据过滤表达式 |
四、主要能力
4.1 模型接入:一个接口,多个实现
Spring AI 覆盖了主流厂商:OpenAI、Anthropic Claude、Google Gemini、Amazon Bedrock、Mistral、DeepSeek、Ollama,以及国内的智谱、MiniMax、千帆。每个厂商一个 starter,配置前缀各不相同,但注入的接口类型一致。
同时引入多个厂商 starter 时,ChatModel 会有多个候选 Bean。用 spring.ai.model.chat=openai 指定默认实现,或者在注入时用 @Qualifier 精确选择。多模型场景(主力模型 + 便宜模型兜底 + 本地模型兜底)正是这套抽象最划算的地方。
4.2 提示词:模板化 + 外部化
字符串拼接式的提示词维护成本极高,改一个字要重新编译。Spring AI 用模板引擎处理这件事,占位符用 {}:
模板可以外置成 src/main/resources/prompts/xxx.st 文件,用 @Value("classpath:prompts/xxx.st") Resource 注入。好处是调提示词不用改 Java 代码,还能按环境切换不同版本。
系统提示词和用户提示词分开写很重要。系统提示定角色、定输出格式、定禁止事项;用户提示只放事实和问题。把约束混在用户输入里,模型遵守的概率会明显下降。
4.3 对话记忆:让多轮对话有上下文
模型 API 本身是无状态的,多轮对话靠每次把历史消息重发一遍。Spring AI 把"存历史、取历史、截断历史"封装成 ChatMemory + MessageChatMemoryAdvisor。
配好之后,每次调用必须显式指定会话 id,否则所有用户会共用一份历史------这是上线后最容易暴露的隐私事故:
默认的 MessageWindowChatMemory 把历史放在内存里,重启即丢、多实例不共享。生产环境换成 JDBC 仓储(JdbcChatMemoryRepository),历史落库、按 conversation_id 隔离,多副本部署才不会串话。
maxMessages 的取值是个权衡:设大 token 成本高、响应慢,设小会出现"聊了十轮忘了第一轮说的要求"。经验值是 10 到 20 条,长会话场景配合摘要压缩(把早期对话摘要成一条 SystemMessage)效果更好。
2.0 线变化:记忆相关 Advisor 全部要求显式传
conversationId,PromptChatMemoryAdvisor已标记废弃。上面这种"每调用显式传参"的写法在两条线上都能跑,建议直接按这个来。
4.4 函数调用:让模型调用你的方法
模型不知道实时库存、不知道订单状态、不能查数据库。函数调用(Tool Calling)解决的就是这个缺口:你把方法暴露出去,模型在需要时自己决定调哪个、传什么参数。
使用时挂到调用链上:
背后是一个循环:模型返回 tool_calls → Spring AI 反射执行对应方法 → 结果包成 ToolResponseMessage 追加到消息列表 → 再次请求模型 → 直到模型给出最终文本。这个循环由框架负责,你只需要写方法。
三个实用细节:
description是模型判断"要不要调"的唯一依据,写得含糊就等于没写。参数说明同样重要,模型要靠它决定传什么值;
@Tool(returnDirect = true)表示工具结果直接作为最终回答返回,不再回炉让模型润色。适合查询类、格式化输出的场景,能省一轮 token;
- 需要传租户 id、用户身份这类上下文时用
ToolContext,不要把敏感信息塞进提示词。
工具方法内部一定要做权限校验和参数校验。模型传来的参数来自自然语言,等价于外部输入,按不可信数据处理。
4.5 RAG:把私有知识接进来
RAG(检索增强生成)的流程是四段:读取 → 切分 → 向量化入库 → 检索后拼进提示词。
检索侧只需要挂一个 Advisor:
QuestionAnswerAdvisor 做的事:把用户问题向量化 → 检索 topK 个片段 → 拼进系统提示词 → 让模型基于片段回答。业务代码零改动。
需要更精细的控制时,用 RetrievalAugmentationAdvisor 这条模块化链路,它把 RAG 拆成六个可替换环节:
| 环节 | 作用 | 内置实现举例 |
|---|---|---|
QueryTransformer |
改写用户问题,提升召回 | RewriteQueryTransformer、TranslationQueryTransformer |
QueryExpander |
一个问题扩成多个查询 | MultiQueryExpander |
DocumentRetriever |
检索文档 | VectorStoreDocumentRetriever |
DocumentJoiner |
多路结果合并去重 | ConcatenationDocumentJoiner |
DocumentPostProcessor |
重排序、过滤 | 按需自定义 |
QueryAugmenter |
拼装最终提示词 | ContextualQueryAugmenter |
RAG 的效果八成取决于两件事:切分粒度 和元数据设计。切分太碎,单块缺上下文;切分太大,一块里混了好几个主题,检索精度下降。实践中的起点是 500 到 1000 token 一块、重叠 10% 到 20%,再按实际召回效果调。元数据(文档来源、章节、更新时间、权限标签)决定了你能不能做"按部门过滤""只看最新版"这类业务约束,入库时就该打好。
4.6 向量存储:换库不改代码
VectorStore 统一了向量库的读写。官方提供 PgVector、Milvus、Redis、Elasticsearch、Qdrant、Chroma、Weaviate、Neo4j、OpenSearch、MariaDB 等实现。
引入 starter 后通常不需要手写 Bean:
initialize-schema: true 会自动建表和索引,开发期省事,生产环境建议关掉改为手工执行 DDL,避免启动时锁表。dimensions 必须和 embedding 模型输出维度一致,写错会在第一次写入时报错。
选型上,已经用 PostgreSQL 的团队优先 PgVector------省一个中间件,事务和备份都能复用;数据量上千万且检索 QPS 高,上 Milvus;已有 ES 集群且检索要和全文搜索混用,用 Elasticsearch 实现。
检索时元数据过滤用表达式:
这一行过滤往往是 RAG 从"能用"到"敢上线"的分界线------没有它,客服机器人会把财务制度念给外部商户听。
4.7 其它值得知道的能力
| 能力 | 说明 |
|---|---|
| 多模态 | UserMessage 可带 Media(图片、音频),用 Media.Format.IMAGE_PNG 之类指定格式 |
| 图像与语音模型 | ImageModel 文生图,TranscriptionModel 语音转文字,SpeechModel 文字转语音 |
| 可观测 | 基于 Micrometer,开启 Actuator 后能看到每次调用的耗时、token 数、提示词内容 |
| 评估 | RelevancyEvaluator、FactCheckingEvaluator,用模型给 RAG 结果打分,适合做回归测试 |
| MCP | 通过 spring-ai-starter-mcp-client 接入 Model Context Protocol 服务端,复用社区现成工具 |
可观测这项在真实项目里价值很高。上线后第一个问题通常是"为什么这次回答慢",没有 token 和耗时的埋点就只能靠猜。加一行 management.endpoints.web.exposure.include=health,metrics,再配好 Observation 就能在 Grafana 里看。
五、相对直接调 API 的优势与代价
直接 RestTemplate 拼 JSON 调 /v1/chat/completions 完全可行,二十行代码就能跑。区别在于功能往上叠的时候。
| 维度 | 直接调 HTTP API | Spring AI |
|---|---|---|
| 首次跑通 | 20 行,最快 | 加依赖、配 key,也很快 |
| 换模型厂商 | 改 DTO、改错误码、改参数名 | 换 starter + 改配置,业务代码不动 |
| 提示词管理 | 字符串拼接散落各处 | 模板 + 外置文件,可版本管理 |
| 多轮记忆 | 自己维护消息数组、自己截断 | ChatMemory + Advisor,换存储只改一处 |
| 函数调用 | 自己解析 tool_calls、自己回灌结果、自己处理循环 |
@Tool 注解,循环由框架跑 |
| RAG | 自己接向量库 SDK、自己拼上下文、自己做过滤 | VectorStore + 一个 Advisor |
| 结构化输出 | 自己写 Schema 提示 + 自己解析 JSON + 自己兜异常 | .entity(Class) |
| 重试与超时 | 自己实现退避策略 | spring.ai.retry.* 统一配置 |
| 可观测 | 自己埋点 | Micrometer 开箱可用 |
| 与现有工程集成 | 手工管理 Bean、事务、配置 | 就是普通的 Spring Bean 和配置项 |
代价同样存在,需要提前接受:
- 抽象有滞后。厂商上了新参数、新接口,Spring AI 要滞后一个版本才支持。追新特性时得回退到原生 SDK;
- 调试链路变长 。Advisor 链、自动配置、反射执行工具,出问题时调用栈比手写 HTTP 深得多。
SimpleLoggerAdvisor和 Actuator 的观测数据基本能覆盖排查需求;
- 依赖体积增加。一个 starter 会带进 HTTP 客户端、JSON 库、重试框架,做很小的事情时显得重;
- 心智成本 。要先理解
ChatModel/ChatClient/Advisor的分工,才写得出符合框架意图的代码。跳过这层理解直接抄,会写出大量绕过框架的手工代码。
判断标准很朴素:这个功能会不会长期存在、会不会换模型、会不会加新能力。一次性脚本、临时验证,直接调 API 更直接;要进生产、要迭代半年的功能,值得用框架。
六、典型适用场景
| 场景 | 适配度 | 说明 |
|---|---|---|
| 已有 Spring Boot 后端加 AI 功能 | 高 | 客服问答、内容摘要、工单分类、信息抽取,都是加一个 Service 的事 |
| 企业知识库问答 | 高 | RAG 全链路有现成组件,权限过滤靠元数据表达式 |
| 需要多模型可切换或降级 | 高 | 主力模型 + 便宜模型 + 本地模型,切换成本极低 |
| Java 团队为主、没有 Python 栈 | 高 | 不用为了调个模型引入新语言和独立服务 |
| 结构化数据抽取与落库 | 高 | entity() + 校验注解,比手工解析 JSON 稳 |
| 复杂多智能体编排 | 中 | 1.x 偏基础,复杂流程看 Spring AI Alibaba Graph 或 2.0 编排能力 |
| 需要第一时间用厂商私有新特性 | 低 | 抽象层滞后,直接调原生 SDK 更合适 |
| 超大规模离线批推理 | 低 | 走厂商的 Batch API 成本更低,不需要实时链路 |
七、上手前提
动手之前的清单,缺一项都会卡住:
| 项 | 要求 | 说明 |
|---|---|---|
| JDK | 17+(2.0 线建议 21+) | Spring AI 基线要求 |
| Spring Boot | 1.1.x 配 3.5.x,2.0.x 配 4.1.x | 版本不能交叉 |
| 构建工具 | Maven 3.6.3+ 或 Gradle | 需要能引 BOM |
| 模型服务 | 云厂商 API Key 或本地 Ollama | 没有 key 就用 Ollama,完全离线 |
| 向量库 | 做 RAG 才需要 | 可以先从 PgVector 或内存版 SimpleVectorStore 起步 |
| 网络 | 能访问模型服务端点 | 国内直连海外服务通常不通,需要代理或换国内厂商 |
| 基础认知 | token 计费、上下文窗口、温度、采样参数 | 不理解这些,调参只能靠猜 |
| 密钥管理 | 环境变量 / 配置中心 / KMS | 硬编码 key 是硬伤 |
一个务实的起步路径:先用 Ollama 在本地跑通第一个案例,不花一分钱也不用配网络;链路通了再接云厂商模型;RAG 先用 SimpleVectorStore 内存版验证切分和检索效果,效果满意再换 PgVector。
八、几个高频坑
1. 依赖坐标用了老名字。 1.0 GA 之后统一是 spring-ai-starter-model-*,老坐标在中央仓库解析不到。
2. 只加 BOM 没加 starter。 BOM 只管版本,不引入任何代码。启动时报 ChatClient.Builder 注入不到,多半是这个原因。
3. 多个厂商 starter 冲突。 两个 ChatModel 实现同时在场,注入时报歧义。用 spring.ai.model.chat=openai 指定,或注入时加 @Qualifier。
4. ChatClient 每次 new。 每个请求重建 Advisor 链,QPS 上来后 GC 压力明显。做成 Bean。
5. 会话 id 不隔离。 所有用户共用一个默认 conversationId,A 能看到 B 的聊天记录。这是必须在上线前压测的项。
6. maxMessages 设得太大。 长会话下 token 线性增长,成本和延迟一起爆。配合摘要压缩。
7. 切分粒度拍脑袋。 按固定字符数切、不考虑语义边界,检索命中率会很低。按 token 切 + 保留重叠,再用真实问题验证召回。
8. 结构化输出用了复杂类型。 嵌套对象、泛型集合、枚举,反序列化成功率明显下降。扁平 record + 简单类型最稳。
9. 流式接口被网关缓冲。 应用层已经流式返回,前端仍是一次性看到结果。检查 Nginx 的 proxy_buffering 和网关的超时配置。
10. 温度设 0 就以为结果确定。 模型推理仍有非确定性,同样的输入可能给出不同表述。涉及金额、法条、医疗建议的场景,必须在代码层做校验,把判断权交给模型是危险的。
九、小结与学习路径
Spring AI 的价值集中在一点:把 AI 应用的通用套路变成 Spring 的 Bean 和接口 。ChatClient 承接调用,Advisor 承接横切逻辑,VectorStore 承接知识,三者构成了这套框架的骨架。
建议的推进顺序:
- 跑通第一个案例,理解请求链路上每一步谁在做什么(本文第一节);
- 加上记忆,做出一段能连续对话的服务,体会会话隔离和窗口截断;
- 接上工具,让模型能查你的数据库,理解工具调用循环;
- 做一次 RAG,用真实文档验证切分和检索效果,这一步最能暴露工程能力差距;
- 补上观测和评估,让系统在出问题时能被定位、在改动后能被验证。
第 4 步之后,问题会从"怎么调模型"变成"怎么让模型答得准"。从那一刻起,你面对的是检索质量、数据治理和提示词工程,这些是框架替不了的部分。
作者:苍风