1. 项目概述:基于Spring AI的RAG知识库问答机器人实战
在当今AI技术快速发展的背景下,构建智能问答系统已成为企业知识管理的重要需求。本项目使用Spring AI框架实现了一个完整的RAG(Retrieval-Augmented Generation)知识库问答机器人,能够处理用户上传的文档并基于文档内容回答相关问题。
这个项目特别适合以下人群:
- Java开发者想要快速上手AI应用开发
- 需要为企业构建内部知识库系统的工程师
- 对RAG技术原理和实践感兴趣的技术人员
- 希望将大模型能力集成到现有Java系统中的开发者
项目核心价值在于:
- 完整展示了RAG技术的实现流程
- 提供了可直接运行的代码实现
- 包含了中文文档处理的优化方案
- 实现了从文档上传到问答的全流程
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RAG技术核心原理解析
2.1 RAG基本概念与优势
RAG(检索增强生成)是一种结合信息检索和文本生成的技术,其核心思想是在生成答案前,先从知识库中检索相关文档片段作为上下文。与传统问答系统相比,RAG具有以下优势:
- 动态上下文:每次问答都基于最新检索的内容,无需将所有知识硬编码到模型中
- 知识可更新:只需更新文档库即可扩展系统知识,无需重新训练模型
- 解释性强:可以追踪答案的来源文档,提高可信度
- 节省资源:不需要将全部知识存储在模型参数中
2.2 RAG工作流程详解
典型的RAG系统工作流程包含以下关键步骤:
-
文档预处理阶段:
- 文档解析(PDF、Word等格式)
- 文本分块(保持语义连贯的段落)
- 向量化(将文本转换为数值向量)
- 存储到向量数据库
-
问答阶段:
- 用户提问
- 问题向量化
- 向量相似度检索
- 构建提示词(问题+检索结果)
- 大模型生成最终答案
2.3 关键技术组件选型
在本项目中,我们选择了以下技术栈:
- Spring AI:作为基础框架,提供与大模型交互的统一API
- HanLP:中文分词工具,优化中文文档处理
- Tika:文档解析库,支持多种文件格式
- 内存向量库:简化部署,适合原型开发
提示:生产环境中建议使用专业向量数据库如Pinecone或Milvus,但本项目的内存实现足够用于学习和演示。
3. 项目实现详解
3.1 环境准备与项目初始化
3.1.1 开发环境要求
- JDK 17或更高版本
- Maven 3.6+
- IDE(IntelliJ IDEA推荐)
- 智谱AI API密钥(或其他兼容的大模型API)
3.1.2 Maven依赖配置
项目核心依赖包括:
xml复制<dependencies>
<!-- Spring AI核心 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<!-- 文档处理 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<!-- 中文分词 -->
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.4</version>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.1.3 应用配置
application.yml关键配置:
yaml复制spring:
ai:
zhipuai:
api-key: ${zhipuai-api-key}
chat:
options:
model: GLM-4-Flash
temperature: 0.1
server:
port: 8080
3.2 自定义向量存储实现
3.2.1 文本向量化方案
由于直接使用大模型的Embedding API可能有成本或权限限制,我们实现了基于文本匹配的简化向量化方案:
java复制public class DocumentQuantizer {
private static final Segment SEGMENT = HanLP.newSegment();
public static float[] quantizeText(String text) {
// 中文分词
List<Term> termList = SEGMENT.seg(text);
String[] words = termList.stream()
.filter(term -> !isStopWord(term.word))
.map(term -> term.word.toLowerCase())
.toArray(String[]::new);
// 词频统计
Map<String, Integer> wordFreq = new HashMap<>();
for (String word : words) {
wordFreq.put(word, wordFreq.getOrDefault(word, 0) + 1);
}
// 生成固定长度向量
return generateFixedLengthVector(wordFreq, 128);
}
}
3.2.2 文档分块策略
合理的文档分块对检索效果至关重要。我们实现了兼顾语义和长度的分块算法:
java复制public List<String> splitText(String text) {
List<String> chunks = new ArrayList<>();
String[] sentences = text.split("(?<=。)|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)");
StringBuilder currentChunk = new StringBuilder();
for (String sentence : sentences) {
if (currentChunk.length() + sentence.length() <= maxChunkSize) {
currentChunk.append(sentence);
} else {
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString());
currentChunk = new StringBuilder();
// 添加重叠部分保持上下文连贯
if (sentence.length() > overlapSize) {
currentChunk.append(sentence.substring(
Math.max(0, sentence.length() - overlapSize)));
}
}
}
}
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString());
}
return chunks;
}
3.2.3 相似度计算
使用余弦相似度衡量文本相关性:
java复制public static double calculateCosineSimilarity(float[] vectorA, float[] vectorB) {
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
normA += Math.pow(vectorA[i], 2);
normB += Math.pow(vectorB[i], 2);
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
3.3 问答服务核心实现
3.3.1 服务架构设计
问答服务主要包含以下组件:
- 文档处理器:解析上传文件并存入向量库
- 检索器:根据问题查找相关文档
- 提示词构建器:组合问题和检索结果
- 大模型客户端:调用API生成最终答案
3.3.2 文档处理流程
java复制private ProceedInfo processFiles(String chatId, Collection<MultipartFile> files) {
files.forEach(file -> {
try {
// 计算文件MD5用于去重
String md5 = calculateHash(chatId, file.getBytes());
// 根据文件类型选择解析器
if (file.getContentType().contains("pdf")) {
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(...);
List<Document> documents = pdfReader.read();
documents.forEach(doc -> doc.getMetadata().put("md5", md5));
vectorStore.add(documents);
} else if (file.getContentType().contains("text")) {
List<Document> documents = new TikaDocumentReader(...).read();
// 同上处理
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
3.3.3 问答核心逻辑
java复制public Flux<String> ask(String chatId, String question, Collection<MultipartFile> files) {
// 处理上传文件
processFiles(chatId, files);
// 构建自定义提示词模板
PromptTemplate customPromptTemplate = PromptTemplate.builder()
.template("""
<query>
Context information:
---------------------
<question_answer_context>
---------------------
Answer the query based on the context.
Rules:
1. If unsure, say you don't know
2. Avoid phrases like "Based on the context..."
""").build();
// 配置RAG Advisor
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.similarityThreshold(0.5)
.topK(3)
.build())
.promptTemplate(customPromptTemplate)
.build();
// 执行问答
return chatClient.prompt()
.system(boltPrompts)
.user(question)
.advisors(qaAdvisor)
.stream()
.content();
}
3.4 前端交互实现
3.4.1 页面控制器
java复制@Controller
public class QaController {
@GetMapping("/chat")
public String chat() {
return "chat";
}
}
3.4.2 API接口
java复制@RestController
@RequestMapping("/api")
public class QaApiController {
@Autowired
private QaBoltService qaBolt;
@PostMapping(path = "/chat/{chatId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> qaPost(@PathVariable String chatId,
@RequestParam String question,
@RequestParam(required = false)
Collection<MultipartFile> files) {
return qaBolt.ask(chatId, question, files != null ? files : Collections.emptyList());
}
}
3.4.3 前端页面关键功能
- 文件上传区域
- 问题输入框
- 答案流式显示区域
- 对话历史记录
4. 项目部署与测试
4.1 运行准备
- 获取智谱AI API密钥
- 配置application.yml或环境变量
- 构建项目:
mvn clean package - 运行:
java -jar target/your-app.jar
4.2 测试流程
- 访问
http://localhost:8080/chat - 上传测试文档(PDF/TXT等)
- 输入相关问题
- 观察系统回答质量和参考文档
4.3 效果优化建议
-
检索优化:
- 尝试不同的分块大小(300-1000字符)
- 调整相似度阈值(0.4-0.6)
- 增加多路召回策略
-
提示词优化:
- 明确回答格式要求
- 控制回答长度
- 指定忽略无关信息
-
性能优化:
- 实现向量库持久化
- 添加缓存层
- 异步处理大文档
5. 生产环境进阶建议
5.1 安全增强
- 实现用户认证和授权
- 文档访问权限控制
- 敏感信息过滤
5.2 可观测性
- 添加日志记录
- 实现问答质量监控
- 收集用户反馈
5.3 扩展功能
- 支持多轮对话
- 添加文档管理界面
- 实现自动知识更新
在实际部署中,我发现文档分块大小对效果影响很大。经过多次测试,对于中文技术文档,500-800字符的分块大小配合50-100字符的重叠区域通常能取得较好效果。另外,在提示词中明确要求模型避免使用"根据上下文"这类短语,可以使回答更加自然。
