本章目标
到第 19 章为止,KnowHub 已经完成了一套基础企业级 RAG 平台的主要链路。
用户可以:
text
上传文档
-> 文档解析
-> 文本切片
-> Embedding 向量化
-> pgvector 存储
-> 向量检索
-> RAG 问答
-> 查看引用来源
-> 记录 qa_log
这说明系统已经"能跑"。
但 RAG 项目还有一个更难的问题:
text
能跑,不等于答得好。
很多 RAG 系统第一版跑通后,会出现这些情况:
- 明明知识库里有答案,却检索不到。
- 检索到了文档,但片段不够准确。
- TopK 返回了很多内容,但有一半无关。
- 问答答案看起来流畅,但引用支撑不充分。
- 用户问得很短,系统不知道该搜什么。
- 文档更新后,旧向量和新文档不一致。
- 模型回答了,但没有识别"资料中没有答案"。
这些问题不是简单换一个大模型就能解决。
RAG 的质量来自整条链路:
text
文档质量
-> 切片质量
-> 向量质量
-> 召回质量
-> 排序质量
-> Prompt 约束
-> 模型生成
-> 引用校验
第 20 章要讲的就是:基础 RAG 完成后,如何继续优化检索质量和回答质量。
本章会严格区分两个层次:
- KnowHub 当前已经实现的基础能力。
- 后续可以演进的高级 RAG 能力。
当前项目已经实现了段落切片、overlap、Embedding、pgvector 余弦检索、TopK、相似度阈值、Prompt 引用约束、无结果不调用模型、qa_log 和引用记录。混合检索、rerank、查询改写、多轮对话记忆、答案后校验等内容,本章会作为高级演进方向讲解,不写成当前已经完成的代码。
20.1 为什么基础 RAG 还需要优化
先看一个很常见的现象。
你上传了一份项目文档,然后问:
text
这个系统怎么做权限隔离?
系统可能会返回正确答案。
但如果你换一种问法:
text
A 用户能不能看到 B 用户的数据?
系统可能就检索不到最相关的片段。
为什么?
因为用户问法和文档原文不一定一致。
文档里可能写的是:
text
Gateway 透传 X-User-Id,下游服务基于当前用户做资源归属校验。
用户问的是:
text
A 用户能不能看到 B 用户的数据?
这两个句子意思接近,但词面差异较大。
RAG 优化就是要解决这类问题。
它不是只追求"能返回一个答案",而是追求:
text
更容易找到正确资料
更少召回无关资料
更稳定地拒绝无法回答的问题
更清楚地展示引用来源
更容易在文档更新后保持一致
所以第 20 章其实是在回答一个高级问题:
text
RAG 项目从 demo 走向更可靠系统,应该优化哪些地方?
RAG 常见问题速查表
遇到 RAG 效果问题时,不要一上来就改代码。先把症状说清楚,再判断问题在检索侧、模型侧,还是数据侧。
| 症状 | 可能原因 | 优先排查 | 优化方案 | 对应章节 |
|---|---|---|---|---|
| 检索到 0 个结果 | 阈值太高、文档未索引、问题与文档无关、Embedding 模型更换后维度不一致 | 查看 search 页的 resultCount、document_chunk_vector 是否有数据、检查 similarity |
降低阈值、确认索引状态、重新生成向量 | 20.5 调优、20.6 元数据过滤 |
| 检索到了结果但答案不准 | TopK 太大引入噪声、检索到的片段本身不准确、Prompt 约束不够严格 | 观察 search 页各片段相似度和内容是否相关 | 调整 TopK、增加 rerank、强化 Prompt 约束 | 20.5 TopK、20.8 rerank、20.10 Prompt |
| 答案流畅但引用支撑不足 | Prompt 允许模型在不参考资料时仍回答、引用约束不够严格 | 检查回答中的结论是否在引用的 content 中有原文对应 | 强制引用编号、低置信度提示、答案后校验 | 20.10 Prompt、20.12 答案后校验 |
| 用户短问题或模糊问题检索差 | 原始问题语义不够明确、缺少上下文 | 检查原始问题是否可以改写成更完整的表述 | 增加查询改写、多路召回、引导用户补充上下文 | 20.9 查询改写 |
| 精确编号或代码检索不到 | 纯向量检索对精确字符串匹配弱 | 检查是否应该添加关键词检索做补充 | 增加关键词检索,与向量结果合并去重 | 20.7 混合检索 |
| 检索耗时很长 | pgvector 索引未创建、数据量大但未执行 ANALYZE、向量维度不一致 | 查看 retrievalCostTimeMs、检查 pgvector 索引状态 |
控制候选池规模、创建 ivfflat/hnsw 索引、执行 ANALYZE | 20.8 rerank 候选池控制、第 11 章 ivfflat 索引 |
| 模型回答偶尔降级 | 模型服务不稳定、超时配置太短、网络抖动 | 查看 modelFallback 比例、model_error_message |
调整 timeout、检查模型服务、优化 Prompt 长度 | 第 14 章 Sentinel 降级 |
| 相同问题不同时间答案不一致 | 模型生成有随机性、文档更新后旧向量未清理、不同用户权限看到不同数据 | 对比两次检索的命中片段是否一致 | 降低 temperature、清理旧向量、确认权限过滤条件 | 20.13 增量索引 |
| 多轮追问时系统不理解指代 | 当前是单轮问答、缺少会话历史 | 检查是否存在会话表和消息表、是否存在多轮对话能力 | 增加会话历史、历史摘要、带上下文的查询改写 | 20.11 多轮对话记忆 |
| 文档更新后仍回答旧内容 | 旧 chunk 或旧向量没有删除、索引任务没有重新执行 | 检查文档版本、chunk 表和向量表是否同步 | 引入版本号、删除旧向量、执行增量索引 | 20.13 增量索引 |
| Prompt 很长但答案仍不稳定 | 上下文里无关内容太多、关键片段排序靠后 | 查看最终进入 Prompt 的 references 顺序和内容 | 控制 TopK、增加 rerank、截断低相关上下文 | 20.8 rerank、20.10 Prompt |
| 用户觉得"答非所问" | 检索命中与问题意图不一致、问题本身有歧义 | 对比原问题、改写问题和召回片段 | 增加问题澄清、查询改写、用户反馈入口 | 20.9 查询改写、20.14 评估 |
总提示:遇到问题先看 qa_log 表里的 modelSuccess、modelFallback、retrievalCostTimeMs、modelCostTimeMs 四个字段。先判断问题在检索侧还是模型侧,再对照本章对应小节找方案。
20.2 当前 KnowHub 已经具备的基础能力
先不要急着讲高级技术。
我们先看当前 KnowHub 已经做到什么。
文档解析和切片
KnowHub 支持 TXT、Markdown、PDF 解析。
解析后的文本会进入 TextChunker。
当前切片配置是:
text
maxLength = 800
overlapLength = 120
也就是说,每个切片最大约 800 个字符,相邻切片之间保留一部分重叠内容。
文本归一化
TextChunker 会做一些清洗:
- 把 Windows 换行统一成
\n。 - 把 tab 替换成空格。
- 去掉部分控制字符。
- 合并多余空格。
- 合并过多空行。
这一步很基础,但很重要。
如果文本里有大量脏字符,后续切片和向量化效果都会受影响。
向量化和 pgvector
KnowHub 使用 embedding 模型把文本变成向量。
当前向量维度是:
text
1024
pgvector 表中对应字段是:
text
embedding vector(1024)
向量检索时,系统使用余弦距离相关计算:
sql
1 - (embedding <=> ?::vector) AS similarity
结果按距离排序,返回最相似的切片。
TopK 和相似度阈值
当前默认配置是:
text
topK = 5
similarityThreshold = 0.30
PgVectorUtils 还会做保护:
- TopK 最小为 1。
- TopK 最大为 20。
- 相似度阈值限制在 -1 到 1。
这可以防止用户传一个特别大的 TopK,把系统一次拖垮。
Prompt 引用约束
当前 Prompt 要求模型:
text
严格基于给定资料回答。
资料中没有答案时,回复无法确定。
答案末尾标明引用。
这说明 KnowHub 已经不是直接把问题丢给模型,而是把检索结果作为上下文交给模型。
日志和引用记录
问答结果会记录:
qaLogId- 检索耗时
- 模型耗时
- 总耗时
modelSuccessmodelFallback- 引用来源
这为后续优化提供了数据基础。
没有日志,就很难知道到底是检索差还是模型差。
20.3 RAG 效果取决于五个环节
很多人优化 RAG 时,一上来就想换模型。
但 RAG 效果不好,原因可能不在模型。
可以先用五个环节分析。
1. 文档质量
如果原文很乱,RAG 很难答好。
比如:
- PDF 解析出来段落错乱。
- 表格内容丢失。
- 标题和正文混在一起。
- 代码块被截断。
- 文档中本来就没有答案。
这些问题不是向量数据库能解决的。
2. 切片质量
切片太短,信息不完整。
切片太长,噪声太多。
切片位置不合理,答案可能被切到两个片段里。
3. 召回质量
召回质量指的是:
text
系统有没有把真正相关的片段找出来。
如果召回失败,模型没有资料可用。
4. 排序质量
召回到了相关内容,但最相关的片段没有排在前面,也会影响结果。
因为 Prompt 长度有限,通常只能把前几个片段交给模型。
5. 生成约束
即使检索正确,prompt 没写好,模型也可能:
- 编造。
- 忽略引用。
- 回答过长。
- 没有说明无法确定。
所以 RAG 优化不是单点优化,而是链路优化。
当前能力 vs. 高级能力
前面 19 章已经搭建了一个完整的 RAG 平台,本章不是推倒重来,而是在已有基础上继续优化。
| 能力维度 | KnowHub 当前已实现 | 本章所述高级演进方向 |
|---|---|---|
| 切片 | 段落切片 + overlap,控制 maxLength 和 overlapLength |
Markdown 标题层级切片、表格感知、代码块整体保留、页码来源保留 |
| 检索 | 单路向量检索 + TopK + 相似度阈值 | 混合检索、多路召回、元数据过滤、查询改写 |
| 排序 | Embedding 余弦相似度排序 | rerank 重排序,把更相关的片段放到 Prompt 前面 |
| Prompt | 四条基础约束:基于资料、无法确定、不编造、标明引用 | 六段式 Prompt、引用强制标注、低置信度提示、上下文长度控制 |
| 对话 | 单轮问答,每次问题独立检索 | 多轮对话记忆、历史摘要、指代消解、带上下文的查询改写 |
| 评估 | qa_log 记录耗时、状态和引用 |
标准问题集、自动评估、用户反馈闭环、质量报表 |
每项高级能力都建立在对应基础能力之上。比如没有稳定的 qa_log,就很难做质量评估;没有基础向量检索,就谈不上 rerank;没有可靠的切片和来源记录,Prompt 再复杂也无法保证引用可信。
20.4 切片策略优化
当前 KnowHub 的切片策略比较适合教学和基础项目。
它做了段落切分、长度限制和 overlap。
但在真实知识库里,文档结构会更复杂。
固定长度切片的问题
最简单的切片方式是固定长度切片。
比如每 800 个字符切一次。
这种方式容易实现,但有问题:
text
可能把一个完整语义切断。
例如一段话前半部分说"系统采用 gateway 鉴权",后半部分说"下游服务只信任 X-User-ID"。
如果切片正好从中间切开,检索结果可能只拿到半句话。
段落切片的优点
Knowhub 当前会先按空行拆段落,再组合成 chunk。
这比纯固定长度更好。
因为自然段通常包含一个相对完整的意思。
Overlap 的价值
Overlap 就是重叠。
它的作用是避免关键上下文在切片边界处丢失。
比如一个 chunk 末尾说:
text
Gateway 校验 Token 后会透传
下一个 chunk 开头说:
text
X-User-Id 给下游服务
如果没有 overlap,两句话可能分离。
有了 overlap,后一个 chunk 会带一点前文,更容易被检索出来。
后续可优化方向
后续可以支持更结构化的切片:
- Markdown 标题层级切片。
- 保留标题路径,比如"第 4 章 / Gateway 鉴权 / 用户透传"。
- 表格按行或按块处理。
- 代码块整体保留。
- PDF 按页码和段落保留来源。
- 超长章节先按标题拆,再按段落拆。
这些优化的目标是:
text
让每个 chunk 尽量语义完整,并且带足够来源信息。
切片策略对比示例
假设原文是一段关于 Gateway 鉴权的技术描述:
text
Gateway 收到请求后,首先校验 Authorization 中的 Token。Token 校验通过后,系统会解析出 userId、username 和 role。随后 Gateway 会删除外部传入的伪造用户头,并重新写入可信的 X-User-Id 和 X-User-Role。下游服务只读取 Gateway 写入的用户身份,不信任前端自己提交的 userId 参数。
如果使用固定长度切片,假设 chunkSize=150,结果可能是:
text
chunk_0:Gateway 收到请求后,首先校验 Authorization 中的 Token。Token 校验通过后,系统会解析
chunk_1:出 userId、username 和 role。随后 Gateway 会删除外部传入的伪造用户头,并重新写入可信的 X-User-Id
chunk_2:和 X-User-Role。下游服务只读取 Gateway 写入的用户身份,不信任前端自己提交的 userId 参数。
可以看到,chunk_0 和 chunk_1 都有语义截断。用户问"下游服务为什么不能信任前端 userId"时,系统可能只召回到半段信息。
如果按段落切片,结果是:
text
chunk_0:Gateway 校验 Token -> 解析用户身份 -> 删除伪造请求头 -> 写入 X-User-Id 和 X-User-Role -> 下游服务读取可信身份。
这一个 chunk 就包含完整语义链路。
结论是:固定长度切片适合教学和理解原理,段落切片更适合语义检索。优化切片策略时,目标是让每个 chunk 尽量自包含,也就是不依赖上下文也能读懂。
20.5 Top K 和相似度阈值调优
KnowHub 当前支持两个重要参数:
text
topK
similarity threshold
它们看起来简单,但对效果影响很大。
TopK 太小
如果 TopK 太小,比如只取 1 条,很容易漏掉答案。
因为最相似的片段不一定就是最完整的片段。
TopK 太大
如果 TopK 太大,比如取 20 条,会带来两个问题:
- Prompt 变长,模型成本增加。
- 无关片段更多,模型可能被干扰。
所以 TopK 不是越大越好。
当前系统限制最大 20,是合理的保护。
阈值太高
相似度阈值太高,会导致检索结果为空。
用户会看到:
text
知识库中未检索到相关内容,无法确定。
这不一定是文档不存在,也可能是阈值设置过严。
阈值太低
阈值太低,会召回很多弱相关内容。
模型拿到这些内容后,可能生成看似合理但依据不稳的答案。
如何调参
调参时不要凭感觉。
推荐流程:
text
准备一组真实问题
观察 search 页返回结果
记录 resultCount 和 similarity
调整 topK
调整 threshold
再观察 chat 答案和引用
第 16 章用户端里的"向量检索"Tab 就是调参工具。
先看系统找到了什么,再看系统怎么回答。
调参实例:从无结果到可用答案
看一个具体例子。
某学生上传了一份《员工报销制度》PDF,正文大约 3000 字。文档索引成功后,他提问:
text
发票什么时候交
初始参数是:
text
topK=3
similarityThreshold=0.50
问答接口返回:
text
知识库中未检索到相关内容,无法确定。
第一步,先不改代码,直接调用检索接口:
text
GET /kb/{kbId}/search?question=发票什么时候交&topK=5&similarityThreshold=0.0
结果发现最高相似度只有 0.18。再看文档状态,chunkCount=6,说明文档确实已经索引成功。问题不在索引,而在阈值:0.50 对这个短问题来说太高。
第二步,把阈值降到 0.15。再次检索后返回 3 条结果,最高相似度仍是 0.18,但内容已经命中:
text
发票应在开具后 30 天内提交。
这说明检索已经找到正确片段,只是相似度绝对值偏低。短问题向量信息少,similarity 偏低是常见现象。
第三步,把问答参数调整为:
text
topK=5
similarityThreshold=0.12
调用:
text
POST /kb/{kbId}/chat
模型成功返回答案,并在 references 中附上报销制度对应片段。
最终这个知识库的默认参数调整为:
text
topK=5
similarityThreshold=0.12
这个例子的经验是:对于短文档或短问题,阈值不宜设太高。应该先用 search 接口观察 similarity 分布,再决定阈值,不要凭经验直接改。
20.6 元数据过滤
当前 KnowHub 已经有基础过滤条件:
text
userId
kbId
documentId
这保证了用户隔离和知识库范围。
但更高级的 RAG 系统通常还会加入更多元数据。
比如:
- 文件类型。
- 文档标签。
- 上传时间。
- 文档版本。
- 业务分类。
- 章节标题。
- 来源部门。
- 权限等级。
为什么元数据重要?
因为向量检索只看语义相似,有时范围太大。
比如一个企业知识库里有:
text
Java 开发规范
财务报销制度
人事制度
产品需求文档
用户问"报销流程是什么",如果不加业务分类过滤,系统可能在很多不相关文档里搜索。
加上 category=财务制度 后,候选范围更小,召回更准确。
在 KnowHub 后续演进中,可以给文档增加:
text
tags
category
sourceType
version
publishedAt
然后在 document_chunk_vector 中同步这些字段,检索时加入 where 条件。
这就是元数据过滤。
20.7 混合检索
当前 KnowHub 主要使用向量检索。
向量检索擅长语义相似。
比如:
text
用户问:A 用户能看到 B 用户的数据吗?
文档写:系统通过资源归属校验实现用户隔离。
这类场景向量检索有优势。
但向量检索不擅长所有场景。
比如:
- 精确编号。
- 错误码。
- 表名。
- 方法名。
- 配置项。
- 产品型号。
用户问:
text
40101 是什么错误?
关键词 "40101" 非常关键。
这时关键词检索可能比纯向量更可靠。
什么是混合检索
混合检索就是同时使用:
text
向量检索 + 关键词检索
然后把结果合并。
常见做法是:
- 向量检索召回一批。
- 关键词检索召回一批。
- 合并去重。
- 重新排序。
- 取最终 TopK。
在 KnowHub 中,后续可以用两种方式演进:
- PostgreSQL 全文检索 + pgvector。
- 引入 ElasticSearch 或 OpenSearch 做关键词检索。
当前项目还没有实现混合检索,所以教材中要把它写成后续优化方向。
20.8 rerank 重排序
召回不是最终答案。
向量检索返回的 TopK 是基于向量相似度排序,但它不一定完全符合用户问题。
rerank 的思路是:
text
先多召回,再精排序。
比如先用向量检索取 Top20,再用 rerank 模型判断每个片段和问题的相关性,最后取 Top5。
为什么这样做?
因为第一阶段召回要尽量不要漏掉答案。
第二阶段重排再把最相关的内容放前面。
Rerank 适合解决什么问题
- Top K 里有无关内容。
- 相似度高但不回答问题。
- 片段语义接近但重点不同。
- 需要更稳定的引用排序。
KnowHub 如何演进
后续可以增加一个 Rerank Service:
text
输入:question + candidate chunks
输出:重新排序后的 chunks
调用位置可以放在:
text
DocumentVectorService.search 之后
KnowledgeChatService 拼 Prompt 之前
这样既不破坏当前基础检索接口,也能逐步增强问答质量。
20.9 查询改写和多路召回
用户的问题经常不适合直接检索。
比如用户问:
text
这个怎么配?
对系统来说,"这个"没有明确对象。
如果是多轮对话,用户上一轮可能说的是:
text
我想部署到 Linux 虚拟机。
那"这个怎么配"其实是问 Linux 虚拟机部署配置。
查询改写
查询改写就是把用户问题改成更适合检索的问题。
比如把:
text
这个怎么配?
改写成:
text
KnowHub 部署到 Linux 虚拟机时,Docker Compose、环境变量和服务地址应该如何配置?
改写后的问题更容易召回正确文档。
多路召回
多路召回是同时使用多个查询。
比如:
- 原始问题。
- 改写问题。
- 提取关键词。
- 加上历史上下文的问题。
然后分别检索,最后合并去重。
这种方式能提高召回率,但成本也更高。
因为每一路可能都要做 Embedding 和检索。
所以要控制:
- 查询数量。
- 候选数量。
- 去重逻辑。
- 总耗时。
当前 KnowHub 是单路检索,后续可以逐步演进到多路召回。
20.10 Prompt 优化
当前 KnowHub 的 Prompt 已经有几个关键约束:
text
严格基于给定资料回答
资料中没有答案时回复无法确定
不要编造资料中不存在的信息
回答末尾标明引用
这是一个很好的基础 Prompt。
但在更复杂场景里,还可以继续优化。
Prompt 可以拆成几部分
一个更清晰的 Prompt 通常包含:
- 角色:你是什么助手。
- 任务:你要完成什么。
- 资料:可用上下文是什么。
- 约束:不能做什么。
- 输出格式:用什么格式回答。
- 无答案策略:资料不足时怎么回答。
控制上下文长度
Prompt 不是越长越好。
塞进太多切片,会带来:
- 成本增加。
- 模型注意力分散。
- 无关内容干扰。
- 响应变慢。
所以 Prompt 优化和检索优化要一起做。
先提升检索质量,再把更少、更准的上下文交给模型。
引用格式
当前 Prompt 要求答案末尾写"引用:引用1、引用2"。
后续可以更严格:
text
每个关键结论后都标注引用
无法从资料证明的内容不要写
引用必须来自给定资料编号
这样更适合企业知识库。
20.11 多轮对话记忆
当前 KnowHub 的主线是单轮问答。
用户问一个问题,系统检索一次,模型回答一次。
但真实用户经常会连续追问。
比如:
text
用户:这个系统怎么做用户隔离?
助手:系统通过 Gateway 透传 X-User-Id...
用户:那 Redis 缓存会不会绕过这个隔离?
第二个问题里的"这个隔离"依赖上一轮上下文。
如果系统只看当前问题,可能无法理解。
多轮对话需要什么
要支持多轮,需要新增:
- 会话表。
- 消息表。
- 历史摘要。
- 当前上下文窗口。
- 多轮问题改写。
检索时不能只用当前问题,可能要把历史上下文合并进去。
但也不能把所有历史都塞进去。
否则会:
- Prompt 太长。
- 成本太高。
- 历史主题污染当前问题。
所以多轮对话不是简单保存聊天记录。
它需要设计上下文压缩和主题判断。
20.12 引用可信度和答案后校验
RAG 的一个优势是有引用。
但要注意:
text
有引用,不等于答案一定正确。
模型可能引用了资料,但答案里的某些句子仍然是推断或编造。
所以高级 RAG 系统会做答案后校验。
可以校验什么
- 答案中的关键结论是否能在引用中找到依据。
- 引用是否真的和问题相关。
- 引用相似度是否过低。
- 答案是否超出了资料范围。
- 无答案问题是否被错误回答。
低置信度提示
如果引用相似度普遍很低,可以提示:
text
当前答案依据较弱,请结合原文确认。
这对企业场景很重要。
因为企业知识库宁愿保守,也不要一本正经地胡说。
KnowHub 如何演进
KnowHub 已经有 qa_log 和 qa_reference。
后续可以在此基础上增加:
- 引用平均相似度。
- 最低相似度。
- 答案置信度。
- 用户反馈。
- 人工标注结果。
这些数据可以帮助持续优化 RAG 效果。
20.13 知识库更新和增量索引
知识库不是一次上传就永远不变。
真实系统中会出现:
- 文档更新。
- 文档删除。
- 文档重新上传。
- 标签修改。
- 文件从本地迁移到对象存储。
- 索引任务失败后重试。
当前 KnowHub 已经有向量重建接口:
text
POST /kb/{kbId}/documents/{documentId}/vectors/rebuild
当前向量写入逻辑是:
text
删除旧向量
-> 遍历 chunk
-> 重新 embedding
-> upsert 到 pgvector
这是很清楚的基础方案。
后续可以演进为更细粒度的增量索引:
- 只更新变化的 chunk。
- 删除文档时同步删除向量。
- 文档版本号控制。
- 索引任务去重。
- RabbitMQ 消息驱动索引。
- MinIO 对象路径和文档元数据保持一致。
- 失败任务补偿和死信队列。
这时第 9 章 RabbitMQ 和第 7 章 MinIO 的价值会更明显。
RabbitMQ 负责让索引任务异步可靠执行。
MinIO 负责让原始文件可被不同服务稳定读取。
20.14 RAG 效果怎么评估
没有评估,就不知道优化有没有用。
RAG 评估可以从几个维度看。
召回命中率
准备一批问题和标准引用,看检索结果里是否包含正确片段。
引用相关性
看返回的引用是不是和问题相关。
不能只看 answer 是否流畅。
无答案识别能力
准备一些知识库里没有答案的问题。
系统应该回答:
text
知识库中未检索到相关内容,无法确定。
而不是强行编造。
耗时
记录:
- 检索耗时。
- 模型耗时。
- 总耗时。
第 18 章已经讲过,RAG 问答耗时明显高于普通接口。
降级率
统计 modelFallback=true 的比例。
如果降级率很高,说明模型服务、网络或超时配置需要检查。
用户反馈
最终还是要看用户是否满意。
可以设计:
text
有帮助 / 没帮助
答案错误
引用不相关
缺少信息
这些反馈可以反过来优化切片、检索和 Prompt。
利用 qa_log 收集基础评估指标
前面讲评估维度,这里给出几条可以直接在 MySQL 中执行的 SQL。
前提是 qa_log 和 qa_reference 表已经创建,详见第 12 章和第 15 章。
查询一:最近 7 天降级率统计
sql
SELECT
COUNT(*) AS total,
SUM(model_fallback = 1) AS fallback_count,
ROUND(SUM(model_fallback = 1) / COUNT(*) * 100, 1) AS fallback_pct
FROM qa_log
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY);
这条 SQL 用来判断模型服务是否稳定。如果 fallback_pct 明显升高,优先检查模型服务、网络和第 14 章的 timeout 配置。
查询二:最近 7 天成功问答的平均耗时分布
sql
SELECT
AVG(retrieval_cost_time_ms) AS avg_retrieval_ms,
AVG(model_cost_time_ms) AS avg_model_ms,
AVG(cost_time_ms) AS avg_total_ms
FROM qa_log
WHERE model_success = 1
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY);
这条 SQL 用来判断慢在哪里。avg_retrieval_ms 高,优先看 Embedding 和 pgvector;avg_model_ms 高,优先看 Prompt 长度和模型服务。
查询三:最近 7 天无检索结果率
sql
SELECT
COUNT(*) AS total,
SUM(model_called = 0) AS no_result_count,
ROUND(SUM(model_called = 0) / COUNT(*) * 100, 1) AS no_result_pct
FROM qa_log
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY);
这条 SQL 用来观察召回质量。如果 no_result_pct 很高,说明切片、阈值、TopK 或用户问题表达需要优化。
查询四:高频被引用的文档片段 Top10
sql
SELECT
c.document_name,
c.chunk_index,
COUNT(*) AS cited_count
FROM qa_reference r
JOIN document_chunk c ON r.chunk_id = c.id
GROUP BY c.document_name, c.chunk_index
ORDER BY cited_count DESC
LIMIT 10;
这条 SQL 可以帮助管理员发现哪些文档片段最常被引用。高频片段可能是核心制度、核心接口说明,也可能是因为某些文档写得更容易被检索命中。
优化要有前后对比。比如调低阈值之后,再看 no_result_pct 是否下降;缩短 Prompt 之后,再看 avg_model_ms 是否下降。这样才能判断优化是否有效,而不是凭感觉说"好像好了一点"。
20.15 KnowHub 的高级演进路线
不要一口气做所有高级功能。
更合理的是分阶段演进。
第一阶段:调参和切片优化
先基于现有能力做:
- 调整
maxLength。 - 调整
overlapLength。 - 调整 TopK。
- 调整相似度阈值。
- 优化 Markdown 标题切片。
- 优化 PDF 解析后的段落清洗。
这是成本最低的一阶段。
第二阶段:元数据过滤
增加:
- 文档标签。
- 文档分类。
- 文档来源。
- 文档版本。
检索时增加过滤条件。
这可以明显减少无关召回。
第三阶段:混合检索和 rerank
加入关键词检索和 reranker。
流程变成:
text
向量召回 + 关键词召回
-> 合并去重
-> rerank
-> 最终 TopK
-> Prompt
这是提升检索质量的重要阶段。
第四阶段:查询改写和多轮对话
当用户问题更复杂时,引入:
- 查询改写。
- 多路召回。
- 会话历史。
- 历史摘要。
这会让系统更像真正的知识库助手。
第五阶段:评估体系
最后补:
- 标准问题集。
- 标准引用。
- 自动评估脚本。
- 用户反馈闭环。
- 问答质量报表。
没有评估体系,高级优化很容易变成凭感觉调参。
20.16 本章和前面章节的关系
第 20 章不是推翻前面的基础架构,而是在基础架构上继续演进。
可以这样对应:
text
第 8 章:文档解析与切片 -> 切片策略优化
第 10 章:Embedding -> 向量质量和查询改写
第 11 章:pgvector -> TopK、阈值、索引和召回优化
第 12 章:RAG 问答 -> Prompt、引用和答案质量
第 13 章:Redis -> 热点结果、状态和未来缓存优化
第 14 章:Sentinel -> 高成本问答保护
第 15 章:日志排障 -> qa_log 质量分析
第 16 章:用户端 -> 检索调参和问答反馈入口
第 17 章:管理端 -> 全局日志和质量观察
第 18 章:测试验证 -> 优化前后效果对比
第 19 章:部署交付 -> 高级能力上线基础
第 20 章:高级 RAG 优化 -> 让系统从能用走向更好用
基础 RAG 解决"有没有"。
高级优化解决"好不好"。
本章小结
这一章我们讲了高级 RAG 技术与检索优化。
KnowHub 当前已经具备基础 RAG 能力:文本解析、归一化、切片、overlap、Embedding、pgvector 余弦检索、TopK、相似度阈值、Prompt 引用约束、无结果不调用模型、qa_log 和引用记录。本章在这个基础上进一步讲解了切片策略优化、TopK 和阈值调优、元数据过滤、混合检索、rerank、多路召回、查询改写、多轮对话记忆、引用可信度、答案后校验、增量索引和 RAG 效果评估。
本章最重要的原则是:不要把高级名词堆在系统上,而要先找问题。检索不到,就看切片和召回;召回不准,就看阈值、TopK、元数据和 rerank;答案不可信,就看 Prompt、引用和后校验;系统变慢,就看 Embedding、pgvector、模型调用和缓存。优化必须围绕真实问题展开。
下一章,我们会进入生产环境演进、性能优化与面试表达,把前面所有章节沉淀成项目复盘、工程边界、性能优化和对外表达方式。
本章关键术语速查表
这些术语不需要都背下来,遇到对应问题时回来查即可。面试和答辩中能准确使用其中 5 到 8 个术语,已经能体现 RAG 领域的工程理解力。
| 术语 | 一句话解释 |
|---|---|
| Embedding | 把文本变成数字向量,让系统能计算语义相似度,详见第 10 章。 |
| chunk / 切片 | 把长文档拆成多个短片段,每个片段独立向量化和检索,详见第 8 章。 |
| TopK | 召回多少个最相似片段,太小漏答案,太大引入噪声,详见本章 20.5。 |
| 相似度阈值 | 只保留相似度超过该值的片段,太高无结果,太低召回弱相关,详见本章 20.5。 |
| overlap | 相邻切片之间的重叠部分,防止关键信息刚好落在边界处,详见本章 20.4。 |
| 元数据过滤 | 通过文件类型、标签、分类等字段缩小检索范围,减少无关召回,详见本章 20.6。 |
| 混合检索 | 同时使用向量检索和关键词检索,互补两种检索方式的短板,详见本章 20.7。 |
| rerank / 重排序 | 召回一批候选后,用更精细的模型重新排顺序,把最相关的片段放前面,详见本章 20.8。 |
| 查询改写 | 把用户输入的模糊或短问题改成更完整、更适合检索的表达,详见本章 20.9。 |
| 多路召回 | 用多个不同查询分别检索,合并结果,提高命中率,详见本章 20.9。 |
| 多轮对话 | 系统记住前几轮对话,理解用户当前问题中的指代,比如"这个""它",详见本章 20.11。 |
| 幻觉 | 大模型在没有资料依据的情况下编造听起来合理但不准确的回答,详见本章 20.12。 |
| 置信度 | 系统对答案可靠程度的评估,引用相似度越低,置信度越低,详见本章 20.12。 |
| 增量索引 | 只重新索引发生变化的文档或 chunk,而非全量重建,详见本章 20.13。 |
思考题
- 为什么基础 RAG 能跑通后,仍然可能回答不好?
- RAG 效果主要受哪些环节影响?
- KnowHub 当前
TextChunker的maxLength和overlapLength分别解决什么问题? - TopK 太小和太大会分别带来什么问题?
- 相似度阈值太高和太低会分别造成什么后果?
- 为什么元数据过滤能提高检索质量?
- 向量检索和关键词检索分别擅长什么场景?
- rerank 为什么通常放在初次召回之后?
- 查询改写解决的是什么问题?
- 多轮对话为什么不能简单把所有历史消息都塞进 Prompt?
- 为什么有引用不等于答案一定正确?
- 如果让你给 KnowHub 设计下一阶段 RAG 优化路线,你会先做哪三件事?
思考题参考答案
1. 为什么基础 RAG 能跑通后,仍然可能回答不好?
因为"能跑"只代表链路通了,不代表链路质量高。基础 RAG 只是完成了「上传文档 -> 解析 -> 切片 -> 向量化 -> 检索 -> 问答」这条主链路,但检索是否命中、切片是否语义完整、Prompt 约束是否严格、引用是否可信,这些质量维度都没有被保证。用户问法和文档原文词面不一致时,纯向量检索可能召回不到最相关的片段;切片把完整语义切断时,模型只能拿到半句话;Prompt 约束不严时,模型可能编造或忽略引用。所以基础 RAG 跑通后,仍可能检索不到、答不准、引用支撑不足。
2. RAG 效果主要受哪些环节影响?
主要受五个环节影响:文档质量、切片质量、召回质量、排序质量、生成约束。文档本身很乱,后续环节再优化也难答好;切片太短或太长都会影响信息完整性;召回失败则模型没有资料可用;排序不好会让最相关的片段排不到前面;Prompt 约束不严,模型可能编造或忽略引用。所以 RAG 优化是链路优化,不是单点优化。
3. KnowHub 当前 TextChunker 的 maxLength 和 overlapLength 分别解决什么问题?
maxLength 控制每个切片的最大长度,解决切片过长导致噪声太多、向量化效果变差的问题;overlapLength 控制相邻切片之间的重叠长度,解决关键上下文刚好落在切片边界处而丢失的问题。两者配合,让每个切片既不过长,又能保留边界处的上下文。
4. TopK 太小和太大会分别带来什么问题?
TopK 太小,比如只取 1 条,容易漏掉答案,因为最相似的片段不一定就是最完整的片段;TopK 太大,比如取 20 条,会带来两个问题:一是 Prompt 变长、模型成本增加,二是无关片段更多、模型可能被干扰。所以 TopK 不是越大越好,当前系统限制最大 20 是合理的保护。
5. 相似度阈值太高和太低会分别造成什么后果?
阈值太高会导致检索结果为空,用户看到「知识库中未检索到相关内容,无法确定」,这不一定是文档不存在,也可能是阈值设置过严;阈值太低会召回很多弱相关内容,模型拿到这些内容后,可能生成看似合理但依据不稳的答案。调参时应先用 search 接口观察 similarity 分布,再决定阈值。
6. 为什么元数据过滤能提高检索质量?
因为向量检索只看语义相似,有时检索范围太大。比如企业知识库里有 Java 开发规范、财务报销制度、人事制度、产品需求文档,用户问「报销流程是什么」时,如果不加业务分类过滤,系统可能在很多不相关文档里搜索。加上 category=财务制度 后,候选范围更小,召回更准确,也能减少无关内容对模型的干扰。
7. 向量检索和关键词检索分别擅长什么场景?
向量检索擅长语义相似场景,比如用户问「A 用户能不能看到 B 用户的数据」,文档写「系统通过资源归属校验实现用户隔离」,两者词面差异大但语义接近,向量检索能命中;关键词检索擅长精确匹配场景,比如精确编号、错误码、表名、方法名、配置项、产品型号,用户问「40101 是什么错误」时,关键词「40101」非常关键,关键词检索比纯向量更可靠。
8. rerank 为什么通常放在初次召回之后?
因为第一阶段召回的目标是「尽量不要漏掉答案」,所以要尽量多召回,比如取 Top20;但向量相似度排序不一定完全符合用户问题,TopK 里可能混入无关内容。rerank 放在初次召回之后,用更精细的模型对候选片段重新排序,把最相关的内容放到 Prompt 前面,既保证不遗漏,又提升排序质量。
9. 查询改写解决的是什么问题?
查询改写解决的是「用户问题不适合直接检索」的问题。用户的问题经常很短或很模糊,比如「这个怎么配?」,对系统来说「这个」没有明确对象。查询改写把这类问题改写成更完整、更适合检索的表达,比如「KnowHub 部署到 Linux 虚拟机时,Docker Compose、环境变量和服务地址应该如何配置?」,改写后的问题更容易召回正确文档。
10. 多轮对话为什么不能简单把所有历史消息都塞进 Prompt?
因为把所有历史都塞进去会带来三个问题:一是 Prompt 太长,模型成本增加;二是模型注意力分散,无关历史会干扰当前问题;三是历史主题污染当前问题,比如用户上一轮在聊部署,这一轮问「那 Redis 呢」,如果塞入大量无关历史,模型可能理解偏。所以多轮对话需要设计上下文压缩和主题判断,只保留与当前问题相关的历史。
11. 为什么有引用不等于答案一定正确?
因为模型可能引用了资料,但答案里的某些句子仍然是推断或编造。引用只能说明「模型参考了某段资料」,不能证明「答案中的每个结论都能在引用中找到依据」。所以高级 RAG 系统会做答案后校验,检查关键结论是否能在引用中找到原文对应、引用是否真的和问题相关、引用相似度是否过低、答案是否超出资料范围。
12. 如果让你给 KnowHub 设计下一阶段 RAG 优化路线,你会先做哪三件事?
我会先做这三件事:
- 先建立评估基线 :基于现有
qa_log和qa_reference,准备一组标准问题集,统计召回命中率、无答案识别率、降级率和平均耗时,作为优化前后的对比基准。没有评估,后续优化很容易变成凭感觉调参。 - 调参和切片优化:这是成本最低的一步。先用 search 接口观察真实问题的 similarity 分布,调整 TopK 和相似度阈值;同时优化切片策略,让每个 chunk 尽量语义自包含,减少边界截断。
- 引入元数据过滤:给文档增加标签、分类、来源、版本等字段,在检索时加入过滤条件,缩小候选范围,减少无关召回。这一步实现成本低,但对检索质量的提升非常明显。
做完这三件事后,再根据评估结果决定是否进入混合检索、rerank、查询改写等更高级的演进方向。