🤖 Spring AI 接入大模型(入门到 RAG)¶
这是 Java 学习路线 阶段4(进阶架构) 的 Spring AI 章,也是补齐"0-1 到资深"路线里当前完全空白的 AI 能力。 前置建议:先完成 Spring Boot 完整项目实战(本篇的"AI 摘要"示例直接复用那套文章 CRUD)。 依据 Spring AI 官方文档(1.0 GA,2025-05 发布,API 稳定)。
📌 适用版本 / 更新日期:Spring AI 1.0.x / Spring Boot 3.3 / Java 17;最后更新 2026-08。
读完你能做什么
- 用
ChatClient三行代码跑通第一个对话 - 结构化输出:把 LLM 文本变 Java 对象(
BeanOutputConverter) - RAG:把文档灌进向量库,让模型"基于你的资料回答"
- 流式响应、限流降级、密钥安全(生产必备) —— 给你的博客加一个"AI 自动生成文章摘要"功能。
0. 全景:Spring AI 在你应用里的位置¶
| 能力 | 核心类 | 你什么时候用 |
|---|---|---|
| 对话 | ChatClient |
聊天、摘要、改写 |
| 结构化 | BeanOutputConverter |
让模型返回可解析对象 |
| 嵌入 | EmbeddingModel |
把文本变向量 |
| 检索 | VectorStore |
RAG 知识库 |
| 编排 | Advisor |
日志/重试/限流/脱敏 |
1. 加依赖¶
pom.xml:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-starter</artifactId>
</dependency>
<!-- 选一个模型:OpenAI / 通义千问 / Ollama -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
版本管理
Spring AI 1.0 起版本号独立于 Spring Boot,用官方 BOM 锁版本,别手写 <version> 撞错。详见 Spring AI 官方。
2. 第一个对话(ChatClient)¶
@RestController
@RequiredArgsConstructor
public class ChatController {
private final ChatClient chatClient; // 注入 Builder 即可自动配好
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/ai/chat")
public Result<String> chat(@RequestParam String msg) {
String answer = chatClient.prompt()
.user(msg)
.call()
.content();
return Result.ok(answer);
}
}
application.yml(密钥走环境变量,绝不硬编码):
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY} # 启动: export OPENAI_API_KEY=sk-xxx
chat:
options:
model: gpt-4o-mini
3. 结构化输出(让模型返回 Java 对象)¶
record ArticleSummary(String title, String summary, List<String> tags) {}
@GetMapping("/ai/summary")
public Result<ArticleSummary> summary(@RequestParam Long id) {
ArticleDTO a = articleService.detail(id); // 复用实战篇的 Service
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<ArticleSummary>() {});
ArticleSummary s = chatClient.prompt()
.user(u -> u.text("""
为下面文章生成一句话摘要和 3 个标签,用 JSON 输出:
{format}
标题:{title}
正文:{content}
""")
.param("format", converter.getFormat())
.param("title", a.getTitle())
.param("content", a.getContent()))
.call()
.entity(ArticleSummary.class); // 直接拿到对象
return Result.ok(s);
}
为什么结构化输出重要
- 前端要的是
{title, summary, tags}而不是一段自由文本;BeanOutputConverter让模型严格按格式输出,省去手写正则解析(Spring AI 官方·输出转换)。
4. RAG:让模型"基于你的资料回答"¶
思路:把文档切块 →
EmbeddingModel向量化 → 存VectorStore→ 用户提问时先检索相关块 → 拼进 Prompt 让模型作答。
@Configuration
public class RagConfig {
@Bean
public CommandLineRunner loadDocs(VectorStore store, EmbeddingModel embed) {
return args -> {
// 真实项目从文件/DB 读;这里是示例文档
store.add(List.of(
new Document("Spring Boot 自动配置靠 @EnableAutoConfiguration 与条件注解", Map.of("src","doc1")),
new Document("MyBatis 用 #{} 预编译防 SQL 注入", Map.of("src","doc2"))
));
};
}
}
@RestController
@RequiredArgsConstructor
public class RagController {
private final ChatClient chatClient;
private final VectorStore vectorStore;
@GetMapping("/ai/rag")
public Result<String> rag(@RequestParam String q) {
List<Document> docs = vectorStore.similaritySearch(SearchRequest.builder(q).topK(3).build());
String context = docs.stream().map(Document::getContent).collect(Collectors.joining("\n"));
String ans = chatClient.prompt()
.system("你只能根据下面资料回答,资料没有就回答'不知道':\n" + context)
.user(q)
.call()
.content();
return Result.ok(ans);
}
}
application.yml 向量库(以 Redis 为例,复用 Redis 从零):
spring:
ai:
vectorstore:
redis:
uri: redis://localhost:6379
index: blog-rag
RAG 能复用你已学的东西
- 向量库可落在 MySQL / Redis / PG(你路线里都有),不必另起一套。
- 切块粒度、topK、重排序是效果关键,属于"调参"不是"换框架"。
5. 流式响应(打字机效果)¶
@GetMapping(value = "/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String msg) {
return chatClient.prompt()
.user(msg)
.stream()
.content();
}
流式前提
- 流式返回
Flux<String>,命令式应用需引入spring-boot-starter-webflux(官方说明)。 - 前端用
EventSource/fetch流式读取,体验更好但超时处理要更稳。
6. 生产化:限流、超时、降级¶
@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder b, RestClient.Builder rc) {
return b.defaultOptions(ChatOptions.builder().timeout(Duration.ofSeconds(20)).build())
.build();
}
}
| 问题 | 做法 |
|---|---|
| 模型超时/挂了 | @Retryable(spring-retry)或 Advisor 重试;失败返回兜底文案 |
| 被刷接口 | 网关/拦截器限流(结合 高并发) |
| Token 费用失控 | 估算字数、限制 maxTokens、缓存高频问题结果到 Redis |
| 内容合规 | 输入/输出做敏感词与内容审核(结合 安全) |
合规红线
- 用户 PII(手机号/身份证)不要原样传第三方模型;脱敏后再发(认证与授权)。
- 模型输出不可直接当事实展示,关键场景需人工复核。
7. 自测清单¶
- 配好密钥环境变量,跑通
/ai/chat - 结构化输出拿到
ArticleSummary对象 - 向量库灌入文档,
/ai/rag能基于资料作答 - 流式接口前端能渲染打字机效果
- 模型超时时有降级兜底,密钥不在代码里
8. Advisor 链(日志 / 脱敏 / 重试)¶
Advisor是 Spring AI 的请求/响应拦截链,类似 ServletFilter或 AOP,贯穿每次ChatClient调用。来源 Spring AI · Advisor API。
// 自定义 Advisor:请求前脱敏手机号,响应后打日志
public class MaskingAndLogAdvisor implements Advisor {
private static final Pattern PHONE = Pattern.compile("1[3-9]\\d{9}");
@Override
public AdvisedResponse adviseCall(AdvisedRequest request, CallAdvisorChain chain) {
// 1. 入参脱敏(绝不让 PII 直传第三方模型,[合规红线](#_6-生产化限流超时降级))
String masked = PHONE.matcher(request.userText()).replaceAll("138****0000");
AdvisedRequest maskedReq = request.mutate().userText(masked).build();
// 2. 放行
AdvisedResponse resp = chain.nextCall(maskedReq);
// 3. 响应后日志
log.info("ai resp: {}", resp.response().getResult().getOutput().getText());
return resp;
}
@Override public String getName() { return "masking-log"; }
}
// 串成链:日志 → 脱敏 → 重试
@Bean
public ChatClient chatClient(ChatClient.Builder b) {
return b.defaultAdvisors(
new LoggingAdvisor(), // 官方日志 Advisor
new MaskingAndLogAdvisor(), // 自定义脱敏
new RetryAdvisor(3, Duration.ofSeconds(1)) // 失败重试(需 spring-retry)
).build();
}
| 内置/常用 Advisor | 作用 |
|---|---|
LoggingAdvisor |
打印请求/响应,调试用 |
QuestionAnswerAdvisor |
RAG 检索并拼上下文(§4 底层用的就是这个) |
RetryAdvisor |
调用失败自动重试 |
| 自定义 | 脱敏/审计/限流/注入系统提示 |
Advisor 价值
- 把"脱敏、日志、重试、限流、审计"从业务代码里抽离,可复用可组合。
- 顺序敏感:脱敏必须在"发往模型"之前,日志可在前后。
9. 多模型路由(主模型 + 兜底)¶
生产不把鸡蛋放一个篮子:主模型(如 OpenAI)超时/限流时,切到通义或本地 Ollama 兜底。来源 Spring AI · ChatClient。
@Configuration
public class MultiModelConfig {
@Bean ChatClient openAi(ChatClient.Builder b) {
return b.build(); // 默认 OpenAI(application.yml 配好)
}
@Bean ChatClient qwen(ChatClient.Builder b) {
return b.build(); // 另配通义 Starter 时区分
}
@Bean ChatClient ollama(ChatClient.Builder b) {
return b.build(); // 本地 Ollama,离线兜底
}
}
@Service
@RequiredArgsConstructor
public class ResilientChatService {
private final ChatClient openAi, qwen, ollama;
public String ask(String msg) {
// 主 → 通义 → 本地,逐级降级
return tryModel(openAi, msg)
.or(() -> tryModel(qwen, msg))
.or(() -> tryModel(ollama, msg))
.orElse("服务暂时不可用,请稍后再试"); // 兜底文案
}
private Optional<String> tryModel(ChatClient c, String msg) {
try { return Optional.ofNullable(c.prompt().user(msg).call().content()); }
catch (Exception e) { return Optional.empty(); } // 失败即切下一个
}
}
成本与延迟权衡
- 本地 Ollama 免费但能力弱/慢,只作兜底;主模型按质量选。
- 路由逻辑可下沉到网关或用 Advisor 统一封装,避免散落业务。
10. 工程化测试:Testcontainers 集成测试¶
单测用
@MockBean隔离(实战篇 §10),但 RAG/VectorStore 必须连真向量库才测得准。来源 Spring AI · Testing · Testcontainers。
pom.xml 加:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgres</artifactId> <!-- 或 redis,看你用的 VectorStore -->
<scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class RagServiceIT {
@Container
static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("pgvector/pgvector:pg16")
.withDatabaseName("rag").withUsername("u").withPassword("p");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", pg::getJdbcUrl);
r.add("spring.datasource.username", pg::getUsername);
r.add("spring.datasource.password", pg::getPassword);
}
@Autowired VectorStore vectorStore;
@Autowired ChatClient chatClient;
@Test
void rag_shouldAnswerFromDocs() {
vectorStore.add(List.of(new Document("Spring AI 用 Advisor 做拦截链")));
String ans = chatClient.prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore))
.user("Spring AI 用什么做拦截链?")
.call().content();
assertTrue(ans.contains("Advisor"));
}
}
为什么用 Testcontainers
- 测试时拉起真实数据库/向量库容器,跑完自动销毁,CI 可复现,告别"我本地能过"。
- 与
@MockBean分层:单测 mock、集成测试用真实容器。
11. 自测清单¶
- 配好密钥环境变量,跑通
/ai/chat - 结构化输出拿到
ArticleSummary对象 - 向量库灌入文档,
/ai/rag能基于资料作答 - 流式接口前端能渲染打字机效果
- 模型超时时有降级兜底,密钥不在代码里
- Advisor 链实现脱敏/日志/重试
- 多模型路由主→兜底可切换
- Testcontainers 集成测试连真向量库通过
12. 下一步¶
- 把 RAG 接到 博客实战 做"站内知识问答"
- 多模型路由下沉到 Advisor / 网关统一封装
- 用
EvaluationAPI 做回答质量评测 - 接
spring-ai-mcp让模型调用你的业务工具(MCP)