系列导航

本篇先不引入向量数据库,只用 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,就能清楚看到向量数据库究竟替换了哪一段工作。

本篇自测

  1. RAG 一定需要向量数据库吗?
  2. bge-m3 和 qwen3 在这条链路中分别做什么?
  3. qwen3 最终看到的是向量还是 Prompt?
  4. 没有资料达到阈值时为什么不调用聊天模型?

参考答案:不一定,小规模资料可以先做内存检索;bge-m3 负责向量化和检索,qwen3 根据资料生成回答;模型看到的是组装后的 Prompt;避免基于无关资料回答并减少无效调用。


本篇小结

  1. 完整 RAG 包含加载切分、向量检索、阈值过滤、Prompt 组装和模型生成。
  2. 向量只用于寻找相关资料,真正发送给 qwen3 的是人类可读的 Chunk 正文。
  3. 内存版适合学习流程,但每次运行都会重新计算全部知识向量。

下一篇

👉 本专栏下一篇:《第7篇:使用 Qdrant 保存和检索向量》

完整代码都在 GitHub(欢迎 Star ⭐)

本专栏的全部示例代码都已开源,包含 5 个可独立运行的 Maven 模块、自动化测试和完整分篇教程。建议 Fork / Clone 下来,边读边跑:

🔗 https://github.com/bysbsh/ai-rag-learning-guide

  • 代码与教程同步更新,对照每一篇动手实践效果最好。
  • 如果这份教程帮到了你,点个 Star 就是对我最大的支持,也方便你之后找回最新版本。
  • 遇到问题或发现错漏,欢迎在仓库提 Issue / PR。项目采用 MIT 协议,可自由学习与二次创作。
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐