SpringAI全流程实战手册
本文带你从零开始,构建一个企业级智能问答系统,涵盖RAG架构、向量数据库、对话上下文、异步处理、监控告警等完整链路,最终产出一套可部署到K8s的生产级代码。
当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: 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世界的实战地图。
更多推荐


所有评论(0)