------ 构建 · 检索 · 评测 · 运营,业务全覆盖,直通生产
上传文档、切块、向量化、检索------这只是 RAG 的"能跑通"版本。真实场景里你会遇到:扫描版 PDF 读不出、切块把句子拦腰截断、检索答非所问。本文把工业级 RAG 知识库的完整流程拆成构建、检索、评测三段,逐参数讲清含义
前言
在《02-知识库向量检索》里,我们实现了 RAG 的最小闭环:提取 → 分块 → 向量化 → 余弦检索。但那只是教学版,距离"好用"还差得远:
- 扫描版 PDF 提取出来是空白;
- 固定 300 字切块,把"结石高发人群为 30~50 岁成年人,儿童则罕见"切成两半,两半都检索不到;
- 用户问"35 岁得结石的概率多大",文档里写的是"结石高发人群为 30~50 岁成年人",字面不像、向量也不够近。
这些问题分别对应构建期的解析/分割/索引 、检索期的召回/重排 、以及评测期的指标度量。下面按这三段展开。
整体架构

RAG 完整流程
两条流水线、一个闭环,外加一层横切的生产运营:
- 构建(离线) :文档 → 解析 → 清洗过滤 → 分割 → 向量化 → 入库。跑一次,反复用。
- 检索(在线) :提问 → 查询变换 → 多路召回 → Rerank → Top-K → 拼 Prompt 生成回答。每次提问都走。
- 评测(闭环) :用测试集度量检索和生成质量,badcase 回流,指导调参重建。
- 生产运营(横切) :脱敏在入库前、语义缓存在检索前、审查审计在出库、运营飞轮在回答后------四个插入点对应图中 ④ 层,详见第五部分。
一个核心认知贯穿全文:构建期参数决定数据以什么形态入库,改任何一项都要重建库;检索期参数每次查询随时可调。
第一部分:知识库构建
1. 文档解析
解析的目标是把千差万别的文件格式统一成纯文本。不同格式的处理方式完全不同:
| 格式 | 处理方式 | 典型工具 |
|---|---|---|
| TXT / MD | 直接读取 | fs.readFileSync / Python open |
Word .docx |
解压 XML 提取文本 | Node mammoth、Python python-docx |
Word .doc |
老二进制格式,需先转换 | LibreOffice 转 .docx |
| Excel | 逐 sheet 转成文本(行拼接或 Markdown 表格) | pandas、openpyxl |
| PDF(文本型) | 直接提取文字层 | pdf-parse、PyPDF2 |
| PDF(扫描型) | 没有文字层,必须 OCR | PaddleOCR、Tesseract |
| 图片 | OCR 提文字 + 多模态大模型图像理解 | qwen-vl 等生成图片描述 |
我们项目里的实现(utils/rag.js)覆盖了前三类:
javascript
export async function extractText(filePath) {
if (filePath.endsWith(".pdf")) {
const parser = new PDFParse({ data: fs.readFileSync(filePath) });
const { text } = await parser.getText(); // 文本型 PDF
return text;
}
if (filePath.endsWith(".docx")) {
const { value } = await mammoth.extractRawText({ buffer: fs.readFileSync(filePath) });
return value;
}
return fs.readFileSync(filePath, "utf-8"); // txt / md
}
举例 :一份《结石诊疗手册》可能同时包含三种"页面"------第 1 页是正常文字、第 3 页是扫描影印件、第 5 页是一张泌尿系统解剖图。工业级解析器(如 MinerU、LlamaParse、Unstructured)会逐页分流:文字页直接提取,扫描页走 OCR,解剖图走多模态模型生成一段描述文字("图中展示肾脏、输尿管与膀胱的位置关系,结石多见于肾脏与输尿管"),让图片内容也变成可检索的文本。
数据清洗发生在解析之后:统一全角/半角、删除多余空行、页眉页脚去重。不清洗的后果是同样的句子因空白差异变成不同的块,浪费存储还干扰相似度。
2. 文档分割
分割决定"一个向量代表多大一段语义",是构建期影响检索质量最大的参数。
分段标识:按什么规则切
| 切分方式 | 说明 | 适用场景 |
|---|---|---|
| 按页 | 每页一块 | 页即语义单元的 PPT、合同 |
| 按版面 | 识别标题/段落/表格的视觉边界切 | 排版复杂的 PDF |
| 按标题层级 | 沿 H1/H2/H3 切 | Markdown、技术文档 |
| 按双换行 | 一个段落一块 | 文章、小说 |
| 按换行 | 一行一块 | 日志、诗歌 |
| 自定义分隔符 | 按 ---、第X章 等切 |
有明确标记的文档 |
| 固定长度 | 按字符数滑动窗口切 | 兜底方案,我们手写版用的就是它 |
分段长度与重叠占比
- 分段长度(chunkSize) :块的上限。太大→一个向量里塞多个主题,语义被"稀释",检索不精准;太小→句子不完整,LLM 拿到的上下文残缺。中文经验值 300~1000 字符。
- 重叠占比(overlap) :相邻块重叠的字符数(火山引擎用占比表示,我们用绝对值)。作用是防止一句话正好在切分点被截断。

滑动窗口分块示例
举例分析:手册里有一句"流行病学:结石高发人群为 30~50 岁成年人,发病率约 10%;10 岁以下儿童罕见。"共 55 字。
- chunkSize=30 且无重叠时,它可能被切成"流行病学:结石高发人群为 30~50 岁成年人,发病率约"和"10%;10 岁以下儿童罕见。"------前半缺儿童对照、后半缺主体,问"5 岁儿童得结石的概率"两个块都匹配不好;
- 加上 overlap,总有一个块完整包含这句话,召回才有保障。
进阶:语义分割(Semantic Chunking) ------不按字数,而是计算相邻句子的向量相似度,相似度骤降处就是主题切换点,在那里切。效果更好但成本更高,属于"固定长度调不动了再上"的手段。
3. 内容处理规则
解析阶段的开关项,决定"要不要把非文字内容变成文字":
| 规则 | 作用 | 代价 |
|---|---|---|
| 移除空白字符 | 清洗噪声,节省块长度 | 几乎无 |
| 启用 OCR | 扫描页/图片中的文字可检索 | 构建变慢,OCR 有识别错误 |
| 图像理解 | 多模态模型给图表生成描述,图表内容可检索 | 调用大模型,有成本 |
判断标准:用户会不会针对这些内容提问。会问解剖图里结石的形成部位,就开图像理解;手册全是文字版,OCR 就是浪费。
4. 内容过滤规则
把"检索时纯噪声"的内容在入库前删掉:
| 规则 | 为什么要过滤 |
|---|---|
| 过滤参考资料 | 文末参考文献列表对问答无用,却可能被"作者是谁"类问题误命中 |
| 过滤链接和邮箱地址 | URL/邮箱没有语义,向量化后是噪声向量,还会稀释所在块的语义 |
举例 :块内容是"技术支持请联系 support@xx.com,详见 xx.com/help"。如果不过滤... URL 的字符模式带偏,真正想检索这段话含义的查询反而相似度下降。
5. 分段关联信息
块从文档里切出来后就失去了全局上下文------它不知道自己属于哪个文件、哪个章节。关联信息就是把元数据拼回块文本一起向量化:
举例:原始块只有一句"高发人群为 30~50 岁成年人,发病率约 10%。"脱离上下文,检索"35 岁得结石的概率多大"时它并不突出。关联标题后变成:
css
[结石诊疗手册 > 第 2 章 流行病学 > 2.1 年龄分布] 高发人群为 30~50 岁成年人,发病率约 10%。
标题里的"流行病学""年龄分布"给块补上了主题语义,命中率明显提升。同时这些元数据也单独存储,用于过滤 (只在某个文件里搜)和溯源 (回答时标注出处)。我们手写版把 source 存进了 metadata,但还没拼进向量文本------火山引擎这两个勾选框做的就是"拼进去"这件事。
6. 索引增强策略
同一个块,建多路索引,从不同角度提高召回:
| 策略 | 做法 | 解决什么问题 |
|---|---|---|
| 分句索引 | 块内每句话再单独建索引 | 短查询用句子级匹配更准 |
| 生成假设问题 | LLM 为每个块预生成几个"它能回答的问题",把问题建索引 | 用户提问是问句,"问句 vs 问句"相似度高于"问句 vs 陈述" |
| 生成摘要 | 为块生成摘要并索引 | 在"大意"层面匹配,抗表述差异 |
| 生成关键词 | 抽取关键词建关键词索引 | 支撑向量+关键词混合检索 |
举例(假设问题) :块内容"结石高发人群为 30~50 岁成年人,发病率约 10%;10 岁以下儿童罕见。"预生成问题:"35 岁得结石的概率多大?""儿童会得结石吗?"。用户问"35 岁得结石的概率多大"时,是拿问题去匹配这些预生成的问题,几乎必然命中------这就是把检索时的 HyDE 思想前移到构建期。
7. 向量数据库存储
不管用什么库,入库的每条记录都是四要素:
| 要素 | 作用 | 我们手写版对应 |
|---|---|---|
id |
唯一主键,支持更新删除 | ${source}_${i} |
document |
原始文本块,命中后直接返回 | text |
embedding |
向量,用于相似度计算 | embedding |
metadata |
来源、标题等,支持过滤与溯源 | source / index |
json
{ "entries": [ { "id": "rag-intro.txt_0", "source": "rag-intro.txt", "index": 0,
"text": "RAG 是检索增强生成......", "embedding": [0.012, -0.034, "..."] } ] }
检索时遍历所有向量逐一算余弦------暴力但透明。数据量上来后换真正的向量库:
| 向量库 | 特点 |
|---|---|
| Chroma | 上手最简单,Python 内嵌即用;Node 版需另起服务 |
| LanceDB | 嵌入式、纯本地文件,Node 生态友好 |
| Milvus / Qdrant | 分布式,百万~十亿级,生产主力 |
| pgvector | 装在 PostgreSQL 里,业务库兼向量库 |
| FAISS | Meta 出品的索引库,常作底层引擎 |
它们快的原因是用 HNSW / IVF 等近似最近邻索引:不逐一比较,而是沿"向量导航图"几跳就逼近最近邻,百万级数据毫秒返回,代价是牺牲一点点精度(近似)。
第二部分:知识库检索

检索链路:查询变换 → 多路召回 → 融合 → Rerank → Top-K
1. 查询变换:HyDE
问题:用户提问是问句("35 岁得结石的概率多大?"),库里存的是陈述句,两者表述形态不同,向量距离偏远。
HyDE(Hypothetical Document Embeddings) :先让 LLM 凭空写一个假设性答案,再拿这个答案的向量去检索:
erlang
提问:35 岁得结石的概率多大?
LLM 假设答案:结石的高发人群是 30~50 岁的成年人,
35 岁正处于该区间,发病率约 10%。
→ 用"假设答案"的向量检索,和真实块的陈述句形态一致,相似度大增
注意 HyDE 不要求假设答案正确------只要表述形态和措辞接近 真实文档即可。同类手段还有多查询扩展(把一个问题改写成 3 个不同表述分别检索,合并结果),覆盖用户措辞和文档措辞的差异。
2. 多路召回
单一向量召回有盲区:向量擅长语义相似,但对专有名词、编号、代码不敏感。多路召回并行跑多条检索路径再融合:
| 路径 | 擅长 | 举例 |
|---|---|---|
| 向量召回 | 语义相似、同义改写 | "得结石"≈"患泌尿系结石" |
| 关键词召回(BM25) | 精确词匹配 | "发病率 10%"、"30~50 岁" 这类精确术语/数值 |
| 元数据过滤 | 范围限定 | 只搜 source=结石诊疗手册.pdf |
| 增强索引(第 6 节) | 问句/摘要/句子级匹配 | 命中预生成的假设问题 |
举例 :问"5 岁儿童得结石的概率",向量路可能把"成年人流行病学数据"排前面(语义接近),而 BM25 路因为精确包含 儿童 把儿童发病率块排第一。两路互补,谁都不漏。
融合常用 RRF(Reciprocal Rank Fusion) :不看分数(各路分数量纲不同),只看排名,对每个文档累加 1/(k+rank),累加值排序。简单、无需调参、效果稳。
3. 检索方式
相似度度量(构建期选定,写入索引配置):
| 度量 | 含义 | 适用 |
|---|---|---|
| 余弦相似度 | 向量夹角,0~1,越大越相似 | 通用默认,我们手写版用的它 |
| 内积 | 向量点积,同时受长度影响 | 向量已归一化时等价余弦 |
| 欧氏距离 | 空间直线距离,越小越相似 | 低维特征 |
搜索方式:
- 精确搜索(暴力) :逐一计算,结果绝对准确,O(n) 慢。我们的 JSON 版就是它,几千块以内无感。
- ANN 近似搜索:HNSW(建导航图,几跳逼近)/ IVF(先聚类再类内搜)。百万级以上必选,用 <1% 的精度换几个数量级的速度。
4. Rerank 重排
为什么需要 :向量召回用的是 bi-encoder------query 和 chunk 各自独立编码再比距离,两者之间没有"交互",粗排精度有限。
Rerank 用 cross-encoder :把 query 和 chunk 拼在一起 送进模型,让每个词互相注意力交互,输出一个精细相关性分数。精度高一个档次,但慢得多------所以只能放在召回之后做二级漏斗:
css
全库 10 万块 → 向量召回 Top-50(毫秒级,粗)→ Rerank 精排 → Top-5(百毫秒级,精)→ 进 Prompt
举例 :召回 50 块里有三块都谈"结石发病率",cross-encoder 能读出只有其中一块真正回答了"儿童"的发病率(含"10 岁以下罕见"的数据),把它排到第一。常用模型:bge-reranker-v2-m3(可本地部署)、cohere-rerank(API)。
5. 召回分段数与相似度阈值
两个检索期参数,随时可调、不影响库:
- Top-K(召回分段数) :最终喂给 LLM 的块数。K 太小→信息不全;K 太大→噪声进上下文、挤占上下文窗口、还可能让模型被无关内容带偏。经验值 3~5。注意"召回 Top-50 → Rerank → Top-5"里有两个 K,前者是召回宽度,后者是进 Prompt 的数量。
- 相似度阈值 :低于阈值的块直接丢弃。作用是兜底"知识库没有答案"的场景------所有块相似度都低于阈值时,让模型回答"我不知道",而不是硬编一个幻觉答案。阈值需要用测试集标定,拍脑袋设容易误杀。
6. 加权评分控制:医疗概率例子
向量数据库本身只返回"相似度",不会算"概率" 。那"根据知识库判断得结石的概率"这种需求,加权控制在哪一层?答案是三层:
① 检索层加权(metadata boost) 。入库时给块挂人群元数据:population: 成年人 / population: 儿童。检索时分数不再是纯相似度,而是加权分:
ini
final_score = similarity × weight(人群与查询主体是否匹配)
替"5 岁儿童"提问时,population=儿童 的块被加成、只谈成年人的块被降权------即使成年人流行病学块相似度更高,儿童块也能靠加权翻到前面。
② Rerank 层加权 。cross-encoder 输出的相关性分数可以和向量分数线性组合:score = α·向量分 + β·rerank分,α/β 是人工设定的权重,用来平衡"语义接近"和"精确相关"。
③ 生成层加权(实践中最常用) 。把"风险因素与权重"作为结构化文本写进知识库,让 LLM 按 Prompt 逐项打分:
css
[结石风险评分规则] 年龄 30~50 岁(+4 分);男性(+2 分);
日饮水 <1L(+3 分);家族史(+2 分)。
总分 ≥6 为高风险(概率约 10%),<3 为低风险。
同一知识库,两个查询对比:
| 查询 | 检索命中 | 加权推理 | 结论 |
|---|---|---|---|
| 35 岁得结石概率? | "高发人群 30~50 岁,发病率约 10%" + 评分规则 | 年龄项 +4 分 | 概率大 |
| 5 岁得结石概率? | "10 岁以下儿童罕见,发病率 <0.5%"(metadata boost 翻上来) | 年龄项不命中,儿童数据兜底 | 概率小 |
这就把本文的疑问答清楚了:检索负责"找证据",加权负责"用证据" ------概率不是向量库算的,而是 metadata boost + 规则/LLM 加权推理的结果。要让概率可控、可审计,就把权重写进知识库的结构化规则里(③),而不是交给模型黑盒。
7. 数据权限控制:企业知识库例子
场景:员工查询企业知识库。薪酬制度文档只对 HR 部门可见,技术规范全员可见。不做权限控制,任何员工问一句"公司薪酬结构是什么"就能检索到薪酬文档。
做法是入库时给每个块挂权限元数据,检索时做前置过滤:
css
块 metadata:{ source: "薪酬制度.pdf", visible_roles: ["HR"] }
用户身份: { name: "张三", roles: ["研发"] }
检索:先按权限过滤,再算相似度
entries.filter(e => e.visible_roles.some(r => user.roles.includes(r)))
张三问薪酬,薪酬块在前置过滤里就被排除,检索不到或只命中公开块,模型回答"暂无相关信息/无查询权限";HR 同事问同样的问题,过滤放行、正常回答。同一个问题,不同身份不同答案------这就是权限控制的效果。
两个实现要点:
- 必须前置过滤(把权限条件下推到向量检索),不能后置过滤(先检索再剔除无权的) 。后置过滤有两个问题:无权内容占用 Top-K 名额,导致"有答案却检索不到";还会泄露敏感文档的存在("检索到了但你无权看")。Chroma 的
where、Milvus 的 expr、ES 的 filter 都支持前置过滤;我们的 JSON 教学版就是先filter再算相似度。 - 权限元数据必须在入库时写入 :块本身不知道自己属于谁,权限体系(部门/角色)要接入构建流水线,和
source一样成为 metadata 的一部分。
第三部分:知识库测试集与评测
1. 如何构建测试集
测试集是一组三元组:
json
{ "question": "35 岁成年人得结石的概率多大?",
"ground_truth": "30~50 岁成年人是结石高发人群,发病率约 10%,显著高于儿童",
"relevant_chunks": ["结石诊疗手册.pdf#12"] }
来源三种:
- 人工标注:领域专家写问答,质量最高、成本最高,冷启动先标 50~100 条;
- LLM 生成 + 人工校验:让 LLM 读每个块自动生成问题,人工筛掉烂问题,速度快 10 倍;
- 线上 badcase 回流 :用户真实提问里答错的,补进测试集------这是让知识库持续变好的燃料。
2. 检索层指标:召回得准不准
| 指标 | 含义 | 计算示例 |
|---|---|---|
| Hit Rate@K | 前 K 个结果里命中相关块的提问占比 | 10 题中 8 题的 Top-3 含正确块 → 80% |
| Recall@K | 相关块被捞回的比例(一个问題可能相关多块) | 相关块共 4 个,Top-5 捞回 3 个 → 75% |
| MRR | 第一个正确结果排第几的倒数平均 | 首中排名 1,2,1,3 → (1+0.5+1+0.33)/4 ≈ 0.71 |
| NDCG | 在 MRR 基础上还奖励"相关结果排得越前越好" | 排序质量越高分越接近 1 |
举例:Hit Rate@3 从 60% 提到 85%,意味着每 10 次提问少错 2.5 次------这就是"开 Rerank"或"调 chunkSize"这些动作的量化收益。
3. 生成层指标:回答得好不好
检索对了,LLM 还可能答错。生成层看三点:
| 指标 | 问的是什么 |
|---|---|
| 答案相关性 | 回答是否切题 |
| 忠实度(Faithfulness) | 回答是否都来自上下文,有没有幻觉 |
| 上下文相关性 | 喂给模型的块是否都有用 |
RAGAS 框架的做法很巧妙:用一个大模型当裁判,把"忠实度"拆成可机械验证的检查------把答案拆成若干陈述句,逐句检查能否在上下文里找到依据,比例即忠实度分数。TruLens、LangSmith、Dify 内置评测也提供类似能力。
4. 如何判断知识库是否"最优"
没有一劳永逸的最优,只有控制变量对比出的更优:
| 实验 | 对比 | 看什么 |
|---|---|---|
| chunkSize 300 vs 500 vs 1000 | 重建三个库 | Hit Rate@K、MRR |
| topK 3 vs 5 vs 8 | 只改检索参数 | 答案相关性 vs 忠实度(K 大噪声多,忠实度可能降) |
| Rerank 开 vs 关 | 只改检索链路 | MRR 提升是否值得那百毫秒延迟 |
| 加 BM25 路 vs 纯向量 | 改召回结构 | 编号类提问的 Hit Rate |
流程就是整体架构图里的闭环:跑测试集 → 看指标 → 调一个变量 → 再跑 → 指标涨了就保留。线上 badcase 持续回流测试集,评测集本身也在长大。"最优"是一个迭代方向,不是一个终点。
第四部分:现有框架一览
| 框架/平台 | 定位 | 特点 |
|---|---|---|
| LlamaIndex | 开发库(Python/TS) | 索引与检索抽象最全,RAG 组件最细 |
| LangChain | 开发库 | 生态最大,链式编排,配合 LangSmith 评测 |
| RAGFlow | 开源 RAG 引擎 | 深度文档解析(版面/表格/OCR)是强项 |
| Dify | 开源 LLMOps 平台 | 可视化编排 + 内置知识库与评测 |
| FastGPT | 开源知识库平台 | 开箱即用,多模型接入 |
| 火山引擎 / 百炼 | 云知识库服务 | 本文对照的参数页就来自这类平台,解析/索引/评测全托管 |
选型建议:学习阶段手写(你已经在做的)→ 项目快速落地用 Dify/FastGPT → 深度定制解析用 RAGFlow → 纯代码控制用 LlamaIndex。
第五部分:企业生产环境的业务流程
前四部分讲的是"检索-回答"主线。生产环境在这条主线外面还包了四层业务流程:知识生产与运营、查询侧处理、合规与安全、线上运营飞轮。串起来是一个闭环:知识生产 → 审核发布 → 构建入库 → 意图路由 → 检索回答 → 合规审查 → 反馈运营 → 反哺知识。
1. 知识生产与运营
- 审核发布流:不是谁上传谁生效。文档有 草稿→审核→发布 三态,审核通过才进向量库;金融/医疗行业这是合规硬要求。
- 版本与增量更新:制度改版时旧块按 source 删除、新块增量入库(本文"同 source 先删后写"就是最小实现);生产上还要保留版本号,能回答"按 2025 版制度是怎么规定的"。
- 时效下线 :块带
valid_until元数据,过期文档检索时直接过滤,避免拿作废制度回答。 - 冲突检测:新旧文档说法矛盾时,要么检索时按发布时间加权,要么构建期用 LLM 比对出冲突、推人工裁决。
- 知识缺口挖掘:把线上"没命中/低相似度"的 query 聚类,生成"待补充知识清单"给运营------比人工拍脑袋补文档有效得多。
2. 查询侧流程
- 意图路由:问题先分类------知识库问答、闲聊、还是要调工具(查订单、提工单)?走不同管道。
- 多轮改写:用户第二句问"那儿童呢?",要先用历史把 query 补全成"5 岁儿童得结石的概率"再检索。
- 多知识库编排:HR 库、技术库、产品库分开,先路由选库再检索,而不是全塞一个大库。
- 兜底与转人工:相似度全低于阈值 → 说"不知道"或转人工客服/建工单,而不是硬答;"拒答率"是生产上被盯的指标。
- 引用溯源:回答必须带出处(文档名+页码+原文高亮),用户可点开核对;医疗/法律场景没有引用的答案等于不能用。
3. 合规与数据安全
数据脱敏是数据安全的第一道闸,必须发生在切块和向量化之前。因为向量本身也是泄露面------embedding 从原文算出来,向量库被拖走等于原文半裸奔;检索命中后原文还会进 Prompt,LLM 可能把 PII 复述出去。流水线是:
原文 → PII 识别(正则管手机号/身份证,NER 管姓名/地址,
医疗场景还有病案号)→ 掩码或假名化 → 再切块入库
假名化要注意同一实体全程映射成同一个假名(张三→"患者A"),否则检索时上下文对不上。出库方向(LLM 回答)再过一道掩码,双保险。这对应《个保法》/GDPR 的"数据最小化"------能不进的敏感数据就不进。
其余配套:
- 输出审核:LLM 回答过一道敏感词/合规审查再给用户。
- 全链路审计:谁、什么时候、问了什么、命中哪些块、答了什么,全部落日志,事后可追溯------出纠纷时这是证据链。
- 权限打通:第二部分第 7 节的前置过滤,生产上要和统一身份(SSO/RBAC)集成,而不是自己维护角色表。
4. 线上运营飞轮
- 监控指标:无结果率、命中率、点赞/点踩、回答时延、token 成本,按天看趋势。
- badcase 闭环:点踩和转人工的 case 进标注队列 → 补进测试集 → 调参/补知识 → 回归测试通过才上线------第三部分评测的在线版。
- A/B 与灰度:新分块策略、新 rerank 模型先放 10% 流量对比指标,赢了才全量。
- 语义缓存:相似问题直接返回缓存答案,不打 LLM------见下一节。
5. 语义缓存:把"相似问题"也缓存住
传统缓存 的 key 是查询字符串,"薪酬结构是什么"和"公司薪酬结构?"差一个字就 miss。语义缓存 把 key 换成 query 的向量:新问题先和缓存里的问题算相似度,够像就直接返回存好的答案,整条 RAG 链路(检索+rerank+LLM)全省掉:
css
新问题 → embed → 在 cache 库里找 top-1 相似问题
├─ sim ≥ 0.95 且 隔离维度匹配 → 直接返回缓存答案(毫秒级)
└─ 否则 → 走完整 RAG → 答案生成后把 {query, embedding, answer, sources} 写进 cache
存储结构和向量库几乎同构:{id, query, embedding, answer, sources, kb_version, scope, created_at},现有的 getEmbeddings + cosineSimilarity 直接复用。但真正值钱的是四个设计点,全是坑:
① 阈值必须极高 。用本文的医疗例子看风险:"35 岁得结石的概率多大?"和"5 岁儿童得结石的概率多大?"向量相似度能有 0.9 以上,但答案完全相反。阈值设 0.85,儿童的问题就会命中成年人缓存的答案,静默答错,比不缓存还糟。所以语义缓存阈值一般 ≥0.95,并用"形似意不同"的问题对做测试集标定;再保险一点:关键槽位(年龄/主体/文档范围)抽出来做硬匹配,相似度只当辅助。
② 缓存必须按隔离维度分区------否则权限从缓存泄露 。接第二部分第 7 节的例子:HR 问"公司薪酬结构"得到真实答案并缓存;张三问同一句,若不分区就直接命中 HR 的缓存------数据权限被缓存绕过了 。所以 cache key 除了向量还要带 scope(角色/租户)和 kb_version(知识库重建后版本号+1,旧缓存全部失效)------后者同时解决了失效问题:文档更新不用精确删缓存,bump 版本即可。
③ 不是什么答案都配进缓存。只缓存"高置信"答案:检索 top-1 相似度过阈值、带引用出处的才存;"我不知道"兜底答案不存(或短 TTL),否则知识补充后缓存还挡着;涉及实时数据的(订单、库存)不存。
④ 生产上一般做两层 。L1 精确匹配(归一化字符串 hash,Redis,零成本)→ L2 语义缓存(向量,0.95+)→ L3 完整 RAG。客服场景重复问法极多,两层命中率能做到 30%~60%------语义缓存本质是用一次 embedding 调用(本地模型约 10ms)换掉一次秒级的检索+生成。现成方案有 GPTCache、LangChain SemanticCache、Redis 向量检索;教学项目自建反而把原理暴露得最清楚。
延伸阅读
- RAG 原论文:Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
- HyDE 论文:Precise Zero-Shot Dense Retrieval without Relevance Labels
- RAGAS 论文:RAGAS: Automated Evaluation of Retrieval Augmented Generation
- HNSW 论文:Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs
- RRF 原文:Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods