当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 项目依赖

<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: DEBUG

3.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世界的实战地图。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐