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

0 阅读18分钟

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

面向有 Java 与 Spring 基础的开发者。前半篇跑通一个最小可运行案例,把一次对话调用的完整链路拆开看;后半篇梳理 Spring AI 的概念体系、核心能力、以及"什么情况下该用它"的判断依据。


一、先跑通:一个最小可运行案例

1.1 版本口径先对齐

Spring AI 目前有三条维护线,选哪条首先取决于你项目的 Spring Boot 版本。三条线的 Boot 版本不重叠,跨线升级等于跨 Boot 大版本。

版本线最新发布适配 Spring Boot定位
2.0.x2.0.14.1.x已 GA,编排能力有破坏性变更,新项目可评估
1.1.x1.1.83.5.x生产主流选择,生态适配最全
1.0.x1.0.93.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 一次对话调用的完整链路

启动期(自动装配)

  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 AISpring 生态里的同类抽象的是什么
ChatClient / ChatModelRestClient / 具体 HTTP 实现模型调用协议
VectorStoreJdbcTemplate向量库的读写
ChatMemorySessionRepository会话状态存储
AdvisorHandlerInterceptor / Filter调用链上的横切逻辑
@Tool@RequestMapping把方法暴露成模型可调用的能力

这套抽象带来的直接结果:业务代码里几乎不出现厂商名。换模型是改一行依赖加几行配置,代码不用动。

它不做的事也要说清楚,避免预期错位:

  • 它不做模型训练、微调、推理优化,那是模型层的事;
  • 它不提供提示词的"最佳实践模板库",提示词怎么写仍然靠你的业务理解;
  • 它对 Agent 编排的支持在 1.x 里偏基础,复杂多智能体编排需要看 Spring AI Alibaba 的 Graph 或 2.0 线的编排能力;
  • 它不解决数据合规、内容审核、成本控制,这些要在架构层自己做。

三、核心概念

概念不多,按三层记最省力。

模型层——怎么和模型说话

概念说明
ChatModel单轮对话的底层接口,call(Prompt) 返回 ChatResponse。OpenAiChatModel、OllamaChatModel 都是它的实现
ChatClient面向业务的高层门面,链式 API,内置 Advisor、记忆、工具、结构化输出。日常写业务只用它
Prompt一次请求的完整描述:消息列表 + 调用参数
MessageSystemMessage / 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 APISpring 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 成本更低,不需要实时链路

七、上手前提

动手之前的清单,缺一项都会卡住:

项要求说明
JDK17+(2.0 线建议 21+)Spring AI 基线要求
Spring Boot1.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 步之后,问题会从"怎么调模型"变成"怎么让模型答得准"。从那一刻起,你面对的是检索质量、数据治理和提示词工程,这些是框架替不了的部分。


作者:苍风