
最近在帮一家企业搭建内部知识库问答系统时深刻体会到从零到一整合AI能力的挑战。网上资料要么是纯前端展示要么是后端API调用完整涵盖文档处理、向量存储、RAG检索和大模型集成的全栈实战教程非常稀缺。本文将手把手带你构建一个基于SpringAI SpringBoot Vue RAG PGVector Embedding的企业级智能问答系统。无论你是想快速了解RAG落地流程的Java后端还是希望在前端集成AI对话的Vue开发者都能从本文获得一套可复现的完整方案。1. 项目背景与核心概念解析在深入代码之前我们有必要厘清几个核心概念理解它们如何协同工作构成一个智能问答系统。1.1 什么是RAG检索增强生成RAGRetrieval-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成熟稳定的关系型数据库。PGVectorPostgreSQL的一个扩展使其具备了存储和高效查询向量数据的能力。相比独立的向量数据库如MilvusPGVector与现有技术栈集成更简单运维成本更低。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)。JavaJDK 17 或更高版本。SpringAI目前对JDK 17支持最好。Maven3.6 或Gradle7.x用于项目管理。本文使用Maven。Node.js18 和 npm / yarn用于构建Vue前端。IDEIntelliJ IDEA (推荐) 或 VS Code。Docker Docker Compose强烈推荐使用Docker来部署PostgreSQL和PGVector避免繁琐的环境配置。2.2 后端项目初始化使用 Spring Initializr 或IDE的创建向导生成一个SpringBoot项目。关键依赖选择Project: MavenLanguage: JavaSpring Boot: 3.2.x (确保是3.x版本)Packaging: JarJava: 17Dependencies:Spring Web- 构建Web APISpring Data JPA- 操作数据库用于存储元数据PostgreSQL Driver- 连接PostgreSQLLombok- 简化POJO代码可选但推荐生成项目后在pom.xml中手动添加SpringAI的依赖。由于SpringAI仍在快速发展建议使用其官方提供的BOM物料清单来管理版本避免冲突。!-- 在 pom.xml 的 project 标签下添加 -- dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version0.8.1/version !-- 请查看SpringAI官网获取最新稳定版 -- typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement !-- 然后在 dependencies 中添加具体的starter -- dependencies !-- ... 其他初始依赖 ... -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId !-- 版本由上面的BOM管理 -- /dependency !-- 如果你计划使用Ollama本地模型可以添加这个 -- !-- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-ollama-spring-boot-starter/artifactId /dependency -- /dependencies2.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 vuelatest 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 ListFloat 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 groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version3.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); // 参数块大小、重叠、最小块等 ListDocument 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模型 ListFloat 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()); } }关键点解释TokenTextSplitterSpringAI提供的智能文本分割器按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 JpaRepositoryDocumentChunk, Long { /** * 使用PGVector的余弦相似度运算符 进行搜索 * 按相似度降序排列返回最相关的topK个结果 * :embedding 是一个Float数组参数 */ Query(value SELECT * FROM document_chunk ORDER BY embedding CAST(:embedding AS vector) LIMIT :topK, nativeQuery true) ListDocumentChunk findTopKSimilar(Param(embedding) ListFloat 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. 将用户问题转化为向量 ListFloat questionEmbedding embeddingModel.embed(userQuestion); // 2. 向量检索从数据库中找到最相关的文档片段 ListDocumentChunk 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); MapString, 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; } /** * 流式问答用于前端实时显示 * 返回一个FluxString流这里简化处理实际应使用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 ResponseEntityString 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 ResponseEntityString 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 classchat-container el-upload classupload-demo drag action# :auto-uploadfalse :on-changehandleFileChange :show-file-listfalse accept.pdf el-icon classel-icon--uploadupload-filled //el-icon div classel-upload__text拖拽PDF文件到此处或em点击上传/em/div template #tip div classel-upload__tip仅支持上传PDF文件构建知识库/div /template /el-upload div v-ifuploadStatus classstatus-info{{ uploadStatus }}/div el-divider / div classchat-area div classmessage-list div v-for(msg, index) in messages :keyindex :class[message, msg.role] div classavatar{{ msg.role user ? 你 : AI }}/div div classbubble{{ msg.content }}/div /div div v-ifloading classmessage assistant div classavatarAI/div div classbubbleel-icon classis-loadingLoading //el-icon 思考中.../div /div /div div classinput-area el-input v-modelinputQuestion typetextarea :rows3 placeholder请输入关于知识库的问题... keyup.enter.exacthandleAsk / el-button typeprimary clickhandleAsk :loadingloading发送/el-button /div /div /div /template script setup langts 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 refMessage[]([]) 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 idapp router-view / /div /template4. 系统联调与运行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. 常见问题与排查思路在搭建和运行过程中你可能会遇到以下典型问题问题现象可能原因排查思路与解决方案后端启动失败提示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.enabledtrue。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 brConfiguration brpublic 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密集型操作应改为异步任务如使用SpringAsync避免阻塞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的绝佳实践。你可以在此基础上根据上述优化建议逐步将其打磨成一个更健壮、更智能的生产级应用。