发布于2026-07-12 阅读(0)
扫一扫,手机访问
适合人群:需要构建知识库问答系统的 Ja va 开发者

核心技术:RAG、向量数据库 Milvus、文本 Embedding
说实话,RAG(Retrieval-Augmented Generation,检索增强生成)在当下的企业级 AI 应用中,已经快成为标配了。它解决了一个很实际的问题:大模型再聪明,也看不到你公司内部的文档、产品手册、规章制度这些私有数据。而且,它的知识是有“截止日期”的。
所以,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: true,TesseractActuator 和 MilvusContainer 这两个 Bean 才会被注册到 Spring 容器里。如果你没开这个开关就去运行 RAG 相关的代码,立马会收到一个 Bean 找不到的报错,别问我怎么知道的。
万事开头难,第一步是把你的 PDF 或 Word 文档加载成程序能理解的结构。j-langchain 提供了现成的加载器,很贴心。
比如加载一个 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 也是类似的套路:
ApachePoiDocxLoader loader = ApachePoiDocxLoader.builder()
.filePath("./files/docx/en/Transformer.docx")
.build();
List documents = loader.load();
加载完后,每个 Document 对象都包含了两个关键信息:一是 pageContent 文本内容,二是 metadata 元数据,比如来自哪个文件、第几页等,方便后续溯源。
想象一下,一份 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 个字符的重叠,就能确保这句关键的话至少完整地出现在其中一个块中,不会丢失语义。
现在文本已经被切好了,下一步就是把这些文本块转换成计算机能理解的语言——向量(一长串浮点数),然后存到向量数据库 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 模型?理由很充分:
当然,前提是你得先在本地通过 Docker 把 Milvus 跑起来:
docker run -d --name milvus \ -p 19530:19530 \ milvusdb/milvus:latest standalone
到这里,就进入了最核心的环节。我们需要用一个“管道”把整个流程串起来:用户提问 -> 去 Milvus 中检索 -> 拼接上下文 -> 让大模型基于上下文给出答案。
代码会稍微长一点,但逻辑非常清晰:
@Test
public void retrieveAndAsk() {
// 假设之前已经把文档存入 Milvus,拿到了 vectorStore...
BaseRetriever retriever = vectorStore.asRetriever();
// 1. 定义好 Prompt 模板,告诉大模型:你要基于我给你的材料来回答
BaseRunnable prompt = PromptTemplate.fromTemplate(
"""
请根据以下文档内容回答问题。如果文档中没有相关信息,请说"文档中未找到相关信息"。
文档内容:
${context}
问题:${question}
回答:
"""
);
// 2. 定义一个函数,把检索到的多个文档块拼接成一个长字符串
Function
这个链路的运行过程像是一条流水线:
“Transformer注意力机制...”
→ retriever(去 Milvus 里做相似度检索,捞出最相关的 5 个文档块)
→ formatDocs(把5个文档块拼成一大段文字)
→ prompt(组装成 Prompt:上下文 + 问题)
→ LLM(大模型开始阅读材料并思考)
→ StrOutputParser(把思考结果提取成纯文本)
→ “注意力机制通过计算 Query、Key、Value...”
有时候,我们可能不需要完整的 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());
}
最后,我们做个对比,一目了然:
| 对比项 | 直接问 LLM | RAG |
|---|---|---|
| 私有知识 | 不知道 | 知道(来自你的文档) |
| 知识时效性 | 训练截止日期 | 实时更新 |
| 回答可溯源 | 不行 | 可以(返回来源文档) |
| 成本 | 低 | 稍高(Embedding + 向量库) |
| 幻觉风险 | 高 | 低(基于真实文档) |
离线阶段(建库):
文档 → 加载 → 切分 → Embedding → Milvus
在线阶段(问答):
问题 → Embedding → Milvus检索 → 拼接上下文 → LLM → 回答
整个流程就是这两个阶段,离线把知识库建好,在线直接开问。思路清晰,实现起来也不复杂,上手试试看吧。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8