第7篇:从零搭建本地RAG知识库,内网可用,零API费用
(Java+AI落地实战系列 | 纯本地部署 | 彻底解决大模型幻觉)
大家好,我是三石。
上一篇我们实现了多轮对话记忆,AI终于能记住上下文,对话体验像模像样了。
但很多同学落地业务时又卡了:
- 问公司内部制度、产品手册,AI张嘴就胡说八道,十句有八句是编的
- 政企、金融行业数据不能出内网,公网大模型API根本不敢碰
- 调用量上来后,token费用蹭蹭涨,老板迟迟不肯批预算
这就是企业级AI落地最核心的两个卡点:幻觉问题 + 数据安全问题。
今天我们就用纯Java技术栈,从零搭一套完全本地化的RAG知识库系统——不用公网API、数据全在本地、AI只基于你给的文档回答,从根源解决胡说八道的问题,普通笔记本就能跑。
先给大家打个比方,一秒懂RAG:
普通大模型问答 = 闭卷考试,全靠AI脑子里的知识瞎蒙,答错很正常 RAG知识库问答 = 开卷考试,先给AI发参考资料,要求它只能照着资料答,准确率100%
一、先搞懂RAG的完整工作流程
别被网上的玄乎术语吓住,RAG本质就四步,全程都是Java开发熟悉的逻辑:
- 文档切片:把PDF、Word、制度手册这些长文档,切成几百字的小片段
- 向量化存储:把每个文本片段转成向量,存入向量数据库(相当于给每段内容建索引)
- 相似度检索:用户提问时,把问题也转成向量,从库里找出最相关的3-5段文档
- 增强生成:把「用户问题 + 检索到的相关文档」一起喂给大模型,要求它只基于文档回答
全程数据都跑在你的服务器上,大模型只负责“整理资料输出答案”,不会泄露任何内部数据,也不会瞎编内容。
二、前置准备:本地环境搭建
1. 本地大模型部署工具:Ollama
不用装复杂的Python环境,不用配CUDA,一行命令就能启动开源大模型,对Java开发者极度友好。
安装步骤
- 官网下载安装包:ollama.com/ ,支持Windows/Mac/Linux
- 打开命令行,拉取中文效果最好的7B开源模型(8G内存就能跑)
ollama pull qwen2:7b - 验证启动:执行
ollama run qwen2:7b,能直接在命令行对话就说明部署成功 - 默认本地服务地址:
http://localhost:11434,Java代码直接对接这个地址
2. 技术栈选型(兼容企业Java环境)
| 技术 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 兼容绝大多数存量业务系统 |
| SpringBoot | 2.7.18 | 生产环境主流稳定版 |
| LangChain4j | 0.32.0 | Java生态最成熟的AI应用框架,和SpringBoot版本严格匹配 |
| 向量存储 | InMemory | 入门零依赖,生产可替换为Milvus/PGVector |
| 大模型 | Qwen2-7B | 开源免费,中文效果优秀,本地部署 |
踩坑提醒:LangChain4j版本和SpringBoot版本必须对应,0.32.0适配SpringBoot 2.7.x,乱升版本会直接报NoSuchMethodError。
三、第一步:引入核心Maven依赖
直接复制到pom.xml,不用再找零散依赖:
<dependencies>
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.18</version>
</dependency>
<!-- LangChain4j 核心 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>0.32.0</version>
</dependency>
<!-- Ollama 本地大模型对接 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>0.32.0</version>
</dependency>
<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<optional>true</optional>
</dependency>
</dependencies>
四、第二步:核心配置类
新建RagConfig,初始化三个核心组件:大模型、嵌入模型、向量存储。
@Configuration
public class RagConfig {
/**
* 配置本地聊天大模型
*/
@Bean
public ChatLanguageModel chatLanguageModel() {
return OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("qwen2:7b")
.temperature(0.2) // 知识库场景用低温度,回答更严谨
.timeout(Duration.ofSeconds(60))
.build();
}
/**
* 配置嵌入模型:把文本转成向量
* 入门用同一个qwen2模型即可,生产建议用专用嵌入模型
*/
@Bean
public EmbeddingModel embeddingModel() {
return OllamaEmbeddingModel.builder()
.baseUrl("http://localhost:11434")
.modelName("qwen2:7b")
.build();
}
/**
* 内存向量存储:入门零依赖,服务重启数据会丢
* 生产环境替换为Milvus/PGVector
*/
@Bean
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
}
五、第三步:文档入库服务
把业务文档切成片段,转成向量存入向量库,相当于给AI准备“考试参考资料”。
@Service
public class DocumentService {
@Autowired
private EmbeddingStore<TextSegment> embeddingStore;
@Autowired
private EmbeddingModel embeddingModel;
/**
* 纯文本文档入库
* @param content 文档完整内容
* @param docName 文档名称,用于溯源
*/
public void addTextDocument(String content, String docName) {
// 1. 文档切片:500字符一段,重叠50字符,保证语义完整
DocumentSplitter splitter = DocumentSplitters.recursive(500, 50);
List<TextSegment> segments = splitter.split(Document.from(content));
// 2. 给每个片段打元数据标记,方便后续溯源是哪份文档的内容
segments.forEach(segment ->
segment.metadata().put("docName", docName)
);
// 3. 向量化并存入向量库
EmbeddingStoreIngestor.builder()
.documentSplitter(splitter)
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build()
.ingest(segments);
}
}
小技巧:切片大小不是固定的,短文档可以切小一点,长文档可以切大一点。中文场景建议400-600字符,重叠率10%,检索效果最好。
六、第四步:RAG问答服务实现
用户提问时,先检索相关文档,再让大模型基于文档回答,从根源杜绝幻觉。
@Service
public class RagChatService {
@Autowired
private ChatLanguageModel chatModel;
@Autowired
private EmbeddingStore<TextSegment> embeddingStore;
@Autowired
private EmbeddingModel embeddingModel;
/**
* 基于知识库的问答
* @param question 用户问题
* @return 基于参考文档的回答
*/
public String chatWithKnowledge(String question) {
// 1. 把用户问题转成向量,检索最相关的3条文档片段
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(
embeddingModel.embed(question).content(),
3, // 检索Top3最相关文档
0.7 // 相似度阈值,低于0.7的文档不返回,避免乱凑内容
);
// 2. 拼接检索到的文档作为上下文
StringBuilder context = new StringBuilder("【参考文档】:\n");
for (int i = 0; i < matches.size(); i++) {
TextSegment segment = matches.get(i).embedded();
context.append("文档").append(i + 1).append("(")
.append(segment.metadata().getString("docName"))
.append("):")
.append(segment.text()).append("\n");
}
// 3. 构造强约束提示词,强制大模型只基于文档回答
String prompt = context + "\n" +
"请严格根据上面的参考文档回答用户问题:" + question + "\n" +
"要求:\n" +
"1. 答案必须完全来自参考文档,禁止编造任何内容\n" +
"2. 如果参考文档中没有相关答案,直接回答:暂无相关资料\n" +
"3. 回答简洁准确,分点说明优先";
// 4. 调用本地大模型返回结果
return chatModel.generate(prompt);
}
}
七、第五步:对外接口层
@RestController
@RequestMapping("/api/rag")
public class RagController {
@Autowired
private DocumentService documentService;
@Autowired
private RagChatService ragChatService;
/**
* 文档入库接口
*/
@PostMapping("/addDoc")
public Result<String> addDocument(@RequestBody DocAddRequest request) {
documentService.addTextDocument(request.getContent(), request.getDocName());
return Result.success("文档入库成功");
}
/**
* 知识库问答接口
*/
@GetMapping("/chat")
public Result<String> chat(String question) {
String answer = ragChatService.chatWithKnowledge(question);
return Result.success(answer);
}
// 请求实体
@Data
static class DocAddRequest {
private String content;
private String docName;
}
// 统一返回类
@Data
static class Result<T> {
private Integer code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("success");
result.setData(data);
return result;
}
}
}
八、测试验证:看看效果
第一步:上传测试文档
调用POST接口 http://localhost:8080/api/rag/addDoc
请求体示例(上传公司报销制度):
{
"docName": "公司报销管理制度",
"content": "1. 差旅费报销标准:一线城市住宿上限300元/天,二线城市240元/天,餐补80元/天。2. 办公费用单笔超过1000元需提前提交审批,部门经理签字后方可报销。3. 报销周期为每月15号、30号,提交后3个工作日内到账。4. 交通费用仅限公共交通、网约车,自驾出行按1.2元/公里报销油费。"
}
返回成功即入库完成。
第二步:提问验证
调用GET接口 http://localhost:8080/api/rag/chat?question=一线城市住宿报销标准是多少
正常返回:
根据参考文档,一线城市住宿报销上限为300元/天。
再问一个文档里没有的问题:年假有多少天
返回:
暂无相关资料
可以看到,AI完全基于上传的文档回答,不会编造内容,准确率100%。
九、新手必踩的4个坑,提前帮你避了
坑1:中文切片乱切,检索效果极差
默认的递归切片是按字符硬切,经常把完整的词语、句子切断,导致检索时匹配不上。 解决方案:中文场景把chunkSize调到400-600,重叠率10%-15%;生产环境接入HanLP分词按语义切片,准确率能提升30%以上。
坑2:嵌入模型和向量库维度不匹配,启动直接报错
换嵌入模型的时候,向量维度会变,比如nomic-embed-text是768维,通义embedding是1536维,维度不一样直接报错。 解决方案:换模型前先查官方维度,向量库同步重建对应维度的表,不要混用不同嵌入模型。
坑3:内存向量库重启就丢数据
入门用的InMemory存储,服务一重启所有文档都没了,只能做Demo。 解决方案:生产环境替换为Milvus(分布式高性能)或PGVector(兼容现有PG数据库),LangChain4j都有现成的对接实现,改个配置就行。
坑4:相似度阈值设太低,检索到无关文档
有人为了“总能返回答案”,把相似度阈值设到0.3,结果检索到一堆不相关的文档,AI还是会胡说八道。 解决方案:入门先设0.7,根据业务场景微调,低于阈值直接返回“暂无相关资料”,比瞎答强100倍。
十、生产环境进阶优化方向
现在这套是入门版,真正落地企业项目,还要继续迭代:
十一、本篇小结
今天我们从零搭好了一套完整的本地RAG知识库系统,解决了AI落地最核心的两个痛点:数据安全 + 幻觉问题。 全程跑在内网,零API费用,代码复制就能跑,已经可以用来做内部工具、小范围业务验证。
到这里,我们已经掌握了「大模型对接→流式输出→多轮记忆→RAG知识库」,一个能落地真实业务的AI系统骨架已经完整了。
下篇预告
现在文档入库还要手动复制文本,太麻烦了,真实业务里全是PDF、Word、Excel文件。 所以下一篇我们讲:
第8篇:支持PDF/Word/Excel!Java实现多格式文档自动解析入库
内容会覆盖:
- 主流文档解析工具选型
- PDF/Word/Excel批量读取代码
- 自动去重、清洗、切片全流程
- 上传文件自动入库完整实现