Spring AI 实战
Spring AI 实战
Spring AI 1.0 GA 正式发布,成为 Java 生态构建 AI 应用的标准框架。掌握 Spring AI 核心组件是 2026 年 Java AI 工程师的必备技能。
写在前面
Spring AI 对 Java 开发者的意义,类似于 LangChain 对 Python 开发者。字节、阿里、腾讯的 Java 岗 AI 面试,几乎必考 Spring AI。面试官会问:ChatClient 和 ChatModel 的区别是什么?Advisor 机制怎么用?多轮对话的 ChatMemory 如何实现?VectorStore 如何切换不同向量数据库? 本文系统梳理 Spring AI 核心组件与实战代码。
Spring AI 核心架构
Spring AI 最大的价值是提供了统一抽象层,一套 API 支持所有主流 LLM 和向量数据库:
应用代码(ChatClient / VectorStore API)
↓
Spring AI 抽象层(ChatModel / EmbeddingModel / VectorStore 接口)
↓
实现层(Ollama / OpenAI / Claude / Qwen / Milvus / PGVector / Chroma)核心优势:开发时用 Ollama 本地模型(零费用),生产时改一行配置切换 OpenAI 或 DeepSeek。
核心组件概览
| 组件 | 职责 | 典型实现 |
|---|---|---|
| ChatModel | 底层 LLM 调用接口 | OllamaChatModel、OpenAiChatModel |
| ChatClient | 高级对话封装(带 Advisor 链) | 统一 API |
| EmbeddingModel | 文本向量化 | OllamaEmbeddingModel、OpenAiEmbeddingModel |
| VectorStore | 向量数据库抽象 | PgVectorStore、MilvusVectorStore、ChromaVectorStore |
| DocumentReader | 文档读取 | PdfDocumentReader、TikaDocumentReader |
| TextSplitter | 文档分块 | TokenTextSplitter、RecursiveCharacterTextSplitter |
项目搭建
Maven 依赖(Spring AI 1.0 GA)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
</parent>
<properties>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI OpenAI(兼容 DeepSeek/Qwen 等 OpenAI 协议)-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- PGVector 向量数据库 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<!-- PDF 文档读取 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
</dependencies>
<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>application.yml 配置
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://api.deepseek.com # 切换到 DeepSeek(OpenAI 协议兼容)
chat:
options:
model: deepseek-chat
temperature: 0.7
max-tokens: 2000
embedding:
options:
model: text-embedding-3-small
datasource:
url: jdbc:postgresql://localhost:5432/ai_db
username: ${DB_USER}
password: ${DB_PASS}
# PGVector 自动初始化向量表
ai:
vectorstore:
pgvector:
initialize-schema: true
dimensions: 1536
index-type: HNSW
distance-type: COSINE_DISTANCEChatClient 核心用法
ChatClient vs ChatModel
| 对比项 | ChatModel | ChatClient |
|---|---|---|
| 层级 | 底层接口,直接调 LLM | 高级封装,带 Advisor 链 |
| 功能 | 仅 LLM 调用 | LLM + 对话历史 + RAG 检索 + 自定义拦截 |
| 推荐场景 | 简单 LLM 调用 | 生产应用(推荐) |
基础调用
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
// 推荐通过 Builder 构建,注入 default 配置
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个专业的 Java 技术助手,回答简洁准确。")
.build();
}
// 普通问答
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
// 流式输出(SSE)
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.stream()
.content();
}
// 结构化输出(自动 JSON 解析)
@PostMapping("/extract")
public UserInfo extract(@RequestBody String text) {
return chatClient.prompt()
.user("从以下文本中提取用户信息,返回 JSON:\n" + text)
.call()
.entity(UserInfo.class); // 自动 JSON 反序列化
}
}
record UserInfo(String name, String email, String phone) {}多轮对话(ChatMemory)
@Service
public class ConversationalChatService {
private final ChatClient chatClient;
// 生产用 Redis 持久化,开发用 InMemoryChatMemory
private final ChatMemory chatMemory = new InMemoryChatMemory();
public ConversationalChatService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个智能客服助手,有礼貌地回答用户问题。")
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) // 自动注入对话历史
)
.build();
}
public String chat(String sessionId, String userMessage) {
return chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId)) // 按会话隔离
.call()
.content();
}
}Advisor 机制
Advisor 是 Spring AI 中的拦截器机制,类似 Spring MVC 的 HandlerInterceptor,在 LLM 调用前后执行自定义逻辑。
内置 Advisor
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient ragChatClient(ChatClient.Builder builder,
VectorStore vectorStore) {
return builder
.defaultSystem("""
你是公司知识库助手。回答必须基于提供的参考资料。
如果资料中没有相关信息,请明确说明,不要编造。
""")
.defaultAdvisors(
// RAG 检索 Advisor:自动检索相关文档注入上下文
QuestionAnswerAdvisor.builder(vectorStore)
.withSearchRequest(SearchRequest.defaults()
.withTopK(5)
.withSimilarityThreshold(0.7))
.build(),
// 对话历史 Advisor
new MessageChatMemoryAdvisor(new InMemoryChatMemory()),
// 日志 Advisor(调试用)
new SimpleLoggerAdvisor()
)
.build();
}
}自定义 Advisor(限流 + 成本监控)
@Component
@Slf4j
public class CostMonitorAdvisor implements CallAroundAdvisor {
private final MeterRegistry meterRegistry;
@Override
public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
long startTime = System.currentTimeMillis();
// 执行 LLM 调用
AdvisedResponse response = chain.nextAroundCall(request);
// 记录 Token 消耗和延迟
long duration = System.currentTimeMillis() - startTime;
Usage usage = response.response().getMetadata().getUsage();
meterRegistry.counter("llm.tokens.input").increment(usage.getPromptTokens());
meterRegistry.counter("llm.tokens.output").increment(usage.getCompletionTokens());
meterRegistry.timer("llm.call.duration").record(duration, TimeUnit.MILLISECONDS);
// 估算成本(以 DeepSeek 为例)
double cost = (usage.getPromptTokens() * 0.27 + usage.getCompletionTokens() * 1.1) / 1_000_000;
log.info("LLM 调用 | 输入: {}T | 输出: {}T | 延迟: {}ms | 预估成本: ${:.6f}",
usage.getPromptTokens(), usage.getCompletionTokens(), duration, cost);
return response;
}
@Override
public String getName() { return "CostMonitorAdvisor"; }
@Override
public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }
}Function Calling(工具调用)
Spring AI 用 @Tool 注解实现工具调用,与 Agent 章节配合使用:
@Component
public class WeatherTool {
private final WeatherApiClient weatherClient;
@Tool(description = "查询指定城市的实时天气信息,包括温度、天气状况、风速")
public String getWeather(
@ToolParam(description = "城市名称,如:北京、上海、广州") String city) {
WeatherData data = weatherClient.getCurrentWeather(city);
return String.format("城市:%s,温度:%d°C,天气:%s,风速:%s",
city, data.temperature(), data.condition(), data.windSpeed());
}
@Tool(description = "查询未来 N 天天气预报")
public String getForecast(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "预报天数,1-7 天") int days) {
return weatherClient.getForecast(city, Math.min(days, 7));
}
}
// 使用工具的 ChatClient
@Service
public class WeatherChatService {
private final ChatClient chatClient;
public WeatherChatService(ChatClient.Builder builder, WeatherTool weatherTool) {
this.chatClient = builder
.defaultSystem("你是天气助手,使用工具查询实时天气后再回答,不要凭记忆回答天气问题。")
.defaultTools(weatherTool)
.build();
}
public String chat(String question) {
return chatClient.prompt().user(question).call().content();
}
}VectorStore 完整 RAG 流程
@Service
@Slf4j
public class DocumentIngestionService {
private final VectorStore vectorStore;
private final TokenTextSplitter textSplitter;
/**
* PDF 文档入库
*/
public void ingestPdf(Resource pdfResource, String tenantId) {
log.info("开始处理文档:{}", pdfResource.getFilename());
// 1. 读取 PDF
PagePdfDocumentReader reader = new PagePdfDocumentReader(
pdfResource,
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageBottomMargin(0)
.build()
);
List<Document> rawDocs = reader.get();
// 2. 分块
List<Document> chunks = textSplitter.apply(rawDocs);
// 3. 添加元数据
chunks.forEach(chunk -> {
chunk.getMetadata().put("tenant_id", tenantId);
chunk.getMetadata().put("source", pdfResource.getFilename());
chunk.getMetadata().put("ingested_at", Instant.now().toString());
});
// 4. 向量化并入库(Spring AI 自动调用 EmbeddingModel)
vectorStore.add(chunks);
log.info("文档处理完成:{} 个 chunk 已入库", chunks.size());
}
/**
* 删除某个来源的所有文档
*/
public void deleteBySource(String source) {
vectorStore.delete(
FilterExpressionBuilder.eq("source", source).build()
);
}
}
// RAG 问答 Controller
@RestController
@RequestMapping("/api/knowledge")
public class KnowledgeController {
private final ChatClient ragChatClient;
@PostMapping("/ask")
public ResponseEntity<Map<String, Object>> ask(
@RequestBody AskRequest request) {
// Spring AI QuestionAnswerAdvisor 自动处理 RAG 检索
ChatResponse response = ragChatClient.prompt()
.user(request.question())
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, request.sessionId())
// 传入租户过滤条件
.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "tenant_id == '" + request.tenantId() + "'")
)
.call()
.chatResponse();
return ResponseEntity.ok(Map.of(
"answer", response.getResult().getOutput().getContent(),
"sources", extractSources(response) // 提取引用来源
));
}
}生产最佳实践
1. ChatClient 按业务场景区分
不要用同一个 ChatClient 处理所有场景,按场景配置不同 Temperature 和 System Prompt:代码生成(Temperature=0.1)、创意写作(Temperature=0.8)、问答(Temperature=0.3)。
2. 异常降级
@Service
public class ResilientChatService {
private final ChatClient primaryClient; // GPT-4o
private final ChatClient fallbackClient; // DeepSeek(降级)
public String chat(String question) {
try {
return primaryClient.prompt().user(question).call().content();
} catch (Exception e) {
log.warn("主模型调用失败,降级到备用模型", e);
return fallbackClient.prompt().user(question).call().content();
}
}
}3. 语义缓存
相同或高度相似的问题直接返回缓存,无需调用 LLM,可节省 30-60% 的 Token 费用。
高频面试题
Q: Spring AI 中 ChatClient 和 ChatModel 有什么区别?
ChatModel 是底层 LLM 调用接口,只负责发送请求和接收响应,无状态。ChatClient 是高级封装,在 ChatModel 之上增加了 Advisor 链机制(可以插入对话历史、RAG 检索、日志监控等拦截器),以及 Builder 模式的 default 配置(defaultSystem、defaultAdvisors、defaultTools)。生产中推荐用 ChatClient,开发简单工具时可以直接用 ChatModel。
Q: Advisor 机制的执行顺序是怎样的?
Advisor 按 Order 从小到大顺序执行
aroundCall前置逻辑,然后调用 LLM,再按相反顺序执行后置逻辑(类似 AOP 切面)。内置优先级:SimpleLoggerAdvisor(最外层,记录请求响应)> MessageChatMemoryAdvisor(注入对话历史)> QuestionAnswerAdvisor(注入 RAG 上下文,最靠近 LLM)。
Q: 如何实现多租户的向量数据库隔离?
通过元数据过滤实现逻辑隔离:文档入库时添加
tenant_id元数据,检索时通过FilterExpression过滤tenant_id == 'xxx'。Spring AI 的 VectorStore API 统一支持元数据过滤语法,换数据库时过滤语法不变。物理隔离(不同租户用不同表/集合)性能更好但成本更高,适合安全要求极高的场景。
Q: Spring AI 如何实现流式输出?
使用
chatClient.prompt().stream()而非.call(),返回Flux<String>(响应式流)。在 Controller 层配合MediaType.TEXT_EVENT_STREAM_VALUE实现 SSE(Server-Sent Events),前端使用 EventSource API 接收。生产中建议所有对话接口都支持流式,因为 LLM 响应时间通常 2-20 秒,流式输出能显著改善用户体验。
Q: Spring AI 的 VectorStore 如何切换不同向量数据库?
得益于统一抽象层,切换向量数据库只需:1)修改 pom.xml 引入新数据库的 Starter;2)修改 application.yml 的连接配置;3)业务代码中注入的
VectorStore接口不变。例如从 Chroma 切换到 PGVector,业务代码零改动。这是 Spring AI 设计的核心价值之一。
Q: Function Calling 在 Spring AI 中如何工作?
用
@Tool注解标注方法,Spring AI 自动将其转换为 OpenAI 函数调用协议的 JSON Schema 定义,注册到 ChatClient 的defaultTools中。当 LLM 决定调用某个工具时,Spring AI 自动:解析 LLM 返回的 tool_calls、通过反射执行对应方法、将结果作为 ToolResponseMessage 返回给 LLM,整个流程对业务代码透明。
知识星球深度内容
完整大厂面经(含详细答案、最新更新)、AI 项目源码、1v1 简历修改,扫码加入「AI 工程师加速社区」知识星球获取 👉 立即加入
