从零搭建生产级AI智能客服系统(四):RAG知识库系统实现,让AI只说真话
作者:大洪讲AI
更新时间:2026年6月
本章目标:完整实现RAG检索增强生成系统,支持文档上传、自动切片、向量存储、相似度检索,通过Prompt工程彻底解决大模型幻觉问题
前置条件:第三章流式聊天100%完成,SSE打字机效果正常
前言
上一章我们实现了流畅的SSE流式聊天,体验已经接近主流AI产品。但这时的AI客服还存在三个致命问题,完全无法用于真实业务:
- 胡说八道:大模型会编造不存在的产品参数、价格、售后政策
- 知识过时:大模型训练数据有截止日期,不知道你家最新的产品和活动
- 没有私有知识:大模型不可能知道你们公司内部的业务规则、客户须知
这一章我们就用RAG检索增强生成技术彻底解决这些问题,让AI只能用你上传的知识库内容回答问题,不知道就明确说不知道,绝不编造。
一、什么是RAG?为什么它是企业AI的标配
1.1 RAG核心概念
RAG全称 Retrieval-Augmented Generation(检索增强生成),简单来说就是:
大模型回答问题之前,先去你的私有知识库中检索最相关的内容,然后把这些内容和问题一起喂给大模型,让它只能基于检索到的内容生成回答。
1.2 RAG完整工作流程
用户提问
↓
问题向量化(和文档用同一个嵌入模型)
↓
向量数据库相似度检索,找出Top-K最相关的文档片段
↓
把「系统规则 + 知识库片段 + 用户问题」组装成增强Prompt
↓
大模型基于Prompt生成回答
↓
返回给用户
1.3 为什么不用微调?
| 对比项 | RAG检索增强 | 微调模型 |
|---|---|---|
| 成本 | 极低,只需要向量数据库和嵌入API | 极高,需要大量标注数据和GPU算力 |
| 更新速度 | 实时,上传文档立即生效 | 慢,每次更新都要重新训练 |
| 准确性 | 高,有明确的知识来源,可溯源 | 一般,仍然存在幻觉 |
| 技术门槛 | 低,普通后端就能做 | 高,需要算法工程师 |
结论:对于企业内部知识库、客服问答这类场景,RAG是性价比最高、落地最快的方案,也是目前行业的标准做法。
二、向量数据库选型与环境准备
2.1 为什么选Chroma
我们选用 Chroma 作为向量数据库,原因非常适合中小项目和快速落地:
- ✅ 轻量无依赖,Python一行命令就能启动
- ✅ 本地持久化存储,数据不会丢失
- ✅ LangChain4j原生支持,对接成本极低
- ✅ 免费开源,单机支持百万级向量完全够用
2.2 安装Chroma
打开CMD,执行以下命令安装(需要Python 3.10+):
pip install chromadb
⚠️ 踩坑预警:目前最新版Chroma是1.5.9,已经完全移除了v1 API,只支持v2接口。网上很多老教程都没更新,直接用会报
410 Gone错误。我们的代码会直接适配v2 API,避开这个坑。
2.3 启动Chroma服务
执行以下命令启动Chroma,数据会持久化到本地磁盘:
chroma run --host 0.0.0.0 --port 8000 --path D:\chroma-data
参数说明:
--host 0.0.0.0:允许局域网访问--port 8000:服务端口--path D:\chroma-data:数据持久化路径,不指定默认存在用户目录下
启动成功后,浏览器访问http://localhost:8000/api/v2/collections,返回空数组[]说明服务正常。
三、后端RAG核心代码实现
3.1 第一步:添加Chroma依赖
在pom.xml的dependencies节点中添加Chroma向量存储依赖:
<!-- Chroma向量存储 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-chroma</artifactId>
<version>${langchain4j.version}</version>
</dependency>
刷新Maven依赖,等待下载完成。
3.2 第二步:新增Chroma配置
在application.yml中添加Chroma相关配置:
# 向量数据库配置
chroma:
base-url: http://localhost:8000
collection-name: ai_kefu_knowledge # 集合名称,相当于MySQL的表
3.3 第三步:配置向量存储Bean
修改src/main/java/org/example/config/AiConfig.java,添加Chroma向量存储的Bean配置:
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.TextSegment;
import dev.langchain4j.store.embedding.chroma.ChromaEmbeddingStore;
import dev.langchain4j.store.embedding.chroma.ChromaApiVersion;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// ... 原有其他import保持不变
@Configuration
public class AiConfig {
@Value("${chroma.base-url}")
private String chromaBaseUrl;
@Value("${chroma.collection-name}")
private String chromaCollectionName;
// ... 原有三个模型Bean保持不变
/**
* Chroma向量存储(适配v2 API,兼容Chroma 1.5+)
*/
@Bean
public EmbeddingStore<TextSegment> embeddingStore() {
return ChromaEmbeddingStore.builder()
.baseUrl(chromaBaseUrl)
.collectionName(chromaCollectionName)
.apiVersion(ChromaApiVersion.V2) // 关键:指定v2 API,解决410报错
.build();
}
}
⚠️ 重点提醒:
.apiVersion(ChromaApiVersion.V2)是适配高版本Chroma的核心配置,90%的人遇到410错误都是因为漏了这行。
3.4 第四步:实现文档切片工具类
长文档不能直接向量化,必须切成固定大小的片段,不然检索精度会非常差。我们实现一个带重叠的切片工具,保证上下文连贯。
新建文件:src/main/java/org/example/util/DocSplitter.java
package org.example.util;
import java.util.ArrayList;
import java.util.List;
public class DocSplitter {
// 每个片段的字符数
private static final int CHUNK_SIZE = 300;
// 相邻片段重叠字符数,避免上下文断裂
private static final int CHUNK_OVERLAP = 50;
/**
* 把长文本切成固定大小的片段,带重叠
*/
public static List<String> split(String content) {
List<String> chunks = new ArrayList<>();
if (content == null || content.isEmpty()) {
return chunks;
}
int length = content.length();
int start = 0;
while (start < length) {
int end = Math.min(start + CHUNK_SIZE, length);
chunks.add(content.substring(start, end));
// 移动起始位置,减去重叠部分
start += CHUNK_SIZE - CHUNK_OVERLAP;
// 防止死循环
if (start >= length) {
break;
}
}
return chunks;
}
}
3.5 第五步:实现RAG核心服务
新建文件:src/main/java/org/example/service/RagService.java,这是整个知识库的核心:
package org.example.service;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import org.example.util.DocSplitter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class RagService {
@Autowired
private EmbeddingStore<TextSegment> embeddingStore;
@Autowired
@Qualifier("doubaoEmbeddingModel")
private EmbeddingModel embeddingModel;
// 检索最相关的Top 3片段
private static final int TOP_K = 3;
// 相似度阈值,低于这个分数的结果直接丢弃
private static final double SCORE_THRESHOLD = 0.6;
/**
* 把文档内容存入知识库
*/
public void addDocument(String content) {
// 1. 切片
List<String> chunks = DocSplitter.split(content);
// 2. 批量向量化并存入向量库
for (String chunk : chunks) {
TextSegment segment = TextSegment.from(chunk);
EmbeddingStoreIngestor.ingest(Document.from(segment.text()), embeddingModel, embeddingStore);
}
}
/**
* 检索与问题最相关的知识库内容
*/
public String retrieveRelevantContent(String question) {
// 1. 相似度检索
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(
embeddingModel.embed(question).content(),
TOP_K,
SCORE_THRESHOLD
);
// 2. 拼接检索到的内容
StringBuilder sb = new StringBuilder();
for (int i = 0; i < matches.size(); i++) {
sb.append("【资料").append(i + 1).append("】\n");
sb.append(matches.get(i).embedded().text()).append("\n\n");
}
return sb.toString().trim();
}
/**
* 构建RAG增强Prompt,加防编造规则
*/
public String buildRagPrompt(String question, String relevantContent) {
StringBuilder prompt = new StringBuilder();
prompt.append("你是公司的专业AI客服,必须严格遵守以下规则:\n");
prompt.append("1. 只能使用下面【知识库内容】中的信息回答问题,绝对不能编造任何知识库中没有的内容\n");
prompt.append("2. 如果知识库中没有相关内容,直接回答:\"抱歉,我没有找到相关信息,请您咨询人工客服。\"\n");
prompt.append("3. 回答简洁、专业、有条理,使用\"您\"称呼用户\n");
prompt.append("4. 不要添加任何知识库中没有的解释和延伸\n\n");
prompt.append("【知识库内容】\n");
prompt.append(relevantContent).append("\n\n");
prompt.append("用户问题:").append(question);
return prompt.toString();
}
}
3.6 第六步:Chroma管理工具类(适配v2 API)
因为LangChain4j没有提供统计数量、清空集合的方法,我们自己封装一个工具类调用Chroma的v2 HTTP接口。
新建文件:src/main/java/org/example/util/ChromaUtil.java
package org.example.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class ChromaUtil {
@Value("${chroma.base-url}")
private String chromaBaseUrl;
@Value("${chroma.collection-name}")
private String chromaCollectionName;
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 获取知识库文档片段总数
*/
public String getStats() {
try {
String collectionId = getCollectionIdByName(chromaCollectionName);
if (collectionId == null) {
return "知识库当前共有0个文档片段";
}
String url = chromaBaseUrl + "/api/v2/collections/" + collectionId + "/count";
Integer count = restTemplate.getForObject(url, Integer.class);
return "知识库当前共有" + count + "个文档片段";
} catch (Exception e) {
return "获取统计信息失败:" + e.getMessage();
}
}
/**
* 清空知识库
*/
public String clearKnowledgeBase() {
try {
String collectionId = getCollectionIdByName(chromaCollectionName);
if (collectionId == null) {
return "知识库不存在,无需清空";
}
String url = chromaBaseUrl + "/api/v2/collections/" + collectionId;
restTemplate.delete(url);
return "知识库已清空";
} catch (Exception e) {
return "清空失败:" + e.getMessage();
}
}
/**
* v2 API必须:通过集合名称获取集合ID
*/
private String getCollectionIdByName(String collectionName) {
try {
String url = chromaBaseUrl + "/api/v2/collections";
String response = restTemplate.getForObject(url, String.class);
JsonNode root = objectMapper.readTree(response);
for (JsonNode collection : root) {
if (collectionName.equals(collection.get("name").asText())) {
return collection.get("id").asText();
}
}
return null;
} catch (Exception e) {
return null;
}
}
}
3.7 第七步:知识库管理接口
新建文件:src/main/java/org/example/controller/KnowledgeBaseController.java
package org.example.controller;
import org.example.model.vo.AjaxResult;
import org.example.service.RagService;
import org.example.util.ChromaUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("/api/kb")
public class KnowledgeBaseController {
@Autowired
private RagService ragService;
@Autowired
private ChromaUtil chromaUtil;
/**
* 上传文档到知识库(支持txt/md文件)
*/
@PostMapping("/upload")
public AjaxResult<String> uploadDocument(@RequestParam("file") MultipartFile file) {
try {
// 读取文件内容(UTF-8编码)
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
}
// 存入知识库
ragService.addDocument(content.toString());
return AjaxResult.success("文档上传成功,已添加到知识库");
} catch (Exception e) {
return AjaxResult.error("上传失败:" + e.getMessage());
}
}
/**
* 获取知识库统计信息
*/
@GetMapping("/stats")
public AjaxResult<String> getStats() {
return AjaxResult.success(chromaUtil.getStats());
}
/**
* 清空知识库
*/
@DeleteMapping("/clear")
public AjaxResult<String> clear() {
return AjaxResult.success(chromaUtil.clearKnowledgeBase());
}
}
3.8 第八步:改造流式聊天接口,接入RAG
修改ChatController.java的流式接口,让回答基于知识库内容:
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(@RequestBody ChatRequest request) {
SseEmitter emitter = new SseEmitter(60000L);
// 1. 检索知识库相关内容
String relevantContent = ragService.retrieveRelevantContent(request.getMessage());
// 2. 构建增强Prompt
String prompt;
if (relevantContent.isEmpty()) {
// 没有相关内容,直接返回固定话术
prompt = "抱歉,我没有找到相关信息,请您咨询人工客服。";
// 这里可以直接返回,不走大模型,节省费用
try {
emitter.send(prompt, MediaType.TEXT_PLAIN);
emitter.send("[DONE]", MediaType.TEXT_PLAIN);
emitter.complete();
return emitter;
} catch (IOException e) {
emitter.completeWithError(e);
return emitter;
}
} else {
// 有相关内容,用RAG增强Prompt
prompt = ragService.buildRagPrompt(request.getMessage(), relevantContent);
}
// 3. 调用流式大模型生成回答
streamingChatModel.generate(prompt, new StreamingResponseHandler<AiMessage>() {
@Override
public void onNext(String token) {
try {
emitter.send(token, MediaType.TEXT_PLAIN);
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onComplete(Response<AiMessage> response) {
try {
emitter.send("[DONE]", MediaType.TEXT_PLAIN);
emitter.complete();
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onError(Throwable error) {
emitter.completeWithError(error);
}
});
return emitter;
}
💡 优化点:如果检索不到相关内容,直接返回固定话术,不调用大模型,既节省API费用,又保证不会编造答案。
四、完整功能验证
4.1 准备测试文档
新建一个product.txt文件,写入以下测试内容:
【产品介绍】
AI智能客服系统V2.0是基于豆包大模型开发的企业级客服系统。
产品价格:基础版999元/年,专业版2999元/年,企业版定制报价。
产品功能:支持SSE流式聊天、RAG知识库、意图识别、多轮对话、数据统计。
售后服务:7天无理由退款,1年免费技术支持,工作日9:00-18:00在线客服。
4.2 验证步骤
- 启动Chroma服务:确保8000端口正常运行
- 重启后端项目:控制台无报错
- 上传文档:用Postman调用
POST http://localhost:8080/api/kb/upload,选择刚才的product.txt文件上传 - 查看统计:调用
GET http://localhost:8080/api/kb/stats,返回「知识库当前共有X个文档片段」 - 测试知识库内问题:前端发送「你们产品多少钱?」,AI应该准确回答三个版本的价格
- 测试知识库外问题:前端发送「今天天气怎么样?」,AI应该返回「抱歉,我没有找到相关信息,请您咨询人工客服。」
- 测试防编造:前端发送「你们的产品支持视频会议吗?」,AI应该说找不到相关信息,不会编造
✅ 全部验证通过,说明RAG知识库系统正常工作,AI已经不会胡说八道了。
五、RAG效果调优指南
效果不好的时候,从这几个维度调整,90%的问题都能解决:
| 问题 | 调整方向 | 建议值 |
|---|---|---|
| 检索不到相关内容 | 减小切片大小,增加Top-K | 切片200-300字符,Top-K 3-5 |
| 回答上下文断裂 | 增加切片重叠大小 | 重叠占切片的15%-20% |
| 回答有无关内容 | 提高相似度阈值,减少Top-K | 阈值0.6-0.8,Top-K 2-3 |
| 还是会编造内容 | 强化Prompt规则,加重惩罚语气 | 明确说明「编造内容会造成严重后果」 |
| 长文档效果差 | 按章节/段落手动切分,再上传 | 避免把不相关的内容切到同一个片段 |
六、常见问题排查
问题1:启动报错Chroma连接失败
- 检查Chroma服务是否正常启动
- 检查
chroma.base-url地址和端口是否正确 - 检查防火墙是否拦截了8000端口
问题2:调用接口返回410 Gone
- 99%是因为没加
.apiVersion(ChromaApiVersion.V2)配置 - 检查Chroma版本是否在1.0以上
问题3:上传文档成功但检索不到
- 检查嵌入模型是否和上传时用的是同一个
- 调大
TOP_K或者降低SCORE_THRESHOLD - 检查文档内容是否和问题语义相关
问题4:AI还是会编造内容
- 检查Prompt中的规则是否足够明确
- 检查检索到的内容是否真的包含答案
- 把模型的
temperature调到0.1-0.3,降低创造性
七、本章总结
本章我们完成了RAG知识库系统的完整实现:
- ✅ 理解了RAG检索增强生成的核心原理和优势
- ✅ 完成了Chroma向量数据库的安装启动,适配v2 API
- ✅ 实现了文档自动切片、批量向量化、存储入库
- ✅ 实现了相似度检索和RAG增强Prompt构建
- ✅ 开发了完整的知识库管理接口(上传、统计、清空)
- ✅ 改造了流式聊天接口,回答完全基于知识库
- ✅ 通过Prompt工程彻底解决了大模型幻觉问题
现在我们的AI客服已经具备了私有知识库问答能力,可以准确回答业务相关的问题,不知道的就明确说不知道,完全满足基础的企业客服需求。
下章预告
下一章我们将实现多轮对话与上下文管理,包括会话持久化、MySQL数据库建表、滑动窗口上下文策略、对话历史管理,让AI能够记住之前的对话内容,支持自然的多轮交流。
关注我,第一时间收到更新通知!
更多推荐



所有评论(0)