楼层: 首页/ 软件技术/ Spring AI/ RAG:让 AI 读你的私有文档再回答
六

RAG:让 AI 读你的私有文档再回答

Retrieval-Augmented Generation

大模型有个大问题:它不知道你公司的产品手册、合同、内部 FAQ。你直接问"我们公司退货政策是啥",它要么瞎编,要么说"我不知道"。RAG(检索增强生成)就是把你的文档喂给 AI,让它基于你自己的资料回答,减少胡说八道。

论RAG 七步流水线

① 文档加载:把 PDF、Word、Markdown 读进来。

② 切分:长文档切成一段一段(chunk),太长塞不进模型上下文,太短丢上下文。

③ 向量化(Embedding):把每段文字转成一串数字(向量),语义相近的文字向量也相近。

④ 存入向量库:把向量 + 原文存进 Redis / PGVector / Milvus。

⑤ 检索:用户提问时,把问题也向量化,去向量库找语义最相近的几段。

⑥ 拼 Prompt:把"找到的文档片段 + 用户问题"拼在一起发给大模型。

⑦ 生成:大模型基于这些片段回答,答得有依据。

Spring AI 里搭 RAG(核心是 VectorStore)

// 1. 往向量库存文档(启动时跑一次) @Autowired private VectorStore vectorStore; public void loadDocs() { // 加载一个 PDF,切分后向量化入库 List<Document> docs = new TokenTextSplitter().apply( new PagePdfDocumentReader("classpath:产品手册.pdf").get() ); vectorStore.add(docs); // 自动向量化并存储 } // 2. 问答时,先检索再生成 @GetMapping("/askDoc") public String askDoc(@RequestParam String q) { // 用 Advisor 自动做检索:先找相关文档,拼进 prompt return chatClient.prompt(q) .advisors(a -> a.advisors(QuestionAnswerAdvisor.builder(vectorStore).build())) .call() .content(); }

文档加载器与文本切分

RAG 第一步是把文档读进来。Spring AI 提供各种 DocumentReader:PDF、Word、Markdown、HTML、TXT。读完是一整篇,太长塞不进模型,必须切分。TokenTextSplitter 按 token 数切,每段留一点重叠(overlap)防止把一句话从中间切断。

加载器读什么
PagePdfDocumentReaderPDF 文件。扫描件 PDF 要先 OCR。
TikaDocumentReader万能,Word/PPT/HTML 都能读(底层 Apache Tika)。
MarkdownDocumentReaderMarkdown,按标题结构切更合理。
TextReader纯文本。

切分参数怎么调

// chunkSize:每段大概多少 token,一般 500~1000 // minChunkSizeChars:每段最少多少字符 // minChunkLengthToEmbed:太短的段就别向量化了 // maxNumChunks:最多切多少段,防止文档爆炸 // keepSeparator:保留 Markdown 标题分隔符,帮助切块 TokenTextSplitter splitter = new TokenTextSplitter(); List<Document> chunks = splitter.apply(documents); // chunk 太大:检索时一段里混了多个主题,不准 // chunk 太小:一段就一两句话,没上下文 // 调参是 RAG 效果好坏的关键之一

向量数据库怎么选

向量库适合场景
Redis Stack已有 Redis,小数据量,上手快。
PGVector已有 PostgreSQL,加个插件就能用,业务数据+向量放一起。
Milvus大规模向量检索,专业向量数据库,生产大数据量选它。
Chroma / 内存版学习 demo 用,重启就丢,别上生产。

检索器与检索增强对话

RAG 的核心是 VectorStore(向量库)和 QuestionAnswerAdvisor(自动检索拼上下文的顾问)。配好 Advisor 后,你不用手动写"先检索再拼接"——Spring AI 自动帮你干:用户提问 → 向量化 → 查向量库拿最相关几段 → 拼进 Prompt → 发给模型。

手动检索(想自己控制时)

// 不用 Advisor,自己控制检索过程 List<Document> hits = vectorStore.similaritySearch( SearchRequest.query(q).withTopK(5) // 取最相关的 5 段 ); // 把 hits 的原文拼进 prompt,再发给模型 String context = hits.stream() .map(Document::getText) .collect(Collectors.joining("\n---\n")); String answer = chatClient.prompt() .system("根据下面资料回答,资料里没有就说不知道:\n" + context) .user(q) .call().content();

Embedding 模型:入库和检索必须用同一个

把文字变成向量的那个模型叫 Embedding 模型。你把文档入库时用的 Embedding 模型,和用户提问检索时用的,必须是同一个。不同模型产出的向量空间完全不同——你用模型 A 把文档存进去,用模型 B 把问题向量化去搜,语义再相关也搜不出来,因为坐标对不上。这是 RAG 新手最隐蔽的坑。

注意点说明
模型一致入库和检索用同一个 Embedding 模型,别中途换。
换了模型要重建索引真要换 Embedding 模型,所有文档得重新向量化入库。
向量维度固定每个模型输出固定维度(比如 1024),跟向量库配置对得上。
RAG 检索不到相关文档,最常见三个原因

① 切分太大或太小:太大一段塞太多主题,检索不准;太小丢上下文。要调 chunk size 和 overlap。② Embedding 模型不匹配:入库和检索用的必须是同一个 Embedding 模型,换了模型向量空间对不上,检索全歪。③ 文档质量差:PDF 扫描件 OCR 出来一堆乱码,向量化了也是垃圾。先确保文档能正常读。

重排 Rerank:粗筛之后再精排一遍

向量检索是粗筛:靠语义相似度快速捞出 top-K(比如 20 段)。但相似度高不等于最该回答问题的那段——经常把沾边但没营养的排前面。重排(Rerank)是第二道精筛:用一个更强的跨编码模型(cross-encoder),把"问题 + 每段候选"一对对重新打分,挑出真正最相关的 top-3~5,再拼进 Prompt。

论为什么要两步走

cross-encoder 精排质量高但慢(每段都要和问题一起过一次模型),不可能对全库几十万段跑一遍。所以标准流水线是:向量粗筛 top20(快)→ Rerank 精排 top5(准)→ 拼进 Prompt。这样既快又准。Spring AI 里可用 QuestionAnswerAdvisor 配合重排模型,或接 Rerank Model,具体 API 以当前版本为准。

// 思路:先向量粗筛 20 段,再用 Rerank 模型精排留下最相关的 5 段
List<Document> rough = vectorStore.similaritySearch(
    SearchRequest.query(q).withTopK(20));
List<Document> refined = rerankModel.rank(q, rough, 5); // 精排留 5 段
// 把 refined 拼进 system prompt,再调模型
Rerank 不是免费的

精排模型要额外调一次模型(或本地推理),多花 token/算力和延迟。文档少、问答简单时,向量检索 top5 直接用就够;只有粗筛召回不准、答非所问时,才上 Rerank。别一上来就堆最复杂的管线。