RAG(检索增强生成)技术是当前构建知识库问答系统的核心技术之一。本文将详细介绍如何使用Spring AI框架从零开始构建一个支持文档上传的知识库问答机器人。这个项目特别适合对大模型应用开发感兴趣的开发者和初学者,通过一个完整的实现方案,帮助大家深入理解RAG技术的核心原理和实践应用。
在传统问答系统中,大语言模型(LLM)往往受限于其训练数据的时效性和专业性。RAG技术通过动态检索相关知识片段,为LLM提供上下文信息,有效解决了这一问题。本项目将展示如何实现一个基于内存的自定义文本向量库,结合HanLP中文分词工具,构建完整的RAG问答系统。
RAG(Retrieval-Augmented Generation)是一种结合信息检索和文本生成的技术。其核心工作流程包含三个关键步骤:
从系统设计角度看,RAG的核心作用是:在LLM生成响应之前,由系统动态构造一个"最小且相关的知识上下文"。这里有两个关键特征:
RAG技术相比传统问答系统具有显著优势:
在一个典型的企业知识库问答系统中,RAG处于以下位置:
code复制用户提问 → 查询理解 → RAG检索 → LLM生成 → 答案返回
RAG主要作用是在用户提问与向LLM发起请求之间,负责检索关联文档并构建上下文。这种架构使得系统能够:
项目采用标准的Spring Boot应用结构,主要模块如下:
code复制D05-rag-qa-bot/
├── src/main/java/com/git/hui/springai/app/
│ ├── D05Application.java # 启动类
│ ├── mvc/
│ │ ├── QaApiController.java # API控制器
│ │ └── QaController.java # 页面控制器
│ ├── qa/QaBoltService.java # 问答服务
│ └── vectorstore/
│ ├── DocumentChunker.java # 文档分块工具
│ ├── DocumentQuantizer.java # 文档量化器
│ └── TextBasedVectorStore.java # 文本向量存储
├── src/main/resources/
│ ├── application.yml # 配置文件
│ ├── prompts/qa-prompts.pt # 提示词模板
│ └── templates/chat.html # 前端页面
└── pom.xml # 依赖配置
项目使用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>
<!-- RAG支持 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
<!-- 智谱大模型 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
</dependency>
<!-- 中文分词 -->
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.4</version>
</dependency>
</dependencies>
项目实现了一个基于内存的自定义向量存储TextBasedVectorStore,核心功能包括:
关键实现代码:
java复制public class TextBasedVectorStore extends AbstractObservationVectorStore {
@Getter
protected Map<String, SimpleVectorStoreContent> store = new ConcurrentHashMap();
@Override
public void doAdd(List<Document> documents) {
// 文档分块
List<Document> chunkers = DocumentChunker.DEFAULT_CHUNKER.chunkDocuments(documents);
// 向量化并存储
chunkers.forEach(document -> {
float[] embedding = DocumentQuantizer.quantizeDocument(document);
SimpleVectorStoreContent storeContent = new SimpleVectorStoreContent(
document.getId(),
document.getText(),
document.getMetadata(),
embedding
);
this.store.put(document.getId(), storeContent);
});
}
@Override
public List<Document> doSimilaritySearch(SearchRequest request) {
final float[] userQueryEmbedding = this.getUserQueryEmbedding(request.getQuery());
return this.store.values().stream()
.map((content) -> content.toDocument(
DocumentQuantizer.calculateCosineSimilarity(userQueryEmbedding, content.getEmbedding())
))
.filter((document) -> document.getScore() >= request.getSimilarityThreshold())
.sorted(Comparator.comparing(Document::getScore).reversed())
.limit((long) request.getTopK())
.toList();
}
}
合理的文档分块是RAG系统的关键环节。本项目实现了基于固定尺寸的分块策略:
java复制public class DocumentChunker {
private final int maxChunkSize; // 最大块大小(字符数)
private final int overlapSize; // 块间重叠大小
public List<Document> chunkDocument(Document document) {
String content = document.getText();
List<String> chunks = splitText(content);
List<Document> chunkedDocuments = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
String chunkId = document.getId() + "_chunk_" + i;
Document chunkDoc = new Document(chunkId, chunks.get(i), new HashMap<>(document.getMetadata()));
chunkedDocuments.add(chunkDoc);
}
return chunkedDocuments;
}
private List<String> splitText(String text) {
// 实现细节:按句子边界分割,处理超长句子,添加重叠区域等
}
}
使用HanLP进行中文分词,实现简单的文本向量化:
java复制public class DocumentQuantizer {
private static final Segment SEGMENT = HanLP.newSegment();
public static float[] quantizeText(String text) {
String[] words = preprocessText(text);
Map<String, Integer> wordFreq = countWordFrequency(words);
return generateFixedLengthVector(wordFreq, 128); // 生成128维向量
}
private static String[] preprocessText(String text) {
List<Term> termList = SEGMENT.seg(text);
return termList.stream()
.filter(term -> !isStopWord(term.word))
.map(term -> term.word.toLowerCase())
.toArray(String[]::new);
}
}
问答服务的核心时序如下:
文档处理阶段:
问答阶段:
java复制@Service
public class QaBoltService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public QaBoltService(ChatClient.Builder builder, VectorStore vectorStore) {
this.vectorStore = vectorStore;
this.chatClient = builder.defaultAdvisors(
// RAG支持
RetrievalAugmentationAdvisor.builder()
.documentRetriever(
VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build()
)
.build()
).build();
}
public Flux<String> ask(String chatId, String question, Collection<MultipartFile> files) {
processFiles(chatId, files); // 处理上传的文档
PromptTemplate customPromptTemplate = PromptTemplate.builder()
.template("""
<query>
Context information is below.
---------------------
<question_answer_context>
---------------------
Given the context information and no prior knowledge, answer the query.
""").build();
var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(3).build())
.promptTemplate(customPromptTemplate)
.build();
return chatClient.prompt()
.user(question)
.advisors(qaAdvisor)
.stream().content();
}
}
java复制private ProceedInfo processFiles(String chatId, Collection<MultipartFile> files) {
files.forEach(file -> {
try {
var data = new ByteArrayResource(file.getBytes());
if (file.getContentType().equals("application/pdf")) {
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(data);
List<Document> documents = pdfReader.read();
vectorStore.add(documents);
} else if (file.getContentType().startsWith("text/")) {
List<Document> documents = new TikaDocumentReader(data).read();
vectorStore.add(documents);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return new ProceedInfo();
}
在application.yml中配置关键参数:
yaml复制spring:
ai:
zhipuai:
api-key: ${zhipuai-api-key}
chat:
options:
model: GLM-4-Flash
temperature: 0.1
server:
port: 8080
主启动类非常简单:
java复制@SpringBootApplication
public class D05Application {
@Bean
public VectorStore vectorStore() {
return TextBasedVectorStore.builder().build();
}
public static void main(String[] args) {
SpringApplication.run(D05Application.class, args);
}
}
D05Application主类http://localhost:8080/chat检索优化:
系统扩展:
工程化改进:
对于生产环境,建议考虑以下改进:
问题1:中文文档分块效果不理想
解决方案:调整分块策略,优先在标点符号处分块,适当减小分块大小
问题2:特定格式文档解析失败
解决方案:检查Tika的兼容性,必要时添加自定义解析器
问题1:检索结果不相关
解决方案:调整相似度阈值,优化向量化方法,添加查询扩展
问题2:检索速度慢
解决方案:实现索引优化,考虑使用专业向量数据库
问题1:生成答案不准确
解决方案:优化提示词模板,增加检索结果数量,调整temperature参数
问题2:API调用超时
解决方案:实现重试机制,添加本地缓存,考虑使用更高性能的模型
本项目实现了一个完整的RAG知识库问答系统,核心技术要点包括:
项目源码地址:
https://github.com/liuyueyi/spring-ai-demo/tree/master/app-projects/D05-rag-qa-bot
对于希望进一步学习大模型技术的开发者,建议关注以下方向: