Spring AI 实战:从一次可运行的模型调用到 RAG 体系

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 这条链路里到底发生了什么

看起来只有三行,实际经过了两段过程。

启动期(自动装配)

  1. spring-ai-starter-model-openai 引入自动配置模块,读取 spring.ai.openai.* 配置项;
  1. 自动配置构造出 HTTP 客户端与 OpenAiChatModel(ChatModel 接口的实现),注册为 Bean;
  1. 同时注册一个 ChatClient.Builder 原型 Bean,交给你的业务类构造 ChatClient。

调用期(一次请求)

  1. prompt() 返回一个请求规格对象(ChatClientRequestSpec),user() 往里追加一条 UserMessage,system() 追加 SystemMessage;
  1. call() 进入 Advisor 拦截链。记忆 Advisor 从会话存储里读出历史消息拼进消息列表,检索 Advisor 去向量库捞相关文档片段,日志 Advisor 打印请求;
  1. 组装成 Prompt(消息列表 + 调用参数),交给 ChatModel.call();
  1. 具体厂商实现把 Prompt 序列化成 HTTP 请求,发到 /v1/chat/completions;
  1. 响应反序列化成 ChatResponse,其中包含生成的候选结果(generations)和 token 用量(usage);Advisor 链的后置逻辑再把本轮问答写回记忆;
  1. 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 承接知识,三者构成了这套框架的骨架。

建议的推进顺序:

  1. 跑通第一个案例,理解请求链路上每一步谁在做什么(本文第一节);
  1. 加上记忆,做出一段能连续对话的服务,体会会话隔离和窗口截断;
  1. 接上工具,让模型能查你的数据库,理解工具调用循环;
  1. 做一次 RAG,用真实文档验证切分和检索效果,这一步最能暴露工程能力差距;
  1. 补上观测和评估,让系统在出问题时能被定位、在改动后能被验证。

第 4 步之后,问题会从"怎么调模型"变成"怎么让模型答得准"。从那一刻起,你面对的是检索质量、数据治理和提示词工程,这些是框架替不了的部分。


作者:苍风

相关推荐
前端小万1 小时前
探索副业五个月,最终以放弃告终
前端·后端·面试
用户87359865635871 小时前
Bias-Variance Tradeoff
后端
今天早睡1 小时前
TypedDict 还是 Pydantic?
后端
木易 士心1 小时前
Java 后端驱动远程手机自动化:ADB + Python + 内网穿透实战全解
java·后端·智能手机·自动化
薛定谔的悦1 小时前
从论文公式到 BMS 固件:FreeRTOS 与 EKF SOC 的工程结合
后端
用户1494484813201 小时前
从入口拦截到最终对账:我如何设计一套高并发秒杀系统
后端·架构
Jeromefromhk1 小时前
把服务器当代码管
后端
闽林致远1 小时前
C++ 插件化后台服务架构实践
后端
用户125758524361 小时前
GoFrame Vue3开源后台管理系统推荐:数据库与Worker要求怎么验
数据库·后端·go