SpringAI全流程实战手册

当Java开发者遇上大模型,会擦出怎样的火花?SpringAI给出了答案,用Spring的方式,把AI能力变成业务开发的常规武器。

本文带你从零开始,构建一个企业级智能问答系统,涵盖RAG架构、向量数据库、对话上下文、异步处理、监控告警等完整链路,最终产出一套可部署到K8s的生产级代码。

一、为什么Java开发者需要SpringAI?

在企业级AI应用开发领域,Python一度是绝对的主角,LangChain、LlamaIndex等框架生态丰富,Java开发者只能望洋兴叹。

SpringAI的出现改变了这一格局。作为Spring官方出品的AI集成框架,它延续了Spring"约定优于配置"的哲学,让Java开发者能够以熟悉的Spring风格接入OpenAI、Azure、Ollama等大语言模型。

SpringAI的核心价值:

统一抽象:ChatClientEmbeddingClient等接口屏蔽了不同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 项目依赖

XML 复制代码
<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 配置文件

XML 复制代码
# 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: DEBUG

3.3 数据库初始化

sql 复制代码
-- 启用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

java 复制代码
@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 文档处理服务

java 复制代码
@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问答服务

java 复制代码
@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接口

java 复制代码
@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 多轮对话上下文管理

java 复制代码
@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 流式输出

java 复制代码
@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 缓存优化

java 复制代码
@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 监控指标

java 复制代码
@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镜像

java 复制代码
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部署

XML 复制代码
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世界的实战地图。

相关推荐
莫逸风2 天前
【AgentScope】1. HarnessAgent 总览详解
springai·agentscope·agnet
Maiko Star3 天前
理解 RAG 的“为什么”与 Spring AI 实战初体验
人工智能·rag·springai
Maiko Star3 天前
SpringAI 模型 API 调用中的错误处理、重试与熔断降级实战
错误处理·springai
装不满的克莱因瓶6 天前
SpringAI Alibaba Tool工具调用机制实战-注解注册与函数调用全流程
人工智能·ai·tools·智能体·springai·tool
小当家.1058 天前
Spring AI vs LangChain4j:Java 后端接大模型,两条路线怎么选
java·人工智能·spring·langchain·springai
装不满的克莱因瓶10 天前
新版AI开发框架SpringAIAlibaba vs AgentScope 选型指南
java·开发语言·人工智能·ai·agent·alibaba·springai
奋斗的老史16 天前
Spring AI + Docling 企业级文档解析完全指南
springai·langchain4j·ai应用开发
Maiko Star16 天前
* SpringAI多模型共存指南(如何配置多模型)
人工智能·springai
架构源启17 天前
Spring AI 进阶系列- Agent 智能体开发:ReAct模式、多步推理与自主Agent实战
人工智能·spring·react·ai agent·智能体·springai
奋斗的老史19 天前
基于SpringAI开发的通用RAG脚手框架,适配各种场景
springai·ai应用开发