SpringAI全流程实战手册
当Java开发者遇上大模型,会擦出怎样的火花?SpringAI给出了答案,用Spring的方式,把AI能力变成业务开发的常规武器。
本文带你从零开始,构建一个企业级智能问答系统,涵盖RAG架构、向量数据库、对话上下文、异步处理、监控告警等完整链路,最终产出一套可部署到K8s的生产级代码。
一、为什么Java开发者需要SpringAI?
在企业级AI应用开发领域,Python一度是绝对的主角,LangChain、LlamaIndex等框架生态丰富,Java开发者只能望洋兴叹。
SpringAI的出现改变了这一格局。作为Spring官方出品的AI集成框架,它延续了Spring“约定优于配置”的哲学,让Java开发者能够以熟悉的Spring风格接入OpenAI、Azure、Ollama等大语言模型。
SpringAI的核心价值:
统一抽象:ChatClient、EmbeddingClient等接口屏蔽了不同AI服务商的差异
生态融合:与Spring Boot、Spring Data、Spring Cloud无缝集成
生产就绪:自带监控、缓存、重试等企业级特性
本文将以一个完整的智能问答系统为案例,带你走通SpringAI开发的全流程。
二、系统架构全景图
2.1 整体架构
┌─────────────────────────────────────────────────────────────┐ │ API Gateway │ │ (负载均衡 / 限流 / 认证) │ └─────────────────────────┬───────────────────────────────────┘ │ ┌─────────────────────────▼───────────────────────────────────┐ │ SpringAI 应用层 │ ├─────────────────────────────────────────────────────────────┤ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 问答API │ │ 文档管理 │ │ 对话管理 │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ ┌──────▼──────────────────▼──────────────────▼───────┐ │ │ │ SpringAI 核心服务层 │ │ │ │ ChatClient │ EmbeddingClient │ VectorStore │ │ │ └──────┬──────────────────────────────────────────────┘ │ │ │ │ │ ┌──────▼──────────────────────────────────────────────┐ │ │ │ 基础设施层 │ │ │ │ PostgreSQL+pgvector │ Redis │ OpenAI API │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘2.2 核心组件职责
| 组件 | 职责 | 技术选型 |
|---|---|---|
| ChatClient | 与大模型对话,生成回答 | SpringAI封装 |
| EmbeddingClient | 文本向量化,支持语义检索 | SpringAI封装 |
| VectorStore | 存储和检索文档向量 | PGVector |
| 对话管理 | 维护多轮对话上下文 | 内存缓存 + Redis |
| 文档处理 | 文档分割、向量化、存储 | 自定义服务 |
三、环境搭建
3.1 项目依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> </parent> <dependencies> <!-- SpringAI OpenAI --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>0.8.1</version> </dependency> <!-- PGVector向量数据库 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId> <version>0.8.1</version> </dependency> <!-- 常规依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> </dependencies>3.2 配置文件
# application.yml spring: datasource: url: jdbc:postgresql://localhost:5432/ai_qa_system username: postgres password: ${DB_PASSWORD} ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-3.5-turbo temperature: 0.7 max-tokens: 2000 embedding: options: model: text-embedding-ada-002 vectorstore: pgvector: index-type: HNSW distance-type: COSINE dimensions: 1536 logging: level: org.springframework.ai: DEBUG3.3 数据库初始化
-- 启用PGVector扩展 CREATE EXTENSION IF NOT EXISTS vector; -- 文档存储表 CREATE TABLE IF NOT EXISTS knowledge_docs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content TEXT NOT NULL, metadata JSONB, embedding vector(1536), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- HNSW索引(余弦相似度) CREATE INDEX IF NOT EXISTS docs_embedding_idx ON knowledge_docs USING hnsw (embedding vector_cosine_ops);四、核心实现
4.1 文档实体与Repository
@Entity @Table(name = "knowledge_docs") @Data @NoArgsConstructor @AllArgsConstructor public class KnowledgeDocument { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; @Column(columnDefinition = "TEXT") private String content; @Column(columnDefinition = "jsonb") private Map<String, Object> metadata; @Column(columnDefinition = "vector(1536)") private float[] embedding; private LocalDateTime createdAt; } @Repository public interface KnowledgeDocumentRepository extends JpaRepository<KnowledgeDocument, UUID> { @Query(value = "SELECT * FROM knowledge_docs ORDER BY embedding <=> cast(:embedding as vector) LIMIT :topK", nativeQuery = true) List<KnowledgeDocument> findSimilarDocuments( @Param("embedding") float[] embedding, @Param("topK") int topK ); }4.2 文档处理服务
@Service @Slf4j public class DocumentProcessingService { @Autowired private EmbeddingClient embeddingClient; @Autowired private KnowledgeDocumentRepository repository; /** * 处理文档:分割 → 向量化 → 存储 */ @Transactional public void processDocument(String content, Map<String, Object> metadata) { // 1. 文档分割(按500字符分块,重叠50字符) List<String> chunks = splitText(content, 500, 50); // 2. 批量向量化 List<List<Double>> embeddings = embeddingClient.embed(chunks); // 3. 存储到向量库 for (int i = 0; i < chunks.size(); i++) { KnowledgeDocument doc = new KnowledgeDocument(); doc.setContent(chunks.get(i)); Map<String, Object> docMeta = new HashMap<>(metadata); docMeta.put("chunk_index", i); docMeta.put("total_chunks", chunks.size()); doc.setMetadata(docMeta); doc.setEmbedding(toFloatArray(embeddings.get(i))); repository.save(doc); } log.info("文档处理完成: {} 个分块", chunks.size()); } /** * 语义检索 */ public List<KnowledgeDocument> search(String query, int topK) { List<Double> queryVector = embeddingClient.embed(query); return repository.findSimilarDocuments(toFloatArray(queryVector), topK); } private List<String> splitText(String text, int chunkSize, int overlap) { // 按句号、换行等自然边界分割 List<String> chunks = new ArrayList<>(); // ... 实现略 return chunks; } private float[] toFloatArray(List<Double> list) { float[] result = new float[list.size()]; for (int i = 0; i < list.size(); i++) { result[i] = list.get(i).floatValue(); } return result; } }4.3 RAG问答服务
@Service @Slf4j public class IntelligentQAService { @Autowired private ChatClient chatClient; @Autowired private DocumentProcessingService documentService; /** * RAG问答:检索 → 增强 → 生成 */ public AnswerResponse ask(String question) { // 1. 检索相关文档片段 List<KnowledgeDocument> docs = documentService.search(question, 5); // 2. 构建增强Prompt String prompt = buildRAGPrompt(question, docs); // 3. 调用大模型生成答案 String answer = chatClient.call(new UserMessage(prompt)); // 4. 返回结果(附引用来源) return AnswerResponse.builder() .question(question) .answer(answer) .sources(docs.stream() .map(d -> d.getMetadata().get("source")) .collect(Collectors.toList())) .build(); } private String buildRAGPrompt(String question, List<KnowledgeDocument> docs) { StringBuilder context = new StringBuilder(); for (int i = 0; i < docs.size(); i++) { context.append(String.format("[参考%d]: %s\n\n", i + 1, docs.get(i).getContent())); } return String.format(""" 你是一个专业的智能助手。请基于以下参考信息回答用户问题。 如果参考信息不足以回答问题,请明确告知用户“根据现有资料无法回答该问题”。 【参考信息】 %s 【用户问题】 %s 请给出准确、简洁的回答: """, context.toString(), question); } }4.4 REST API接口
@RestController @RequestMapping("/api/qa") @Tag(name = "智能问答", description = "基于SpringAI的RAG问答接口") public class QAController { @Autowired private IntelligentQAService qaService; @Autowired private DocumentProcessingService documentService; @PostMapping("/ask") public ResponseEntity<AnswerResponse> ask(@RequestBody @Valid QuestionRequest request) { AnswerResponse response = qaService.ask(request.getQuestion()); return ResponseEntity.ok(response); } @PostMapping("/documents") public ResponseEntity<Void> uploadDocument(@RequestBody DocumentUploadRequest request) { documentService.processDocument(request.getContent(), request.getMetadata()); return ResponseEntity.ok().build(); } }五、高级特性
5.1 多轮对话上下文管理
@Component public class ConversationManager { private final Map<String, List<Message>> sessions = new ConcurrentHashMap<>(); private static final int MAX_HISTORY = 20; public Prompt createContextualPrompt(String sessionId, String userInput) { List<Message> history = sessions.getOrDefault(sessionId, new ArrayList<>()); List<Message> messages = new ArrayList<>(history); messages.add(new UserMessage(userInput)); return new Prompt(messages); } public void appendResponse(String sessionId, String assistantResponse) { sessions.computeIfAbsent(sessionId, k -> new ArrayList<>()) .add(new AssistantMessage(assistantResponse)); // 保持最近N条记录 List<Message> history = sessions.get(sessionId); if (history.size() > MAX_HISTORY) { sessions.put(sessionId, new ArrayList<>(history.subList(history.size() - MAX_HISTORY, history.size()))); } } }5.2 流式输出
@PostMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<String>> askStream(@RequestBody QuestionRequest request) { return chatClient.stream(new UserMessage(request.getQuestion())) .map(chunk -> ServerSentEvent.builder(chunk.getResult().getOutput().getContent()).build()) .onErrorResume(e -> Flux.just(ServerSentEvent.builder("错误: " + e.getMessage()).build())); }5.3 缓存优化
@Service @Slf4j public class CachedQAService { @Autowired private IntelligentQAService qaService; // 相同问题1小时内直接返回缓存结果 @Cacheable(value = "qa_cache", key = "#question", unless = "#result == null") public AnswerResponse getCachedAnswer(String question) { return qaService.ask(question); } }5.4 监控指标
@Component public class AIMetrics { private final MeterRegistry meterRegistry; private final Timer ragTimer; private final Counter errorCounter; public AIMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.ragTimer = Timer.builder("ai.rag.duration") .description("RAG问答耗时") .register(meterRegistry); this.errorCounter = Counter.builder("ai.errors.total") .description("AI调用错误总数") .register(meterRegistry); } public <T> T recordRAG(Supplier<T> supplier) { return ragTimer.record(supplier); } public void recordError(String type) { errorCounter.increment(); meterRegistry.counter("ai.errors", "type", type).increment(); } }六、部署与运维
6.1 Docker镜像
FROM openjdk:17-jdk-slim AS builder WORKDIR /app COPY . . RUN ./mvnw package -DskipTests FROM openjdk:17-jdk-slim WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENV JAVA_OPTS="-Xms512m -Xmx1024m" ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]6.2 Kubernetes部署
apiVersion: apps/v1 kind: Deployment metadata: name: springai-qa spec: replicas: 3 selector: matchLabels: app: springai-qa template: metadata: labels: app: springai-qa spec: containers: - name: app image: springai-qa:latest ports: - containerPort: 8080 env: - name: OPENAI_API_KEY valueFrom: secretKeyRef: name: openai-secret key: api-key resources: requests: memory: "1Gi" cpu: "500m" limits: memory: "2Gi" cpu: "2000m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 30七、总结
本文从零构建了一个企业级智能问答系统,核心要点如下:
| 模块 | 技术选型 | 关键考量 |
|---|---|---|
| AI接入 | SpringAI + OpenAI | 统一抽象,便于切换模型 |
| 向量存储 | PostgreSQL + pgvector | 降低架构复杂度,事务支持 |
| 文档检索 | RAG架构 | 增强回答准确性,减少幻觉 |
| 对话管理 | 内存会话 + 多轮上下文 | 支持连续对话 |
| 性能 | Caffeine缓存 + 异步批处理 | 高并发场景优化 |
| 可观测性 | Micrometer + Prometheus | 实时监控AI调用耗时与错误率 |
SpringAI让Java开发者不再是大模型时代的旁观者。通过这套框架,你可以像写普通Spring Boot应用一样,将AI能力融入企业级系统。
下一步可以拓展的方向:
接入私有化部署模型
引入Agent多智能体协作
增加Rerank模块提升检索精度
支持多模态文档(PDF、Word、图片)
希望本文能成为你进入SpringAI世界的实战地图。
