1. 项目概述
在人工智能技术快速发展的今天,RAG(检索增强生成)技术已成为构建知识库问答系统的核心技术之一。作为一名长期从事AI应用开发的工程师,我将分享如何使用Spring AI框架从零构建一个完整的RAG知识库问答机器人。这个项目特别适合想要快速上手大模型应用开发的初学者,因为它采用了一套低成本的技术方案:无需安装专业的向量数据库,通过自定义基于内存的向量存储和HanLP中文分词实现文档向量化,再结合智谱AI大模型,就能实现文档上传、解析、分块入库及基于上下文的智能问答功能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RAG技术原理详解
2.1 RAG核心概念
RAG(检索增强生成)是一种结合了信息检索和文本生成的技术。它的基本工作流程可以分为三个关键步骤:
- 用户提出问题
- 系统从知识库中检索相关信息
- 大语言模型基于检索到的信息生成答案
从系统设计角度来看,RAG的核心作用可以被描述为:在LLM调用生成响应之前,由系统动态构造一个"最小且相关的知识上下文"。这里有两个关键词特别重要:
- 动态:每次问题都不同,检索的知识也不同(比如用户问A产品时找A的文档,问B产品时找B的文档)
- 最小:只注入必要信息(比如用户问"A产品的定价",就只塞定价相关的片段,而非整份产品手册)
2.2 RAG技术优势
RAG技术有几个显著优势:
- 解决上下文窗口限制:不再需要把所有知识塞进窗口,而是只在需要时"临时调取"相关部分,既避免了窗口溢出,又减少了注意力竞争
- 知识更新灵活:只需更新向量数据库中的文档,就能让模型获取最新知识,无需重新训练模型
- 降低幻觉风险:基于检索到的真实文档生成答案,减少了模型编造信息的可能性
2.3 RAG在企业知识库中的应用
在企业知识库场景中,RAG主要位于用户提问与向LLM发起请求这个中间环节,负责检索关联的文档构建上下文。这种架构使得系统能够:
- 快速响应企业内部政策、产品文档等查询
- 支持多文档、多格式的知识管理
- 实现知识的细粒度检索和精准回答
3. 项目架构与实现
3.1 整体项目结构
项目采用标准的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 # 依赖配置
3.2 技术选型与依赖配置
3.2.1 Maven依赖
项目使用Maven进行依赖管理,核心依赖包括:
xml复制<dependencies>
<!-- 向量数据库相关 -->
<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>
3.2.2 应用配置
在application.yml中配置API密钥和相关参数:
yaml复制spring:
ai:
zhipuai:
api-key: ${zhipuai-api-key}
chat:
options:
model: GLM-4-Flash
temperature: 0.1
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
server:
port: 8080
4. 核心组件实现
4.1 自定义向量存储实现
4.1.1 TextBasedVectorStore
我们实现了一个基于内存的自定义向量存储TextBasedVectorStore,核心功能包括:
- 文档添加:将文档分块、向量化后存储
- 相似度搜索:基于余弦相似度查找相关文档
java复制public class TextBasedVectorStore extends AbstractObservationVectorStore {
@Getter
protected Map<String, SimpleVectorStoreContent> store = new ConcurrentHashMap();
private Set<String> persistMd5 = new CopyOnWriteArraySet<>();
@Override
public void doAdd(List<Document> documents) {
// 过滤重复文档
List<Document> mutableDocuments = documents.stream()
.filter(doc -> !persistMd5.contains(doc.getMetadata().get("md5")))
.collect(Collectors.toList());
// 文档分块
List<Document> chunkers = DocumentChunker.DEFAULT_CHUNKER.chunkDocuments(mutableDocuments);
// 向量化并存储
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();
}
}
4.1.2 文档分块器(DocumentChunker)
文档分块是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 chunk = chunks.get(i);
Document chunkDoc = new Document(
document.getId() + "_chunk_" + i,
chunk,
new HashMap<>(document.getMetadata())
);
chunkDoc.getMetadata().put("chunk_index", i);
chunkedDocuments.add(chunkDoc);
}
return chunkedDocuments;
}
private List<String> splitText(String text) {
// 实现细节:按语义边界分割文本,处理超长句子等
}
}
4.1.3 文档量化器(DocumentQuantizer)
使用HanLP进行中文分词和简单的向量化:
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 = countWordFrequency(words);
// 生成固定长度向量
return generateFixedLengthVector(wordFreq, 128);
}
public static double calculateCosineSimilarity(float[] vectorA, float[] vectorB) {
// 实现余弦相似度计算
}
}
4.2 问答服务实现
4.2.1 QaBoltService核心逻辑
问答服务是系统的核心,主要流程包括:
- 处理上传的文档
- 构建RAG增强的ChatClient
- 执行问答并返回结果
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 = createQAPromptTemplate();
// 构建问答请求
var requestSpec = chatClient.prompt()
.system(boltPrompts)
.user(question)
.advisors(QuestionAnswerAdvisor.builder(vectorStore)
.promptTemplate(customPromptTemplate)
.build());
return requestSpec.stream().content();
}
private void processFiles(String chatId, Collection<MultipartFile> files) {
// 实现文档解析和存储
}
}
4.2.2 文档处理流程
文档处理包括解析、分块和向量化:
java复制private void processFiles(String chatId, Collection<MultipartFile> files) {
files.forEach(file -> {
try {
// 根据文件类型选择解析器
List<Document> documents;
if (file.getContentType().equals("application/pdf")) {
documents = new PagePdfDocumentReader(
new ByteArrayResource(file.getBytes())
).read();
} else {
documents = new TikaDocumentReader(
new ByteArrayResource(file.getBytes())
).read();
}
// 添加元数据并存储
documents.forEach(doc -> doc.getMetadata().put("md5", calculateHash(file.getBytes())));
vectorStore.add(documents);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
5. 系统部署与测试
5.1 启动配置
在启动类中注册我们的自定义向量存储:
java复制@SpringBootApplication
public class D05Application {
@Bean
public VectorStore vectorStore() {
return TextBasedVectorStore.builder().build();
}
public static void main(String[] args) {
SpringApplication.run(D05Application.class, args);
}
}
5.2 提示词设计
在resources/prompts/qa-prompts.pt中定义系统提示词:
code复制## 角色设定
你是一个智能问答助手,专门负责根据用户提供的文档内容进行准确的回答和信息提取。
## 核心任务
- 仔细阅读并理解用户上传的文档内容
- 基于文档中的信息回答用户的问题
- 提供准确、相关且基于文档的答案
- 当问题超出文档范围时,明确告知用户该信息未在文档中提及
## 回答规范
- 严格基于文档内容作答,不得编造信息
- 引用文档中的具体信息时,请保持原文准确性
- 对于不确定的内容,应诚实表达不确定性,而非猜测
5.3 测试流程
- 启动应用:运行
D05Application主类 - 访问页面:打开
http://localhost:8080/chat - 上传文档:支持PDF、Word或文本文件
- 提问测试:输入关于文档的问题获取答案
6. 项目优化与扩展
6.1 性能优化建议
- 索引优化:为频繁查询的字段建立索引
- 缓存策略:实现查询结果缓存
- 批量处理:支持文档的批量上传和处理
6.2 功能扩展方向
- 多格式支持:增加Excel、PPT等文档格式的解析
- 多语言支持:增强对英文等其他语言的处理能力
- 知识图谱:结合知识图谱技术提升问答准确性
6.3 工程化考量
在实际生产环境中,还需要考虑:
- 安全隐私:不同用户的知识库文件隔离
- 监控告警:实现系统运行状态的监控
- 高可用:确保服务的稳定性和可靠性
这个项目虽然简单,但完整展示了RAG技术的核心流程和实现方法。通过这个基础框架,开发者可以快速构建自己的知识库问答系统,并根据实际需求进行扩展和优化。
