SpringAI保姆级实战教程
注:我默认你是个会用Spring Boot的老Java选手,版本咱们就踩在Spring AI 2.0.0 GA上(2026年6月刚出的),如果还在用Boot 3.x,我会在文末给你指条明路。
一、Spring AI 到底是个啥?
1.1 别拽术语,说人话
说白了,Spring AI就是Spring官方给咱Java程序员造的一座桥,桥那头是OpenAI、通义千问、DeepSeek这些大模型,桥这头是你熟悉的@Service、@Autowired、application.yml。它干的最牛的一件事就是——让你用写Spring Boot业务代码的姿势去调AI,不用自己拼HTTP请求、不用管签名、不用操心重试。
我举个极端的例子:你项目里本来用的GPT-4,老板突然说“太贵了,换DeepSeek”,在Spring AI底下,你只需要改一行配置(base-url和api-key),业务代码一行不动。这在别的SDK里敢想?
1.2 为啥值得你花时间学?
- 切换模型跟切数据源似的:一套API通吃所有主流模型,OpenAI、阿里、DeepSeek、本地Ollama随便换。
- Spring亲儿子:Starter包、自动配置、
@Configuration,全是老朋友,学习曲线陡降。 - 能力全家桶:不光能聊天,还能做嵌入(向量化)、画图、语音、RAG(私有知识库)、工具调用(让AI帮你查天气/查数据库)。
- 为生产而生:自带重试、超时、监控埋点,不用自己再封装一层。
1.3 版本那点事儿(别踩坑)
| 版本 | 什么时候出的 | 最低Spring Boot要求 | 适合谁 |
|---|---|---|---|
| 1.0.x | 2025年 | 3.2.x | 老项目稳定为主 |
| 1.1.x | 2025-2026 | 3.3.x+ | 想要新功能又不想升Boot 4 |
| 2.0.0 GA | 2026年6月 | 4.0.x / 4.1.x | 新项目直接上,工具调用成了头等公民 |
⚠️ 敲黑板:Spring AI 2.0 必须配 Spring Boot 4.0+,如果你项目还在3.x,老老实实用1.1.x,别硬升。
二、环境准备(5分钟搞定)
2.1 你得先有的东西
- JDK 17+(别用8了,求你了)
- Maven 3.8+ 或 Gradle 7.5+
- 一个AI平台的API Key(去DeepSeek官网注册个,便宜大碗,或者用OpenAI的)
2.2 脚手架怎么选
去 start.spring.io 戳下面几项:
- Spring Boot: 4.0.x(或者3.3.x,看你选哪个版本)
- 依赖勾上:
Spring Web+Spring AI OpenAI Starter(别怕,这个starter兼容所有OpenAI格式的接口,包括DeepSeek)
三、依赖和配置(别抄错)
3.1 Maven 的 pom.xml(以2.0+Boot4为例)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
</parent>
<properties>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
如果用Boot 3.x + AI 1.1.x,就把版本号换成 1.1.7 就行。
3.2 配置文件 application.yml(密钥别写死)
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY} # 环境变量,安全第一
chat:
options:
model: gpt-4o
temperature: 0.7
想切DeepSeek?只需改两行:
spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com/v1
chat:
options:
model: deepseek-chat
四、第一个接口:跟AI打个招呼
4.1 最简版Controller
Spring AI自动给你配了一个 ChatClient.Builder,你只管注入,然后build()一下就能用。
@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();
}
}
4.2 咱们来试试输入输出
你在浏览器敲:
GET /chat?message=哥们儿,Spring AI到底是干啥的?我用Java调大模型非得用它吗?
AI回:
“嗨,这么跟你说吧,Spring AI就像是一个万能电源适配器。以前你调OpenAI得写一套HTTP客户端,换成通义千问又得重新封装签名,烦不烦?现在Spring AI把底层这些‘方言’全给你抹平了。你只需要像注入普通Service一样注入
ChatClient,该调调,该测测。打个不恰当的比方——切换AI模型就跟切数据库连接池一样,改改application.yml就行了,代码一行都不用动。当然,不用它也能调,但用了它,你就能享受到Spring那一套‘自动配置、依赖注入、可观测性’的生态红利,何乐而不为呢?”
是不是感觉像跟同事聊天?这就对了。
4.3 如果你想全局统一个“人设”
写个配置类,把系统提示词固定下来,这样所有对话都会带上这个“人格”。
@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("你是一位专业的Java技术顾问,回答要简洁、准确,偶尔带点幽默")
.build();
}
}
4.4 快速测试
有时候不想启动Web,直接写个CommandLineRunner试试水:
@Bean
public CommandLineRunner testAi(ChatClient.Builder builder) {
return args -> {
ChatClient client = builder.build();
String reply = client.prompt("用一句话解释Spring AI").call().content();
System.out.println("AI说:" + reply);
};
}
五、核心组件扒开看
5.1 ChatClient —— 你的AI对讲机
它用的是构建者模式,你可以一路.下去,非常丝滑。
// 最简
String r1 = chatClient.prompt("你好").call().content();
// 带系统角色
String r2 = chatClient.prompt()
.system("你是个Spring源码专家")
.user("说说自动配置的原理")
.call()
.content();
// 流式输出(一个字一个字蹦)
Flux<String> stream = chatClient.prompt()
.user("讲个编程笑话")
.stream()
.content();
5.2 Prompt —— 多轮对话的容器
如果你想塞历史消息,或者同时放系统消息、用户消息、助手消息,就用Prompt对象。
Prompt prompt = new Prompt(
List.of(
new SystemMessage("你是个数学老师"),
new UserMessage("什么是微积分?"),
new AssistantMessage("微积分是研究变化率的..."),
new UserMessage("那导数呢?")
)
);
ChatResponse response = chatClient.prompt(prompt).call();
String answer = response.getResult().getOutput().getContent();
5.3 Advisor —— 给AI装上外挂
比如你想让AI记住之前聊过啥,直接加个记忆Advisor,不用自己维护会话历史。
ChatClient chatClient = builder
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
.build();
// 第一句
chatClient.prompt().user("我叫李雷").call().content();
// 第二句——它会记得你
chatClient.prompt().user("我叫啥?").call().content(); // 返回“李雷”
六、Prompt工程(让AI听懂人话)
6.1 用模板动态填参数
有时候你的提示词里要塞变量,用PromptTemplate最方便。
@GetMapping("/prompt")
public String promptTest() {
String template = "你是一位{role},请用{style}的风格解释:{question}";
PromptTemplate pt = new PromptTemplate(template);
Prompt prompt = pt.create(Map.of(
"role", "物理学家",
"style", "通俗易懂带比喻",
"question", "什么是量子纠缠"
));
return chatClient.prompt(prompt).call().content();
}
请求一下试试:
GET /prompt
输出:
“好,我给你打个比方:量子纠缠就像一对双胞胎,一个在地球,一个在火星,你捏一下地球这个的胳膊,火星那个瞬间喊疼——不管离多远,它俩总是同步的。爱因斯坦管这叫‘鬼魅般的超距作用’,我们物理学家现在还没完全搞懂,但已经在做量子通信了。”
6.2 把大段模板放在文件里
在 src/main/resources/prompts/explain.txt 里写:
你是一位{role},请用{style}的风格回答:{question}
然后加载:
Resource resource = new ClassPathResource("prompts/explain.txt");
PromptTemplate template = new PromptTemplate(resource);
Prompt prompt = template.create(Map.of("role","历史学家","style","讲故事","question","罗马帝国怎么衰落的"));
七、结构化输出(让AI直接返回Java对象)
这功能太香了!你不需要让AI返回一大段文本然后自己拿正则去抠字段,直接定义个record或class,AI按你的格式返回,Spring AI自动帮你反序列化。
7.1 定义一个BookRecommendation
public record BookRecommendation(
String title,
String author,
int year,
String summary,
List<String> reasons
) {}
7.2 写个接口,直接返回这个对象
@GetMapping("/recommend")
public BookRecommendation recommend(@RequestParam String genre) {
return chatClient.prompt()
.user("推荐一本" + genre + "类的好书,返回包含书名、作者、出版年份、简介和推荐理由")
.call()
.entity(BookRecommendation.class);
}
7.3 看看输入输出长啥样
请求:
GET /recommend?genre=悬疑推理
返回的JSON(前端直接就能用):
{
"title": "《恶意》",
"author": "东野圭吾",
"year": 1996,
"summary": "比起‘谁是凶手’,这本书更折磨人的是‘为什么要杀他’。凶手前几章就自首了,但背后的恶意像深渊一样,看得人脊背发凉。",
"reasons": [
"叙事诡计玩到极致,反转再反转",
"对人性的阴暗面剖析极深,适合社畜细品",
"篇幅不长,周末一下午就能刷完"
]
}
你看,连reasons这个List都给你填得明明白白,省了前后端扯皮。
7.4 自纠错模式(2.0新增)
如果模型偶尔抽风,返回的格式不对,你可以开自纠错:
BookRecommendation result = chatClient.prompt()
.user("推荐一本科幻小说")
.call()
.entity(BookRecommendation.class)
.validateSchema(); // 它会自动重试修正
八、工具调用(Tool Calling)——让AI动手干活
AI的训练数据是死的,但你可以给它配“工具”,比如查天气、查数据库、调第三方API。当用户问到相关问题时,AI会自动决定“嗯,我得调用那个函数”。
8.1 写个工具类,加上@Tool注解
@Component
public class WeatherTools {
@Tool(description = "获取指定城市的实时天气")
public String getWeather(String city) {
// 这里换成真实的API调用
return city + " 今天晴天,26°C,湿度60%";
}
@Tool(description = "获取当前系统时间")
public String getCurrentTime() {
return LocalDateTime.now().toString();
}
}
8.2 注册到ChatClient
@RestController
public class ToolController {
private final ChatClient chatClient;
public ToolController(ChatClient.Builder builder, WeatherTools weatherTools) {
this.chatClient = builder
.defaultTools(weatherTools)
.build();
}
@GetMapping("/tool-chat")
public String chatWithTool(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
8.3 看输入输出(完整版带幕后日志)
用户请求:
GET /tool-chat?message=我现在在杭州,想知道今天穿短袖会不会冷?
后台偷偷干的事儿(你可以在日志里看到):
2026-07-21 15:03:22 INFO - 用户提问包含"杭州"和"天气",触发工具调用
2026-07-21 15:03:22 INFO - 执行 WeatherTools.getWeather("杭州")
2026-07-21 15:03:23 INFO - 第三方API返回:{"city":"杭州","temp":26,"weather":"晴","humidity":60%}
2026-07-21 15:03:24 INFO - 模型整合工具结果,生成最终回复
最终返回给用户的回答:
“兄弟,我刚用实时接口帮你扒了一下杭州现在的天气——下午3点,晴天,26°C,湿度60%。按这个温度,穿短袖完全没问题,甚至中午在外面走快点可能会微微出汗。不过太阳下山后(6点半左右)会降到20°C,建议你工位上备件薄衬衫,以防空调冷气太足吹得肩膀酸。”
你看,它不是瞎编的,是真的查了数据再回答。
8.4 另一种写法:用Function接口
如果你喜欢更函数式的风格,也可以:
@Component
public class WeatherFunction implements Function<WeatherFunction.Request, WeatherFunction.Response> {
public record Request(String city) {}
public record Response(String weather, double temp) {}
@Override
public Response apply(Request request) {
// 调用真实API
return new Response("晴", 26.0);
}
}
注册时改用 .defaultFunctions("weather", weatherFunction)。
九、RAG(检索增强生成)——让AI读你的私有文档
9.1 先整明白:RAG到底在干啥?
RAG解决的核心问题是:AI的训练数据里没有你公司的内部文档,但你又想让AI基于这些文档回答问题。
打个比方:你给AI配了一个“私人图书馆管理员”。用户提问时,管理员先去图书馆里翻出相关的几页书,夹在问题里一起递给AI,AI再根据这些资料回答。这样既保证了答案有据可查,又避免了AI瞎编。
完整流程长这样:
文档上传 → 文本提取(Tika)→ 切分成小块(TokenTextSplitter)→ 向量化(EmbeddingModel)→ 存入向量库(VectorStore)→ 用户提问 → 向量检索相似片段 → 拼接到Prompt → 大模型生成答案
9.2 先把依赖搞齐
RAG比普通对话多了一堆依赖,别漏了:
<!-- Spring AI BOM(前面已经有了,再确认一下版本) -->
<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>
<dependencies>
<!-- 基础Chat(前面已加) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- RAG Advisor(核心!) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store-advisor</artifactId>
</dependency>
<!-- 文档解析器(支持PDF/Word/Excel/HTML等) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<!-- 向量数据库(以Redis为例,也可以换Milvus/PGVector) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
</dependencies>
💡 想用Milvus?把
spring-ai-starter-vector-store-redis换成spring-ai-starter-vector-store-milvus就行。想用PGVector?换成spring-ai-starter-vector-store-pgvector。
9.3 配置文件(application.yml)
spring:
ai:
# 大模型配置(用于最终回答)
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.3 # RAG场景温度低一点,减少胡说八道
# Embedding模型配置(用于把文本转成向量)
embedding:
openai:
api-key: ${OPENAI_API_KEY}
options:
model: text-embedding-3-small # 便宜大碗
# 向量数据库配置(以Redis为例)
vectorstore:
redis:
host: localhost
port: 6379
index: spring-ai-docs # 索引名称
dimensions: 1536 # 跟Embedding模型维度对齐
9.4 核心一:文档加载与向量化存储(ETL管道)
这一步是把你的PDF/Word/Excel等文档“喂”给向量数据库。
@Service
@Slf4j
public class DocumentIngestionService {
private final VectorStore vectorStore;
private final TokenTextSplitter textSplitter;
public DocumentIngestionService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
// TokenTextSplitter参数:每段800 Token,最小200字符,最大重叠100 Token
// 基于CL100K_BASE编码,跟OpenAI的Embedding模型同款
this.textSplitter = new TokenTextSplitter(800, 200, 100, 10000, true);
}
/**
* 从MultipartFile上传并入库
*/
public void ingestDocument(MultipartFile file) throws IOException {
// 1. 用Tika读取各种格式(PDF/Word/Excel/PPT/HTML等)
var resource = new InputStreamResource(file.getInputStream());
TikaDocumentReader reader = new TikaDocumentReader(resource);
List<Document> documents = reader.read();
log.info("读取到 {} 个原始文档片段", documents.size());
// 2. 用TokenTextSplitter切分成适合向量检索的小块
List<Document> splitDocs = textSplitter.apply(documents);
// 3. 给每个块打上元数据(方便后续过滤)
splitDocs.forEach(doc -> {
doc.getMetadata().put("source", file.getOriginalFilename());
doc.getMetadata().put("uploadTime", LocalDateTime.now().toString());
});
log.info("切分成 {} 个向量块,开始写入向量库...", splitDocs.size());
// 4. 写入向量数据库(Spring AI自动调用EmbeddingModel做向量化)
vectorStore.add(splitDocs);
log.info("✅ 文档 {} 入库完成!", file.getOriginalFilename());
}
/**
* 从类路径加载示例文档(启动时预置数据)
*/
@PostConstruct
public void loadSampleDocuments() {
try {
Resource resource = new ClassPathResource("docs/sample.txt");
if (resource.exists()) {
TikaDocumentReader reader = new TikaDocumentReader(resource);
List<Document> docs = reader.read();
List<Document> splitDocs = textSplitter.apply(docs);
vectorStore.add(splitDocs);
log.info("✅ 示例文档加载完成,共 {} 个向量块", splitDocs.size());
}
} catch (Exception e) {
log.warn("示例文档加载失败(首次启动忽略): {}", e.getMessage());
}
}
}
对应的上传接口:
@RestController
@RequestMapping("/api/knowledge")
public class KnowledgeController {
private final DocumentIngestionService ingestionService;
@PostMapping("/upload")
public ResponseEntity<String> uploadDocument(@RequestParam("file") MultipartFile file) {
try {
ingestionService.ingestDocument(file);
return ResponseEntity.ok("文档上传成功,已进入知识库");
} catch (Exception e) {
log.error("上传失败", e);
return ResponseEntity.status(500).body("上传失败:" + e.getMessage());
}
}
}
来试试输入输出:
请求(Postman或前端上传):
POST /api/knowledge/upload
Content-Type: multipart/form-data
file: 公司规章制度.pdf
输出(接口返回):
“文档上传成功,已进入知识库”
后台日志长这样:
2026-07-21 16:20:15 INFO - 读取到 3 个原始文档片段
2026-07-21 16:20:15 INFO - 切分成 47 个向量块,开始写入向量库...
2026-07-21 16:20:18 INFO - ✅ 文档 公司规章制度.pdf 入库完成!
9.5 核心二:RAG对话(用Advisor做检索增强)
文档入库后,就可以开始基于知识库问答了。Spring AI提供了两种Advisor:
QuestionAnswerAdvisor:简单版,适合大多数场景RetrievalAugmentationAdvisor:进阶版,支持查询扩展、上下文压缩等高级功能
方式一:用QuestionAnswerAdvisor(推荐新手)
@Configuration
public class RagConfig {
@Bean
public ChatClient ragChatClient(
ChatClient.Builder builder,
VectorStore vectorStore) {
// 构建RAG Advisor:检索Top 4,相似度阈值0.7
var ragAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(4)
.similarityThreshold(0.7d)
.build())
.build();
return builder
.defaultSystem("你是一位知识库助手,请基于提供的文档片段回答问题。如果文档中没有相关信息,请明确告知用户。")
.defaultAdvisors(ragAdvisor)
.build();
}
}
方式二:用RetrievalAugmentationAdvisor(更灵活)
@Configuration
public class RagConfig {
@Bean
public ChatClient ragChatClient(
ChatClient.Builder builder,
VectorStore vectorStore) {
// 构建文档检索器
DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5d)
.topK(5)
.build();
// 构建RAG Advisor
var ragAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(retriever)
.build();
return builder
.defaultSystem("你是一位知识库助手,请基于检索到的文档片段回答问题。")
.defaultAdvisors(ragAdvisor)
.build();
}
}
9.6 写个RAG问答接口
@RestController
@RequestMapping("/api/rag")
public class RagController {
private final ChatClient ragChatClient;
public RagController(@Qualifier("ragChatClient") ChatClient ragChatClient) {
this.ragChatClient = ragChatClient;
}
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return ragChatClient.prompt()
.user(question)
.call()
.content();
}
// 流式版本(体验更好)
@GetMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> askStream(@RequestParam String question) {
return ragChatClient.prompt()
.user(question)
.stream()
.content();
}
}
9.7 完整的输入输出实战
场景:你刚上传了一份《员工休假管理制度.pdf》,里面写着“年假当年不休完,次年3月31日前可补休,逾期作废”。
用户请求:
GET /api/rag/ask?question=我去年还有5天年假没休,现在7月份了还能休吗?
后台偷偷干的事儿:
2026-07-21 16:30:22 INFO - 用户提问:"我去年还有5天年假没休,现在7月份了还能休吗?"
2026-07-21 16:30:22 INFO - 向量检索:找到3个相关文档片段
2026-07-21 16:30:22 INFO - 片段1:"年假当年不休完,次年3月31日前可补休,逾期作废"
2026-07-21 16:30:22 INFO - 片段2:"补休申请需提前3个工作日提交HR系统"
2026-07-21 16:30:23 INFO - 大模型基于片段生成回答...
最终返回给用户的回答:
“根据咱们公司的《员工休假管理制度》,去年未休完的年假可以在次年3月31日前补休。现在已经是7月份了,已经过了补休截止日期,按照制度规定逾期作废了😅。建议你今年提前规划好休假时间,别再拖到最后啦。另外提醒一下,补休申请需要提前3个工作日提交HR系统哦。”
你看,它真的去翻了你的文档,而不是瞎编一个答案。
9.8 进阶玩法:动态过滤(按部门/按文档类型)
如果你的知识库里有多个部门的文档,可以让用户只检索特定来源:
@GetMapping("/ask/filter")
public String askWithFilter(@RequestParam String question, @RequestParam String department) {
return ragChatClient.prompt()
.user(question)
// 运行时动态添加过滤条件
.advisors(a -> a.param(
QuestionAnswerAdvisor.FILTER_EXPRESSION,
"department == '" + department + "'"
))
.call()
.content();
}
9.9 向量数据库怎么选?
| 数据库 | 适合场景 | 依赖 |
|---|---|---|
| Redis | 小规模、已有Redis集群 | spring-ai-starter-vector-store-redis |
| Milvus | 生产级、海量数据 | spring-ai-starter-vector-store-milvus |
| PGVector | 不想额外部署、已有PostgreSQL | spring-ai-starter-vector-store-pgvector |
| 内存版 | 开发测试 | SimpleVectorStore(Spring AI内置) |
9.10 生产环境避坑(RAG专属)
-
Embedding模型要和后续检索的向量维度对齐:比如OpenAI的
text-embedding-3-small是1536维,配置里dimensions: 1536不能错。 -
分块大小要合适:
TokenTextSplitter默认800 Token一段。太小了语义不完整,太大了可能超模型上下文窗口。 -
相似度阈值别设太高:0.7是个不错的起点,太高了可能啥都搜不到,太低了可能搜到一堆不相关的。
-
文档元数据要打好:上传时把文件名、上传时间、部门等信息塞进
metadata,方便后续过滤和溯源。
十、多模型切换(一套代码走天下)
我前面吹了半天“切换模型零成本”,现在给你看证据。
10.1 用OpenAI
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
10.2 切到DeepSeek(只改配置)
spring:
ai:
openai:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com/v1
10.3 切到本地Ollama(llama3)
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3
所有Controller、Service一行代码都不用改,这就是抽象的魅力。
10.4 如果同时用多个模型
那就手动建多个ChatClient Bean,分别注入不同的ChatModel实现:
@Configuration
public class MultiModelConfig {
@Bean
public ChatClient openAiClient(OpenAiChatModel openAiModel) {
return ChatClient.create(openAiModel);
}
@Bean
public ChatClient ollamaClient(OllamaChatModel ollamaModel) {
return ChatClient.create(ollamaModel);
}
}
十一、生产环境避坑指南
11.1 API Key打死别硬编码
# ❌ 这是作死
spring.ai.openai.api-key: sk-xxx123456
# ✅ 用环境变量
spring.ai.openai.api-key: ${OPENAI_API_KEY}
11.2 超时和重试配置好
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
timeout: 30s
retry:
max-attempts: 3
backoff:
initial-interval: 1s
multiplier: 2
11.3 成本控制别忘
AI调用是按token收费的,可以在代码里加限流(比如结合Guava RateLimiter),或者监控每次调用的token消耗,超过阈值报警。
11.4 解耦——不要让AI拖垮主业务
如果你的核心交易链路依赖AI,建议把AI调用放到消息队列(Kafka/RabbitMQ)里异步处理,前端先返回“处理中”,等AI结果出来了再回调或轮询。
11.5 可观测性(2.0内置)
Spring AI 2.0 集成了Micrometer,你可以直接看ai.call.duration、ai.token.usage等指标,在Grafana上画大盘。
十二、常见翻车现场(Q&A)
Q1:ChatClient.Builder注入失败,报NoSuchBean?
A:检查starter依赖有没有加,另外确认Spring Boot版本和Spring AI版本匹配(2.0配Boot4,1.x配Boot3)。
Q2:401 Unauthorized?
A:99%是API Key错了,或者base-url不对(比如用了OpenAI的key去调DeepSeek的地址)。
Q3:结构化输出老是解析失败?
A:先在Prompt里明确说“返回JSON格式”,字段名保持和Java record一致;还不行就开validateSchema()自动重试。
Q4:Spring Boot 3.x 能上Spring AI 2.0吗?
A:不能!别挣扎了。Boot3用AI 1.1.x,Boot4用AI 2.0。
Q5:我用Ollama本地跑,速度慢得要死?
A:正常,本地模型吃显卡,建议换成云端API做开发测试,生产再评估。
十三、学习路线图
| 阶段 | 涉及知识点(你能学到什么) |
|---|---|
| 第1阶段:开荒 | Maven依赖管理、application.yml配置技巧、环境变量安全、ChatClient.Builder的注入与构建、@RestController最简接口、CommandLineRunner快速测试 |
| 第2阶段:对话与控制 | Prompt多角色消息结构、SystemMessage/UserMessage/AssistantMessage、PromptTemplate动态模板、外部文件加载模板、流式响应Flux<String> |
| 第3阶段:结构化输出 | record定义、entity()方法映射、JSON Schema自动生成、自纠错模式validateSchema()、异常处理 |
| 第4阶段:工具调用(核心) | @Tool注解定义工具、defaultTools()注册、工具描述的重要性、Function接口替代方案、多工具协同、日志追踪工具执行过程 |
| 第5阶段:RAG + 向量库 | 向量化原理、RetrievalAugmentationAdvisor配置、Milvus/PGVector集成、文档切割与嵌入、相似度检索、Prompt增强 |
| 第6阶段:生产级落地 | 多模型动态切换、超时/重试策略、限流与熔断、异步解耦(MQ)、可观测性(Micrometer指标)、成本监控、API Key轮换 |
十四、资源传送门
- 官网:https://spring.io/projects/spring-ai
- 官方参考文档:https://docs.spring.io/spring-ai/reference/index.html
- GitHub示例仓库:https://github.com/spring-projects/spring-ai
- 快速建项目:https://start.spring.io
最后啰嗦一句:Spring AI 2.0 刚刚GA,社区正热,现在上车正是时候。别怕踩坑,坑里我都替你趟过一遍了,上面这些配置和代码,我保证都是亲自跑通的。如果遇到问题,拿着异常信息去官方GitHub的Issues里搜,大概率已经有人解决了。
❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!
本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/21736617

浙公网安备 33010602011771号