商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 使用Java实现RAG(检索增强生成)的完整指南

使用Java实现RAG(检索增强生成)的完整指南

  发布于2026-07-12 阅读(0)

扫一扫,手机访问

适合人群:需要构建知识库问答系统的 Ja va 开发者

使用Ja va实现RAG(检索增强生成)的完整指南

核心技术:RAG、向量数据库 Milvus、文本 Embedding

什么是 RAG?

说实话,RAG(Retrieval-Augmented Generation,检索增强生成)在当下的企业级 AI 应用中,已经快成为标配了。它解决了一个很实际的问题:大模型再聪明,也看不到你公司内部的文档、产品手册、规章制度这些私有数据。而且,它的知识是有“截止日期”的。

所以,RAG 的思路特别直接:

用户提问 → 先去你的私有知识库里翻一翻,找到相关的内容 → 把找到的材料和问题一起喂给大模型 → 让它基于这些材料给出一个靠谱的回答。

这招的好处显而易见:不用花大价钱去微调模型,成本极低;知识库可以随时更新,今天改,明天就能生效;最关键的是,答案有根有据,你能告诉用户这个结论是从哪份文档里扒出来的。

完整的 RAG 流程长什么样?

如果把整个流程画出来,大概是这么一条线:

PDF/Word文档

[文档加载器] PdfboxLoader / ApachePoiDocxLoader

[文本切分器] StanfordNLPTextSplitter

[Embedding 模型] OllamaEmbeddings

[向量数据库] Milvus(存储)

↓ (查询时)

用户问题 → [向量检索] → 相关文档块 → [LLM] → 最终回答

别被这一堆组件吓到,一步步拆解开来,其实并不复杂。

先把环境搭起来:前置配置

在正式开始之前,得先确保一些关键组件能被 Spring Boot 容器识别。这里依赖 Milvus 和 Tesseract OCR,需要在 application.yml 里明确告诉 Spring:“嘿,我要用它们了”:

rag:
  ocr:
    tesseract:
      use: true   # 开启 Tesseract OCR,用于 PDF 中的图片文字识别
  vector:
    milvus:
      use: true   # 启用 Milvus 向量数据库

多说一句,j-langchain 框架很聪明,它通过 @ConditionalOnProperty 来控制这些组件的“出生”。只有当你配了 use: trueTesseractActuatorMilvusContainer 这两个 Bean 才会被注册到 Spring 容器里。如果你没开这个开关就去运行 RAG 相关的代码,立马会收到一个 Bean 找不到的报错,别问我怎么知道的。

Step 1:把文档“吃”进来

万事开头难,第一步是把你的 PDF 或 Word 文档加载成程序能理解的结构。j-langchain 提供了现成的加载器,很贴心。

加载 PDF

比如加载一个 PDF,代码非常简洁:

@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 loader = ApachePoiDocxLoader.builder()
    .filePath("./files/docx/en/Transformer.docx")
    .build();

List documents = loader.load();

加载完后,每个 Document 对象都包含了两个关键信息:一是 pageContent 文本内容,二是 metadata 元数据,比如来自哪个文件、第几页等,方便后续溯源。

Step 2:切分文本,别让大模型“噎着”

想象一下,一份 PDF 可能一页就有几千个字。如果直接把这么大一段文本扔给 Embedding 模型,效果会很差,而且大模型的上下文窗口(context window)也有限。所以,我们需要把长文档切成一个一个的小块。

@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(重叠部分)?

这个问题很关键。如果一句话恰好跨了两个块,没有重叠,那这句话的意思就被拦腰斩断了,无论落在哪个块里都不完整。有了这 100 个字符的重叠,就能确保这句关键的话至少完整地出现在其中一个块中,不会丢失语义。

Step 3:把文本变成向量,存入 Milvus

现在文本已经被切好了,下一步就是把这些文本块转换成计算机能理解的语言——向量(一长串浮点数),然后存到向量数据库 Milvus 里。

@Test
public void embedAndStore() {
    // ... 假设上面加载、切分的步骤已经完成,得到了 splits ...

    VectorStore vectorStore = Milvus.fromDocuments(
        splits,
        OllamaEmbeddings.builder()
            .model("nomic-embed-text")  // 本地运行的 Embedding 模型,完全免费
            .vectorSize(768)            // 生成的向量维度
            .build(),
        "MyKnowledgeBase"               // 在 Milvus 中创建的 Collection 名称
    );

    System.out.println("向量化完成!");
}

为什么选择本地的 nomic-embed-text 模型?理由很充分:

  • 零成本:不用去调 OpenAI 的付费 API。
  • 隐私安全:所有数据都在你自己的机器上处理,没有泄露风险。
  • 效果好:这个模型在中文和英文的 Embedding 任务上表现都相当不错,足够用了。

当然,前提是你得先在本地通过 Docker 把 Milvus 跑起来:

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

Step 4:组装核心链路——RAG 问答

到这里,就进入了最核心的环节。我们需要用一个“管道”把整个流程串起来:用户提问 -> 去 Milvus 中检索 -> 拼接上下文 -> 让大模型基于上下文给出答案。

代码会稍微长一点,但逻辑非常清晰:

@Test
public void retrieveAndAsk() {
    // 假设之前已经把文档存入 Milvus,拿到了 vectorStore...
    BaseRetriever retriever = vectorStore.asRetriever();

    // 1. 定义好 Prompt 模板,告诉大模型:你要基于我给你的材料来回答
    BaseRunnable prompt = PromptTemplate.fromTemplate(
        """
        请根据以下文档内容回答问题。如果文档中没有相关信息,请说"文档中未找到相关信息"。
        
        文档内容:
        ${context}
        
        问题:${question}
        
        回答:
        """
    );

    // 2. 定义一个函数,把检索到的多个文档块拼接成一个长字符串
    Function formatDocs = input -> {
        List docs = (List) input;
        StringBuilder sb = new StringBuilder();
        for (Document doc : docs) {
            sb.append(doc.getPageContent()).append("\n\n");
        }
        return sb.toString();
    };

    // 3. 组装整个调用链路
    FlowInstance ragChain = chainActor.builder()
        .next(retriever)   // 输入问题,返回最相关的文档块
        .next(formatDocs)  // 将文档块列表拼接成一个字符串
        .next(input -> Map.of(
            "context",  input,
            "question", ContextBus.get().getFlowParam()  // 获取原始的提问
        ))
        .next(prompt)      // 组装 Prompt
        .next(llm)         // 交给大模型处理
        .next(new StrOutputParser()) // 从结果中提取出最终的文本
        .build();

    // 4. 执行链路,提出你的问题
    ChatGeneration result = chainActor.invoke(
        ragChain,
        "Transformer 模型中的注意力机制是如何工作的?"
    );

    System.out.println(result.getText());
}

这个链路的运行过程像是一条流水线:

“Transformer注意力机制...”

→ retriever(去 Milvus 里做相似度检索,捞出最相关的 5 个文档块)

→ formatDocs(把5个文档块拼成一大段文字)

→ 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字以内):\n\n${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 凭什么比直接问大模型强?

最后,我们做个对比,一目了然:

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

总结一下整个架构

离线阶段(建库):

文档 → 加载 → 切分 → Embedding → Milvus

在线阶段(问答):

问题 → Embedding → Milvus检索 → 拼接上下文 → LLM → 回答

整个流程就是这两个阶段,离线把知识库建好,在线直接开问。思路清晰,实现起来也不复杂,上手试试看吧。

本文转载于:https://www.jb51.net/program/362842efx.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注