Spring Boot + AI 企业应用开发教程
前言
随着大语言模型技术的快速发展,将 AI 能力集成到企业级 Java 应用中已成为行业趋势。Spring Boot 作为 Java 生态中最流行的微服务框架,通过 Spring AI 项目提供了优雅的 AI 集成方案。
本教程将系统性地讲解如何使用 Spring Boot + Spring AI 构建企业级 AI 应用,从框架核心概念到多模型适配,从 Function Calling 到 RAG 集成,从流式输出到生产部署,最终通过一个完整的企业级 AI 客服系统实战项目,帮助 Java 开发者快速掌握 AI 应用开发的全栈技能。
第一章:Spring AI 框架概述 — Java 生态的 AI 集成方案
1.1 为什么选择 Spring AI
在 Java 生态中集成 AI 能力,开发者面临多个选择:直接调用 OpenAI API、使用 LangChain4j、或者选择 Spring AI。Spring AI 具有以下核心优势:
与 Spring 生态无缝集成:Spring AI 遵循 Spring 的设计哲学,提供了统一的抽象层,使得切换不同的 AI 模型提供商(OpenAI、Ollama、Azure、通义千问等)只需要修改配置,不需要改动业务代码。
企业级特性:与 Spring Security、Spring Data、Spring Cloud 等组件天然兼容,可以轻松实现认证授权、数据持久化、微服务通信等企业级需求。
统一抽象:Spring AI 提供了 ChatClient、EmbeddingClient、ImageClient 等核心抽象,屏蔽了底层模型 API 的差异。
1.2 项目初始化
使用 Spring Initializr 创建项目:
<!-- pom.xml 核心依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<dependencies>
<!-- Spring AI BOM -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI OpenAI Starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI Ollama Starter(本地模型) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI PgVector Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- WebFlux(用于流式输出) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
1.3 基础配置
# application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://api.openai.com
chat:
options:
model: gpt-4o
temperature: 0.7
max-tokens: 2000
embedding:
options:
model: text-embedding-3-small
ollama:
base-url: http://localhost:11434
chat:
options:
model: qwen2.5:7b
datasource:
url: jdbc:postgresql://localhost:5432/ai_app
username: postgres
password: ${DB_PASSWORD}
第二章:Spring AI 核心抽象
2.1 ChatClient — 对话客户端
ChatClient 是 Spring AI 中最核心的接口,用于与大语言模型进行对话:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("你是一个专业的AI助手,善于用简洁清晰的中文回答问题。")
.build();
}
/**
* 简单对话
*/
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
/**
* 带系统提示的对话
*/
public String chatWithSystem(String systemPrompt, String userMessage) {
return chatClient.prompt()
.system(systemPrompt)
.user(userMessage)
.call()
.content();
}
/**
* 获取完整的 ChatResponse(包含元数据)
*/
public ChatResponse chatWithMetadata(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.chatResponse();
}
/**
* 多轮对话
*/
public String multiTurnChat(List<Message> conversationHistory, String newMessage) {
conversationHistory.add(new org.springframework.ai.chat.messages.UserMessage(newMessage));
Prompt prompt = new Prompt(conversationHistory);
ChatResponse response = chatClient.prompt(prompt).call().chatResponse();
String assistantReply = response.getResult().getOutput().getContent();
conversationHistory.add(new org.springframework.ai.chat.messages.AssistantMessage(assistantReply));
return assistantReply;
}
}
2.2 EmbeddingClient — 向量化客户端
EmbeddingClient 用于将文本转化为向量表示,是 RAG 系统的基础:
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.stereotype.Service;
@Service
public class EmbeddingService {
private final EmbeddingClient embeddingClient;
public EmbeddingService(EmbeddingClient embeddingClient) {
this.embeddingClient = embeddingClient;
}
/**
* 单文本向量化
*/
public float[] embed(String text) {
List<float[]> embeddings = embeddingClient.embed(List.of(text));
return embeddings.get(0);
}
/**
* 批量文本向量化
*/
public List<float[]> batchEmbed(List<String> texts) {
return embeddingClient.embed(texts);
}
/**
* 计算两个文本的相似度
*/
public double similarity(String text1, String text2) {
float[] vec1 = embed(text1);
float[] vec2 = embed(text2);
double dotProduct = 0, norm1 = 0, norm2 = 0;
for (int i = 0; i < vec1.length; i++) {
dotProduct += vec1[i] * vec2[i];
norm1 += vec1[i] * vec1[i];
norm2 += vec2[i] * vec2[i];
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
}
2.3 ImageClient — 图像生成客户端
import org.springframework.ai.image.ImageClient;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.stereotype.Service;
@Service
public class ImageService {
private final ImageClient imageClient;
public ImageService(ImageClient imageClient) {
this.imageClient = imageClient;
}
/**
* 生成图片
*/
public String generateImage(String description) {
ImageResponse response = imageClient.call(
new ImagePrompt(description)
);
return response.getResult().getOutput().getUrl();
}
}
第三章:多模型适配 — OpenAI、Ollama、Azure、通义千问
3.1 多模型配置策略
在企业应用中,通常需要支持多个模型提供商,以实现成本优化、故障切换和功能差异化:
# application.yml - 多模型配置
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
chat:
options:
model: gpt-4o
temperature: 0.7
ollama:
base-url: http://localhost:11434
chat:
options:
model: qwen2.5:7b
# 自定义模型配置
app:
ai:
models:
fast:
provider: ollama
model: qwen2.5:7b
smart:
provider: openai
model: gpt-4o
balanced:
provider: openai
model: gpt-4o-mini
3.2 模型路由器
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
@Component
public class ModelRouter {
private final Map<String, ChatClient> chatClients;
public ModelRouter(List<ChatClient.Builder> builders) {
this.chatClients = new HashMap<>();
// 根据配置初始化不同的 ChatClient
}
/**
* 根据任务类型选择模型
*/
public ChatClient route(TaskType taskType) {
return switch (taskType) {
case SIMPLE_QA -> chatClients.get("fast"); // 简单问答用小模型
case COMPLEX_ANALYSIS -> chatClients.get("smart"); // 复杂分析用大模型
case CODE_GENERATION -> chatClients.get("smart");
case TRANSLATION -> chatClients.get("balanced");
default -> chatClients.get("balanced");
};
}
public enum TaskType {
SIMPLE_QA, COMPLEX_ANALYSIS, CODE_GENERATION, TRANSLATION, SUMMARIZATION
}
}
3.3 通义千问集成
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
@Configuration
public class QwenConfig {
@Bean
public ChatClient qwenChatClient() {
// 通义千问兼容 OpenAI API 格式
RestClient.Builder restClientBuilder = RestClient.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.defaultHeader("Authorization", "Bearer " + System.getenv("DASHSCOPE_API_KEY"));
// 使用 OpenAI 兼容模式
return ChatClient.builder(
new org.springframework.ai.openai.OpenAiChatModel(
new org.springframework.ai.openai.OpenAiApi(
"https://dashscope.aliyuncs.com/compatible-mode/v1",
System.getenv("DASHSCOPE_API_KEY")
),
org.springframework.ai.openai.OpenAiChatOptions.builder()
.model("qwen-plus")
.temperature(0.7)
.build()
)
).build();
}
}
第四章:Function Calling 在 Spring Boot 中的实现
4.1 Function Calling 概述
Function Calling 允许大模型调用外部函数,获取实时数据或执行特定操作。这是构建 AI Agent 的核心能力。
4.2 定义 Function
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Description;
import org.springframework.stereotype.Service;
import java.util.function.Function;
@Service
public class AIFunctions {
/**
* 查询天气函数
*/
@Bean
@Description("获取指定城市的当前天气信息")
public Function<WeatherRequest, WeatherResponse> getWeather() {
return request -> {
// 调用天气 API
String weather = callWeatherAPI(request.city());
return new WeatherResponse(
request.city(),
weather,
"25°C",
"晴天"
);
};
}
/**
* 查询订单函数
*/
@Bean
@Description("根据订单号查询订单状态和详情")
public Function<OrderRequest, OrderResponse> getOrderStatus() {
return request -> {
// 查询数据库
Order order = orderRepository.findByOrderNo(request.orderNo());
return new OrderResponse(
order.getOrderNo(),
order.getStatus().name(),
order.getCreateTime(),
order.getTrackingNo()
);
};
}
/**
* 发送通知函数
*/
@Bean
@Description("向指定用户发送通知消息")
public Function<NotificationRequest, NotificationResponse> sendNotification() {
return request -> {
notificationService.send(request.userId(), request.message());
return new NotificationResponse(true, "通知发送成功");
};
}
// 请求/响应记录
public record WeatherRequest(String city) {}
public record WeatherResponse(String city, String description, String temperature, String condition) {}
public record OrderRequest(String orderNo) {}
public record OrderResponse(String orderNo, String status, String createTime, String trackingNo) {}
public record NotificationRequest(String userId, String message) {}
public record NotificationResponse(boolean success, String message) {}
}
4.3 使用 Function Calling
@Service
public class FunctionCallingService {
private final ChatClient chatClient;
public FunctionCallingService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是一个智能助手,可以查询天气、订单状态,并发送通知。")
.defaultFunctions("getWeather", "getOrderStatus", "sendNotification")
.build();
}
/**
* 带 Function Calling 的对话
*/
public String chatWithFunctions(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
/**
* 示例:
* 用户:"北京今天天气怎么样?"
* AI 会自动调用 getWeather("北京") 函数,然后基于返回结果生成回答
*/
}
4.4 动态 Function 注册
@Service
public class DynamicFunctionService {
private final ChatClient.Builder chatClientBuilder;
public DynamicFunctionService(ChatClient.Builder chatClientBuilder) {
this.chatClientBuilder = chatClientBuilder;
}
/**
* 根据用户权限动态注册可用函数
*/
public String chatWithDynamicFunctions(String userMessage, Set<String> permissions) {
ChatClient.Builder builder = chatClientBuilder.copy();
// 根据权限动态添加函数
if (permissions.contains("weather")) {
builder.defaultFunctions("getWeather");
}
if (permissions.contains("order:read")) {
builder.defaultFunctions("getOrderStatus");
}
if (permissions.contains("notification:send")) {
builder.defaultFunctions("sendNotification");
}
return builder.build()
.prompt()
.user(userMessage)
.call()
.content();
}
}
第五章:RAG 集成 — VectorStore、DocumentReader、Advisor
5.1 RAG 架构概览
Spring AI 提供了完整的 RAG 支持,核心组件包括:
- DocumentReader:文档读取器,支持 PDF、Word、HTML 等格式
- DocumentTransformer:文档转换器,包括分块、清洗等
- DocumentWriter:文档写入器,将文档向量化后存入 VectorStore
- VectorStore:向量存储,支持 PgVector、Milvus、Chroma 等
- Advisor:顾问模式,用于在对话流程中注入 RAG 检索结果
5.2 文档加载与处理
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.util.List;
@Service
public class DocumentService {
private final VectorStore vectorStore;
public DocumentService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* 加载 PDF 文档
*/
public List<Document> loadPdf(InputStream pdfStream) {
PagePdfDocumentReader reader = new PagePdfDocumentReader(pdfStream);
return reader.get();
}
/**
* 使用 Tika 加载多种格式文档
*/
public List<Document> loadWithTika(InputStream stream, String mediaType) {
TikaDocumentReader reader = new TikaDocumentReader(stream, mediaType);
return reader.get();
}
/**
* 文档分块
*/
public List<Document> splitDocuments(List<Document> documents) {
TokenTextSplitter splitter = new TokenTextSplitter(
800, // 默认 chunk size
200, // min chunk size
10, // min chunk length tokens
5000, // max num chunks
true // keep separator
);
return splitter.apply(documents);
}
/**
* 完整的文档入库流程
*/
public int ingestDocument(InputStream stream, String mediaType, Map<String, Object> metadata) {
// 1. 加载文档
List<Document> documents = loadWithTika(stream, mediaType);
// 2. 添加元数据
documents.forEach(doc -> {
doc.getMetadata().putAll(metadata);
doc.getMetadata().put("ingest_time", Instant.now().toString());
});
// 3. 分块
List<Document> chunks = splitDocuments(documents);
// 4. 写入向量数据库
vectorStore.add(chunks);
return chunks.size();
}
}
5.3 VectorStore 集成
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;
@Service
public class VectorStoreService {
private final VectorStore vectorStore;
public VectorStoreService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* 基本相似度搜索
*/
public List<Document> search(String query, int topK) {
SearchRequest request = SearchRequest.query(query)
.withTopK(topK);
return vectorStore.similaritySearch(request);
}
/**
* 带过滤条件的搜索
*/
public List<Document> searchWithFilter(String query, int topK, String category) {
FilterExpressionBuilder b = new FilterExpressionBuilder();
SearchRequest request = SearchRequest.query(query)
.withTopK(topK)
.withFilterExpression(b.eq("category", category).build());
return vectorStore.similaritySearch(request);
}
/**
* 带相似度阈值的搜索
*/
public List<Document> searchWithThreshold(String query, int topK, double threshold) {
SearchRequest request = SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(threshold);
return vectorStore.similaritySearch(request);
}
}
5.4 Advisor 模式实现 RAG
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
@Service
public class RAGService {
private final ChatClient chatClient;
public RAGService(ChatClient.Builder builder, VectorStore vectorStore) {
// 创建 RAG Advisor
QuestionAnswerAdvisor ragAdvisor = new QuestionAnswerAdvisor(
vectorStore,
SearchRequest.defaults().withTopK(5)
);
this.chatClient = builder
.defaultSystem("你是一个企业知识库助手,请基于提供的参考资料回答问题。如果资料中没有相关信息,请明确告知。")
.defaultAdvisors(ragAdvisor)
.build();
}
/**
* RAG 问答
*/
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
/**
* 带对话记忆的 RAG 问答
*/
public String askWithMemory(String question, String sessionId) {
return chatClient.prompt()
.user(question)
.advisors(new MessageChatMemoryAdvisor(chatMemory, sessionId))
.call()
.content();
}
}
第六章:流式输出与 SSE 实现
6.1 流式输出的价值
在 AI 应用中,流式输出(Streaming)能显著提升用户体验。用户不必等待完整回答生成,而是可以像打字一样逐字看到回答。
6.2 Spring AI 流式 API
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class StreamingChatService {
private final ChatClient chatClient;
public StreamingChatService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 流式对话
*/
public Flux<String> streamChat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.stream()
.content();
}
/**
* 流式对话(包含元数据)
*/
public Flux<ChatResponse> streamChatWithMetadata(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.stream()
.chatResponse();
}
}
6.3 SSE(Server-Sent Events)控制器
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final StreamingChatService streamingChatService;
private final ChatService chatService;
public ChatController(StreamingChatService streamingChatService, ChatService chatService) {
this.streamingChatService = streamingChatService;
this.chatService = chatService;
}
/**
* 流式聊天接口(SSE)
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestBody ChatRequest request) {
return streamingChatService.streamChat(request.message())
.map(chunk -> "data: " + chunk + "\n\n");
}
/**
* 非流式聊天接口
*/
@PostMapping("/call")
public ChatResponse callChat(@RequestBody ChatRequest request) {
String response = chatService.chat(request.message());
return new ChatResponse(response);
}
/**
* 带 RAG 的流式聊天
*/
@PostMapping(value = "/rag/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamRAGChat(@RequestBody ChatRequest request) {
return ragService.streamAsk(request.message())
.map(chunk -> "data: " + chunk + "\n\n");
}
public record ChatRequest(String message, String sessionId) {}
public record ChatResponse(String content) {}
}
6.4 前端 SSE 接收(Vue3)
// Vue3 组合式 API
import { ref } from 'vue'
export function useStreamingChat() {
const messages = ref([])
const isStreaming = ref(false)
async function sendMessage(content) {
// 添加用户消息
messages.value.push({ role: 'user', content })
// 创建 AI 消息占位
const aiMessage = { role: 'assistant', content: '' }
messages.value.push(aiMessage)
isStreaming.value = true
try {
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: content })
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value)
const lines = text.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const chunk = line.slice(6)
aiMessage.content += chunk
// 触发响应式更新
messages.value = [...messages.value]
}
}
}
} finally {
isStreaming.value = false
}
}
return { messages, isStreaming, sendMessage }
}
第七章:对话记忆管理
7.1 ChatMemory 架构
Spring AI 通过 ChatMemory 和 MessageChatMemoryAdvisor 实现对话记忆管理:
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatMemoryConfig {
/**
* 基于窗口的对话记忆(保留最近 N 条消息)
*/
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(20) // 保留最近 20 条消息
.build();
}
/**
* 基于 Token 的对话记忆(适合长对话)
*/
@Bean
public ChatMemory tokenBasedChatMemory() {
return TokenWindowChatMemory.builder()
.maxTokens(4000)
.build();
}
}
7.2 持久化对话记忆
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.repository.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class JdbcChatMemoryRepository implements ChatMemoryRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcChatMemoryRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
initTable();
}
private void initTable() {
jdbcTemplate.execute("""
CREATE TABLE IF NOT EXISTS chat_memory (
id BIGSERIAL PRIMARY KEY,
conversation_id VARCHAR(255) NOT NULL,
message_type VARCHAR(50) NOT NULL,
content TEXT NOT NULL,
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""");
}
@Override
public List<Message> findByConversationId(String conversationId) {
return jdbcTemplate.query(
"SELECT message_type, content FROM chat_memory WHERE conversation_id = ? ORDER BY created_at",
(rs, rowNum) -> deserializeMessage(
rs.getString("message_type"),
rs.getString("content")
),
conversationId
);
}
@Override
public void saveAll(String conversationId, List<Message> messages) {
// 先删除旧消息
jdbcTemplate.update("DELETE FROM chat_memory WHERE conversation_id = ?", conversationId);
// 批量插入
jdbcTemplate.batchUpdate(
"INSERT INTO chat_memory (conversation_id, message_type, content) VALUES (?, ?, ?)",
messages.stream().map(msg -> new Object[]{
conversationId,
msg.getMessageType().name(),
msg.getContent()
}).toList()
);
}
@Override
public void deleteByConversationId(String conversationId) {
jdbcTemplate.update("DELETE FROM chat_memory WHERE conversation_id = ?", conversationId);
}
}
7.3 对话记忆服务
@Service
public class ConversationService {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
public ConversationService(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatMemory = chatMemory;
this.chatClient = builder
.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
.build();
}
/**
* 带记忆的对话
*/
public String chat(String sessionId, String message) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
.call()
.content();
}
/**
* 获取对话历史
*/
public List<Message> getHistory(String sessionId) {
return chatMemory.get(sessionId);
}
/**
* 清除对话历史
*/
public void clearHistory(String sessionId) {
chatMemory.clear(sessionId);
}
}
第八章:向量数据库集成
8.1 PgVector 集成
PgVector 是基于 PostgreSQL 的向量数据库,适合已有 PostgreSQL 基础设施的企业:
# application.yml
spring:
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
datasource:
url: jdbc:postgresql://localhost:5432/ai_app
@Configuration
public class PgVectorConfig {
@Bean
public VectorStore pgVectorStore(JdbcTemplate jdbcTemplate, EmbeddingClient embeddingClient) {
return PgVectorStore.builder(jdbcTemplate, embeddingClient)
.dimensions(1536)
.indexType(IndexType.HNSW)
.distanceType(COSINE_DISTANCE)
.initializeSchema(true)
.build();
}
}
8.2 Milvus 集成
Milvus 是专业级分布式向量数据库,适合大规模向量检索场景:
@Configuration
public class MilvusConfig {
@Bean
public VectorStore milvusVectorStore(EmbeddingClient embeddingClient) {
return MilvusVectorStore.builder(
new MilvusServiceClient(
Param.newBuilder()
.withHost("localhost")
.withPort(19530)
.build()
),
embeddingClient
)
.collectionName("documents")
.indexType(IndexType.HNSW)
.metricType(MetricType.COSINE)
.build();
}
}
8.3 Chroma 集成
Chroma 是轻量级向量数据库,适合开发和测试环境:
@Configuration
public class ChromaConfig {
@Bean
public VectorStore chromaVectorStore(EmbeddingClient embeddingClient) {
return new ChromaVectorStore(
new ChromaApi("http://localhost:8000"),
embeddingClient,
"documents"
);
}
}
8.4 多向量存储策略
@Service
public class MultiVectorStoreService {
private final Map<String, VectorStore> vectorStores;
public MultiVectorStoreService(
@Qualifier("pgVectorStore") VectorStore pgStore,
@Qualifier("milvusVectorStore") VectorStore milvusStore) {
this.vectorStores = Map.of(
"documents", pgStore,
"knowledge", milvusStore
);
}
/**
* 根据数据类型选择向量存储
*/
public List<Document> search(String query, String storeType, int topK) {
VectorStore store = vectorStores.get(storeType);
if (store == null) {
throw new IllegalArgumentException("Unknown store type: " + storeType);
}
return store.similaritySearch(
SearchRequest.query(query).withTopK(topK)
);
}
/**
* 跨存储联合搜索
*/
public List<Document> federatedSearch(String query, int topK) {
List<Document> allResults = new ArrayList<>();
for (VectorStore store : vectorStores.values()) {
allResults.addAll(
store.similaritySearch(SearchRequest.query(query).withTopK(topK))
);
}
// 按相似度排序并去重
return allResults.stream()
.sorted(Comparator.comparingDouble(Document::getScore).reversed())
.limit(topK)
.toList();
}
}
第九章:安全与认证 — Spring Security + AI
9.1 安全架构设计
企业级 AI 应用需要完善的认证授权机制:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/chat/**").authenticated()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
9.2 API Key 认证
@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {
private final ApiKeyRepository apiKeyRepository;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String apiKey = request.getHeader("X-API-Key");
if (apiKey == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
Optional<ApiKeyEntity> keyEntity = apiKeyRepository.findByKeyAndActive(apiKey, true);
if (keyEntity.isEmpty()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
ApiKeyEntity entity = keyEntity.get();
// 检查速率限制
if (isRateLimited(entity.getUserId())) {
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
return;
}
// 设置认证信息
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
entity.getUserId(), null, List.of(new SimpleGrantedAuthority("ROLE_USER"))
);
SecurityContextHolder.getContext().setAuthentication(auth);
chain.doFilter(request, response);
}
}
9.3 内容安全过滤
@Service
public class ContentSafetyService {
private final ChatClient moderationClient;
public ContentSafetyService(ChatClient.Builder builder) {
this.moderationClient = builder
.defaultSystem("你是一个内容安全审核员。判断以下内容是否安全,返回 SAFE 或 UNSAFE。")
.build();
}
/**
* 检查用户输入是否安全
*/
public boolean isInputSafe(String input) {
// 简单的关键词过滤
List<String> blockedKeywords = List.of("暴力", "色情", "违法");
for (String keyword : blockedKeywords) {
if (input.contains(keyword)) {
return false;
}
}
// 使用 AI 进行深度审核(可选)
String result = moderationClient.prompt()
.user("请审核以下内容是否安全:" + input)
.call()
.content();
return result.contains("SAFE");
}
/**
* 过滤 AI 输出中的敏感信息
*/
public String filterOutput(String output) {
// 脱敏处理:手机号、身份证号、邮箱等
output = output.replaceAll("\\b1[3-9]\\d{9}\\b", "1**********");
output = output.replaceAll("\\b\\d{17}[\\dXx]\\b", "*******************");
output = output.replaceAll("\\b[\\w.-]+@[\\w.-]+\\.\\w+\\b", "***@***.***");
return output;
}
}
第十章:生产部署
10.1 Docker 化部署
# Dockerfile
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# 复制构建产物
COPY target/ai-app.jar app.jar
# 环境变量
ENV JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC"
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# docker-compose.yml
version: '3.8'
services:
ai-app:
build: .
ports:
- "8080:8080"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- DB_URL=jdbc:postgresql://postgres:5432/ai_app
- DB_USER=postgres
- DB_PASSWORD=${DB_PASSWORD}
depends_on:
- postgres
- redis
postgres:
image: pgvector/pgvector:pg16
environment:
- POSTGRES_DB=ai_app
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- ai-app
volumes:
pgdata:
redisdata:
10.2 监控与指标
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "ai-app");
}
}
@Service
public class AIMetricsService {
private final Counter chatRequestCounter;
private final Timer chatLatencyTimer;
private final Counter tokenUsageCounter;
public AIMetricsService(MeterRegistry registry) {
this.chatRequestCounter = Counter.builder("ai.chat.requests.total")
.description("Total chat requests")
.register(registry);
this.chatLatencyTimer = Timer.builder("ai.chat.latency")
.description("Chat request latency")
.register(registry);
this.tokenUsageCounter = Counter.builder("ai.tokens.usage")
.description("Token usage")
.register(registry);
}
public void recordChatRequest(String model, long latencyMs, int tokensUsed) {
chatRequestCounter.increment();
chatLatencyTimer.record(latencyMs, TimeUnit.MILLISECONDS);
tokenUsageCounter.increment(tokensUsed);
}
}
10.3 性能优化
@Configuration
public class PerformanceConfig {
/**
* 连接池配置
*/
@Bean
public RestClientCustomizer restClientCustomizer() {
return restClient -> {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(30))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(30, TimeUnit.SECONDS))
);
restClient.requestFactory(new JdkClientHttpRequestFactory(
httpClient
));
};
}
/**
* 异步线程池配置
*/
@Bean("aiTaskExecutor")
public Executor aiTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("ai-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
第十一章:实战项目 — 企业级 AI 客服系统
11.1 系统架构
┌─────────────────────────────────────────────┐
│ 前端(Vue3) │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 对话界面 │ │ 知识库管理 │ │ 数据统计 │ │
│ └─────────┘ └──────────┘ └────────────┘ │
├─────────────────────────────────────────────┤
│ API 网关(Nginx) │
├─────────────────────────────────────────────┤
│ Spring Boot 后端 │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 对话服务 │ │ RAG 服务 │ │ 管理服务 │ │
│ └─────────┘ └──────────┘ └────────────┘ │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 记忆管理 │ │ 内容安全 │ │ Function │ │
│ │ │ │ │ │ Calling │ │
│ └─────────┘ └──────────┘ └────────────┘ │
├─────────────────────────────────────────────┤
│ 数据层 │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ │
│ │ PostgreSQL│ │ Redis │ │ MinIO │ │
│ │ +PgVector│ │ 缓存 │ │ 文件存储 │ │
│ └─────────┘ └──────────┘ └────────────┘ │
└─────────────────────────────────────────────┘
11.2 核心数据模型
// 对话实体
@Entity
@Table(name = "conversations")
public class Conversation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String sessionId;
private Long userId;
private String title;
@Enumerated(EnumType.STRING)
private ConversationStatus status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
// 消息实体
@Entity
@Table(name = "messages")
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long conversationId;
@Enumerated(EnumType.STRING)
private MessageRole role; // USER, ASSISTANT, SYSTEM
@Column(columnDefinition = "TEXT")
private String content;
private String model;
private Integer promptTokens;
private Integer completionTokens;
@Column(columnDefinition = "jsonb")
private String metadata;
private LocalDateTime createdAt;
}
// 知识库文档实体
@Entity
@Table(name = "knowledge_documents")
public class KnowledgeDocument {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String category;
@Column(columnDefinition = "TEXT")
private String content;
private String filePath;
private Long fileSize;
@Enumerated(EnumType.STRING)
private DocumentStatus status;
private Integer chunkCount;
private LocalDateTime createdAt;
private LocalDateTime processedAt;
}
11.3 客服对话服务
@Service
@Slf4j
public class CustomerServiceChat {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
private final VectorStore vectorStore;
private final ContentSafetyService safetyService;
private final OrderFunction orderFunction;
private final ConversationRepository conversationRepo;
private final MessageRepository messageRepo;
public CustomerServiceChat(
ChatClient.Builder builder,
ChatMemory chatMemory,
VectorStore vectorStore,
ContentSafetyService safetyService,
OrderFunction orderFunction,
ConversationRepository conversationRepo,
MessageRepository messageRepo) {
this.chatMemory = chatMemory;
this.vectorStore = vectorStore;
this.safetyService = safetyService;
this.orderFunction = orderFunction;
this.conversationRepo = conversationRepo;
this.messageRepo = messageRepo;
// 构建带 RAG 和记忆的 ChatClient
this.chatClient = builder
.defaultSystem("""
你是一个专业的客服助手。请遵守以下规则:
1. 基于知识库中的信息回答问题,不要编造信息
2. 如果知识库中没有相关信息,引导用户联系人工客服
3. 回答要简洁、专业、友好
4. 涉及订单问题时,使用订单查询功能获取实时信息
5. 如果用户情绪激动,保持耐心和同理心
""")
.defaultAdvisors(
new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults().withTopK(5)),
new MessageChatMemoryAdvisor(chatMemory)
)
.defaultFunctions("getOrderStatus", "createTicket")
.build();
}
/**
* 处理用户消息
*/
@Transactional
public ChatResult handleUserMessage(String sessionId, String userMessage) {
// 1. 内容安全检查
if (!safetyService.isInputSafe(userMessage)) {
return new ChatResult("抱歉,您的消息包含不当内容,请重新表述。", false);
}
// 2. 保存用户消息
saveMessage(sessionId, MessageRole.USER, userMessage);
// 3. 获取 AI 回答
String aiResponse = chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
.call()
.content();
// 4. 输出安全过滤
aiResponse = safetyService.filterOutput(aiResponse);
// 5. 保存 AI 回答
saveMessage(sessionId, MessageRole.ASSISTANT, aiResponse);
return new ChatResult(aiResponse, true);
}
/**
* 流式处理用户消息
*/
public Flux<String> streamUserMessage(String sessionId, String userMessage) {
if (!safetyService.isInputSafe(userMessage)) {
return Flux.just("抱歉,您的消息包含不当内容,请重新表述。");
}
saveMessage(sessionId, MessageRole.USER, userMessage);
return chatClient.prompt()
.user(userMessage)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
.stream()
.content()
.doOnComplete(() -> {
// 流完成后保存完整回答(可选)
});
}
private void saveMessage(String sessionId, MessageRole role, String content) {
Conversation conv = conversationRepo.findBySessionId(sessionId)
.orElseGet(() -> {
Conversation c = new Conversation();
c.setSessionId(sessionId);
c.setStatus(ConversationStatus.ACTIVE);
c.setCreatedAt(LocalDateTime.now());
return conversationRepo.save(c);
});
Message msg = new Message();
msg.setConversationId(conv.getId());
msg.setRole(role);
msg.setContent(content);
msg.setCreatedAt(LocalDateTime.now());
messageRepo.save(msg);
}
public record ChatResult(String content, boolean success) {}
}
11.4 客服 REST API
@RestController
@RequestMapping("/api/customer-service")
public class CustomerServiceController {
private final CustomerServiceChat chatService;
public CustomerServiceController(CustomerServiceChat chatService) {
this.chatService = chatService;
}
/**
* 普通对话接口
*/
@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(@RequestBody @Valid ChatRequest request,
@AuthenticationPrincipal Long userId) {
var result = chatService.handleUserMessage(request.sessionId(), request.message());
return ResponseEntity.ok(new ChatResponse(result.content(), result.success()));
}
/**
* 流式对话接口(SSE)
*/
@PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestBody @Valid ChatRequest request) {
return chatService.streamUserMessage(request.sessionId(), request.message())
.map(chunk -> "data: " + chunk + "\n\n");
}
/**
* 获取对话历史
*/
@GetMapping("/conversations/{sessionId}/messages")
public ResponseEntity<List<Message>> getHistory(@PathVariable String sessionId) {
return ResponseEntity.ok(chatService.getConversationHistory(sessionId));
}
/**
* 结束对话
*/
@PostMapping("/conversations/{sessionId}/close")
public ResponseEntity<Void> closeConversation(@PathVariable String sessionId) {
chatService.closeConversation(sessionId);
return ResponseEntity.ok().build();
}
public record ChatRequest(
@NotBlank String sessionId,
@NotBlank String message
) {}
public record ChatResponse(String content, boolean success) {}
}
11.5 Vue3 前端实现
<!-- ChatView.vue -->
<template>
<div class="chat-container">
<div class="chat-header">
<h2>AI 客服</h2>
<span class="status" :class="{ online: isConnected }">
{{ isConnected ? '在线' : '离线' }}
</span>
</div>
<div class="messages" ref="messagesRef">
<div
v-for="(msg, index) in messages"
:key="index"
class="message"
:class="msg.role"
>
<div class="avatar">
{{ msg.role === 'user' ? '👤' : '🤖' }}
</div>
<div class="content">
<div class="text" v-html="formatMessage(msg.content)"></div>
<div class="time">{{ formatTime(msg.timestamp) }}</div>
</div>
</div>
<div v-if="isStreaming" class="message assistant">
<div class="avatar">🤖</div>
<div class="content">
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
</div>
</div>
</div>
<div class="input-area">
<textarea
v-model="inputText"
@keydown.enter.exact.prevent="sendMessage"
placeholder="输入您的问题..."
:disabled="isStreaming"
></textarea>
<button @click="sendMessage" :disabled="!inputText.trim() || isStreaming">
发送
</button>
</div>
</div>
</template>
<script setup>
import { ref, nextTick, onMounted } from 'vue'
const messages = ref([])
const inputText = ref('')
const isStreaming = ref(false)
const isConnected = ref(true)
const messagesRef = ref(null)
const sessionId = ref(crypto.randomUUID())
async function sendMessage() {
const text = inputText.value.trim()
if (!text || isStreaming.value) return
inputText.value = ''
messages.value.push({
role: 'user',
content: text,
timestamp: new Date()
})
const aiMessage = {
role: 'assistant',
content: '',
timestamp: new Date()
}
messages.value.push(aiMessage)
isStreaming.value = true
await scrollToBottom()
try {
const response = await fetch('/api/customer-service/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: sessionId.value,
message: text
})
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value)
const lines = text.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
aiMessage.content += line.slice(6)
messages.value = [...messages.value]
await scrollToBottom()
}
}
}
} catch (error) {
aiMessage.content = '抱歉,服务暂时不可用,请稍后重试。'
} finally {
isStreaming.value = false
}
}
function formatMessage(content) {
return content
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
}
function formatTime(date) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
async function scrollToBottom() {
await nextTick()
if (messagesRef.value) {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
}
</script>
<style scoped>
.chat-container {
max-width: 800px;
margin: 0 auto;
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
.chat-header {
padding: 16px 24px;
background: #1a73e8;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.message {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.message.user {
flex-direction: row-reverse;
}
.message .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.message .content {
max-width: 70%;
}
.message .text {
padding: 12px 16px;
border-radius: 12px;
line-height: 1.6;
font-size: 15px;
}
.message.assistant .text {
background: white;
border: 1px solid #e0e0e0;
}
.message.user .text {
background: #1a73e8;
color: white;
}
.input-area {
padding: 16px 24px;
background: white;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 12px;
}
.input-area textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: none;
height: 48px;
font-size: 15px;
}
.input-area button {
padding: 12px 24px;
background: #1a73e8;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 15px;
}
.input-area button:disabled {
background: #ccc;
cursor: not-allowed;
}
.typing-indicator span {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #999;
margin: 0 2px;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
</style>
总结
本教程系统性地讲解了 Spring Boot + Spring AI 构建企业级 AI 应用的完整技术栈。核心要点回顾:
- Spring AI 统一抽象:ChatClient、EmbeddingClient、ImageClient 屏蔽了底层模型差异,切换模型只需改配置
- Function Calling 是 AI Agent 的基础:让大模型能够调用外部工具,获取实时数据
- RAG 是企业知识库的核心:通过 VectorStore + Advisor 模式实现文档检索增强
- 流式输出提升体验:SSE 实现逐字输出,减少用户等待焦虑
- 对话记忆是多轮对话的关键:支持窗口记忆、Token 限制和持久化
- 安全是企业应用的生命线:认证授权、内容安全过滤、数据脱敏缺一不可
- 生产部署要关注性能和可观测性:连接池、异步处理、指标监控
通过本教程的学习,你将能够独立构建一个完整的企业级 AI 应用系统。Spring Boot + Spring AI 的组合,让 Java 开发者也能轻松驾驭大模型应用开发。