当前位置: 首页 > news >正文

基于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)仅依赖其训练时学到的“世界知识”来回答问题,这会导致两个严重问题:

  1. 知识滞后:模型无法知晓训练数据截止日期之后的新信息或企业内部私有数据。
  2. 事实性“幻觉”:模型可能会编造看似合理但完全错误的答案。

RAG通过引入一个“外部知识库”来解决这些问题。其工作流程可以简化为三步:

  1. 索引:将企业内部的非结构化文档(如PDF、Word、TXT)通过Embedding模型转化为向量,并存储到向量数据库中。
  2. 检索:当用户提问时,将问题同样转化为向量,并在向量数据库中进行相似度搜索,找出与问题最相关的文档片段。
  3. 增强生成:将检索到的相关文档片段作为“上下文”或“参考依据”,连同用户问题一起提交给大语言模型,指令模型基于这些给定的上下文来生成答案。

这样一来,答案的准确性和事实性得到了极大保障,因为模型是在“引用”你提供的材料说话。

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 API
    • Spring Data JPA- 操作数据库(用于存储元数据)
    • PostgreSQL Driver- 连接PostgreSQL
    • Lombok- 简化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 axios

3. 核心模块设计与实现

我们将系统拆解为几个核心模块,逐个击破。

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 # 启动服务,默认端口11434

3.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:7b

3.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 启动顺序与验证

  1. 启动向量数据库:在项目根目录运行docker-compose up -d
  2. 启动Ollama服务:确保Ollama服务在运行,并且已拉取bge-small-zh-v1.5qwen2.5:7b模型。
  3. 启动SpringBoot后端:在IDE中运行AiKnowledgeApplication主类,或使用命令mvn spring-boot:run。观察控制台,确保无报错,JPA成功创建表。
  4. 启动Vue前端:进入ai-knowledge-frontend目录,运行npm run dev
  5. 访问系统:打开浏览器,访问http://localhost:5173(Vite默认端口)。

4.2 完整操作流程测试

  1. 上传知识文档:在前端页面,拖拽一个PDF文件(如公司产品手册、技术规范)进行上传。观察后端控制台日志,应显示文档解析和向量存储的过程。
  2. 进行问答测试:在输入框提问,例如“我们公司的主要产品是什么?”或“根据文档,项目开发的流程有哪些步骤?”。系统应能返回基于上传文档内容的准确答案。
  3. 测试未知问题:提问一个文档中完全没有涉及的问题,例如“明天的天气怎么样?”。系统应回复“根据现有资料,我无法回答这个问题”或类似的表述,而不是胡编乱造。

5. 常见问题与排查思路

在搭建和运行过程中,你可能会遇到以下典型问题:

问题现象可能原因排查思路与解决方案
后端启动失败,提示BeanCreationExceptionSpringAI依赖冲突或配置缺失。1. 检查pom.xml中SpringAI BOM和starter版本是否兼容。
2. 确保application.yml中正确配置了spring.ai.ollama.base-url
3. 确认Ollama服务已启动 (curl http://localhost:11434/api/tags)。
上传PDF后,后台报NoSuchBeanDefinitionException: EmbeddingModelEmbedding模型未正确启用或注入。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的VectorStoreKeywordVectorStore组合。
  • 重排序:初步检索出较多结果(如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的绝佳实践。你可以在此基础上,根据上述优化建议,逐步将其打磨成一个更健壮、更智能的生产级应用。

http://www.jsqmd.com/news/1358732/

相关文章:

  • SpringBoot3+React18+MySQL 游戏攻略平台源码 前后端分离实战
  • zteOnu终极指南:三步解锁中兴光猫隐藏功能,获得完全控制权
  • 天津古驰包包回收就来毓典奢品汇15369396611闲置贵重物品回收指南 - mazhaoyun11
  • HexEdit:Windows平台开源十六进制编辑器完整使用指南
  • 廉江找电子厂普工完整避坑攻略(躲开所有中介套路) - 资讯报道
  • ComfyUI-VideoHelperSuite视频合成终极指南:告别单帧输出的烦恼
  • 用于自动驾驶和信号管理的多类别鸟瞰图道路场景数据集
  • React Native动画在OpenHarmony中的优化实践
  • 墓碑自动磨生产商十大品牌深度测评,2026口碑实力榜,避坑指南价格透明 - mypinpai
  • Python新手如何通过结构化练习从看懂到会写?刻意练习路径全解析
  • Windows平台SRS流媒体服务器实战指南:从零搭建到高性能部署
  • 2026年近期苏州GEO优化公司做得比较好的品牌有哪些精选推荐 - 招财兔数字员工
  • Windows服务依赖链断裂:深度解析DNS Client错误1075的排查与修复
  • 2026山东推荐打捆包膜一体机厂家口碑强势出炉,零套路不踩坑,价格透明 - 工业品牌热点
  • 如何将Android手机变成万能USB键盘鼠标:3分钟搞定BIOS/UEFI输入难题
  • Unity引擎AI集成实战:从Barracuda到智能NPC开发
  • Unity UI进阶:UI文本的富文本设置与应用
  • 3步解锁全球化开发:translate.js 网页自动翻译的架构革命
  • 从Spring Boot工程实践出发,打造高性能、高可用的冠军级应用
  • 从零开始:用KLayout开启你的免费版图设计之旅
  • 2026飞达机车销售行业格局解读,实力测评避坑指南 - mypinpai
  • 找回消失的青春记忆:GetQzonehistory帮你完整备份QQ空间历史数据
  • 那是一个不眠之夜:我的四次 Ubuntu 系统安装回忆录
  • 《轧辊测量误差0.02mm到0.005mm的智能升级实践》
  • 端侧AI落地进行时:一个AI硬件产品经理的实战思考
  • Meta Muse Code 终端编程智能体:本地部署、核心能力与实战指南
  • 2026微小型无人机行业应用选型与落地实践白皮书 - 招财兔数字员工
  • C++20协程编程:原理、实现与性能优化
  • 网盘直链下载助手:八大网盘免费高速下载完整指南
  • PyTorch入门避坑指南:掌握3个核心概念与实操技巧,快速构建首个深度学习模型