Java RAG 实战(第 6 篇):Markdown 知识库与 RAG
系列导航
- 所属专栏:《Java 开发者从零实现 RAG 知识库》
- 学习位置:第 6 篇 / 共 12 篇
- 上一篇:《第5篇:Embedding 与语义检索》
- 下一篇:《第7篇:使用 Qdrant 保存和检索向量》
本篇先不引入向量数据库,只用 Markdown、内存向量检索和 qwen3 跑通完整 RAG,重点看清检索资料怎样进入 Prompt。
完成进度
- 1. 把 Markdown 按二级标题切成 Chunk
- 2. 使用 bge-m3 检索最相关的 Chunk
- 3. 使用阈值过滤无关资料
- 4. 把命中资料和问题组装成 Prompt
- 5. 调用 qwen3 生成资料内回答
为什么要学
Embedding 和相似度只能找出“哪段文字更相关”,还不会直接生成最终答案。RAG 的关键是把检索结果转换成人类可读的 Prompt,再让聊天模型严格依据这些资料回答。
先使用内存版,可以暂时避开数据库配置,把注意力集中在完整主链:切分、检索、过滤、组装 Prompt 和生成回答。理解这条链路后,再引入 Qdrant 才能清楚知道向量数据库替换了哪一部分。
第 1 步:运行内存版 Markdown RAG
在引入向量数据库前,先运行 03-markdown-rag,确认完整 RAG 并不等于 Qdrant。这个模块把知识放在 Java 内存中,适合观察每一步的数据怎样流动。
示例知识文件位于:
03-markdown-rag/knowledge/kubernetes.md
程序执行顺序:
MarkdownKnowledgeLoader 读取 Markdown 并按二级标题切成 Chunk
↓
ChunkSearch 用 bge-m3 计算问题与全部 Chunk 的相似度
↓
RelevantChunkSelector 过滤低于 0.60 的结果并选择 Top-2
↓
RagPromptBuilder 把命中资料和原始问题组装成 Prompt
↓
OllamaChatClient 调用 qwen3:14b 生成最终回答
运行默认问题:
mvn -f 03-markdown-rag/pom.xml compile exec:java
也可以传入自己的问题:
mvn -f 03-markdown-rag/pom.xml compile exec:java \
-Dexec.args='密码、令牌和证书应该保存在哪里?'
成功时会先打印候选 Chunk 和相似度,再打印模型回答。这里的候选向量没有保存到数据库,因此每次运行都会重新读取 Markdown,并重新为全部 Chunk 生成 Embedding。
命中资料不是由 Ollama 自动读取的。RagPromptBuilder 会在 Java 中组装类似下面的内容:
System Prompt:只能根据提供的知识库资料回答。
User Prompt:
知识库资料:
[Service Chunk 的标题和正文]
用户问题:
Pod 重建后 IP 变化,怎样提供稳定访问地址?
qwen3 看到的是这个 Prompt,不是向量。向量只在“找资料”这一步中使用。
关键代码:检索结果怎样变成模型回答
对应源码:
03-markdown-rag/src/main/java/com/example/ai/rag/MarkdownRagDemo.java
03-markdown-rag/src/main/java/com/example/ai/rag/RagPromptBuilder.java
MarkdownRagDemo 先过滤结果,没有合格资料时直接返回:
为了突出主调用链,下面片段缩减了日志打印;类名、方法和调用顺序与仓库源码一致。
List<ChunkSearch.Result> relevantResults = selector.select(
results,
MIN_SIMILARITY,
TOP_K
);
if (relevantResults.isEmpty()) {
System.out.println("根据现有资料无法确定。");
return;
}
RagPromptBuilder.Prompt prompt = new RagPromptBuilder().build(
query,
relevantResults,
TOP_K
);
String answer = chatClient.chat(prompt);
RagPromptBuilder 再把最多 TOP_K 条资料按顺序拼接:
StringBuilder userPrompt = new StringBuilder("知识库资料:");
int chunkCount = Math.min(maxChunks, results.size());
for (int index = 0; index < chunkCount; index++) {
MarkdownKnowledgeLoader.Chunk chunk = results.get(index).chunk();
userPrompt.append("\n\n[资料 ")
.append(index + 1)
.append(":")
.append(chunk.title())
.append("]\n")
.append(chunk.content());
}
userPrompt.append("\n\n用户问题:\n").append(query);
这是 RAG 中“检索”与“生成”真正接上的位置。前面的 relevantResults 来自向量检索,后面的 prompt 才会发给 qwen3。
运行这一阶段的测试:
mvn -f 03-markdown-rag/pom.xml test
测试覆盖 Markdown 切分、检索排序、阈值过滤、Prompt 组装和 Ollama 请求解析。完成这一步后,再把相同知识迁移到 Qdrant,就能清楚看到向量数据库究竟替换了哪一段工作。
本篇自测
- RAG 一定需要向量数据库吗?
- bge-m3 和 qwen3 在这条链路中分别做什么?
- qwen3 最终看到的是向量还是 Prompt?
- 没有资料达到阈值时为什么不调用聊天模型?
参考答案:不一定,小规模资料可以先做内存检索;bge-m3 负责向量化和检索,qwen3 根据资料生成回答;模型看到的是组装后的 Prompt;避免基于无关资料回答并减少无效调用。
本篇小结
- 完整 RAG 包含加载切分、向量检索、阈值过滤、Prompt 组装和模型生成。
- 向量只用于寻找相关资料,真正发送给 qwen3 的是人类可读的 Chunk 正文。
- 内存版适合学习流程,但每次运行都会重新计算全部知识向量。
下一篇
👉 本专栏下一篇:《第7篇:使用 Qdrant 保存和检索向量》
完整代码都在 GitHub(欢迎 Star ⭐)
本专栏的全部示例代码都已开源,包含 5 个可独立运行的 Maven 模块、自动化测试和完整分篇教程。建议 Fork / Clone 下来,边读边跑:
🔗 https://github.com/bysbsh/ai-rag-learning-guide
- 代码与教程同步更新,对照每一篇动手实践效果最好。
- 如果这份教程帮到了你,点个 Star 就是对我最大的支持,也方便你之后找回最新版本。
- 遇到问题或发现错漏,欢迎在仓库提 Issue / PR。项目采用 MIT 协议,可自由学习与二次创作。
更多推荐

所有评论(0)