Java RAG实战指南:PDF加载与智能问答全流程解析

2026-05-28阅读 0热度 0
其他

什么是 RAG?

如果你正在为企业寻找AI落地方案,那么RAG(检索增强生成)这个名字你一定不陌生。它几乎是当前最主流、最实用的企业级AI应用范式。

用 Ja va 实现 RAG:从 PDF 加载到智能问答全流程

它要解决的核心痛点非常明确:通用大模型的知识库有截止日期,而且不可能包含你公司的私有数据,比如内部文档、产品手册或者规章制度。RAG的思路很巧妙,它绕开了重新训练模型的巨大成本,转而采用“外设知识库”的方式:当用户提问时,系统会先去你的私有知识库中检索相关内容,然后把检索到的片段和问题一起交给大模型,最终生成一个基于你私有知识的精准回答。

这种模式的优势显而易见:

  • 成本极低:无需对模型进行微调,省时省力。
  • 更新灵活:知识库可以随时增删改查,新知识能立刻生效。
  • 答案可溯源:系统能明确告诉你,答案具体出自哪份文档的哪个部分,可信度大大提升。

完整 RAG 流程

一个标准的RAG流程,可以清晰地分为离线建库和在线问答两个阶段。用一条流水线来概括就是:

PDF/Word文档 ↓[文档加载器] PdfboxLoader / ApachePoiDocxLoader ↓[文本切分器] StanfordNLPTextSplitter ↓[Embedding 模型] OllamaEmbeddings ↓[向量数据库] Milvus(存储) ↓ (查询时)用户问题 → [向量检索] → 相关文档块 → [LLM] → 最终回答

前置配置

在动手搭建之前,有两点需要提前准备好。我们的RAG管道依赖于Milvus向量数据库和Tesseract OCR(用于识别PDF中的图片文字),需要在项目的application.yml配置文件中明确启用:

rag: ocr: tesseract: use: true # 启用 Tesseract OCR(PDF 图片文字识别) vector: milvus: use: true # 启用 Milvus 向量数据库

Step 1:加载文档

万事开头难,但好在j-langchain这类工具包已经为我们封装好了多种文档加载器,让第一步变得很简单。

加载 PDF

处理PDF,我们可以使用PdfboxLoader。下面是一个典型的加载示例:

@Test public void loadPdfDocuments() { PdfboxLoader loader = PdfboxLoader.builder() .filePath("./files/pdf/en/Transformer.pdf") .build(); loader.setExtractImages(false); // 不提取图片,只提取文本 List documents = loader.load(); System.out.println("总页数:" + documents.size()); // 每个 Document 对应 PDF 的一页 }

加载 Word 文档

对于Word文档,ApachePoiDocxLoader是更好的选择,用法同样直观:

ApachePoiDocxLoader loader = ApachePoiDocxLoader.builder() .filePath("./files/docx/en/Transformer.docx") .build(); List documents = loader.load();

无论哪种格式,加载后得到的每个Document对象都包含两个核心部分:

  • pageContent:页面的纯文本内容。
  • metadata:来源文件、页码等元数据,这对后续的答案溯源至关重要。

Step 2:文本切分

直接拿一整页甚至整个文档去做向量化,效果往往很差。一方面,过长的文本会丢失语义焦点;另一方面,它很容易超出大模型的上下文窗口限制。因此,我们需要把长文档切成更易处理的小块。

@Test public void splitDocuments() { List documents = loader.load(); System.out.println("切分前:" + documents.size() + " 页"); StanfordNLPTextSplitter splitter = StanfordNLPTextSplitter.builder() .chunkSize(1000) // 每块最多 1000 字符 .chunkOverlap(100) // 相邻块重叠 100 字符,保证上下文连贯 .build(); List splits = splitter.splitDocument(documents); System.out.println("切分后:" + splits.size() + " 块"); }

这里有个关键参数chunkOverlap(块重叠)。为什么要设置重叠?想象一下,如果一句话正好被切分点从中间断开,那么前后两个块都可能无法完整理解这句话的语义。设置一定的重叠度,就能确保这类关键信息至少能完整地出现在某一个块里,有效避免了语义被硬生生截断的问题。

Step 3:向量化并存入 Milvus

接下来是构建知识库的核心步骤:将上一步得到的文本块,通过Embedding模型转换成高维向量(可以理解为一串有意义的数字),然后存储到向量数据库中。

@Test public void embedAndStore() { // ... 加载、切分文档 ... VectorStore vectorStore = Milvus.fromDocuments(splits, OllamaEmbeddings.builder() .model("nomic-embed-text") // 本地 Embedding 模型,免费 .vectorSize(768) // 向量维度 .build(), "MyKnowledgeBase" // Milvus collection 名称 ); System.out.println("向量化完成!"); }

你可能会问,为什么选择本地Embedding模型nomic-embed-text?这背后有几个非常实际的考量:

  • 零成本:完全本地运行,不需要调用OpenAI等付费API。
  • 隐私安全:所有数据都在本地处理,没有隐私泄露风险。
  • 效果优秀:这个开源模型在中英文文本的向量表示上表现相当出色。

至于向量数据库Milvus,用Docker可以一键启动,非常方便:

docker run -d --name milvus -p 19530:19530 milvusdb/milvus:latest standalone

Step 4:完整 RAG 问答链

现在来到了最激动人心的环节:让整个系统跑起来,实现智能问答。这个过程的核心逻辑是:用用户的问题去向量库中检索最相关的文档块,将这些块拼接成上下文,最后指导大模型基于此上下文生成答案。

@Test public void retrieveAndAsk() { // 假设文档已经存入 Milvus... BaseRetriever retriever = vectorStore.asRetriever(); BaseRunnable prompt = PromptTemplate.fromTemplate(""" 请根据以下文档内容回答问题。如果文档中没有相关信息,请说"文档中未找到相关信息"。 文档内容:${context} 问题:${question} 回答:"""); Function formatDocs = input -> { List docs = (List) input; StringBuilder sb = new StringBuilder(); for (Document doc : docs) { sb.append(doc.getPageContent()).append("nn"); } return sb.toString(); }; FlowInstance ragChain = chainActor.builder() .next(retriever) // 向量检索:输入问题,返回相关文档列表 .next(formatDocs) // 将文档列表拼接为字符串 .next(input -> Map.of( "context", input, "question", ContextBus.get().getFlowParam() // 获取原始问题 )) .next(prompt) .next(llm) .next(new StrOutputParser()) .build(); ChatGeneration result = chainActor.invoke(ragChain, "Transformer 模型中的注意力机制是如何工作的?"); System.out.println(result.getText()); }

我们可以把这条链路的每一步拆解来看:

"Transformer注意力机制..." → retriever(相似度检索,返回最相关的5个文档块)→ formatDocs(拼接文档块为字符串)→ prompt(组装 Prompt:上下文 + 问题)→ LLM(基于上下文生成回答)→ StrOutputParser(提取文本)→ "注意力机制通过计算 Query、Key、Value..."

Step 5:文档摘要(轻量版)

并不是所有场景都需要动用完整的RAG架构。有时候,你只是想让大模型快速读一下文档并做个摘要。这种轻量级任务,完全可以绕过向量数据库。

@Test public void documentSummary() { // 加载 PDF List documents = loader.load(); String content = documents.stream() .map(Document::getPageContent) .collect(Collectors.joining("n")); // 长文档截取首尾 String textToSummarize = content.length() < 2000 ? content : content.substring(0, 1000) + "n...n" + content.substring(content.length() - 1000); FlowInstance chain = chainActor.builder() .next(PromptTemplate.fromTemplate("请对以下内容摘要(100字以内):nn${text}")) .next(ChatOllama.builder().model("qwen2.5:0.5b").build()) .next(new StrOutputParser()) .build(); ChatGeneration result = chainActor.invoke(chain, Map.of("text", textToSummarize)); System.out.println(result.getText()); }

RAG vs 直接问 LLM

对比项 直接问 LLM RAG
私有知识 不知道 知道(来自你的文档)
知识时效性 训练截止日期 实时更新
回答可溯源 不行 可以(返回来源文档)
成本 稍高(Embedding + 向量库)
幻觉风险 低(基于真实文档)

完整架构

最后,让我们从架构层面再回顾一下RAG的全貌,它清晰地分为两个阶段:

离线阶段(建库):文档 → 加载 → 切分 → Embedding → Milvus 在线阶段(问答):问题 → Embedding → Milvus检索 → 拼接上下文 → LLM → 回答

离线阶段负责构建知识库,将非结构化的文档转化为结构化的向量存储。在线阶段则响应用户查询,通过语义检索找到相关知识,并交由大模型合成最终答案。这套组合拳,正是RAG能够精准、可控地利用私有知识的关键所在。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策