基于SpringAI与PGVector构建企业级RAG知识库问答系统全栈实战
最近在帮一家企业搭建内部知识库问答系统时,深刻体会到从零到一整合AI能力的挑战。网上资料要么是纯前端展示,要么是后端API调用,完整涵盖文档处理、向量存储、RAG检索和大模型集成的全栈实战教程非常稀缺。本文将手把手带你构建一个基于SpringAI + SpringBoot + Vue + RAG + PGVector + Embedding的企业级智能问答系统。无论你是想快速了解RAG落地流程的Java后端,还是希望在前端集成AI对话的Vue开发者,都能从本文获得一套可复现的完整方案。
1. 项目背景与核心概念解析
在深入代码之前,我们有必要厘清几个核心概念,理解它们如何协同工作,构成一个智能问答系统。
1.1 什么是RAG(检索增强生成)?
RAG(Retrieval-Augmented Generation)是当前构建企业知识库问答系统的核心技术范式。它的核心思想是:先检索,再生成。
传统的大语言模型(LLM)仅依赖其训练时学到的“世界知识”来回答问题,这会导致两个严重问题:
- 知识滞后:模型无法知晓训练数据截止日期之后的新信息或企业内部私有数据。
- 事实性“幻觉”:模型可能会编造看似合理但完全错误的答案。
RAG通过引入一个“外部知识库”来解决这些问题。其工作流程可以简化为三步:
- 索引:将企业内部的非结构化文档(如PDF、Word、TXT)通过Embedding模型转化为向量,并存储到向量数据库中。
- 检索:当用户提问时,将问题同样转化为向量,并在向量数据库中进行相似度搜索,找出与问题最相关的文档片段。
- 增强生成:将检索到的相关文档片段作为“上下文”或“参考依据”,连同用户问题一起提交给大语言模型,指令模型基于这些给定的上下文来生成答案。
这样一来,答案的准确性和事实性得到了极大保障,因为模型是在“引用”你提供的材料说话。
1.2 技术栈选型与角色
我们的全栈系统技术栈分工明确:
- SpringBoot + SpringAI (后端):作为系统的“大脑”和“调度中心”。
SpringBoot:提供稳健的Web服务、依赖管理和项目骨架。SpringAI:一个新兴的Spring官方项目,它抽象了不同AI供应商(OpenAI、Azure OpenAI、Ollama等)的API,提供了统一的编程接口,极大简化了AI功能的集成。我们将用它来调用Embedding模型和Chat大模型。
- PGVector + PostgreSQL (数据层):作为系统的“长期记忆”。
PostgreSQL:成熟稳定的关系型数据库。PGVector:PostgreSQL的一个扩展,使其具备了存储和高效查询向量数据的能力。相比独立的向量数据库(如Milvus),PGVector与现有技术栈集成更简单,运维成本更低。
- Vue 3 (前端):作为系统的“交互界面”。
- 构建一个简洁、现代的聊天界面,用于提问和展示流式回答。
- Embedding 模型:作为系统的“理解器”。
- 负责将文本(无论是文档还是问题)转化为数学向量(一组数字)。我们选用开源的
BAAI/bge-small-zh-v1.5模型,它对中文语义理解效果好,且可以本地部署。
- 负责将文本(无论是文档还是问题)转化为数学向量(一组数字)。我们选用开源的
整个系统的数据流如下图所示(概念示意):
用户提问 -> Vue前端 -> SpringBoot后端 -> (问题文本) -> Embedding模型 -> (问题向量) -> PGVector向量数据库 -> (检索相似文档片段) -> 组合“上下文+问题” -> Chat大模型 (如Qwen) -> 生成答案 -> 流式返回 -> Vue前端渲染2. 环境准备与项目初始化
“工欲善其事,必先利其器”。在开始编码前,请确保你的开发环境已就绪。
2.1 基础环境与工具
- 操作系统:Windows 10/11, macOS 或 Linux (本文以Mac/Linux命令为例,Windows用户请使用PowerShell或WSL)。
- Java:JDK 17 或更高版本。SpringAI目前对JDK 17+支持最好。
- Maven:3.6+ 或Gradle7.x+,用于项目管理。本文使用Maven。
- Node.js:18+ 和 npm / yarn,用于构建Vue前端。
- IDE:IntelliJ IDEA (推荐) 或 VS Code。
- Docker & Docker Compose:强烈推荐使用Docker来部署PostgreSQL和PGVector,避免繁琐的环境配置。
2.2 后端项目初始化
使用 Spring Initializr 或IDE的创建向导,生成一个SpringBoot项目。
关键依赖选择:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.x (确保是3.x版本)
- Packaging: Jar
- Java: 17
- Dependencies:
Spring Web- 构建Web APISpring Data JPA- 操作数据库(用于存储元数据)PostgreSQL Driver- 连接PostgreSQLLombok- 简化POJO代码(可选但推荐)
生成项目后,在pom.xml中手动添加SpringAI的依赖。由于SpringAI仍在快速发展,建议使用其官方提供的BOM(物料清单)来管理版本,避免冲突。
<!-- 在 pom.xml 的 <project> 标签下添加 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>0.8.1</version> <!-- 请查看SpringAI官网获取最新稳定版 --> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- 然后在 <dependencies> 中添加具体的starter --> <dependencies> <!-- ... 其他初始依赖 ... --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <!-- 版本由上面的BOM管理 --> </dependency> <!-- 如果你计划使用Ollama本地模型,可以添加这个 --> <!-- <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency> --> </dependencies>2.3 数据库环境搭建 (使用Docker)
创建docker-compose.yml文件,一键启动带PGVector扩展的PostgreSQL。
# docker-compose.yml version: '3.8' services: postgres: image: ankane/pgvector:latest # 这个镜像已包含pgvector扩展 container_name: ai-knowledge-pg environment: POSTGRES_DB: ai_knowledge POSTGRES_USER: admin POSTGRES_PASSWORD: admin123 ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data restart: unless-stopped volumes: postgres_data:在项目根目录下运行命令启动数据库:
docker-compose up -d使用数据库连接工具(如DBeaver、pgAdmin)连接localhost:5432,数据库ai_knowledge,验证PGVector扩展是否启用:
SELECT * FROM pg_extension WHERE extname = 'vector';2.4 前端项目初始化
使用Vue CLI或Vite快速创建一个Vue 3项目。
# 使用Vite (推荐) npm create vue@latest ai-knowledge-frontend # 按照提示选择项目配置,建议添加 TypeScript 和 Router cd ai-knowledge-frontend npm install # 安装UI组件库,这里以Element Plus为例 npm install element-plus @element-plus/icons-vue # 安装axios用于HTTP请求 npm install axios3. 核心模块设计与实现
我们将系统拆解为几个核心模块,逐个击破。
3.1 数据模型与向量存储设计
首先,在后端定义我们的核心领域模型。我们需要一个实体来代表“文档片段”,它包含原始文本、对应的向量以及一些元数据。
// File: src/main/java/com/example/aiknowledge/entity/DocumentChunk.java package com.example.aiknowledge.entity; import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; import java.time.LocalDateTime; import java.util.List; @Entity @Table(name = "document_chunk") @Data public class DocumentChunk { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(columnDefinition = "text") private String originalText; // 原始的文本片段 @Column(columnDefinition = "vector(768)") // 假设我们的Embedding模型输出768维向量 @JdbcTypeCode(SqlTypes.VECTOR) private List<Float> embedding; // 存储向量,PGVector的特定类型 private String sourceFileName; // 来源文件名 private Integer chunkIndex; // 在文件中的片段序号 private LocalDateTime createdAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); } }注意:@JdbcTypeCode(SqlTypes.VECTOR)和columnDefinition = "vector(768)"是Hibernate与PGVector交互的关键。768需要与你选用的Embedding模型的输出维度一致。
对应的数据库表初始化SQL(可由JPA自动生成,但了解其结构很重要):
CREATE TABLE document_chunk ( id BIGSERIAL PRIMARY KEY, original_text TEXT, embedding VECTOR(768), source_file_name VARCHAR(255), chunk_index INT, created_at TIMESTAMP ); -- 创建向量索引以加速相似度搜索 CREATE INDEX ON document_chunk USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);3.2 文档解析与向量化服务
这是RAG的“索引”阶段。我们需要一个服务来读取上传的文档(如PDF),将其拆分成有意义的片段(Chunk),然后调用Embedding模型为每个片段生成向量,最后存入数据库。
步骤1:添加文档处理依赖处理PDF等格式需要额外库,例如Apache PDFBox。
<!-- pom.xml --> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>3.0.0</version> </dependency>步骤2:实现文档解析与分块逻辑
// File: src/main/java/com/example/aiknowledge/service/DocumentProcessingService.java package com.example.aiknowledge.service; import com.example.aiknowledge.entity.DocumentChunk; import com.example.aiknowledge.repository.DocumentChunkRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.reader.TextReader; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.List; import java.util.stream.Collectors; @Service @Slf4j @RequiredArgsConstructor public class DocumentProcessingService { private final EmbeddingModel embeddingModel; // SpringAI自动注入 private final DocumentChunkRepository chunkRepository; /** * 处理上传的PDF文件 */ public void processPdfFile(MultipartFile file, String fileName) throws Exception { // 1. 提取PDF文本 String fullText; try (InputStream is = file.getInputStream(); PDDocument document = PDDocument.load(is)) { PDFTextStripper stripper = new PDFTextStripper(); fullText = stripper.getText(document); } // 2. 文本分块 (使用SpringAI提供的分块器) TokenTextSplitter splitter = new TokenTextSplitter(500, 100, 10, 1000); // 参数:块大小、重叠、最小块等 List<Document> textChunks = splitter.split(new org.springframework.ai.document.Document(fullText)); // 3. 为每个文本块生成向量并保存 for (int i = 0; i < textChunks.size(); i++) { String chunkText = textChunks.get(i).getContent(); // 调用Embedding模型 List<Float> embeddingVector = embeddingModel.embed(chunkText); DocumentChunk chunk = new DocumentChunk(); chunk.setOriginalText(chunkText); chunk.setEmbedding(embeddingVector); chunk.setSourceFileName(fileName); chunk.setChunkIndex(i); chunkRepository.save(chunk); log.info("已保存文档片段 {},长度: {}", i, chunkText.length()); } log.info("文件 {} 处理完成,共 {} 个片段", fileName, textChunks.size()); } }关键点解释:
TokenTextSplitter:SpringAI提供的智能文本分割器,按Token数分割,并保留一定的重叠部分,避免语义被硬切断。embeddingModel.embed():这是SpringAI抽象层的方法,无论底层是OpenAI、Azure还是本地Ollama模型,调用方式一致。我们需要在配置中指定使用哪个模型。
步骤3:配置SpringAI使用本地Embedding模型我们配置SpringAI使用通过Ollama运行的本地Embedding模型,以节省成本并保证数据隐私。
在application.yml中配置:
# application.yml spring: ai: ollama: base-url: http://localhost:11434 # Ollama服务地址 embedding: enabled: true model: bge-small-zh-v1.5 # 使用的Embedding模型名称,需先在Ollama中pull openai: # 如果使用OpenAI的Embedding,在这里配置api-key和base-url # 但本例使用本地Ollama,所以不需要配置 chat: enabled: false # 暂时禁用OpenAI的Chat,我们用另一个模型 # 数据库配置 datasource: url: jdbc:postgresql://localhost:5432/ai_knowledge username: admin password: admin123 driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect jdbc: batch_size: 20注意:你需要先安装并运行 Ollama ,然后拉取对应的Embedding模型:
ollama pull bge-small-zh-v1.5 ollama serve # 启动服务,默认端口114343.3 向量检索与RAG问答服务
这是RAG的“检索”与“生成”阶段。当用户提问时,服务需要检索相关文档,并调用大模型生成答案。
步骤1:实现向量相似度检索Spring Data JPA本身不支持向量查询,我们需要使用原生SQL或JPA的@Query注解。
首先,在Repository中定义自定义查询方法:
// File: src/main/java/com/example/aiknowledge/repository/DocumentChunkRepository.java package com.example.aiknowledge.repository; import com.example.aiknowledge.entity.DocumentChunk; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface DocumentChunkRepository extends JpaRepository<DocumentChunk, Long> { /** * 使用PGVector的余弦相似度运算符 <=> 进行搜索 * 按相似度降序排列,返回最相关的topK个结果 * :embedding 是一个Float数组参数 */ @Query(value = "SELECT * FROM document_chunk ORDER BY embedding <=> CAST(:embedding AS vector) LIMIT :topK", nativeQuery = true) List<DocumentChunk> findTopKSimilar(@Param("embedding") List<Float> embedding, @Param("topK") int topK); }步骤2:构建RAG问答链
// File: src/main/java/com/example/aiknowledge/service/RagQAService.java package com.example.aiknowledge.service; import com.example.aiknowledge.entity.DocumentChunk; import com.example.aiknowledge.repository.DocumentChunkRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Service @Slf4j @RequiredArgsConstructor public class RagQAService { private final EmbeddingModel embeddingModel; private final DocumentChunkRepository chunkRepository; private final ChatClient chatClient; // 用于对话的ChatClient /** * RAG问答核心方法 * @param userQuestion 用户问题 * @param topK 检索最相关的文档片段数量 * @return 大模型生成的答案 */ public String answerQuestion(String userQuestion, int topK) { // 1. 将用户问题转化为向量 List<Float> questionEmbedding = embeddingModel.embed(userQuestion); // 2. 向量检索:从数据库中找到最相关的文档片段 List<DocumentChunk> relevantChunks = chunkRepository.findTopKSimilar(questionEmbedding, topK); String context = relevantChunks.stream() .map(DocumentChunk::getOriginalText) .collect(Collectors.joining("\n\n---\n\n")); log.info("检索到 {} 个相关片段,上下文长度: {}", relevantChunks.size(), context.length()); // 3. 构建Prompt模板,将上下文和问题组合 // 这是一个非常关键的Prompt,它指导模型如何利用上下文 String promptTemplate = """ 你是一个专业的企业知识库助手,请严格根据以下提供的上下文信息来回答问题。 如果上下文中的信息不足以回答问题,请直接说“根据现有资料,我无法回答这个问题”,不要编造信息。 上下文信息: {context} 用户问题:{question} 请基于上下文信息,给出准确、简洁的回答: """; PromptTemplate template = new PromptTemplate(promptTemplate); Map<String, Object> variables = new HashMap<>(); variables.put("context", context); variables.put("question", userQuestion); Prompt prompt = template.create(variables); // 4. 调用大语言模型生成答案 ChatResponse response = chatClient.prompt(prompt).call().chatResponse(); String answer = response.getResult().getOutput().getContent(); return answer; } /** * 流式问答(用于前端实时显示) * 返回一个Flux<String>流,这里简化处理,实际应使用ChatClient的流式调用 */ public String answerQuestionStream(String userQuestion, int topK) { // 实现逻辑与非流式类似,但调用 chatClient.prompt(prompt).stream()... // 为了简化示例,此处返回非流式结果。实际项目强烈建议实现流式响应。 return answerQuestion(userQuestion, topK); } }步骤3:配置Chat大模型我们同样使用Ollama本地运行的Qwen模型进行对话。在application.yml中补充配置:
# application.yml (续) spring: ai: ollama: base-url: http://localhost:11434 chat: enabled: true model: qwen2.5:7b # 或其他你喜欢的模型,如llama3.2, mistral等拉取并运行Qwen模型:
ollama pull qwen2.5:7b3.4 构建RESTful API控制器
现在,我们将服务暴露给前端调用。
// File: src/main/java/com/example/aiknowledge/controller/KnowledgeController.java package com.example.aiknowledge.controller; import com.example.aiknowledge.service.DocumentProcessingService; import com.example.aiknowledge.service.RagQAService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/knowledge") @RequiredArgsConstructor public class KnowledgeController { private final DocumentProcessingService documentService; private final RagQAService ragService; @PostMapping("/upload") public ResponseEntity<String> uploadDocument(@RequestParam("file") MultipartFile file) { try { String fileName = file.getOriginalFilename(); documentService.processPdfFile(file, fileName); return ResponseEntity.ok("文档 '" + fileName + "' 上传并处理成功!"); } catch (Exception e) { e.printStackTrace(); return ResponseEntity.internalServerError().body("文档处理失败: " + e.getMessage()); } } @PostMapping("/ask") public ResponseEntity<String> askQuestion(@RequestBody QuestionRequest request) { try { // 默认检索最相关的5个片段 String answer = ragService.answerQuestion(request.getQuestion(), 5); return ResponseEntity.ok(answer); } catch (Exception e) { e.printStackTrace(); return ResponseEntity.internalServerError().body("系统处理问题时出错: " + e.getMessage()); } } // 用于接收提问的请求体 public static class QuestionRequest { private String question; // getter and setter public String getQuestion() { return question; } public void setQuestion(String question) { this.question = question; } } }3.5 前端Vue 3界面开发
前端主要负责提供一个简洁的文件上传区和聊天界面。
核心组件ChatView.vue:
<!-- File: src/views/ChatView.vue --> <template> <div class="chat-container"> <el-upload class="upload-demo" drag action="#" :auto-upload="false" :on-change="handleFileChange" :show-file-list="false" accept=".pdf" > <el-icon class="el-icon--upload"><upload-filled /></el-icon> <div class="el-upload__text">拖拽PDF文件到此处,或<em>点击上传</em></div> <template #tip> <div class="el-upload__tip">仅支持上传PDF文件构建知识库</div> </template> </el-upload> <div v-if="uploadStatus" class="status-info">{{ uploadStatus }}</div> <el-divider /> <div class="chat-area"> <div class="message-list"> <div v-for="(msg, index) in messages" :key="index" :class="['message', msg.role]"> <div class="avatar">{{ msg.role === 'user' ? '你' : 'AI' }}</div> <div class="bubble">{{ msg.content }}</div> </div> <div v-if="loading" class="message assistant"> <div class="avatar">AI</div> <div class="bubble"><el-icon class="is-loading"><Loading /></el-icon> 思考中...</div> </div> </div> <div class="input-area"> <el-input v-model="inputQuestion" type="textarea" :rows="3" placeholder="请输入关于知识库的问题..." @keyup.enter.exact="handleAsk" /> <el-button type="primary" @click="handleAsk" :loading="loading">发送</el-button> </div> </div> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import { ElMessage, ElLoading } from 'element-plus' import { UploadFilled, Loading } from '@element-plus/icons-vue' import axios from 'axios' const API_BASE = 'http://localhost:8080/api/knowledge' interface Message { role: 'user' | 'assistant' content: string } const inputQuestion = ref('') const messages = ref<Message[]>([]) const loading = ref(false) const uploadStatus = ref('') const handleFileChange = async (file: any) => { const formData = new FormData() formData.append('file', file.raw) uploadStatus.value = `正在上传 ${file.name}...` try { const response = await axios.post(`${API_BASE}/upload`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) uploadStatus.value = response.data ElMessage.success('文档处理成功!') } catch (error) { console.error('上传失败:', error) uploadStatus.value = '上传失败' ElMessage.error('文档处理失败,请检查控制台日志。') } } const handleAsk = async () => { const question = inputQuestion.value.trim() if (!question) return if (loading.value) return // 添加用户消息 messages.value.push({ role: 'user', content: question }) inputQuestion.value = '' loading.value = true try { const response = await axios.post(`${API_BASE}/ask`, { question }) // 添加AI回复 messages.value.push({ role: 'assistant', content: response.data }) } catch (error) { console.error('提问失败:', error) ElMessage.error('提问失败,请检查后端服务。') messages.value.push({ role: 'assistant', content: '抱歉,我暂时无法回答这个问题。' }) } finally { loading.value = false } } </script> <style scoped> .chat-container { max-width: 800px; margin: 20px auto; padding: 20px; } .upload-demo { margin-bottom: 20px; } .status-info { margin-top: 10px; color: #67c23a; text-align: center; } .chat-area { border: 1px solid #ebeef5; border-radius: 4px; padding: 20px; } .message-list { min-height: 400px; max-height: 500px; overflow-y: auto; margin-bottom: 20px; } .message { display: flex; margin-bottom: 15px; } .message.user { flex-direction: row-reverse; } .message.user .bubble { background-color: #409eff; color: white; } .avatar { width: 40px; height: 40px; border-radius: 50%; background: #f0f2f5; display: flex; align-items: center; justify-content: center; margin: 0 10px; font-weight: bold; } .bubble { max-width: 70%; padding: 10px 15px; border-radius: 18px; background-color: #f0f2f5; word-break: break-word; } .input-area { display: flex; gap: 10px; } </style>配置路由和主应用:
// File: src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' import ChatView from '../views/ChatView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'chat', component: ChatView } ] }) export default router<!-- File: src/App.vue --> <template> <div id="app"> <router-view /> </div> </template>4. 系统联调与运行
4.1 启动顺序与验证
- 启动向量数据库:在项目根目录运行
docker-compose up -d。 - 启动Ollama服务:确保Ollama服务在运行,并且已拉取
bge-small-zh-v1.5和qwen2.5:7b模型。 - 启动SpringBoot后端:在IDE中运行
AiKnowledgeApplication主类,或使用命令mvn spring-boot:run。观察控制台,确保无报错,JPA成功创建表。 - 启动Vue前端:进入
ai-knowledge-frontend目录,运行npm run dev。 - 访问系统:打开浏览器,访问
http://localhost:5173(Vite默认端口)。
4.2 完整操作流程测试
- 上传知识文档:在前端页面,拖拽一个PDF文件(如公司产品手册、技术规范)进行上传。观察后端控制台日志,应显示文档解析和向量存储的过程。
- 进行问答测试:在输入框提问,例如“我们公司的主要产品是什么?”或“根据文档,项目开发的流程有哪些步骤?”。系统应能返回基于上传文档内容的准确答案。
- 测试未知问题:提问一个文档中完全没有涉及的问题,例如“明天的天气怎么样?”。系统应回复“根据现有资料,我无法回答这个问题”或类似的表述,而不是胡编乱造。
5. 常见问题与排查思路
在搭建和运行过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
后端启动失败,提示BeanCreationException | SpringAI依赖冲突或配置缺失。 | 1. 检查pom.xml中SpringAI BOM和starter版本是否兼容。2. 确保 application.yml中正确配置了spring.ai.ollama.base-url。3. 确认Ollama服务已启动 ( curl http://localhost:11434/api/tags)。 |
上传PDF后,后台报NoSuchBeanDefinitionException: EmbeddingModel | Embedding模型未正确启用或注入。 | 1. 在application.yml中确认spring.ai.ollama.embedding.enabled=true。2. 检查是否错误地引入了多个AI供应商的starter导致冲突,可尝试暂时注释掉其他。 |
提问后返回错误,日志显示PSQLException: 运算符不存在: vector <=> record | 向量维度不匹配或PGVector扩展未启用。 | 1. 确认DocumentChunk实体中@Column(columnDefinition = "vector(768)")的维度与模型输出一致。2. 在数据库执行 SELECT * FROM pg_extension WHERE extname = 'vector';确认扩展已安装。3. 检查JPA是否成功创建了带 vector类型的表。 |
| 前端调用API报跨域错误 (CORS) | 后端未配置允许前端域名的跨域请求。 | 在后端添加一个CORS配置类:java <br>@Configuration <br>public class CorsConfig implements WebMvcConfigurer { <br> @Override <br> public void addCorsMappings(CorsRegistry registry) { <br> registry.addMapping("/api/**") <br> .allowedOrigins("http://localhost:5173") <br> .allowedMethods("*") <br> .allowCredentials(true); <br> } <br>} <br> |
| 问答响应速度很慢 | 1. 本地模型首次加载。 2. 向量检索未建索引。 3. 上下文过长。 | 1. 首次调用需等待模型加载,后续会快。 2. 为 document_chunk表的embedding列创建向量索引(见3.1节SQL)。3. 调整 TokenTextSplitter参数和检索的topK值,避免上下文过长。 |
| 答案质量不高,答非所问 | 1. 文档分块不合理。 2. 检索的topK值不合适。 3. Prompt指令不够清晰。 | 1. 优化分块策略(如按段落、按标题),调整块大小和重叠。 2. 尝试调整 topK值(如3, 5, 10)。3. 精心设计Prompt,明确指令模型“严格基于上下文”。 |
6. 进阶优化与最佳实践
一个可用的Demo只是起点,要投入生产环境,还需要考虑以下方面:
6.1 性能优化
- 向量索引优化:PGVector默认的
ivfflat索引需要足够的数据量才能有效。对于大规模数据,考虑使用hnsw索引(如果PGVector版本支持)或评估专业的向量数据库如Milvus、Weaviate。 - 异步处理:文档解析和向量化是CPU/IO密集型操作,应改为异步任务(如使用Spring
@Async),避免阻塞HTTP请求。 - 缓存策略:对常见问题及答案可以引入缓存(如Redis),减少重复的向量检索和模型调用。
6.2 效果提升
- 混合检索:结合向量检索(语义相似)和关键词检索(BM25),提升召回率。可以使用SpringAI的
VectorStore和KeywordVectorStore组合。 - 重排序:初步检索出较多结果(如top 20)后,使用一个更轻量的交叉编码器模型对结果进行重排序,选出最相关的top 5,提升精度。
- 元数据过滤:在检索时,除了向量相似度,还可以加入来源、日期等元数据过滤条件。
- Prompt工程:不断优化系统Prompt,可以要求模型在答案中引用来源片段,增加可信度。
6.3 工程化与可维护性
- 配置外部化:将模型名称、topK值、分块大小等参数移至
application.yml或配置中心。 - 健康检查与监控:为Ollama服务、数据库连接添加健康检查端点。监控API响应时间、错误率。
- 文件格式支持:扩展
DocumentProcessingService,支持Word、Excel、PPT、TXT、Markdown等多种格式。 - 对话历史:在前端和后端维护对话历史,实现多轮对话上下文。
- 权限控制:为知识库增加简单的权限管理,例如不同部门只能访问上传的特定文档。
6.4 安全与成本
- 数据隐私:使用本地化模型(如Ollama)是保障企业内部数据不出域的最佳方式。如果必须使用云端API,需评估供应商的数据合规协议。
- 输入输出检查:对用户上传的文件进行病毒扫描、大小和类型限制。对模型的输出内容进行必要的安全过滤。
- 成本控制:如果使用按Token计费的云API,需要在代码层面估算Token消耗,并设置用量告警。
通过以上步骤,你已经成功搭建了一个功能完整、架构清晰的企业级AI知识库问答系统。这个项目涵盖了从文档处理、向量存储、语义检索到AI生成的全链路,是学习RAG和SpringAI的绝佳实践。你可以在此基础上,根据上述优化建议,逐步将其打磨成一个更健壮、更智能的生产级应用。