☰
Spring AI + Ollama 本地搭建 RAG:从文档切分到知识库问答实战
2026/10/1 13:11:30 网站建设 项目流程

上个月我接了个内部需求:把公司那本 80 页的产品手册喂给大模型,做成一个能回答员工提问的问答机器人。我一开始想得很简单,把手册全文塞进 Prompt 就行。结果 40 多页读进去,上下文窗口直接报警,模型越到后面越像失忆,开始拿通用知识忽悠人。后来我换成 RAG(检索增强生成)方案,用 Spring AI 做了一套完整的“文档觉醒”流程,才真正体会到这件事的边界在哪里。

这篇是第 4 集,前几集写的是模型接入和提示词玩法,这一集专门讲 RAG 从零落地:本地跑通、代码可复现、坑都踩给你看。整个过程全程用 Ollama 本地模型,不需要外部 API Key,数据不出内网,适合刚接触 RAG 的 Java 工程师,也适合想快速给业务方演示“AI 能回答私有文档问题”的小伙伴。我会把索引、检索、问答三段完整拆开,讲清楚每一步为什么这么做,以及我第一次实测时是怎么翻车、怎么排查的。

1. 为什么非让AI“看文档”不行:大模型的记忆边界与私有知识盲区

1.1 大模型的知识边界在哪里

大模型聊天再厉害,它的知识也停留在训练数据截止的那一刻。你问它“2025 年发布的某设备怎么配置”,它大概率会一本正经地编一个答案。原因很简单:它没见过你的产品手册、你的内部流程、你的历史项目文档,这些东西根本不在它的记忆里。

我习惯把大模型比作“通识课毕业生”,而不是“你们公司员工手册考试通过者”。它懂语言、懂逻辑、懂常识,但唯独不懂你塞在共享盘里的那些私有资料。想让它懂,传统上有两条路:微调和 RAG。微调是让模型“记住”你的数据分布,RAG 是让模型在回答时“临时翻书”。对大多数业务场景,RAG 的性价比明显更高。

1.2 为什么先选 RAG 而不是微调

很多人一听“让 AI 学会看文档”,第一反应是微调。但实际上,微调是给模型“换脑子”,需要高质量标注数据、GPU 资源、以及反复的实验;RAG 是给模型“配一本查阅手册”,只需要把文档清洗、切块、建索引。下面这张表我每次做技术选型都会拿出来比一遍:

对比维度微调RAG
数据需求需要成百上千对高质量问答样本有原始文档即可,清洗成本低
知识更新更新一次要重新训练重新跑一遍索引即可
幻觉控制仍然会编造,且无法溯源可以引用原始文档片段,可解释
实施门槛需要 GPU 资源、训练经验普通 Java 工程就能跑
回答速度推理速度受模型规模影响多一步检索,但整体可控

微调不是没用,但在“已有文档、需要基于文档回答”这种场景下,RAG 几乎是更稳的第一步。尤其当你手里是一堆持续更新的产品文档时,RAG 的“改文档重新入库”优势是微调完全比不了的。

1.3 RAG 到底适合解决什么问题

我做完第一个 RAG 原型之后,体会最深的一点是:它最适合“从已有资料中找答案”的知识密集型任务。典型场景包括:

  • 产品手册、操作文档的智能客服,用户问“怎么重置密码”,系统从手册里找到对应章节回答
  • 企业内部制度、流程问答,比如“报销额度超过 5000 要走什么审批”
  • 技术方案评审辅助,从过去的历史方案文档里检索经验教训
  • 代码仓库问答,把 README、设计文档、关键模块注释切片后提供检索

但也要说清楚,RAG 不适合需要多步数学推理、严格逻辑推导的任务。比如让 AI“基于文档数据算一下季度增长率并对比三年趋势”,它就算检索到了数据,也可能算错。这是生成模型本身的能力边界,不是 RAG 能全包的。

2. RAG链路拆解:切分、向量化与召回,搞懂原理才能合理调参

2.1 两阶段运行模型:先“上架图书”,再“查目录取书”

RAG 全流程可以拆成两段,一段离线、一段在线。离线阶段叫索引构建:把文档加载进来,切成一段一段的小块,用向量模型把每一块“翻译”成一组数字,然后存进向量库。在线阶段叫查询问答:把用户问题也“翻译”成数字,去向量库里找最接近的几块文本,把这些文本拼到一个提示词里,最后交给大模型生成回答。

我常用一个图书馆类比:离线阶段是“给新书编目上架”,每一页都做了标签;在线阶段是“用户报一个问题,图书管理员先去目录里找最相关的几页,再把这几页递给一位专家,专家根据这几页给出回答”。如果你把整本书直接扔给专家,他翻不过来;如果图书管理员找错了页,专家再厉害也答不准。后面所有调参,本质上都是在调“图书管理员”的找书水平。

2.2 文档切分:为什么不能把整篇文档直接向量化

很多人第一次做 RAG,会试图把整篇 PDF 做一次向量化再存进去。结果检索时要么匹配模糊,要么把大段的无关内容一起塞给模型。根本原因在于:一段文字越长,它的向量就越“平均”,丢失了局部语义。比如整本手册的向量可能接近“公司产品介绍”,而你问的是“重置密码”,相关细节早就被平均掉了。

所以必须先切块,也就是 Chunking。Spring AI 里我常用TokenTextSplitter,它按 token 数切分,而不是按字符数。核心参数有两个:

  • chunkSize:每一块包含多少 token,决定语义粒度
  • chunkOverlap:相邻两块之间重叠多少 token,保证跨块上下文不丢

切块太小,答案会被拆散,模型看到的是残缺步骤;切块太大,噪声变多,检索命中率下降。300 个 token 加 50 个 token 重叠是我比较推荐的起点,但绝对不是标准答案。文档句式、术语密度都影响最优值,后面第 6 章会用实测数据说明。

这里顺带回应一个热词:很多人问“有没有本地的 RAG 文本拆解工具”。TokenTextSplitter就是纯本地执行的,不调用外部 API,对数据敏感的场景特别友好。

2.3 向量化与向量库:把文字变成可计算的距离

切块之后,每一块文本需要交给 Embedding 模型生成向量。向量是什么?简单理解,就是一组浮点数,比如 768 维或 1024 维,语义越接近的文本,向量方向越接近。你问“怎么改密码”,和文档里“重置管理员口令”这段,字面上完全不同,但向量空间里挨得很近,这就是语义检索的价值。

向量库负责存储这些向量并提供相似度搜索。Spring AI 里有几个选择:

向量库特点适合阶段
SimpleVectorStore内存实现,零配置入门学习、Demo 演示
Chroma本地文件持久化,轻量小团队、本地原型
PgVectorPostgres 扩展,复用现有库生产环境常见选择
Milvus / Redis分布式或复用缓存体系大规模、高并发

我建议入门阶段无脑用SimpleVectorStore,因为它的依赖最少,跑通流程最重要。但你要知道它是纯内存的,重启就没了,生产别用它。

2.4 检索召回与生成:RAG 的上限由召回决定

检索阶段最核心的操作是相似度搜索。Spring AI 里通过VectorStore.similaritySearch完成,你可以指定topK,也就是返回最相似的几块文档。这一步决定了模型“看”什么。模型最后生成的质量,高度依赖召回质量——如果答案所在的那块文档根本没被找回来,模型就只能瞎编。

这里有个很容易被忽略的观点:RAG 的成功率上限其实是召回率决定的。生成模型只要没被超出上下文窗口,基本能把召回的内容复述明白;真正让回答跑偏的,往往不是模型不会答,而是检索阶段压根没把正确答案捞上来。所以我每次排查问题,第一步永远是看“召回了什么”,而不是急着调 Prompt。这也是后文排查记录的核心思路。

3. 实测环境准备:Ollama拉模型 + Spring Boot工程初始化,一步不能少

3.1 安装 Ollama 并拉取对话模型、向量模型

先说模型层。为了不依赖外部 API Key,我把 ChatGPT 这类云端接口放一边,直接用 Ollama 在本地跑开源模型。Ollama 的安装很简单,官网下载对应系统的安装包即可,macOS、Linux、Windows 都有客户端。

装好之后,拉两个模型,一个负责对话生成,一个负责向量化:

ollama pull qwen2.5:7b ollama pull nomic-embed-text

qwen2.5:7b是对话模型,7B 参数,16G 内存的机器跑起来比较舒服;如果机器只有 8G 内存,建议换qwen2.5:3b,损失一点回答质量但更流畅。nomic-embed-text是向量模型,几百 MB,专门用来把文本变成向量。

验证是否装好,执行ollama list,能看到这两个模型就说明没问题。你也可以直接调 API 试一下:

curl http://localhost:11434/api/tags

能返回 JSON 模型列表,就说明 Ollama 服务起来了。

3.2 创建 Spring Boot 工程和 Spring AI 依赖

Spring AI 已经出了 1.0 GA,版本迭代很快。我当前的工程结构是 JDK 17 + Spring Boot 3.x + Spring AI BOM 1.0.0。Maven 的pom.xml核心部分如下:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.3</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pdf-document-reader</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-vector-store-simple</artifactId> </dependency> </dependencies>

其中spring-ai-starter-model-ollama负责自动装配 Ollama 的客户端、ChatModel 和 EmbeddingModel;spring-ai-pdf-document-reader负责解析 PDF;spring-ai-vector-store-simple提供内存向量库实现。

3.3 application.yml 配置:版本差异是这里最大的坑

接下来是最容易踩坑的配置文件。Spring AI 版本迭代快,Ollama 相关属性名改过好几次。我当前跑通的配置如下:

spring: ai: ollama: base-url: http://localhost:11434 chat: model: qwen2.5:7b embedding: model: nomic-embed-text

base-url不写也有默认值,但我建议显式写出来。chat.model指对话模型,embedding.model指向量模型,这两个配置必须和你在 Ollama 里拉取的模型名称严格一致,哪怕少一个 tag 后缀都会报模型找不到。

注意:如果你用的是 Spring AI 1.0 之前的版本,属性名可能是spring.ai.ollama.chat.options.model。如果你的 IDE 没有弹出对应属性的自动提示,多半是版本里 key 变了。我会建议你在配置类里看一眼自动配置的属性前缀,或者直接用启动日志验证最终用的模型名,而不是死记配置 key。

3.4 验证模型链路是否连通

配置完了别急着写业务代码,先做一次最基础的连通性测试。写一个CommandLineRunner,启动时直接调一次对话模型:

@Component public class ModelCheckRunner implements CommandLineRunner { private final ChatModel chatModel; public ModelCheckRunner(ChatModel chatModel) { this.chatModel = chatModel; } @Override public void run(String... args) { String response = chatModel.call("请回复'连接正常'四个字"); System.out.println("对话模型返回: " + response); } }

如果启动后看到“连接正常”,说明 ChatModel 通了。Embedding 模型暂时不用单独验证,等到索引构建时自然会用上。如果报错说 model not found,大概率是ollama pull没执行成功,或配置里的模型名和ollama list不一致。

4. 索引构建实战:文档加载、TokenTextSplitter 切分与向量库入库

4.1 文档加载:从 PDF 到可处理的 Document 列表

索引构建的第一步是读文档。Spring AI 里的PagePdfDocumentReader可以把 PDF 按页读成一个个Document对象。Document是 Spring AI 的统一文档模型,里面有文本内容,还可以带元数据。

我把测试用的《智能考勤一体机操作手册.pdf》放到了src/main/resources/docs/目录下,加载代码很简单:

PagePdfDocumentReader reader = new PagePdfDocumentReader( new ClassPathResource("docs/manual.pdf")); List<Document> documents = reader.read();

这个 reader 对文本型 PDF 很好用,但如果是扫描件(本质是图片),它就无能为力了。你需要先 OCR 把图片转成文字,再做后续处理。顺带回应一个热词:“RAG 知识库能存储图片吗?”文本向量库本身不存图片语义,如果文档里有图表信息,要么用 OCR 把图片里的文字抽出来,要么走多模态 Embedding,普通文本 RAG 是不直接处理图片的。

如果你要加载的不是 PDF 而是 Markdown 或纯文本,Spring AI 也有对应的TextReader,把文件路径传进去就能读。思路完全一样,都是先变成Document列表。

4.2 TokenTextSplitter 参数:300 和 50 的含义

文档读进来之后,直接整页存进向量库是常见错误。一页 PDF 往往超过 500 个 token,语义跨度也可能很大,例如一页既有功能介绍又有参数表。我用TokenTextSplitter做切分:

TokenTextSplitter splitter = TokenTextSplitter.builder() .withChunkSize(300) .withChunkOverlap(50) .build(); List<Document> chunks = splitter.apply(documents);

chunkSize=300表示目标每个文档块约 300 个 token。我选这个值的理由有两个:第一,300 个 token 能覆盖“一个操作步骤 + 几句说明”这样相对完整的信息单元;第二,这个体量塞进后续 Prompt 时,3~5 个块加起来不会超出常见模型的上下文窗口。chunkOverlap=50表示相邻块共享 50 个 token,避免一句话被拦腰切断后,下一块开头接不上。

如果你问我“这套参数通用吗”,我会说:不通用。中文文档里术语密度高、表格多,最优参数往往要试 2~3 轮。第一次先用 300/50 跑通全流程,再根据检索效果调整,这是最务实的路径。

4.3 选择 SimpleVectorStore :入门阶段的耐心之选

向量库我这里选SimpleVectorStore,配置方式极其简单:

VectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build();

它把向量放在内存里,不需要额外安装数据库,也不需要配置连接信息。缺点前面说过,进程重启数据就没了。但入门阶段这正是优点:没有外部依赖,出问题一定出在自己的代码或模型配置上,排查范围小。

Spring AI 的设计是:向量库不感知你用的是哪种 Embedding 模型,它只负责把Document里的内容用传入的embeddingModel转成向量再存起来。所以构建VectorStore时一定要传入同一个EmbeddingModelbean。

4.4 完整的索引构建代码

我把索引构建放在一个RagService里,后续问答也复用这个类。为了不被 import 干扰思路,下面的代码我故意省略 import,IDE 里用自动导入即可:

@Service public class RagService { private final VectorStore vectorStore; private final ChatModel chatModel; public RagService(EmbeddingModel embeddingModel, ChatModel chatModel) { this.vectorStore = SimpleVectorStore.builder(embeddingModel).build(); this.chatModel = chatModel; } public void buildIndex() { PagePdfDocumentReader reader = new PagePdfDocumentReader( new ClassPathResource("docs/manual.pdf")); List<Document> documents = reader.read(); TokenTextSplitter splitter = TokenTextSplitter.builder() .withChunkSize(300) .withChunkOverlap(50) .build(); List<Document> chunks = splitter.apply(documents); vectorStore.add(chunks); System.out.println("索引完成,chunk 总数: " + chunks.size()); } }

这里省略了所有 import,因为 Spring AI 不同版本的包名确实动过几次,尤其是TokenTextSplitter,从transformer.splitter到document.splitter都出现过。我用 IDE 的自动导入能少走弯路,也建议你这样做。

vectorStore.add(chunks)这一步会依次调用 EmbeddingModel 为每个 chunk 生成向量。nomic-embed-text是本地模型,速度很快,一篇 20 页的 PDF 几秒钟就能完成索引。

4.5 用一个 Controller 暴露操作入口

为了方便测试,我加了一个 Controller,用 HTTP 接口触发索引构建和后续的问答:

@RestController @RequestMapping("/rag") public class RagController { private final RagService ragService; public RagController(RagService ragService) { this.ragService = ragService; } @PostMapping("/build") public String build() { ragService.buildIndex(); return "索引构建完成"; } @PostMapping("/ask") public String ask(@RequestBody Map<String, String> body) { return ragService.ask(body.get("question")); } }

启动 Spring Boot 后,执行:

curl -X POST http://localhost:8080/rag/build

看到“索引构建完成”,说明文档已经切块并入库,在线问答阶段可以开始了。

5. 问答阶段实战:相似度搜索、上下文拼装与QuestionAnswerAdvisor

5.1 检索:怎么把问题变成“查文档”的动作

问答阶段的第一个动作是检索。Spring AI 的VectorStore提供了similaritySearch,传入用户问题就能返回最相似的文档块。我习惯用SearchRequest显式指定topK:

List<Document> matches = vectorStore.similaritySearch( SearchRequest.builder(question) .withTopK(3) .build());

topK=3表示返回最相似的 3 个文档块。这个值不是越大越好:太大,模型会被无关噪声干扰;太小,正确答案可能漏掉。我建议先从 3 开始,后面根据命中情况调整。

检索结果里每个Document带有一个分数,表示向量相似度。通常超过 0.7 才算比较相关,但不同 Embedding 模型的分数分布差别很大,不要直接套用网上的阈值,先打印几次结果看看自己的分数长什么样。

5.2 上下文拼装:提示词里一定要堵住“编造”的口子

检索完下一步,是把召回的文档块拼进提示词。这里有一个我每次都必须做的动作:在提示词里明确禁止编造。因为大模型天生有“流畅地胡扯”的倾向,尤其当答案不在检索内容里时,它会试图用训练知识补全。下面是经过几次教训后我固定下来的 Prompt 模板:

String context = matches.stream() .map(Document::getText) .collect(Collectors.joining("\n\n---\n\n")); String prompt = """ 你是一个产品手册问答助手。 请严格根据下面的资料回答用户问题。 资料里没有的内容,直接回答“资料中没有找到”,不要编造。 资料: %s 用户问题: %s """.formatted(context, question); String answer = chatModel.call(prompt);

这段代码里最关键的句子是“资料里没有的内容,直接回答‘资料中没有找到’”。实测下来,它能明显减少幻觉,但并不能完全消除。如果召回的文档块本身包含错误信息,模型也会一本正经地复述错误,这是 RAG 的固有风险,只能靠提升召回质量来缓解。

5.3 QuestionAnswerAdvisor:Spring AI 自带的 RAG 顾问

如果你觉得手拼 Prompt 有点繁琐,Spring AI 还提供了一个现成组件QuestionAnswerAdvisor。它的作用相当于是把“检索 + 拼上下文 + 问答”封装成了一个顾问对象,配合ChatModel直接使用:

QuestionAnswerAdvisor advisor = new QuestionAnswerAdvisor(vectorStore); String answer = chatModel.call( new Prompt(question, advisor) ).getResult().getOutput().getText();

QuestionAnswerAdvisor会自动把检索到的文档注入 Prompt,你不需要手动拼资料段。我个人建议:第一次做 RAG,还是先手写 Prompt 一遍,把检索结果和最终回答的前因后果看清楚;跑通了再换顾问组件,这样你对链路里每一步发生的“魔法”都有体感。

除了QuestionAnswerAdvisor,Spring AI 还有QueryTransformer和QueryExpander这类进阶组件,可以把用户问题改写得更适合检索。不过初学者先不用碰,等基础链路稳定了再考虑。

5.4 接口测试:用 curl 直接验证问答效果

索引构建完成后,直接调问答接口:

curl -X POST http://localhost:8080/rag/ask \ -H "Content-Type: application/json" \ -d '{"question": "如何重置管理员密码"}'

正常情况下,模型会从手册检索到的段落里提取步骤回答。如果它回答“资料中没有找到”,别慌——这是提示词在起作用,说明检索到的文档块里确实没有直接答案。这时候要做的不是怀疑模型,而是去看第 6 章的排错套路。

5.5 这一步最容易踩的三个坑

第一个坑:对话模型没配置对。很多人启动后没验证就开问,结果 Ollama 默认用了没有拉取的模型,或者配置 key 不匹配,报 404。所以第 3 章的连通性验证真的别跳过。

第二个坑:Embedding 模型和向量库不一致。比如索引阶段用nomic-embed-text,后来换成了别的模型,旧向量和新向量空间不一致,检索分数会变得毫无意义。换模型必须重新构建索引。

第三个坑:上下文长度估算错误。如果把topK调到 5,每块又 500 token,光资料就 2500 token,加上问题和其他系统提示词,很容易逼近小上下文模型的限制。建议切分时用 300 token 左右,topK控制在 3~5,给模型留足生成空间。

6. 第一次跑RAG就翻车:召回质量、切块参数与相似度阈值的完整排查

6.1 我的测试场景和三个问题

演示环境里我放了一份 20 页的《智能考勤一体机操作手册》,里面有管理员密码重置、考勤方式、数据导出三大部分。我准备了三个问题:

  1. “如何重置管理员密码”
  2. “支持哪些考勤方式”
  3. “数据导出支持哪些格式”

这三个问题覆盖了“单一知识点”“分散知识点”“表格型信息”三种情况,很适合用来暴露 RAG 的不同问题。

6.2 翻车现象:有的漏步骤,有的胡说,有的答非所问

我用最朴素的参数(chunk_size=500,overlap=50,topK=1)跑了一轮,结果非常打脸:

  • 问题 1 的回答只说了“进入系统管理,找到管理员设置”,但漏掉了“恢复出厂设置后的首次登录仍默认 admin123”这一步
  • 问题 2 直接把模型训练记忆里的内容搬出来了,说支持“钉钉打卡、微信打卡”,手册里写的其实是刷卡、人脸、指纹
  • 问题 3 更离谱,问的是导出格式,它回答了一堆导出操作步骤,最后也没说清支持 CSV 还是 Excel

这三个问题本质上都是同一个毛病:模型没看到真正包含答案的文档块。

6.3 排查链路:先看召回,再查切块

我的排查习惯是:先打印召回结果,而不是急着调 Prompt。于是在ask方法里临时加了段输出,把每个召回块的前 100 个字符和相似度分数打出来:

for (Document match : matches) { System.out.println("score=" + match.getScore() + ", 内容开头=" + match.getText().substring(0, Math.min(100, match.getText().length()))); }

日志一出来,问题就清楚了:

  • 问题 1 的召回块是“登录界面说明”,不是“管理员密码重置”,答案当然缺步骤
  • 问题 2 的召回块里只有一行提到了“刷卡”,其他都是打卡设备的硬件参数,模型被噪声带偏
  • 问题 3 的召回块是“数据导出操作前准备”,而真正的“支持格式”表格在另一块文档里,因为 chunk 太大被平均掉了

接着查切块:把 500 token 改成更小粒度后,发现“管理员密码重置”的步骤分布在两个相邻 chunk,overlap 50 不够,中间一句话被切断。这就是为什么 300/80 的组合在实测里更稳:chunk 小到能保留局部细节,overlap 大到能承接跨块语义。

6.4 参数对比:同一份文档,不同切分检索配置的命中差异

我花了一个下午做了组简单的参数对比。命中判定是“召回的前几个 chunk 里是否包含完整答案”。注意这是我那一份 20 页文档、三个问题上的结果,不是通用基准,但趋势很有参考价值:

chunk_sizeoverlaptopK问题1 密码重置问题2 考勤方式问题3 导出格式整体感受
500501漏步骤答非所问错误召回太粗,噪声大
300503部分命中幻觉减少但仍有错勉强命中初步可用,缺上下文
200805完整命中正确但夹杂噪声完整命中召回率高,上下文偏碎
300803完整命中正确正确当前文档的最优解

最终我采用的是chunk_size=300、overlap=80、topK=3。我特别想强调,这个组合只对我这份操作手册最优。如果你的文档是长篇小说或者论文,最优参数一定会变。但排查流程是通用的:先打印召回,再调切分,最后调 topK。

6.5 复盘:真正的瓶颈在召回和数据整理

这次翻车让我彻底接受了开头那个判断:RAG 的瓶颈很少在模型生成,而在召回和数据整理。文档切得太碎,关键步骤被拦腰截断;切得太粗,关键信息被噪声稀释;topK 太小,正确答案根本进不了 Prompt。这些都是数据工程问题,不是模型理解力问题。

另外我意识到,“知识割裂”是比参数更隐蔽的坑。比如“导出格式”这个答案在文档的表格里,而“导出步骤”在正文里,它们被分到了不同的 chunk。只靠向量相似度,模型很难同时把两段知识拼起来。这种问题靠调参解决不了,得靠第 7 章里的重排序、混合检索,甚至 GraphRAG 的路径。

7. 从朴素RAG往前走:重排序、Agentic RAG与GraphRAG的工程化路线

7.1 朴素 RAG 的瓶颈到底在哪里

把基础链路跑通之后,你很快会碰到朴素 RAG 的天花板。我总结下来有三类最明显的瓶颈:

第一,召回不全。向量相似度擅长“语义相近”,但不擅长“术语精确匹配”。比如文档里写的是“口令”,用户问的是“密码”,语义模型能兜住;但如果是设备型号“XA-200”,用户问“XA200”,很多向量模型就匹配不上。这时候需要加关键词检索。

第二,碎片化。答案跨多个 chunk 时,模型需要跳着读,简单拼接 Prompt 很容易丢失逻辑关系。这也是"知识割裂"的来源。

第三,多跳推理弱。问“某功能影响了哪些模块”,答案分散在三页不同章节,朴素 RAG 只能各自召回,没法串联。这类问题靠加大 topK 也不行,因为噪声会同步增长。

7.2 改进第一步:重排序与混合检索

我建议工程上第一个升级动作是加重排序(Rerank)和混合检索。思路是:先用向量检索召回 20 个候选块,再用一个 Rerank 模型精排,取前 3 个最相关的块给大模型。这样既保住了召回率,又控制了噪声。

混合检索则是把向量检索和 BM25 关键词检索的结果做合并。Spring AI 周边生态里已经有这类实现,LangChain4j 等 Java 框架也提供了类似能力。它特别适合文档里充满编号、型号、专有名词的场景。如果你还没扛到那一步,至少可以在相似度检索之外,加一层元数据过滤,比如按“章节”或“文档类型”限定候选范围。

7.3 Agentic RAG:让模型自己决定怎么查

再往前走一步,就是热词里反复出现的 Agentic RAG。朴素 RAG 是“查一次,答一次”,Agentic RAG 是让模型像一个有工具的人:先分析问题,决定要不要检索、检索哪类知识、要不要追问澄清,甚至根据第一次检索结果决定是否再查一次。

举个例子,用户问“怎么导出考勤数据”,Agent 会先意识到“考勤数据导出”可能涉及权限、格式、周期三个子问题,于是先查“导出权限”,再查“导出格式”,最后结合两次结果给出完整回答。代价是代码复杂度和延迟都上升,不是所有场景都值得。Spring AI 的 advisor 机制和 ChatClient 给了基础支持,但真要做到生产可用的 Agentic RAG,你还是得自己做规划、工具调用、记忆管理那一套。

7.4 GraphRAG 与本体 RAG:治“知识割裂”的重型武器

热词里还有 GraphRAG 和 ontology RAG。它们解决的核心问题,正是前面说的“知识割裂”。GraphRAG 会把文档里的实体(比如某个功能、某个硬件)和关系(“依赖”“包含”“影响”)抽取出来,构建成一张知识图谱。回答跨文档问题时,模型可以先在图谱上跳转,把相关实体串起来,再去定位具体文档块。

本体 RAG 更进一步,先用领域本体定义好概念关系,再让切分和检索服从这套结构。比如考勤系统的本体里定义了“管理员”“考勤方式”“数据导出”这些概念,索引阶段就把文档块打上本体标签,检索阶段可以按概念过滤。效果好是真好,但建本体、做抽取都是不小的前置成本。小项目一开始就上 GraphRAG,大概率是在给自己找麻烦。

7.5 用数据说话:怎么评估 RAG 到底变好没有

很多人调参全凭感觉,我强烈建议你准备一套小评测集。30~50 条问答就够了,每条标注出“正确答案在哪几段文档里”。然后跑两个指标:

  • Hit Rate:回答的问题里,检索返回的前 K 个文档块中,有多少包含了答案所在段落
  • Context Relevance:召回的上下文与问题的相关程度,可以人工打分,也可以用模型粗评

每次改切分参数、topK、加不加重排序,都先在这套评测集上测一遍,用数字说话。我自己做 RAG 调优时,流程永远是:跑评测、看失败案例、改参数、再跑评测。而不是盯着某一个成功案例就以为大功告成,那样只会顾此失彼。

我自己的体会是,RAG 不是什么高深莫测的东西,它本质上就是把“搜索引擎”接到“大模型”前面的一套系统工程。刚开始做,别急着追 Agentic、GraphRAG 这些时髦词,先把朴素链路跑通,把召回打印出来看一遍,用一组小评测数据驱动调参,你的效果大概率已经超过 80% 的临时方案。等到这 80% 都不够用了,再根据评测数据决定要不要升级架构,那时候你的每一步都会走得很踏实。

现在这套 Spring AI RAG 已经在我的本地跑得比较稳了。我仍然保留一个习惯:每次正式回答之前,先看它会召回哪些文档片段。这个动作帮我省掉了大量调 Prompt 的时间。如果你也正在搭自己的知识库问答,希望这次的记录能帮你少走一圈弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询