笔记 | 用 Spring AI 搭 RAG 问答:把公司文档喂给大模型(全链路实战)

最近在折腾项目的时候碰到了这个知识点,查了不少资料,索性整理出来分享给大家。

这篇文章是《Java 后端 + AI 落地(Spring AI 主线)》系列第 4 篇。第 2、3 篇解决了"接得上、跑得起来、数据不出内网",但模型再大也不了解你公司的制度、流程和产品参数——它的知识止于训练数据,而你的业务知识每天都在变。这篇讲 RAG(检索增强生成)的全链路:文档怎么切、向量库怎么选、混合检索为什么在中文场景是必需项、以及一句我最想让人记住的判断:RAG 的效果八成由检索决定,模型只负责表达。 文中还会处理企业 RAG 的生死线——权限过滤为什么一定要在检索层切断,绝不能交给提示词去"请它别说"。

📌 版本声明:这篇文章基于 2026-09-27 的版本线撰写:Spring AI 2.0.1 + Spring Boot 4.1.x + Java 21(配置键名与 Advisor 名称请以官方 Reference Documentation 为准)。向量库示例以 PostgreSQL + pgvector 与 Redis 为主,这两种是 Java 团队最容易落地的选择。文中分块长度、top-k、阈值等数值均为经验起点,一定要用你自己的文档与测试集标定——RAG 没有通用参数,只有"你这套语料上的最优参数"。涉及文档权限与数据安全的部分(5.2、6.4 节),请务必与安全的同事确认后再上线。

文章目录

2️⃣ 核心概念:四个决定效果上限的选择 3️⃣ 环境准备:先把"垃圾进"的问题堵住 4️⃣ 核心实现:从入库到问答 5️⃣ 进阶与优化 6️⃣ 适用边界与风险提示 7️⃣ 总结 参考资料

1️⃣ 为什么模型"不知道我们公司的制度"

1.1 让模型"知道"一件事,只有三条路

这个问题几乎所有团队都在第二周遇到过:业务方试用完第一版问答,第一句反馈就是"它怎么不知道我们公司的报销标准"。 这不是模型的问题——模型的知识来自训练数据,而你公司的制度文件从来不在里面,也不该在里面。

让模型掌握"你的知识",工程上只有三条路:

方案做法更新时效成本特征可追溯性适用与致命缺陷微调(Fine-tuning)用你的数据继续训练模型权重天~周级(要重训、要重新验证)训练成本高 + 每次更新都要重来一遍弱(知识在参数里,说不清来源)适合"风格与格式对齐",不适合承载会变的事实长上下文全塞每次请求把相关文档全部塞进提示词实时按 token 计费,每次都付中(能给出原文)适合"一次性的长文分析",不适合"有 5000 篇文档的知识库"RAG先把文档切块入向量库,每次只检索最相关的几块塞进提示词实时(改文档即生效)嵌入只做一次(增量),每次只付几块的 token强(每块都能回指原文)这篇文章主题;效果取决于检索质量

这张表里最需要纠正的误解是"微调可以注入知识"。 我见过团队花了两个月做微调,期望模型能准确背出公司最新的产品参数——结果它学会的是"回答这类问题时该用哪种语气和格式",而具体数字仍然会记错、会编。 原因是微调改变的是模型的"行为倾向",而"准确记住并复述一条具体的事实"这件事,参数量级不够时它就是做不到,而且会随时间漂移。

所以我的结论很明确:微调解决"怎么说",RAG 解决"说什么"。 两者不冲突,但如果你的目标是让系统知道"公司制度、产品参数、历史工单"这类会变的事实,RAG 是唯一在成本与时效上都成立的选择——而且它有一个微调永远给不了的东西:可追溯。 在企业场景里,"它能告诉我答案是第 3 页第 2 段"这件事的价值,往往比"答案准确率再高 3 个点"更大,因为这意味着业务方可以自己去核对。

至于"长上下文全塞"这条路,我要多说一句,因为它最近变得诱人了。 上下文窗口越来越大,"把所有文档塞进去"看起来越来越可行。但它有三个绕不过的问题: 一是成本(每次请求都按全部内容计费,问 100 次就付 100 次);二是留意力稀释(上下文越长,模型对中间部分的关注越弱——把 10 万字塞进去,它记住的往往是开头和结尾);三是延迟(输入越长首 token 越慢)。所以正确用法是"用检索把范围缩小到几千字,再交给长窗口去理解",而不是"用长窗口替代检索"。

1.2 RAG 的四个环节,和它们各自的失败模式

RAG 的原理用一句话就能说完:先检索、再生成。但工程上它有四个环节,每个环节都有自己独立的失败方式——而"答得不对"这个最终表现,可以是四个环节里任意一个的锅。这就是为什么很多人调 RAG 觉得像在黑箱里摸索。

失败:同义改写失效
口语提问检索不到书面表述

失败:召回不够
或召回了不该给该用户的文档

失败:噪声淹没信号
不相关内容排在前面

失败:模型忽略片段
或凭记忆编造

用户提问

① 解析与改写
(Query 处理)

② 检索
向量 + 关键词 + 权限过滤

③ 重排与筛选
Rerank / 阈值截断

④ 生成
拼装上下文 + 约束提示词

回答 + 引用

表现为:检索结果全不相关

表现为:答不出 / 越权泄露

表现为:答得跑偏 / 引用错文档

表现为:幻觉 / 引用是假的

排查时必须分段验证:
先看检索出来的片段对不对,再看模型是否用对了

(图 1:RAG 全链路与四个失败点——"答得不对"可能是四个环节里任意一个的锅)

这张图里最有价值的是最后那个方框:排查 RAG 问题时,必须"分段验证"。 具体做法是:把检索出来的片段原样打印出来看——如果检索结果本身就答不了这个问题,那就是①②③的锅,此时去改提示词毫无意义;如果检索结果里有正确答案而模型没用,那才是④的锅。我见过太多团队跳过这一步,直接在提示词里加"请严格按照提供的资料回答",结果改了半天没效果——因为问题根本不在生成环节。

1.3 本篇最重要的一句话:检索决定上限,生成只决定表达

我把这句话放在这里,是因为它能决定你 80% 的优化精力该投在哪里。

论证很直白:假设你的检索在 100 个问题里有 40 个没能召回正确的文档片段,那么无论模型多强,这 40 个问题的答案都不可能正确——因为正确的信息从未进入模型的视野,它只能凭训练数据里的一般知识去猜,而猜就是幻觉的来源。 反过来,只要正确片段进了上下文,即使是本地 7B 模型(第 3 篇)也能生成相当不错的回答——这正是我建议"先优化检索,再考虑升级模型"的原因。

这个判断有一个很实用的推论:不要用"换个更强的模型"来解决 RAG 的效果问题。 换模型能改善的是表达质量(更通顺、更懂指令、更会引用),它改善不了"信息压根没检索到"。我在实际项目里见过的比例大致是:RAG 效果问题中约七八成出在检索侧(含文档解析与分块),一两成出在生成侧。

1.4 什么时候不该用 RAG

RAG 不是万能的,下面这几类需求把它硬套上去会很难受:

需求类型为什么 RAG 不行该怎么做跨文档聚合统计(“今年一共处理了多少起这类问题”)检索是"取最相关的几块",它天生不擅长"遍历全部"走结构化查询:让模型生成 SQL/查询条件,去数据库拿准确结果表格里的数值计算表格被切成文本后,数字之间的关系丢失,模型算术也不可靠结构化入库 + 精确计算,模型只做"理解问题并选对表"需要精确计算或强一致结论模型输出是概率性的规则引擎与数据库;模型只做建议(第 2 篇 6.2 节的同一条红线)文档数量极少(几页)检索的工程成本高于收益直接全塞进上下文,简单可靠需要"全新风格"的写作RAG 管的是知识,不是风格微调或提示词 few-shot

这张表里前两行的共同点是"需要遍历或精确计算",而 RAG 的本质是"近似检索 + 概率生成"——用近似的工具去做需要精确的事,是架构层面的错误,不是调参能解决的。 我的建议是:当业务提出这类问题时,不要试图"优化 RAG 让它也能算",而是明确拆成两条路径——语义问题走 RAG,数值与统计问题走结构化查询,由模型做"路由器"决定走哪条。这也是第 5 篇(函数调用)要解决的问题。


2️⃣ 核心概念:四个决定效果上限的选择

2.1 Embedding:把"语义"变成坐标

用 Java 工程师熟悉的方式解释:Embedding 就是把一段文本映射成一个定长浮点数组(比如 768 或 1024 个数)。这个数组就是这段文本在多维空间里的"坐标",而语义相近的文本,坐标也更接近。

为什么这件事有用? 因为"接近"可以用数学衡量(最常用的是余弦相似度),于是"找相关文档"这个模糊的需求,就变成了一个可以索引、可以排序的数值计算问题——这就是向量检索的全部原理。 没有魔法,只是一次坐标变换加一次最近邻查询。

三个必须知道的实践点:

第一,不同模型的向量不能混用。 每个嵌入模型都有自己的"坐标系",换成另一个模型,同一段文本的坐标会完全不同,两者之间的距离毫无意义。 所以"换嵌入模型 = 全量重嵌入"(第 3 篇 4.5 节已经提过),这是 RAG 里最贵的一次性操作,选型要一次定准。

第二,相似度阈值比 top-k 更重要。 很多人只关心"取前 5 条",但真正决定"该不该回答"的是阈值——如果用户的提问在知识库里根本没有答案,检索仍然会返回"最接近的 5 条"(哪怕它们都不相关),而模型看到 5 段不相关内容后,大概率会努力把它们编成一个答案。 这就是"答非所问"的常见根因。

我的做法是:设一个相似度阈值,低于阈值时明确告诉用户"知识库里没有相关内容",并给出替代建议。 这个"答不出来"的能力,比多答对几个问题更有价值——因为在业务场景里,一个坦诚的"我不知道",远比一个自信的错误答案安全。 这也是 1.3 节判断的直接应用:宁可少答,不可错答。

第三,中文语料的向量质量差异明显。 有些嵌入模型对中文(尤其专业术语、缩写、型号)的表现明显更好。选型的正确方式是:拿你自己的 30~50 条真实问句做一个小测试集,比较几个候选模型的"正确文档排在前 3 的比例"——这半天的工作量,能省掉后面几个月的调参。

2.2 分块(Chunking):RAG 里最被低估的一环

分块看起来是个"技术细节",但它是决定检索质量的第一道关口。 原理很简单:文档太长没法整篇入向量库(一个向量代表不了整篇文档的主题),所以要切成块,每块独立向量化。

原始文档
(含标题 / 段落 / 表格)

策略一:固定长度切分
每 500 字一块,块间重叠 50 字

策略二:按结构切分
标题层级 / 段落 / 列表项为自然边界

策略三:语义切分
按话题转换点切开(成本更高)

优点:实现最简单
缺点:一句话可能被切成两半

优点:块内语义完整
缺点:块长度差异大,需兜底规则

优点:语义最完整
缺点:需要额外模型调用,成本与延迟上升

重叠(overlap)的作用:
避免答案正好落在切口上
代价:存储与检索冗余上升

表格要特殊处理:
按行切开 = 表头与数据分离 = 检索到也没用
做法:整表成块 + 附加一行表意摘要

每块必须带元数据:
来源文件 / 页码 / 章节 / 版本 / 权限标签

(图 2:三种分块策略与两条硬规则——重叠与元数据)

关于分块,我给四条实操建议,都是踩过坑总结出来的:

第一,从"按结构切分 + 定长兜底"开始,不要一上手就做语义切分。 因为你的文档通常有标题层级和段落,按结构切出来的块,语义完整性天然更好;语义切分要额外调用模型,成本和复杂度都上一个台阶,适合在"结构切分已经做到位、效果仍不够"时再上。

第二,块长是"召回精度"与"上下文完整性"的权衡。 块太小:语义碎片化,"答案需要前后两句才能读懂"的情况检索不到;块太大:一个块里混了多个主题,向量变"模糊",检索精度下降,而且塞进提示词时浪费 token。经验起点是几百字量级,接着用你的测试集标定。

第三,重叠不是越大越好。 它的作用是防止答案正好落在切缝上,但代价是同一段内容在多个块里重复出现(结果是模型收到"同一段话说了三遍"的上下文,占用 token 还容易让引用编号混乱)。

第四,表格是最容易毁掉的地方。 表格一旦按行切开,表头与数据就分离了——检索到"某型号 320"这一行,但看不出"320"是哪一列的意思。 正确做法是整表作为一个块,并在块首附加一句"这张表是关于什么的"的表意摘要(通常由模型生成一次,或由人工模板拼出)——这样检索时既能匹配到表,又能让模型看懂表。

2.3 向量库选型:先看"你已经有的是什么"

这条原则与第 1 篇选向量库的建议一致,但要展开讲清代价:

方案优点代价什么情况下选它PostgreSQL + pgvector复用关系库,事务与备份体系成熟;能在一个 SQL 里同时做元数据过滤与向量检索需要装扩展;数据量极大时索引性能不如专用库首选:团队已有 PG、数据量在百万级以内Redis若已在用 Redis,零新增组件;延迟低向量能力相对基础(视版本而定);持久化与内存成本要单独评估已有 Redis 且对延迟敏感、数据量中等Elasticsearch / OpenSearch原生支持关键词 + 向量混合检索(最省事)多一套集群要运维;向量能力与专用库仍有差距已经在用 ES,且你本来就需要混合检索(2.4 节)Milvus 等专用向量库千万级以上规模、分布式、索引算法最全一套新的分布式组件:运维、备份、监控全都要新建文档规模确实很大(留意先确认真需要,别提前上)

我的选型建议顺序是:能用现有 PG 就用 pgvector;已经用 ES 且要做混合检索就选 ES;只有数据量逼近千万级、或对向量性能有明确要求时,才引入专用向量库。

这里有一条被低估的判断:向量库的选型不只影响性能,还影响"权限过滤"的实现难度(5.2 节的生死线)。如果你必须做权限过滤,那么"能不能在检索时高效地按元数据过滤"这一条,比"索引有多快"重要得多——因为向量检索天然是"全局最近邻",而权限是"子集内的最近邻",两者在实现上差别很大:有些向量库能高效支持"带过滤条件的向量检索",有些则要先取回再过滤(那会导致"取回的都被过滤掉了,结果为空",而真正相关的文档根本没被取回)。这一条在选型阶段就要验证。

2.4 混合检索:中文场景的必需项

纯向量检索有一个系统性弱点:它对"精确字符串"不敏感。

用例子说明: 你的知识库里有《XX-2026-003 号内部管理办法》。用户直接问"XX-2026-003 讲了什么"——向量检索很可能召不回它,因为这个编号本身没有语义,模型只知道它"看起来像一串编号",无法判断它和知识库里哪段文本"语义相近"。同样的失效场景还有:产品型号、错误码、人名、专有缩写、以及"用户打错了一两个字"的情况。

解决方案是混合检索:向量检索(管语义) + 关键词检索(管精确匹配),两路召回后合并、去重、再排序。这不是"锦上添花",在中企业知识库场景里它是必需品——因为企业文档里充满了"没有语义但有意义"的字符串。

实现上有一个很容易忽略的点:两路的分数不可直接比较。 余弦相似度是 0~1 的量,BM25 的分数是一个没有上界的数值,把它们直接相加是错的。 正确做法有几种(归一化后加权、按排名做融合、或交给 rerank 模型统一排序),我在实践中更倾向最后一种:让两路各自取一批候选,合并去重后交给重排序模型统一打分(2.5 节)。

2.5 Rerank:用一次额外计算换排序质量

两阶段检索是目前最实用的架构:先用便宜的检索方式从全量里取回一大批候选(比如 20~50 条),再用一个更"贵"但更准的模型把这批候选重新排序,最后只取前 3~5 条给模型。

是

否

用户提问

向量检索
top 20

关键词检索(BM25)
top 20

合并去重
(同一分块只留一条)

权限与元数据过滤
(必须在送模型之前)

重排序模型 Rerank
对候选逐条打分

取 top 3~5

相似度 / 相关性
低于阈值?

明确回答:知识库中没有相关内容
并给出替代建议

拼装上下文 + 引用编号

生成回答

(图 3:混合检索 + 重排的两阶段流程——留意"权限过滤"发生在送模型之前,"低于阈值就答不出来"是刻意设计的)

为什么两阶段比"直接取 top 5"好? 因为检索模型是"快而粗",重排序模型是"慢而准":前者要在百万条里快速筛,只能做粗略的语义匹配;后者只需要在 20 条里精挑细选,可以用更复杂的计算。代价是额外一次模型调用带来的延迟(这是可以接受的,因为 RAG 的总耗时里,生成回答往往才是大头)。

Rerank 的收益在什么场景最大? "召回多但排序差"的场景——典型表现是"正确答案其实检索到了,但排在第五、第六位,而 top 3 里全是同主题但不相关的片段"。 如果你观察到的现象是这个,加 rerank 的收益会十分明显;而如果观察到的现象是"正确答案压根没被召回",那 rerank 帮不了你——那是分块、嵌入模型或混合检索的问题。 这两类问题的区分方式还是 1.2 节说的那一步:把检索结果打出来看。


3️⃣ 环境准备:先把"垃圾进"的问题堵住

3.1 组件清单

一个最小的 RAG 系统需要四个组件,缺一不可:

组件作用选型建议文档解析把 PDF / Word / Excel / HTML 转成纯文本(保留结构)优先用成熟库;扫描件必须走 OCR(见 3.4)嵌入模型文本 → 向量本地(第 3 篇的 Ollama)或云端嵌入接口;中文效果要实测向量库存向量 + 元数据 + 支持检索见 2.3 节,能用现有就用现有生成模型读上下文 + 生成回答云端或本地,与前三者解耦(第 2、3 篇已证明可以做到)

3.2 依赖


    org.springframework.ai
    spring-ai-starter-vector-store-pgvector

    org.springframework.boot
    spring-boot-starter-jdbc

注意这里和第 2、3 篇的同一条纪律:只引 starter,版本交给 BOM。 另外向量库的 starter 往往同时引入数据库驱动,与业务已有的驱动版本可能冲突——如果你用的是项目里已有的数据源,请确认 starter 没有引入第二个驱动版本(mvn dependency:tree 自查,方式同第 2 篇)。

3.3 表结构(以 pgvector 为例)

-- ① 扩展:需要在数据库侧安装
CREATE EXTENSION IF NOT EXISTS vector;

-- ② 分块表:向量 + 元数据一起存,这是"可追溯"的物理基础
CREATE TABLE kb_chunk (
    id           BIGSERIAL PRIMARY KEY,
    doc_id       BIGINT       NOT NULL,      -- 指向文档主表
    chunk_no     INT          NOT NULL,      -- 块序号,用于还原上下文顺序
    content      TEXT         NOT NULL,      -- 原始文本(检索命中后要给人看/给模型用)
    embedding    vector(1024) NOT NULL,      -- 维度必须与嵌入模型一致
    -- 以下元数据是"检索时过滤"和"展示引用"的依据,不能省
    source_name  TEXT         NOT NULL,      -- 文件名
    page_from    INT,
    page_to      INT,
    section_path TEXT,                       -- 章节路径,如"第3章 > 3.2"
    doc_version  TEXT,                       -- 文档版本,用于识别过期内容
    acl_tags     TEXT[]       NOT NULL,      -- 权限标签(5.2 节的生死线)
    created_at   TIMESTAMPTZ  NOT NULL DEFAULT now()
);

-- ③ 向量索引:HNSW 一类近似索引,规模上来后必须有
CREATE INDEX idx_kb_chunk_embedding ON kb_chunk
    USING hnsw (embedding vector_cosine_ops);

-- ④ 元数据索引:权限过滤与文档定位都会被频繁用到
CREATE INDEX idx_kb_chunk_acl  ON kb_chunk USING gin (acl_tags);
CREATE INDEX idx_kb_chunk_doc  ON kb_chunk (doc_id, chunk_no);

这段 SQL 里我想强调两点,它们都是"事后补代价极高"的设计。

第一,acl_tags 权限标签必须从一开始就有。 我在 5.2 节会详细讲"为什么权限必须在检索层切断",但技术前提是"每个块上都带着权限标签"——如果你的表里没有这一列,你就只能做"全库检索后再过滤",而这种做法在权限严格时会导致"结果为空但原因是全被过滤掉了",正确文档根本没被取回(2.3 节提到的那个坑)。补这一列意味着全量重跑入库流程——文档多的时候是几天的工作量。

第二,content 与 embedding 要存在同一张表里。 有些设计把向量存在向量库、原文存在别处,看起来更"解耦",实际每次检索命中都要再查一次另一个库,延迟和一致性都是负担。在千万级以下,把两者放一起是更务实的选择。

3.4 文档解析:RAG 的隐性天花板

这一节是我最想让人重视、但最常被跳过的部分。

"垃圾进、垃圾出"这句话在 RAG 上体现得极其彻底——因为检索只会在你入库的内容里找答案。如果解析环节把表格拍平成了乱序文字、把双栏 PDF 的行序打乱了、把扫描件当成了空文本,那么后面所有环节再优秀也没用。 而解析的问题在系统上线后极难注意到,因为它不报错——入库成功了,检索也返回了结果,只是结果里是一堆读不通的文字。

文档类型常见坑处置方式PDF(文字版)双栏排版导致行序错乱;页眉页脚混进正文;换行处被硬切断按栏切分;用规则剔除重复出现的页眉页脚(重复率高的一行,大概率是页眉)PDF(扫描件)没有任何文本层,直接解析得到空字符串必须走 OCR;OCR 错误率要用真实样本抽查(错误率高时检索会整体失效)Word / 带样式的文档标题层级丢失(影响按结构分块)保留样式信息(标题级别),用于生成 section_pathExcel 表格被拍平成一行行的字符串,列头与数据分离转成"表意摘要 + 结构化文本";数值型问题建议另走结构化查询(1.4 节)HTML / 富文本导航、广告、页脚混入抽取正文区,剔除重复模板内容所有类型同一份文档的多个版本同时入库用 doc_version 标记,入库新版本时把旧版本标记失效(否则会答出过期内容,见 Q4)

否,扫描件

是

不合格

合格

原始文件
PDF / Word / Excel / HTML

有文本层?

OCR 识别
抽查错误率

抽取文本 + 保留结构
标题层级 / 段落 / 表格

清洗:
去页眉页脚 / 合并断行 / 去重复模板

分块(按结构 + 定长兜底)
+ 重叠

打元数据:
来源 / 页码 / 章节 / 版本 / 权限标签

嵌入(批量)

写入向量库

抽样验证:
用真实问题检索,人工看片段是否可用

可对外服务

(图 4:入库(ETL)流程——注意最后那个"抽样验证"环节:它必须在正式上线前做,因为解析问题是静默的)

这张图里第 4 步到第 5 步之间有一个"抽样验证"的回环,这是我在所有 RAG 项目里都会强制要求的一步。 做法很土:准备 20~30 条真实业务问题,入库搞定后逐条检索,人工看返回的片段能不能读懂、有没有答到点上。 这半天的工作量,是我见过的性价比最高的一步——因为它能在上线前暴露"表格拍平了"“扫描件是空的”"双栏错乱了"这三类静默故障,而这三类问题一旦上线,业务方的反馈会是"你这个 AI 胡说八道",而真正的原因是解析,你会在提示词上白折腾两周。


4️⃣ 核心实现:从入库到问答

4.1 入库:一次做完,之后只做增量

@Service
public class KbIngestService {

    private final VectorStore vectorStore;
    private final TokenTextSplitter splitter;   // 定长 + 重叠,作为结构切分的兜底

    // 关键设计:入库不是"一次调用",而是"切分 → 清洗 → 打元数据 → 批量嵌入 → 写库"
    public void ingest(KbDoc doc, List sections) {
        List docs = new ArrayList();

        for (KbSection s : sections) {          // sections 已按"标题层级"切好(结构优先)
            // 结构切出来的长小节再做定长+重叠的二次切分(兜底)
            List pieces = s.text().length() > 800
                    ? splitter.split(...)        // 长则再切
                    : List.of(s.text());          // 短则整段成块

            for (int i = 0; i < pieces.size(); i++) {
                docs.add(new Document(pieces.get(i), Map.of(
                        "doc_id",       doc.id(),
                        "chunk_no",     i,
                        "source_name",  doc.fileName(),
                        "page_from",    s.pageFrom(),
                        "page_to",      s.pageTo(),
                        "section_path", s.path(),        // 如 "第3章 > 3.2"
                        "doc_version",  doc.version(),
                        // 权限标签:检索时会用它做过滤,绝不能省
                        "acl_tags",     String.join(",", doc.aclTags())
                )));
            }
        }

        // 批量写入:一次 100~500 条,避免逐条往返
        vectorStore.add(docs);
    }
}

这段代码里有三个刻意的设计,每个都对应一个坑:

第一,先按结构切、再定长兜底,而不是直接定长切。 顺序反了(先定长)会毁掉语义完整性——因为定长切分不认识"标题",它会把"A 章节的最后一段"和"B 章节的第一段"切进同一个块,结果是这个块的向量"同时像两个主题",检索精度下降。

第二,元数据在入库时就打全,而不是检索时再补。 因为检索时补不回来——你无法从一段纯文本反推出"它来自哪份文件的第几页"。而 page_from/page_to/section_path 三个字段是引用回传的物理基础(4.5 节),没有它们,用户看到的引用永远是"来自某份文档",而不是"来自第 3 页第 2 节"。

第三,批量写入而不是逐条写入。 嵌入是网络调用(即使本地模型也有开销),逐条写入会让一份几百页的文档耗时从分钟级变成小时级——这是入库环节最常见的性能问题,而且它只在"第一次全量导入"时暴露,很容易被忽略。

4.2 检索:阈值与过滤比 top-k 更重要

@Service
public class KbSearchService {

    private final VectorStore vectorStore;

    public List search(String question, UserCtx user) {
        SearchRequest req = SearchRequest.builder()
                .query(question)
                .topK(20)                                  // 先多取,交给后续筛选/重排
                .similarityThreshold(0.45)                 // 阈值:低于它的结果宁可不要
                // 权限过滤必须在检索时生效(5.2 节),不是取回后再筛
                .filterExpression("acl_tags in [" + user.aclTagExpr() + "]")
                .build();

        return vectorStore.similaritySearch(req);
    }
}

这段代码只有十几行,但里面有两个决定系统性格的选择。

第一个选择:topK(20) 而不是 topK(5)。 原因是"多取再筛"比"少取直接用"更可控——前面取回 20 条是给阈值截断和重排留出操作空间。反过来说,如果检索本身就准(比如混合检索做得很好),取 10 条也够;但如果你的检索"召回不稳",把 top-k 从 5 调到 20 是成本最低的改善手段(代价是上下文变长、token 变多)。

第二个选择:阈值 0.45 这个数。 我要强调它是"起点"而不是"标准答案"——不同嵌入模型的相似度分布完全不同,你必须用自己语料上的正例与负例去标定:取一批"确实有答案"的问题,看正确答案的相似度落在什么区间;再取一批"知识库里没有答案"的问题,看最高相似度是多少;两条分布之间的空隙,就是你的阈值。 不做这一步标定,"阈值"这个参数就是拍脑袋。

还有一个容易被忽略的细节:filterExpression 写在检索请求里,而不是"取回后过滤"。 这两者在功能上看起来一样,在结果上完全不同——"取回后过滤"意味着被权限过滤掉的那些块占据了 top-20 的名额,如果用户只能看到 5% 的文档,那么"取回 20 条再过滤"很可能只剩 1 条甚至 0 条,而真正相关的、用户有权看的那条可能排在第 200 位,永远不会被取回。 这就是 2.3 节提醒"选型时要验证能否高效支持带过滤的向量检索"的原因。

4.3 用 Advisor 把 RAG 接进链子

Spring AI 提供的最省力的一条路径是 RAG Advisor:它把"检索 → 拼装上下文 → 注入提示词"这三步封装成一个 Advisor,挂到 ChatClient 上即可。

@Configuration
public class RagConfig {

    @Bean
    ChatClient ragChatClient(ChatClient.Builder builder,
                            VectorStore vectorStore,
                            KbSearchService kbSearchService) {
        return builder
                .defaultSystem(SYSTEM_PROMPT)          // 见 4.4
                // RAG Advisor:负责检索并把片段拼进提示词
                .defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore)
                        .searchRequest(SearchRequest.builder()
                                .topK(5)
                                .similarityThreshold(0.45)
                                .build())
                        .build())
                .build();
    }
}

这里有一条从第 1 篇延续过来的纪律必须重申:Advisor 的顺序决定行为。 第 1 篇 2.2 节讲过"RAG 检索 Advisor 要放在工具循环之前"——因为放在循环内层会导致"每次工具调用后重新进入模型都重跑一遍检索",一次提问可能触发 5 次向量检索,成本翻 5 倍,上下文里还会堆进 5 份相同片段(现象是"回复里同一段引用出现好几次",根因是顺序)。如果你在第 5 篇要加函数调用,请回头确认这条顺序。

另外提醒一个"看起来能用、实际会出错"的做法:不要用 Advisor 把"权限过滤"藏起来。 因为 Advisor 里拿到的是框架上下文,而"当前用户是谁、他看到哪些文档"是业务概念——把权限塞进 Advisor 的隐式逻辑里,会让"谁能看到什么"这件事变得难以审计。我的做法是:权限过滤的表达式由业务层显式构造并传入检索请求(4.2 节),Advisor 只负责检索与拼装。

4.4 提示词:三条约束,一条都不能少

RAG 的效果有很大一部分由提示词决定,而后半句更重要:这部分提示词是所有提示词里最不该"自由发挥"的,因为它直接决定"模型会不会编"。我要求三条约束必须显式写进去:

private static final String SYSTEM_PROMPT = """
        你是企业知识库助手。请严格遵循以下规则:

        1. 只依据下面提供的"资料片段"回答问题。片段中没有的信息,一律回答
           "根据现有资料无法确认",并建议用户联系相关部门或提供更多线索。
        2. 每条关键结论后面标注来源编号,格式为 [1] [2],编号必须来自本次提供的片段。
        3. 如果多个片段的说法不一致,明确指出冲突并分别给出出处,不要自行取舍。

        输出要求:先给结论,再给依据;不要复述问题;不要编造片段中不存在的数字与条款。
        """;

这三条约束分别对应三类事故:

  • 第一条(只用片段) 对着幻觉。不写这一条,模型会用训练数据里的"一般常识"去补全你的业务问题——而这类错误的可怕之处是"它读起来十分合理"。
  • 第二条(标引用) 对着不可追溯。有了编号,业务方能自己核对;而"编号必须来自本次片段"这半句是防"引用造假"的(6.3 节会给出校验方法)。
  • 第三条(多源冲突要指出) 对着知识库内部不一致。这是企业场景的高频情况:新旧版本的制度同时在库、两个部门的流程有出入。如果不要求它指出冲突,模型会随机选一个说法给你——而用户无从知道它选的是哪一版。
还有一个反直觉的建议:不要在这段提示词里写太多"语气要求"。 我见过把系统提示词写成两千字的"角色设定 + 服务态度 + 输出格式细则",结果是本地模型(第 3 篇 5.2 节)率先在长指令上丢三落四,而云端模型也会因为"规则太多、优先级不明"而在某些 case 上放弃遵守前三条。规则越少越硬,效果越稳——这是 RAG 提示词与"角色扮演类"提示词最大的区别。

4.5 引用回传:让用户能点到原文

"每条结论标了 [1] [2]“只是第一步,真正让业务方信任的是"点一下能跳到原文那一页”。 实现方式就是把检索片段的元数据一起返回给前端:

public record Citation(String no, String sourceName, Integer pageFrom,
                       Integer pageTo, String sectionPath, String snippet) {}

// 组装:编号 → 元数据。编号顺序必须与提示词里注入片段的顺序严格一致
private List toCitations(List docs) {
    List list = new ArrayList();
    for (int i = 0; i < docs.size(); i++) {
        Document d = docs.get(i);
        list.add(new Citation(
                "[" + (i + 1) + "]",
                (String) d.getMetadata().get("source_name"),
                (Integer) d.getMetadata().get("page_from"),
                (Integer) d.getMetadata().get("page_to"),
                (String) d.getMetadata().get("section_path"),
                // snippet 用于前端展示"命中的原句",不要返回整块,避免信息过载
                abbreviate(d.getText(), 120)));
    }
    return list;
}

这段代码里最要紧的是那句注释:编号顺序必须与注入顺序严格一致。 这是一个十分容易出错的细节——如果注入提示词的片段顺序与返回给前端的编号顺序不一致,用户点 [2] 看到的却是第 3 段,整个"可追溯"就变成了误导,而这类错误在测试时很难注意到(因为测试时你只看答案对不对,没人去核对引用指向)。我的做法是:编号在"拼装上下文"的那一处统一生成,前端只做展示,绝不在前端重新编号。

模型

向量库

检索服务

Controller

用户

模型

向量库

检索服务

Controller

用户

alt

[全部低于阈值]

[有可用片段]

提问(携带用户身份)

检索(问题 + 权限表达式)

带过滤的向量检索 + 关键词检索

候选片段(含元数据)

合并去重 → 重排 → 阈值截断

无可用片段

明确回答"知识库中无法确认" + 建议

top 3~5 片段(生成统一编号)

系统提示词 + 片段(带编号)+ 问题

回答(正文 + [编号] 引用)

校验引用编号是否都在本次片段范围内

回答 + 可点击的引用列表(来源 / 页码 / 章节)

记录:命中片段 ID / 相似度 / 是否降级 / 用户反馈入口

(图 5:一次 RAG 问答的完整时序——注意"全部低于阈值"的那条分支是被刻意设计出来的,以及最后那道"引用编号校验")

4.6 怎么测 RAG:两个指标必须分开

RAG 的测试比普通接口复杂,因为它有两个可能出错的环节。所以我要求"两个指标分开测",而不是只看"答案对不对"。

准备一份测试集(30~50 条就够),每条包含三样东西: 一个真实问句、它应该命中的文档/章节(人工标注)、一个可接受的答案要点。

指标怎么算它反映什么低了该查哪检索命中率(Recall@k)正确文档是否出现在取回的 k 条里系统的效果上限分块 / 嵌入模型 / 混合检索 / 查询改写答案正确率生成结果是否包含标注要点、且引用指向正确表达与约束能力提示词 / 上下文数量 / 模型能力 / 是否忽略片段越权率(必测)是否返回了该用户无权查看的内容合规生死线权限过滤(5.2 节),必须为 0拒答正确率对"知识库中无答案"的问题,是否正确拒答防幻觉阈值 / 提示词第一条约束

为什么必须分开测? 因为它直接决定你下一步该改什么。 举例:如果答案为"检索命中率 92%、答案正确率 61%“,那你的问题是提示词或上下文数量(信息进来了但没用好);如果是"检索命中率 58%、答案正确率 55%”,那你不用碰提示词,先去改分块与检索。 这两个数字的差值,就是"该优化哪一侧"的路标。

最后强调"越权率必须为 0":它不是"要尽量低"的指标,而是准入门槛——只要出现一次,整个系统就不能上线,因为它意味着一次数据泄露事故。 而验证方法必须包含"刻意构造越权测试":用低权限账号去问高权限文档里的具体问题,看系统是拒答还是泄露。 只测"正常用户能拿到答案"是不够的——权限类问题永远不会在正常用例里暴露。


5️⃣ 进阶与优化

5.1 检索质量优化的四招(按性价比排序)

如果你的检索命中率不达标,按这个顺序试,不要跳步:

1. 加混合检索(性价比最高)。如果现在只有向量检索,先补上关键词检索——这一步通常能解决"编号/型号/专有名词查不到"的一整类问题,实现成本也不高(2.4 节)。
2. 改分块(收益大但要重跑入库)。如果观察到"答案需要跨两段才能读懂"或"块里混了多个主题",说明块长或切分策略不对(2.2 节)。代价是必须全量重新入库,所以要先把新策略在小样本上验证过再全量跑。
3. 加查询改写(针对"口语提问")。用户的口语表述与文档的书面表述差异很大(问"报销要多久"而文档写"费用核销时限")。做法是先用一次模型调用把问题改写成若干个检索友好的查询(或生成几个同义表述,分别检索后合并)。代价是每次提问多一次模型调用。
4. 加 Rerank(针对"召回对但排序差")。判断方法见 2.5 节:把检索结果打出来看,正确答案在不在里面但有靠后,那就是 rerank 的用武之地。

注意第 2 招与其他三招的性质不同:其他三招是"检索端优化",第 2 招是"数据端优化",而数据端优化的收益往往最大、也最容易被跳过——因为它要重跑入库,很多人宁愿在检索端反复调参,也不愿意承认"我的分块从一开始就是错的"。

5.2 ⚠️ 权限过滤:企业 RAG 的生死线

这一节是整篇文章里我最想让每个团队都读两遍的部分,因为它涉及的是"事故"而不是"效果"。

先讲一个必须纠正的侥幸心理:不要用提示词去实现权限控制。 我见过这样的实现:检索时不加过滤,把所有候选片段都塞进提示词,接着在系统提示里写一句"如果资料涉及用户无权查看的内容,请不要回答"。 这个方案有两个致命问题:

第一,它依赖模型的"自觉"。 模型可能不遵守,尤其在你用了个性化的提示词、或者用户输入里带了诱导性表述时(提示注入,系列一第 7 篇)。而且它可能"遵守得不彻底"——不直接回答,但把内容摘要出来,或者用"根据相关规定"的方式带出关键数字,这同样是泄露。

第二,更根本的问题:敏感内容已经进了提示词,也就是已经离开了你的访问控制边界。 它会出现在日志里、出现在链路追踪里、出现在你为了排查问题而临时打开的调试开关里。 “检索到了再请它别说”,本质上等于"先把文件发给外人再请他保密"——这个类比就是它的全部荒谬之处。

否(异常,需告警)

是

用户提问

构造权限过滤表达式
基于用户身份 / 部门 / 角色 / 密级

向量检索(带过滤)
只在用户可见的子集内做最近邻

关键词检索(带过滤)

合并去重 + 重排 + 阈值截断

命中片段
是否全部在权限范围内?

丢弃 + 记审计事件
不得进入提示词

拼装上下文(此时才第一次出现原文)

审计日志

模型生成

回答 + 引用(引用同样限于权限内)

(图 6:权限过滤在检索链路上的位置——两个关键点:过滤发生在检索时,以及拼装上下文之前还有一道兜底校验)

这张图上有两个设计要点,我把它们当成硬要求:

第一,过滤发生在"检索时"而不是"取回后"(4.2 节已解释:后者会让权限外的块占满候选名额,导致"有权看的文档反而取不回来")。

第二,在拼装上下文之前加一道"兜底校验"。 为什么检索已经过滤了还要再查一遍?因为过滤表达式是由代码拼出来的,而代码会写错。 我在实践里见过真实的 bug:某个取值拼接时把"或"写成了"且"(或者相反),结果过滤条件比预期宽松——这类问题不会报错,只会静默地多返回一些东西。 所以我在"原文第一次进入提示词"这个位置加一个断言式的检查:任何不在权限范围内的片段,一律丢弃并记一条审计事件。 它不是"以防万一"的冗余,它是"最后一道物理闸门"——因为它保护的不只是这次回答,而是"原文离开访问控制边界"这个不可逆的动作。

最后补一条常被忽略的边界:权限过滤不只用于向量检索,关键词检索必须用同一套过滤。 我见过只给向量检索加了过滤、忘给 BM25 那一路加的实现——结果是通过关键词搜索可以搜到越权文档,而这个漏洞在功能测试时完全测不出来(因为正常用例不会去搜越权内容)。

5.3 更新与版本:RAG 的运维难点

RAG 系统上线后,真正的复杂度来自"文档一直在变"。 四件事必须想清楚:

第一,增量更新。 如果一份 300 页的文档只改了第 5 章,重新全量入库是浪费(嵌入是有成本的,本地模型也要算时间)。做法是按文档粒度做"整份替换"——先删掉该 doc_id 的所有块,再重新入库新版本。为什么不用"按块比对差异"? 因为分块结果会随内容变化而整体位移(改一段可能导致后面所有块的边界都变了),按块 diff 得不偿失。

第二,旧版本必须失效,而不是共存。 如果新旧两版同时在库里,检索可能同时召回两版内容——结果是模型看到冲突信息,随机选一个给你(4.4 节第三条约束就是为了缓解这个,但根治办法是让旧版本不可检索)。做法是用 doc_version 与"生效状态"字段控制,入库新版本时把旧版本标记失效。

第三,删除是难题。 文档被删除时,它在向量库里的块也必须删除——否则会出现"文件已经不在了,AI 还能答出里面的内容"这种尴尬且合规上危险的情况。这在技术上不难(按 doc_id 删除即可),难在"触发时机":如果文档系统删除文件时没有通知到 RAG 系统,这个遗漏会长期存在。 所以我的建议是"定期对账":把文档系统的清单与向量库的 doc_id 集合做一次比对,注意到孤儿块就清理——这是一条很土但很有效的运维措施。

第四,为"重嵌入"预留排期。 换嵌入模型、改分块策略都会触发全量重建(2.1 节)。这是一项"数小时到数天"的停机性或高负载操作,必须提前规划窗口——不要等到效果不满意才临时决定重跑,那时候你既没有窗口也没有验证方案。

5.4 成本与延迟:钱花在哪里

RAG 的成本结构与"纯对话"不同,值得单独说清,因为它比想象中便宜,也比想象中容易被误解:

环节成本特征优化方向入库时嵌入一次性(按块数 × 每块 token 计费)只在增量更新时付费;分块策略确定后再全量跑,避免反复重跑检索几乎可忽略(向量库的 CPU/内存开销)索引与过滤条件的效率;这部分不是成本大头Rerank按候选条数计费只对 top 20 做重排,不要对全量做生成每次请求的主要成本,且输入 token 因注入片段而显著变大控制注入片段数量与长度(这是最有效的杠杆);简单问题走低价模型档位

这张表里最实用的结论是:RAG 的成本大头在"生成"的输入 token 上,而不在检索或嵌入。 所以"少注入但不失真"是成本优化的核心——把 top-k 从 5 提到 20 会让输入 token 翻几倍,而收益往往有限(尤其是当你已经有 rerank 的时候)。我的建议是:注入 3~5 段,每段控制在几百字,并且明确"宁少而准"——这与 1.3 节的判断是同一件事:质量取决于最相关的那几段,而不是段数多少。

延迟方面记一个量级认知就够:检索通常在几十毫秒到几百毫秒,Rerank 是几百毫秒,生成的耗时取决于输出长度与是否流式。 所以"用户等待的体感"主要由生成决定——这也意味着,如果用户抱怨慢,第一步应该看的是"有没有开流式输出"(第 8 篇),而不是去优化那个只占几十毫秒的检索。

5.5 评估闭环:让线上反馈变成测试集

RAG 系统的效果是"会漂移的"——文档在变、业务在变、用户在变。所以必须有一条把线上反馈回流到测试集的路径,否则你的测试集三个月后就失效了。

做法很简单:在回答下方放"有用 / 没用"两个按钮,并要求"没用"时可选填原因。 接着每周做一次归并:把被点"没用"的问题按原因分类(检索没找到 / 答案不对 / 引用错 / 拒答了但其实有答案),把其中典型的问题加进测试集。

这一步的价值在于它把"用户抱怨"变成了"可回归的用例"。 没有这条闭环的团队,会遇到一种很典型的困境:效果越来越差、但说不清差在哪,只能凭感觉改提示词。 而有了闭环之后,"这次改动让哪几个用例从错变对、哪几个从对变错"是能算出来的——这才是"优化"而不是"折腾"。


6️⃣ 适用边界与风险提示

6.1 ⚠️ 适用场景

  • 静态知识为主的问答:制度、规章、操作手册、产品文档、历史工单——特征是"答案存在于文档里,且表述相对稳定"。
  • 需要"可追溯"的场景:这是 RAG 相对微调和纯生成的核心优势,业务方能核对原文,也是它能在政企场景通过评审的关键。
  • 文档持续更新的场景:改文档即生效,不需要重新训练。
  • 与本地模型组合(第 3 篇):这是内网合规场景最常见的组合——RAG 补上本地模型知识不足的短板,本地模型保证数据不出网。

6.2 ⚠️ 不适用场景

需求为什么不行替代方案跨文档聚合与统计检索是"取最相关几块",天生不适合遍历结构化查询:模型生成查询条件,数据库给准确结果精确数值计算模型算术不可靠,表格切片后关系丢失结构化数据 + 计算引擎;模型只做"理解与路由"需要全局摘要(整本书)检索只给你几块,"全局视角"取不到分层摘要(先分段摘要再汇总),或长上下文(1.1 节)图片、图纸、音视频内容纯文本 RAG 覆盖不到多模态方案;或先人工转成文字描述文档量极少(几页)工程成本高于收益直接全量塞进上下文要求 100% 准确且零幻觉概率系统做不到,只能把风险降到可接受并留人工兜底作为"辅助检索"而非"裁决依据"

6.3 ⚠️ 幻觉与"引用的诚实性"

即使做了 RAG,模型仍可能犯错,而且它的错误会以一种尤其危险的形式出现:带假引用的错误答案。

三类必须防的情况:

1. 编造引用编号。 模型可能写出 [7],而本次只提供了 5 段资料。这是最容易校验、也最该校验的一类——做法是在返回前检查回答里出现的所有编号是否都落在本次片段范围内,越界的编号直接标记为异常(图 5 时序里那道校验)。
2. 引用正确但结论错。 模型引用了 [2],但 [2] 说的是另一回事。这类问题自动化检测很难,只能靠测试集与人工抽检。
3. 片段之间冲突而模型自行"抹平"。 新旧两版制度同时被检索到时,模型可能给你一个"看起来两版都满足"的答案,而事实上它自己调和出了一个不存在的规则。 这就是 4.4 节第三条约束的价值——要求它指出冲突而不是自行取舍。

我的建议是把"引用校验"做成一个必须通过的门禁:编号越界直接降级回答(改为"资料不足,建议核实"),而不是把带假引用的答案发给用户。 因为一个带假引用的答案,比一个明确的"我不知道"危险得多——前者会让用户建立错误的信任。

6.4 ⚠️ 数据安全:向量库是"第二份数据副本"

这一条经常被忽略,因为它在架构图上不显眼。

你为了做 RAG,把公司文档的原文(content 字段)与向量都存进了向量库。这意味着:系统里从此多了一份数据的副本,而它可能不在原有数据治理的覆盖范围内。

四件必须确认的事:

1. 权限模型要与源系统一致。 文档在源系统里只有 A 部门能看,那么向量库里对应的块也必须只有 A 部门能检索到——这就是 acl_tags 与 5.2 节的全部意义。 上线前请做一次全量对账:源系统的可见性清单与向量库的标签是否一一对应。
2. 备份与导出同样受管控。 向量库的备份文件里包含原文——如果备份被随意拷贝到个人设备,等于数据泄露,而这一层往往没人管。
3. 日志与调试信息不能存明文片段。 与第 3 篇 6.4 节同一条纪律:内网不等于可以存明文。 调试期尤其危险(临时打开的全量日志开关,经常忘记关)。
4. 向量的可反推风险要知情。 研究表明嵌入向量在某些条件下可以部分还原原文信息,所以不要把"只有向量、没有明文"当作安全前提——向量库本身就该按敏感数据来管。

6.5 六个高频问题

Q1:检索不到怎么办?

先按 5.1 节的顺序排查,但第一步永远是"打印检索结果看"。 三个最常见的具体原因:① 分块把答案切断了(答案需要跨两段);② 提问是口语、文档是书面语(需要查询改写);③ 专有名词/编号没有语义(需要混合检索,2.4 节)。注意:在这一步之前不要改提示词。

Q2:检索到了但答错,怎么办?

这是"生成侧"的问题,四个方向: ① 注入片段太多、噪声淹没了信号(先减到 3 段试试);② 提示词缺少"只用片段回答"的硬约束(4.4 节第一条);③ 片段之间冲突而模型自行取舍(加上第三条约束);④ 模型能力确实不足(此时才考虑换模型,但要先用测试集证明前三条都排除了)。

Q3:表格里的问题怎么解?

分两类处理: 如果问题是"这张表讲了什么、某个字段是什么含义" → 整表成块 + 表意摘要(2.2 节)即可;如果问题是"求某个合计、比较两个数值" → 不要交给 RAG,走结构化查询(模型生成 SQL 或调用接口)。判断标准很简单:问题的答案是不是一个需要计算得出的数——是,就换路子。

Q4:文档更新了,为什么还是答旧的?

四个可能: ① 旧版本的块没有被标记失效(5.3 节第二条,最常见);② 增量入库没跑到位(检查该 doc_id 的块数是否已更新);③ 检索同时召回了新旧两版(表现为"回答里前后矛盾");④ 缓存——如果你在应用层做了语义缓存(第 6 篇相关话题),更新文档后必须按 doc_id 失效缓存,这一点极易遗漏。

Q5:权限怎么做才安全?

三条硬要求: ① 权限标签在入库时打全(3.3 节的 acl_tags);② 过滤发生在检索请求里,向量与关键词两路都要加(5.2 节);③ 拼装上下文前做兜底校验 + 审计。并且必须做越权测试,目标值是 0(4.6 节)。绝对不要用提示词去实现权限控制。

Q6:成本怎么算、怎么降?

结构是"一次性的嵌入成本 + 每次的生成成本",降本主战场在生成侧(5.4 节)。三个最有效的动作: ① 控制注入片段数量与长度(从 top-5 × 800 字降到 top-3 × 400 字,输入 token 可显著下降);② 简单问题分流到低价模型档位;③ 避免重复嵌入(分块策略先定好,不要反复全量重跑)。另外别忘了给"入库全量重跑"预估成本——那是唯一会一次性产生较大开销的操作。

片段本身答不了这个问题

片段里有答案但模型没用

是

否

是

否

是

否

是

否

否

是

RAG 效果不达预期

先分段验证:
把检索到的片段打印出来

问题在检索侧:
分块 / 嵌入模型 / 语料解析

问题在生成侧:
提示词约束 / 片段数量 / 模型能力

答案是否跨段?

调整块长与重叠
(需全量重新入库)

提问是否口语化?

加查询改写

是否含编号 / 型号 / 专名?

加混合检索(关键词 + 向量)

换 / 补一个中文效果更好的嵌入模型
并用测试集验证

注入片段是否过多?

减到 3 段左右,宁少而准

提示词是否有硬约束?

补三条约束:只用片段 / 标引用 / 冲突要指出

最后才考虑换更强的生成模型

(图 7:RAG 效果问题的归因树——注意最上面那句话:先分段验证,再决定往哪一侧优化)


7️⃣ 总结

7.1 回到那个问题:让它知道你的知识

这篇文章回答的是一句很朴素的诉求:“它怎么不知道我们公司的制度。” 而答案不只是"上 RAG",而是一条完整的链路:把文档解析干净 → 按结构切开 → 每块带上元数据 → 存进可过滤的向量库 → 用混合检索取回 → 重排 → 只把最相关的几段交给模型 → 要求它标注来源 → 校验引用 → 把线上反馈变成测试集。

其中每一个环节都可以独立地把系统搞坏,而它们最终都表现为同一句话:“AI 答得不对”。 所以最该带走的不是某个参数值,而是那个排查方法——先分段验证,再看往哪边优化。

7.2 本篇的五个核心判断

1. RAG 的效果八成由检索决定,模型只决定表达。 所以"换个更强的模型"通常不是正解,先看检索结果里有没有正确答案。
2. 相似度阈值比 top-k 更重要——"能答不出来"是一项功能,不是缺陷;宁可拒答,不可错答。
3. 混合检索在中文企业场景是必需品(编号、型号、专名没有语义),而权限过滤必须在检索时生效、且向量与关键词两路都要加。
4. 权限绝不能靠提示词实现——“检索到了再请它别说”,等于把文件发给外人再请他保密;过滤要在原文进入提示词之前搞定,并加一道兜底校验。
5. 文档解析是 RAG 的隐性天花板,而它的故障是静默的——上线前那 20~30 条问题的抽样验证,是性价比最高的一步。

7.3 一页纸速查

我要做什么怎么做判断该不该用 RAG知识会变 + 需要可追溯 → RAG;要遍历统计 / 精确计算 → 结构化查询分块策略按结构切 + 定长兜底,几百字量级,适度重叠;表格整块 + 表意摘要每块必须带的元数据来源 / 页码 / 章节路径 / 版本 / 权限标签选向量库能用现有 PG 就用 pgvector;已有 ES 且要混合检索就选 ES;千万级再上专用库检索参数top-k 先取 20,阈值用自己语料标定(正负例分布之间的空隙)权限过滤写在检索请求里(两路都加),拼装上下文前再兜底校验一次提示词三条硬约束① 只用片段 ② 标注来源编号 ③ 冲突要指出、不自行取舍引用回传编号在"拼装上下文"处统一生成,顺序必须与注入顺序一致;可点击回原文测 RAG检索命中率与答案正确率分开测;越权率必须为 0;拒答正确率单独看效果不达预期先打印检索片段,判断是检索侧还是生成侧,不要先改提示词文档更新按 doc_id 整份替换;旧版本必须标记失效;定期与源系统对账清理孤儿块降本成本在生成的输入 token:注入 3 段左右、每段几百字;简单问题分流到低价档位


参考资料


这篇笔记就先到这里,后面用到新的思路或者发现有问题再补充。

评论 (0)

暂无评论