做 ZarkOne 的 AI 助手时,RAG 不是一个孤立功能,而是一条把站内资料、用户问题、证据和模型回答连接起来的链路。它从最基础的“先查资料再回答”开始,逐步扩展到分块、混合检索、查询增强、重排、上下文选择、多轮证据、引用溯源和观测测试。
一、为什么需要 RAG
网站的内容不是静止的。文章会更新,项目会补充,页面会调整,照片、视频、AI 创作记录也会不断沉淀。用户问 AI 的时候,很多问题并不是通用知识,而是和这个网站当前已有的资料有关。
比如:
这个网站写过哪些文章?
某篇文章到底讲了什么?
这个项目之前做到了哪一步?
当前页面里的内容,能不能作为回答依据?
如果 AI 只靠模型原本的知识回答,它并不知道这些资料的最新状态。模型可以说出一段流畅的话,但这段话未必来自 ZarkOne 当前真实存在的内容。
所以这里需要 RAG。它的作用不是让 AI 显得更聪明,而是让 AI 在回答前先看一眼站内资料。回答应该尽量来自已有证据,而不是只靠模型自己的泛化能力。
简单说,ZarkOne 做 RAG,是为了让 AI 回答三个更实际的问题:
当前网站里到底有什么资料?
这些资料里哪些和用户的问题有关?
回答时应该引用哪些资料作为依据?
二、RAG 到底在做什么

RAG 的全称是 Retrieval-Augmented Generation,中文通常叫“检索增强生成”它不是一个独立按钮,而是 AI 回答前的一段工作流程。
用户提出问题后,系统先判断这个问题是否需要站内资料。如果需要,就去知识库里找相关内容,再把这些内容整理成模型能阅读的上下文。模型最后基于这份上下文组织答案。
可以把它看成一条回答链路:
用户提出问题
系统检索相关资料
系统整理可用上下文
模型基于上下文回答
答案保留可追溯来源
既然想让 AI 了解网站内容,能不能直接把这些内容拿去微调模型?
这就是 RAG 和微调需要区分的地方。微调更像是把长期稳定的能力、风格或领域习惯训练进模型里;RAG 更像是每次回答前临时查资料。对一个内容经常变化的网站来说,RAG 更容易维护:资料变了,更新知识库即可,不需要反复训练模型。
它和普通搜索也不一样。搜索把结果列表交给用户,让用户自己点开判断;RAG 是把检索到的资料交给模型,让模型结合问题和资料生成一段可读答案。
所以,RAG 真正连接的是三件事:用户问了什么、系统查到了什么、模型应该基于什么回答。后面的分块、检索、重排、引用和多轮追问,都是围绕这条链路继续展开。
但随之而来,真正的问题才刚刚开始浮现:
如何让 RAG 检索到的内容足够准确,而不是答非所问?
当用户的提问比较模糊、口语化,甚至带有歧义时,系统该怎么理解他真正想问的是什么?
知识库里那么多文章,应该按什么粒度切分,才能既不丢上下文,又不会塞进一堆无关信息?
检索出来的若干段资料,谁应该排在前面,谁可以舍弃?
模型生成答案时,又该如何确保它是基于这些资料,而不是凭空发挥?
以及,当一次回答不够、用户继续追问时,前一轮的上下文又该如何延续?
这些疑问,才是 RAG 从"能跑起来"走向"真正好用"必须面对的东西。
三、一个基础 RAG 流程怎么跑起来
这一章只引入基础 RAG 必须知道的概念,不提前展开 BM25、HyDE、Reranker 这些后面的内容。先把最小闭环拆清楚:资料怎么被存进去,问题怎么拿来查,查到的内容怎么变成模型可用的上下文。
问题示例:用户到底问了什么
用户问题:Cloudflare SaaS 回源域名怎么配置?
没有 RAG 时,模型只能根据通用知识回答。它可能知道 Cloudflare,也可能知道 DNS,但不一定知道站内有没有写过相关文章[1],更不知道文章里具体用了哪些配置。
有 RAG 时,系统会先拿这个问题去知识库找资料。回答不是立刻生成,而是先经过一次检索。
但检索有一个前提:知识库里必须先有可查的资料。把文章、页面、项目说明整理成可检索内容的过程,可以先理解成资料入库。
资料入库:让系统有东西可查
资料入库不是把整篇文章原封不动复制进去。因为一篇文章可能很长,里面可能同时讲好几个主题;如果整篇作为一个检索对象,系统很难精确找到真正相关的部分。
清洗:去掉导航、按钮、重复布局等无关内容。
分块:按段落或语义切成更小的知识片段。
来源信息:记录标题、URL、所属文章和父级内容,后面才能引用和追溯。
桶(Bucket):另一个维度的组织方式
分块解决的是"一篇文章内部怎么切",而桶解决的是"不同来源的资料怎么分开存"。
可以把桶理解成知识库里的"分区"或"命名空间"。比如:
博客文章一个桶
项目文档一个桶
个人笔记一个桶
检索时可以指定只在某些桶里查,也可以跨桶查。这样做的好处是:不同类型的资料可以用不同的分块策略、不同的权重,甚至不同的嵌入模型;查询时也能根据问题类型先缩小范围,再做精排。
知识块:资料被检索的最小单位
分块后得到的每个小片段,就可以叫知识块。比如一篇 3000 字文章,可能会被切成 8 到 20 个知识块。后面系统真正拿来检索、排序、引用的,通常就是这些知识块。
Embedding:文字的语义坐标
Embedding 是一段文字对应的数字表示。它不是摘要,也不是关键词,而是一组可以计算相似度的数字。
“如何配置域名解析” →
[0.12, -0.03, 0.88, ...]
这个例子只是示意。真实 Embedding 通常有几百到几千维,可以理解成“这段文字在语义空间里的坐标”。
向量模型:把文本变成 Embedding
向量模型的职责不是写文章,也不是直接回答问题。它只负责把文本映射成 Embedding。
文章片段→ 向量模型 →知识块向量用户问题→ 向量模型 →问题向量
所以它叫向量模型,重点是输出向量。语言模型负责生成答案,向量模型负责让文本可以被比较。
向量检索:用相似度找候选资料
有了向量以后,系统就可以比较“问题向量”和“知识块向量”的距离。距离越近,说明语义越相似。
域名解析怎么配和DNS 记录如何设置说法不同,但语义接近。域名解析怎么配和图片裁剪为什么跨域主题不同,距离通常更远。
这一步解决的是“哪些资料可能相关”。它还没有判断这些资料是不是足够完整、是不是应该放进最终上下文。
上下文:真正交给模型看的资料包
检索结果不会直接变成答案。系统会把候选知识块整理成上下文,再交给语言模型。
上下文决定模型回答前能看到什么证据,也决定它应该围绕哪些资料回答。RAG 的关键不只是"搜到内容",而是把合适的内容放进合适的上下文。
以 OpenAI 的 Chat Completions 接口为例,把检索到的知识块拼装进上下文的常见做法如下:
from openai import OpenAI
client = OpenAI()
# 假设这是检索阶段返回的候选知识块
retrieved_chunks = [
"公司年假政策:入职满 1 年可享 5 天带薪年假,满 3 年可享 10 天。",
"请假流程:员工需提前 3 个工作日在 OA 系统提交申请,由直属主管审批。",
]
user_question = "我入职两年了,请几天年假?要怎么申请?"
# 把检索结果整理成上下文,整理(以下已拼接retrieved_chunks)
prompt = f"""
你是公司 HR 助手,请基于以下资料回答员工问题。
【参考资料】
公司年假政策:入职满 1 年可享 5 天带薪年假,满 3 年可享 10 天。
请假流程:员工需提前 3 个工作日在 OA 系统提交申请,由直属主管审批。
【员工问题】
{user_question}
"""
print(response.choices[0].message.content)在这个例子里,检索到的两段知识并没有直接拼到用户问题后面,而是通过 system 消息告诉模型“这些是可信资料,请围绕它们回答”。同样的候选内容,放在不同的位置、配不同的指令,模型给出的答案会有明显差别——这正是上下文工程要解决的问题。
基础闭环

到这里,一个基础 RAG 已经能跑起来。但它只是起点:向量相似不等于答案可靠,查到资料也不等于资料适合直接进入上下文。下一章就从这些问题开始。
四、为什么“能查到”不等于“答得好”
基础 RAG 能跑起来以后,最容易产生一个误解:既然系统已经能查资料,那答案应该就会变好。但实际不是这样。检索只是把候选内容拿出来,候选内容能不能支撑回答,还要看很多细节。
能查到,只是拿到了候选
向量检索通常会返回“看起来相似”的资料片段,但相似不等于正确,也不等于完整。模型最后看到的上下文如果不够准,它依然可能回答得很自信,却没有真正答到问题。
同一个问题:Cloudflare SaaS 回源域名怎么配置?
系统可能检索到 DNS、CNAME、Cloudflare、回源这些相关片段。但如果缺了“当前项目实际配置方式”,答案仍然会变成泛泛而谈。
问题一:知识块切得不对
分块太大,里面会混入多个主题;分块太小,又容易丢掉前后文。比如一个片段只保留了“添加 CNAME 记录”,却没有保留它对应的域名、目标地址和使用场景,模型就很难给出完整答案。
问题二:有些内容必须按字面命中
向量检索擅长的是“意思相近”,但很多场景下,光意思对还不行,必须一字不差地命中原文。这种“必须按字面出现”的内容,就是我说的精确信号。
举几个典型例子:
模型名、版本号:
gemini-3.1-flash-image和gemini-2.5-flash在语义上几乎一样,但代表的是完全不同的东西。如果用向量检索,可能两个都被认为"相关",但用户要的就是那一个具体版本。错误码:
PG42883、HTTP 400、ECONNREFUSED,这些字符串本身没有"语义",必须原样匹配。语义模型看到PG42883大概率只会理解成"某种错误",召回一堆无关的报错文档。路径和链接:
/api/articles/image和/api/articles/images差一个字母,意思相近,但指向完全不同的接口。专有名词:项目名、组件名、配置字段名(比如
max_tokens、top_p),这些往往是自造词或固定写法,语义模型很难准确区分。
为什么向量检索处理不好这些?因为它的工作方式是把文本压成一个“意思向量”,相似的意思距离近。但精确信号的关键不在意思,而在形态—差一个字符就是另一个东西。语义相似度越高,反而越容易把相近但不相同的东西混在一起。
所以这不是“向量检索不好”,而是它本来就只解决一半的问题:
语义相似 → 适合"找相关段落"、"找讲同一件事的内容"
字面匹配 → 适合"找具体名字、错误码、路径、标题"
两种能力是互补的,缺一不可。这也是为什么后面要讲混合检索——把语义检索和关键词检索(BM25、全文索引等)结合起来,让两种信号同时起作用。
问题三:用户问题本身可能很弱
有些问题很短,比如“还有吗?”“这个怎么弄?”;也有些问题很长,前面贴了一大段日志,最后才问一句“这是什么问题”。短问题信号不足,长问题噪声太多,都会影响检索质量。
问题四:top-k 不是上下文设计
很多基础 RAG 会直接取前 5 条、前 10 条检索结果塞给模型。这种做法简单,但不一定合理。排在前面的内容可能来自同一篇文章,也可能重复讲同一个点,真正需要的另一篇资料反而被挤掉。
top-k:
问题五:证据不足时,模型也可能说满
RAG 不是天然防幻觉。检索结果不足、上下文模糊、问题范围太大时,模型仍然可能把不确定的内容说得很确定。所以系统还需要判断:当前证据到底够不够回答?如果不够,就应该收敛表达,或者提示需要更多资料。
所以,“能查到”只是第一步。真正要让 RAG 答得好,需要继续解决检索方式、分块质量、上下文选择和证据充分性这些问题。下一章先从最基础也最常见的一点开始:为什么不能只靠向量检索。
五、混合检索:语义和精确匹配都要有
上一章说到:向量检索擅长找“意思相近”的内容,但有些问题必须按字面命中。混合检索就是为了解决这个矛盾:语义要能找,精确词也不能丢。
这里可以把检索分成两条路:一条看语义相似,一条看字面匹配。最后再把两条路的结果合并成一个候选列表。
关键词检索:先从“有没有这个词”开始
最朴素的检索方式,是看资料里有没有出现用户输入的词。比如用户问 Cloudflare SaaS 回源域名,系统就去找标题、正文、URL 里是否出现这些词。
这种方式很直接,也很有价值。因为很多问题并不是抽象语义问题,而是要找一个具体名字、接口、错误码、路径或标题。
HTTP 400、PG42883这类错误码,最好按字面命中。/api/articles/image这类接口路径,不能只靠语义猜。gemini-3.1-flash-image这类模型名,少一个字符都可能是另一个模型。
TF-IDF:不是所有词都一样重要
只看“有没有这个词”还不够,因为有些词太常见,比如“怎么”“配置”“文章”。这些词到处都有,不能说明资料真的相关。
TF-IDF 的直觉是:一个词在当前资料里出现得多,说明它可能重要;但如果这个词在所有资料里都很常见,它的区分度就应该降低。
简单理解:常见词权重低,稀有但命中的词权重高。
BM25:更成熟的关键词排序
BM25 可以理解为关键词检索里很经典的一种排序函数。它延续了 TF-IDF 的直觉,但比朴素 TF-IDF 更适合真实搜索场景。
词越稀有,越有区分度:比如
Cloudflare SaaS比 “怎么配置” 更能说明相关性。词出现越多,分数会提高,但不会无限提高:重复 100 次同一个词,不应该让文章无脑排第一。
文章长度会被考虑:长文章天然包含更多词,如果不校正,长文章会占便宜。
BM25 的常见形式大致是这样:
score(D, Q) = Σ IDF(q) × f(q, D) × (k1 + 1) / (f(q, D) + k1 × (1 - b + b × |D| / avgdl))
不用被公式吓到。它主要在算三件事:这个词稀不稀有、在这篇资料里出现了多少次、这篇资料是不是过长或过短。
pg_trgm:处理模糊字面匹配
除了 BM25,还有一种常见的字面相似思路是 trigram。PostgreSQL 的 pg_trgm 就是把字符串拆成连续的三字符片段,再比较两个字符串有多少片段相似。
它适合处理拼写差异、标题相似、URL 片段、短文本近似匹配。它不是 BM25 的替代品,更像是关键词检索旁边的一个补充工具。
向量检索:负责语义相似
向量检索解决的是另一类问题:用户没说原词,但意思接近。比如用户问“域名怎么接到网站”,文章里写的是“DNS 解析”“CNAME 记录”“回源域名”,这时向量检索就可能比纯关键词更容易找到相关资料。
所以混合检索不是在两个技术里选一个,而是让它们各做擅长的事。
为什么不能直接把分数加起来
BM25、pg_trgm、向量相似度的分数尺度不一样。BM25 可能是几分、十几分;向量相似度可能在 0 到 1 附近;trigram 又是另一套相似度。如果直接相加,谁的数字范围大,谁就会压过别人。
因此工程上常见做法不是硬加原始分数,而是先看每个检索器自己的排名,再做排名融合。
RRF:把多个排名合成一个排名
RRF 的全称是 Reciprocal Rank Fusion,可以理解成“按名次投票”。它不关心每个检索器给了多少原始分,只关心某个候选在各路结果里排第几。
RRF(d) = Σ 1 / (k + rank_i(d))
这里的 rank_i(d) 是资料 d 在第 i 个检索结果里的名次。k 是一个平滑常数,常见示例里会用 60,让第一名和第二名有差别,但不会差到离谱。
举个简化例子:
向量检索:B 排第 1,A 排第 2,C 排第 6。
关键词检索:A 排第 1,B 排第 4,C 排第 20。
RRF 之后:A 可能超过 B,因为 A 在关键词里非常靠前,在向量里也不差。
如果用 k = 60 粗略算一下:
A = 1/(60+2) + 1/(60+1) ≈ 0.03252B = 1/(60+1) + 1/(60+4) ≈ 0.03202C = 1/(60+6) + 1/(60+20) ≈ 0.02765
这个例子想说明:RRF 会奖励“多路都比较靠前”的资料,也允许某一路检索特别强的结果被保留下来。
这些检索方式最终怎么组合
前面把关键词检索、TF-IDF、BM25、pg_trgm、向量检索和 RRF 分开讲,是为了先弄清每个概念的职责。真正落地时,它们不是孤立存在,而是一起组成一条检索流水线。

可以把混合检索理解成一个“多路召回 + 排名融合”的过程:
用户问题进入检索模块。
向量检索负责找语义相近的知识块。
BM25 负责把关键词重要性算进排序里。
pg_trgm负责补充标题、路径、短文本的近似字面匹配。每一路先得到自己的候选排名。
RRF 根据名次把多个排名融合成一个候选池。
候选池再交给后面的重排、去重、上下文选择。
所以这些术语的关系可以这样看:
向量检索:解决“意思像不像”。
BM25:解决“重要关键词命中得好不好”。
pg_trgm:解决“字面长得像不像”,尤其适合标题、路径、短词。
RRF:不负责检索内容,而是负责把多路检索结果合成一个更稳的排名。
这样做的好处是,系统不会因为用户没说原词就漏掉语义相关内容,也不会因为只看语义而丢掉错误码、模型名、接口路径这类必须按字面命中的证据。
所以这一章的核心不是“BM25 比向量好”,也不是“向量比关键词先进”。真正重要的是:RAG 需要同时理解意思,也要尊重字面证据。
下一步,系统还要处理另一个问题:资料切得好不好,会直接决定检索结果能不能用。
六、分块质量决定知识库的地基
前面讲混合检索时,重点在“怎么把不同检索方式组合起来”。
但还有一个更底层的问题:如果知识本身被切得很差,再好的检索方式也只能在一堆质量不稳定的片段里挑结果。
分块质量决定的是 RAG 的地基。它影响的不只是召回率,也影响最终答案能不能说清楚、能不能引用准确、能不能在多轮追问里延续证据。
但分块没那么简单:不是按长度/固定字数一刀切
最简单的做法,是把文章按固定长度切开,比如每 500 字一块。这个办法实现很快,但它不理解文章结构,也不理解语义边界。
问题一:句子被切断,语义断开
假设有这样一段原文:
Cloudflare SaaS 的回源域名需要配置一个不被代理的子域名,否则会出现循环解析。这个子域名通常叫 origin,指向真实服务器。
如果按固定长度硬切,可能切成:
块 A:Cloudflare SaaS 的回源域名需要配置一个不被代理的子域名,否则会出现循环解
块 B:析。这个子域名通常叫 origin,指向真实服务器。
块 A 句子没说完,块 B 缺少前因。检索时如果只命中其中一块,模型拿到的就是半截语义。
问题二:一个完整概念被拆到两块里
比如一段在讲“回源域名配置的三个步骤”,固定长度切完,步骤一和步骤二在 A 块,步骤三跑到 B 块。用户问“回源怎么配置”,检索可能只召回了 A,模型就只看到两步,漏掉一步。
以上问题不会在检索阶段显得特别明显。系统仍然能返回一半“相关片段”,但这个片段可能缺少前因、缺少步骤、缺少标题路径,最后答案就会变成半截。

通俗易懂:模型回答不一定符合设想,因为知识库并没检索相关内容给模型。
所以分块是有内涵的,常见的几种思路:
按结构分:用标题、段落、列表这些自然边界切,尽量不破坏作者本来的语义单元。
按语义分:借助模型判断"这几句在讲同一件事",把语义连贯的内容归到一块。
重叠分块(overlap):相邻两块之间留一段重叠区域,比如后一块的开头复制前一块的末尾两句。这样即使切到一半,相邻块也能补全上下文。
父子分块:小块用于检索(更精确),命中后向上回溯到所属的大块或整篇文章,把更完整的上下文交给模型。
知识块应该像一小段可引用的证据
一个好的知识块,不只是“长度合适”。它更像一小段可以独立引用的证据:读者能看懂它在讲什么,系统也能知道它来自哪里。
内容完整:至少保留一个问题、一个结论、一个步骤或一个配置说明。
边界清晰:不要把两个不同主题硬塞进同一个块,也不要把一个步骤切成两半。
来源明确:保留文章 ID、URL、标题、章节路径、发布时间等 metadata。
可回溯:最终回答引用它时,用户能回到原文,而不是只看到一段孤立文本。
chunk、overlap 和 parent chunk 分别做什么
分块不是只有一个参数。常见做法里,会同时考虑小块、重叠区和父级上下文。
chunk 负责“找得准”:它要足够小,方便向量检索和关键词检索命中具体内容。
overlap 负责“别切断”:相邻块之间保留少量重叠,避免一句话或一个步骤正好被切开。
parent chunk 负责“答得全”:命中小块后,可以带回它所属的大段落、章节标题或上级上下文。
metadata 负责“找得有线索”:标题路径、标签、URL、来源类型,不只是展示字段,也会影响检索和筛选。
不同资料,切法也不该一样
文章、FAQ、代码、配置、短笔记,适合的切法并不一样。统一按字数切,维护起来最简单,但效果通常不稳定。
文章:优先尊重标题层级、段落、列表和小节边界。
FAQ:问题和答案要绑定,不要把问题留在上一块,把答案切到下一块。
代码和配置:要保留完整片段、文件路径、上下说明和关键注释。
短文本:可能不适合再切,反而更依赖关键词、标题和字面相似度。
所以,好的分块不是越细越好,而是让系统既能精确命中,又能带回足够完整的证据。
资料切好了,下一步才轮到用户的问题。因为用户不会按照知识库的结构来提问,系统还需要把真实输入整理成适合检索的 query。
七、用户的问题需要先被整理成适合检索的 Query
到了这一步,RAG 已经不只是“资料切好以后去查一下”。真正进入系统时,用户的问题往往并不规整:有人只问一句“还有吗”,有人先贴几千字背景,最后才问一句“这是什么问题”,也有人把三个问题塞在同一段里。
如果把这些原文直接丢给检索系统,结果通常会很不稳定。不是模型不聪明,而是检索这件事本身需要一个更清晰的目标。
原样检索为什么容易失真
短追问缺少主语:“还有吗”“那下一步呢”“这个怎么弄”本身没有足够信息,必须结合当前会话和上一轮证据。
长背景噪声太多:用户可能贴了配置、日志、聊天记录、文章段落,真正的问题只藏在最后两句。
错误日志重点分散:日志里有路径、时间、堆栈、依赖版本,但检索真正需要的是错误码、组件名、触发场景和用户想解决的问题。
多问题会互相干扰:“一、为什么这样;二、这里又该怎么做”如果压成一个 Query,很容易把不同主题混在一起。
页面上下文不能丢:如果用户正在看某篇文章、某个项目或选中了页面片段,这些都是强信号,不能只看输入框里的文字。
Query 整理不是替用户编答案
这里要分清楚:整理 Query 不是让系统提前回答,也不是把用户没说的话补成事实。它只做一件事:把用户输入整理成更适合检索的“线索集合”。
一个更好的检索 Query,通常会保留这些东西:
主题:用户到底在问域名、部署、RAG、图片生成,还是视频任务。
实体:文章标题、项目名、模型名、错误码、配置项、页面 URL。
动作:配置、排查、对比、统计、续写、总结、找来源。
约束:当前页面、选中文本、上一轮追问、用户明确限定的范围。
用户原文:“我刚才看那个 Cloudflare SaaS 的内容,origin 那里到底为什么不能直接用主域名?”
检索 Query:Cloudflare SaaS 回源域名 origin 主域名 循环解析 配置原因
前者更像人在聊天,后者更像给检索系统的任务说明。RAG 要做的,就是在不改变用户意图的前提下,把聊天语言整理成检索语言。
不同问题,需要不同检索策略
Query 整理之后,还要判断问题类型。因为“找一段答案”和“了解整站有什么内容”,需要的检索方式并不一样。
答案型问题:用户要解决一个明确问题,重点是找到最相关、最完整、最可靠的证据。
发现型问题:用户问“有哪些文章”“这个网站有什么内容”“一共有多少”,系统不能只拿同一篇文章的几个 chunk 来回答。
追问型问题:用户接着上一轮问“还有吗”“展开说”,系统要理解它依赖哪一轮、哪批证据,而不是把短句当成孤立问题。
混合型问题:用户可能同时要数量、列表、解释和建议,这时系统要拆清楚主任务和辅助任务。
压缩和增强要有边界
长输入需要压缩,但不能压到只剩一个模糊主题;短输入需要增强,但不能凭空扩写成另一个问题。比较稳的做法,是把原始输入、页面信号、会话线索分开看,再生成一个短而清晰的检索目标。
这也是为什么 RAG 后面会出现 HyDE、Reranker、上下文选择这些环节。Query 整理负责把问题送到正确入口,HyDE 负责在“问题太抽象”时帮助召回更多候选,Reranker 再判断哪些候选真的更相关。
所以,第七章这一层可以理解为:先把用户真正想找什么说清楚,再谈系统应该去哪里找。
具体实现
拆分:
如果用正则的话
正则能搞定:
"一、为什么 X?二、怎么办 Y?" → 按"一、二、"拆
"请问 A 是什么?另外 B 怎么用?" → 按"另外"拆
正则立刻死:
"我想知道 Cloudflare 怎么配,还有它和 Vercel 比哪个好" → 是 1 个问题(对比)还是 2 个问题(配置 + 对比)?
"为什么这样不行而那样可以" → 表面 1 句话,实际隐含 2 个子问题
"RAG 里 query 拆分用正则行不行不行的话用什么" → 人都要读两遍所以工业界基本都用LLM拆分:
prompt = """判断用户问题包含几个独立子问题。
规则:若多个子问题主题不同,输出多个 query;若是同一主题的多个方面,保留为 1 个。
输出 JSON: {"queries": [...]}"""分类:
如果关键字匹配的话:
# 80% 的情况,规则就够了
def classify(query: str) -> str:
if re.search(r"多少|几篇|几个|总共|一共", query):
return "count" # 走 SQL count
if re.search(r"有哪些|列一下|列表|清单", query):
return "list" # 走全表/分类检索
if len(query) < 10 and has_history():
return "followup" # 走追问补全
return "answer" # 默认走向量检索翻车场景:
"博客里关于 AI 的东西有几篇具体写 RAG 的,都讲了什么"
→ 命中"多少",但其实是问"RAG 具体写了什么",不是问数量
"给我看看最近写了啥"
→ 没有任何关键词,语言搭配千变万化,但其实是 list 型补全:
用户上一轮:Cloudflare SaaS 的 origin 为什么不能用主域名?
用户这一轮:还有其他方法吗? ->"还有",指什么,还需要检索什么东西?本轮会话无准确关键字Query的预处理工程,实际上 LLM 协助的情况非常多,因为我们对正则、关键字匹配,语义理解,单靠简单的逻辑,是不够的。当然,我们可以积累数据量,不断清洗数据, 提升关键字判断情况,做到规则越来越充足,错判率越来越低。
对于(改写/拆分/分类)大部分靠LLM,少部分能用规则兜底。LLM 需要规则来兜底,预防返回内容超出数量/不符合规范/token 爆炸等情况,规则做不到需要 LLM 来理解语义,补全语义,两者相辅相成。
八、HyDE:只用来帮助检索,不能当作事实证据
上一章说的是:用户的问题要先整理成适合检索的 Query。随之而来下一阶段面临的一类问题更麻烦:用户问得很抽象,或者用了很口语的说法,直接拿去做向量检索时,可能召回不到真正有用的资料。
HyDE 就是为这类场景准备的。它的全称是 Hypothetical Document Embeddings,可以理解为“先让模型想象一段可能相关的资料长什么样,再用这段想象出来的资料去帮助检索”。
HyDE 解决的不是回答问题,而是召回困难
普通向量检索比较依赖语义相似。如果用户的问题太短、太绕、太抽象,向量模型拿到的语义信号就会弱。
用户问题:“这个网站具体是干什么的?”
直接检索可能只看到:网站、具体、干什么
HyDE 可能生成一段假想资料:“这个网站展示个人项目、技术文章、AI 工具实践和作品案例……”
这段假想资料不一定是真的,但它会把“网站是干什么的”扩展成更容易匹配资料的语义方向。然后系统再拿它去检索真实知识库。
关键边界:HyDE 不是证据
这里最容易踩坑的地方是:HyDE 生成的内容看起来像答案,但它不能当答案用。
它不是资料库里的原文。
它没有来源 URL。
它可能猜错方向。
它只能参与召回,不能进入最终引用。
所以一个稳妥的 RAG 系统里,HyDE 的位置应该很清楚:它可以帮助系统找到候选资料,但最终回答必须回到真实 chunk、真实文章、真实 metadata。
HyDE 什么时候有用
抽象问题:“这个网站有什么价值”“这个项目解决了什么问题”。
口语追问:“那这个东西到底干嘛的”“有没有更完整一点”。
概念不完整:用户知道大概方向,但不知道准确术语,比如把“回源域名”说成“那个真实服务器地址”。
跨段落综合:答案可能散在多篇文章或多个段落里,单个关键词不够稳定。
HyDE 什么时候不该乱用
精确匹配问题:用户问具体错误码、模型名、配置项时,关键词、BM25 或 pg_trgm 往往更直接。
数量统计问题:“一共有多少篇文章”需要数据库 count 或结构化索引,不应该靠 HyDE 猜。
强来源问题:用户要引用、链接、原文出处时,必须以真实资料为准。
长输入已经足够明确:如果用户贴了清楚的标题、URL、错误码,再生成一段假想资料反而可能引入噪声。
更稳的用法:让 HyDE 参与召回,不参与定稿
我更倾向把 HyDE 放在“多路召回”的其中一路,而不是让它替代原 query。也就是说,系统可以同时使用:
原始 query:保留用户真实表达,避免偏题。
整理后的 query:提取主题、实体、动作和约束。
HyDE query:为抽象问题补充语义方向。
页面或会话信号:当前文章、选中文本、上一轮引用过的证据。
这些召回路径拿到的候选资料,后面还要统一交给排序和筛选逻辑。HyDE 只是把“可能找不到”变成“多给一些候选”,不能决定最后答案。
这样设计之后,系统就能兼顾两件事:抽象问题不容易漏召回,最终回答又不会被假想内容污染。
下一章就进入 Reranker:当候选资料变多以后,系统还需要重新判断,哪些证据才是真的更相关。
九、Reranker:召回之后还要重新判断谁更相关
前面几章讲的是怎样把资料切好、怎样把问题整理成 Query、怎样用 HyDE 增强召回。到这里,系统手里通常已经有一批候选资料了。
但“候选资料”还不是“最终证据”。粗召回阶段为了不漏,往往会多拿一些结果;这些结果里可能有重复、偏题、只沾了关键词但没有回答价值的片段。Reranker 要解决的,就是召回之后重新判断:谁才是真的更相关。
粗召回负责别漏,精排负责别乱进
可以把检索分成两个阶段:
召回:先尽量把可能相关的资料找出来,宁可多一点,不要一开始就漏掉关键证据。
重排:再把这些候选资料逐条和用户问题对比,重新排出更接近问题意图的顺序。
向量检索、关键词检索、RRF 更像是在做“先把可能相关的资料捞上来”。Reranker 则更像第二轮评审:它不只是看资料和问题在向量空间里近不近,而是更直接地判断“这段资料能不能回答这个问题”。
Reranker 和 Embedding 的差别
Embedding 通常会先把问题和资料分别变成向量,再计算相似度。这个过程快,适合从大量资料里做第一轮筛选。但它是“分别编码再比较”,对细节关系的判断有限。
Reranker 常见做法是把用户问题和候选资料一起交给重排模型,让模型直接判断两者的相关程度。这类模型可以更细地看问题和资料之间的对应关系,比如用户问的是“为什么不能用主域名”,资料里是否真的解释了原因,而不是只出现了“主域名”“配置”“Cloudflare”这些词。
一个简单例子:
用户问:Cloudflare SaaS 的 origin 为什么不能直接用主域名?
候选 A:介绍 Cloudflare SaaS 的回源域名为什么要避开代理和循环解析。
候选 B:介绍 Cloudflare 免费版和企业版功能差异。
两段都可能出现 Cloudflare,但真正能回答问题的是 A。Reranker 的价值就在于把 A 往前排,把 B 往后压。
为什么不能让 Reranker 直接扫全库
Reranker 判断更细,但成本也更高。它通常要把“问题 + 候选资料”成对送进去评分。如果知识库有几千、几万、几十万个知识块,直接让 Reranker 全库扫描,速度和成本都会失控。
所以更合理的方式是:先用便宜、快速的召回方式拿到一批候选,再用 Reranker 精排这批候选。
第一步:向量、关键词、混合检索先召回几十条候选。
第二步:Reranker 只处理这几十条,重新排序。
第三步:后面的上下文选择再决定最终给模型看哪几条。
这也是很多 RAG 系统会设计“候选池”的原因:候选池太小,可能一开始就漏;候选池太大,重排成本又会升高。实际工程里需要在速度、成本和质量之间取一个平衡。
Reranker 解决不了所有问题
Reranker 能提高最终上下文的纯度,但它不是万能修复器。
资料没被召回:如果第一阶段根本没有把正确资料捞上来,Reranker 没机会把它排到前面。
知识块本身切坏了:如果候选片段只有半句话、缺少前因后果,Reranker 最多判断它相关,不能凭空补全上下文。
问题本身不清楚:如果 Query 整理失败,把用户意图带偏,Reranker 也会围绕错误问题排序。
数量和列表问题:“一共有多少篇文章”“有哪些资料”这类问题,核心不是重排,而是结构化统计和多来源覆盖。
所以 Reranker 应该被放在正确的位置:它负责精排候选,不负责替代分块、Query 整理、统计查询或最终证据判断。
重排之后还不能直接塞给模型
即使 Reranker 已经把候选排好了,也不代表前几条就可以无脑塞进上下文。因为上下文还有容量限制,也有覆盖面问题。
比如前 5 条都来自同一篇文章,它们可能分数很高,但信息重复;另一篇文章虽然排第 6,却补充了不同角度。对用户来说,后者可能更有价值。
因此 Reranker 后面还需要一层上下文选择:去重、控制同源数量、保留多样性、判断证据是否足够、决定最终注入哪些内容。
这就进入下一章:不能把 top-k 粗暴塞给模型。RAG 真正难的地方,往往不是“查到了什么”,而是“最终应该让模型看到什么”。
十、上下文选择:不能把 top-k 粗暴塞给模型
Reranker 把候选资料重新排了一遍,但这还不是最后一步。很多基础 RAG 会把重排后的前几条直接塞给模型,这种做法很快,却也很容易把上下文变成一堆“看起来相关”的片段。
上下文选择要解决的是:最终到底让模型看到什么。它不是简单取前几名,而是把候选资料整理成一个能支撑回答的证据包。
top-k是什么
top-k 是检索阶段最常见的一个参数:从候选池里挑出分数最高的前 k 条结果。比如 top-5 就是取排名前 5 的知识块,top-10 就是取前 10 条。它的作用是给后续流程划一个边界——不可能把几万条资料都塞给模型,必须在某个位置截断。
在基础 RAG 里,top-k 通常出现在两个地方:
向量召回阶段,从全量知识块里取 top-N 候选;
Reranker 重排之后,从重排结果里取 top-k 注入上下文。
前者的 k 一般比较大(几十上百),后者的 k 比较小(几条到十几条),因为最终要受模型上下文窗口和成本的约束。
听起来很简单:排好序,取前几条,拼进 Prompt。问题就出在这个“取前几条”太机械了。
top-k 为什么不够
top-k 的问题在于,它只回答“分数最高的是谁”,没有回答“这些内容组合起来是否适合回答”。
重复:前几条可能都来自同一篇文章,换着说同一件事。
偏窄:高分片段能回答一个细节,但不能覆盖用户真正的问题范围。
缺上下文:某个小块分数很高,却缺少前后文、标题或限制条件。
类型不匹配:用户问“有哪些文章”,系统却塞进几段正文 chunk,模型只能基于局部片段猜。
所以,RAG 最终注入给模型的不是“分数榜单”,而应该是“为本轮问题挑出来的证据组合”。
不同问题,需要不同上下文
上下文选择首先要看问题类型。不同问题需要的证据形态不一样。
答案型问题:需要能直接解释原因、步骤或结论的正文证据。
发现型问题:需要多来源覆盖、标题、链接、数量和简要说明,而不是同一篇文章的多个分块。
当前页面问题:需要守住当前页面或当前选中文本的范围,不能随意扩展到全站。
对比型问题:需要至少两组证据,否则模型只能比较一边。
追问型问题:需要知道上一轮证据是什么,本轮是继续展开、找更多,还是换主题。
同一个 top-k,在不同问题里意义完全不同。
用户问“这篇文章里 RAG 怎么做”,上下文应该围绕那篇文章。
用户问“这个网站有什么文章”,上下文应该展示多篇文章的入口和统计。
用户问“还有吗”,上下文应该尽量避开上一轮已经给过的来源。
一个好的上下文选择器会考虑什么
真正有用的选择器,通常会同时考虑几件事:
相关性:资料是否真的贴近用户问题。
完整性:资料能不能支撑一个完整回答,而不是只给半句话。
多样性:不要让同一来源、同一角度重复占满上下文。
来源边界:当前文章、当前项目、站内全局、外部资料,要按问题范围区分。
容量预算:模型上下文有限,不能把候选资料无节制塞进去。
可引用性:最终进入上下文的内容最好能回到原文、标题、URL 或来源记录。
这些判断看起来像“后处理”,但它们直接决定最终回答质量。一个很强的检索系统,如果上下文选择做得粗糙,模型依然会被重复、偏题或证据不足的内容带偏。
上下文不是越多越好
很多人刚做 RAG 时会倾向于多塞资料,觉得给模型看的越多越安全。但上下文太多会带来三个问题:噪声变多、重点变弱、模型更容易抓错线索。
更稳的做法是少而准:让模型看到足够回答的问题证据,同时尽量减少重复、无关和互相冲突的片段。
所以,第十章的核心是:RAG 的上下文不是检索结果的堆叠,而是一次面向回答目标的证据编排。
十一、Answerability:相关不等于可回答
上下文选出来以后,还要问一个更严格的问题:这些资料到底够不够回答?
很多 RAG 出问题,不是因为完全没有找到资料,而是因为找到了一些相关内容,模型就顺势把答案说满了。相关只说明“沾边”,不代表证据已经足够。
为什么需要 Answerability
Answerability 可以理解成“可回答性判断”。它不负责再检索,也不负责生成最终答案,而是判断当前证据状态。
证据足够:资料能够直接支撑回答,可以正常回答并引用。
部分足够:资料能回答一部分,但还有范围、细节或来源缺口。
相关但不足:资料和问题有关,但不能证明用户真正问的结论。
没有证据:知识库没有找到可用资料,不能假装站内有答案。
这一步很像给模型加了一道刹车:不是看到相关词就回答,而是先确认“我能基于这些资料说到什么程度”。
几个容易误判的场景
场景一:用户问数量。
知识库返回了 5 篇文章的片段,不代表网站只有 5 篇。数量问题需要统计来源,而不是让模型看见几条就说几条。
场景二:用户问原因。
资料里出现了相关配置项,但没有解释为什么这样配置,就只能说“有相关配置”,不能强行总结原因。
场景三:用户要站内依据。
模型知道通用知识,但知识库没有证据时,应该区分“通用判断”和“站内资料没有命中”。
Answerability 改变的是回答边界
有了可回答性判断,最终回答就不应该只有“答”或“不答”两种状态,而是有更细的边界。
证据足够时:直接回答,给出引用。
证据部分足够时:先回答能确认的部分,再说明缺口。
相关但不足时:可以提示找到相关资料,但不要把它当作明确结论。
没有证据时:明确说知识库没有找到依据,不编造来源。
这对用户体验很重要。一个负责任的 RAG,不是永远回答得很满,而是知道什么时候该收敛。
所以 Answerability 的价值是:让模型回答得有证据强度,而不是只有语言流畅度。
十二、多轮 RAG:用户说“还有吗”时,系统不能失忆
单轮 RAG 已经够复杂,多轮会更难。因为用户的第二句话、第三句话,往往不会把问题重新说完整。
用户可能只说“还有吗”“展开说”“那这个呢”“换一批”“刚才第二个来源再讲讲”。这些话如果脱离上下文,几乎没有检索价值;但放回对话里,它们指向的是上一轮的证据、来源和问题意图。
多轮不是重复注入上一轮片段
多轮 RAG 最容易做错的一点,是把上一轮检索到的 chunk 一直塞进后续历史。这样看似延续了上下文,实际上会让 token 越来越膨胀,也会让模型被旧证据反复锚定。
更好的做法,是保存结构化的证据记忆,而不是保存一大段隐藏文本。
上一轮问了什么:方便判断本轮是不是继续同一主题。
上一轮用了哪些来源:方便引用追问和避免重复。
证据是否足够:如果上一轮本来就证据不足,下一轮不应该盲目复用。
当前范围是什么:页面、文章、站内全局、工具开关都要保持边界。
几种常见追问要分开处理
继续型:“继续说”“展开讲”通常可以复用上一轮来源。
扩展型:“还有吗”“更多呢”应该继续找新来源,尽量避开上一轮已经给过的内容。
引用型:“第一个来源再说说”应该定位到上一轮具体来源,而不是重新全站搜索。
换题型:“那图片功能呢”可能已经切换主题,需要重新检索。
关闭型:如果用户或工具明确关闭知识库,后续策略不能擅自重新开启。
这里的难点不是识别一个关键词,而是判断本轮和上一轮之间的关系。真正可靠的多轮 RAG,要同时看短句、历史证据、范围权限和用户是否引入了新主题。
多轮的目标是延续证据,不是制造惯性
如果用户说“还有吗”,系统应该知道上一轮已经给过哪些来源,然后尽量找新的;如果用户问“刚才那个为什么”,系统应该能回到上一轮对应来源;如果用户换了主题,系统应该放弃旧证据,重新检索。
所以多轮 RAG 的核心不是“把历史越塞越多”,而是让证据可以被复用、被扩展、被放弃,并且始终遵守当前范围。
十三、引用和观测:让答案可以被追踪,也让系统可以被调试
当 RAG 进入真实使用,只有“答案看起来不错”是不够的。用户要知道答案来自哪里,开发者也要知道系统为什么这么回答。
这就是引用和观测的价值:前者面向读者,后者面向调试。
引用不是装饰,而是证据入口
回答里的 [1]、[2] 不应该只是样式。它们应该能指向具体资料,让用户知道这句话依据了哪篇文章、哪个项目、哪个页面或哪段知识。
用户可以追溯:如果答案重要,用户能回到原文确认。
模型会更克制:有引用要求时,回答更容易围绕证据展开。
错误更容易定位:如果引用错了,就能回到对应来源检查检索和注入。
好的引用不是越多越好,而是和回答里的关键结论对应。引用太少,用户无法验证;引用太多,又会变成噪声。
观测要回答“这轮到底发生了什么”
RAG 链路很长:Query 整理、HyDE、召回、重排、上下文选择、Answerability、多轮策略,每一步都可能影响最终答案。如果没有观测,出了问题只能猜。
一套有用的观测,至少应该能看到:
输入如何变成检索 Query:有没有压缩、有没有使用页面或选中文本信号。
HyDE 是否触发:假设性内容有没有参与召回。
召回了哪些候选:来自哪些来源,分数和排序怎样。
Reranker 做了什么:哪些内容被提前,哪些被压后。
最终注入了什么:模型最后到底看到了哪些资料。
证据状态是什么:本轮是可回答、部分可回答,还是证据不足。
多轮策略怎么判断:本轮是复用、扩展、引用某来源,还是新检索。
观测不是为了堆数据,而是为了改进系统
观测真正有价值,是因为它能把“感觉不准”变成具体问题。
如果 query 被压缩错了,应该改 Query 整理。
如果正确资料根本没召回,应该改分块、索引或召回方式。
如果候选里有正确资料但没进上下文,应该改重排或选择器。
如果证据不足却回答很满,应该加强 Answerability 和回答边界。
没有观测,RAG 会变成一个黑箱;有了观测,才能把每次失败样例变成下一轮系统改进的依据。
十四、RAG 最后变成的是上下文工程
一路讲下来会发现,RAG 并不是某一个技术点。Embedding、BM25、RRF、HyDE、Reranker、Answerability、多轮证据和引用,都只是链路里的局部能力。
真正的目标,是让模型在正确资料、正确范围和正确证据强度下回答。
RAG 的难点不只是“搜”
如果只看表面,RAG 像是“用户问问题,系统查资料,模型回答”。但工程上真正难的是这一串判断:
什么时候需要查知识库,什么时候不需要。
用户的问题应该怎么整理成检索 Query。
资料应该怎么切,才能既好查又好答。
语义检索、关键词检索、字面匹配怎么组合。
候选资料怎么重排,怎么去重,怎么控制来源覆盖。
证据够不够回答,回答应该说到什么程度。
用户继续追问时,哪些证据应该延续,哪些应该放弃。
答案里的引用和后台观测能不能支撑后续排查。
这些问题加在一起,已经超出了“检索功能”的范围。它更像是在设计模型回答前能看到的世界。
RAG 其中预处理的东西,也离不开 LLM 持续辅助
步骤 | 主流实现 | 靠LLM程度 | 翻车概率 |
|---|---|---|---|
Query 改写(补主语) | LLM | 100% | 中 |
Query 拆分(多问题) | LLM | 95% | 高 |
Query 分类(答案型/发现型等) | LLM 或小分类模型 | 80% | 中 |
HyDE | LLM | 100% | 中 |
向量搜索 | 纯算法 | 0% | 低 |
BM25/关键词 | 纯算法 | 0% | 低 |
Rerank | 专用模型 | 0%(靠小模型) | 低 |
答案生成 | LLM | 100% | 中 |
这一堆步骤,几乎离不开 LLM 调用,你可以用逻辑协助你,但是面对多问题,这些语义理解,不如让 LLM 来理解,但面临着期间的步骤越来越多,等模型返回的时间也越来越久,模型在某一步骤不如意需要调整的概率也越来越多。目前所谓“工程化”,就是在这些预处理中,想办法把这些 LLM 调用拆得合理、缓存得聪明、失败时有兜底。
上下文工程决定模型发挥的边界
语言模型很擅长组织表达,但它看到什么、相信什么、围绕什么回答,取决于系统给它的上下文。RAG 的价值不是让模型拥有所有资料,而是让它在每一轮回答前拿到最合适的资料。
资料太少,模型会凭空补;资料太多,模型会被噪声干扰;资料不准,模型会被错误证据带偏;资料没有边界,模型就会把站内证据、通用知识和猜测混在一起。
所以,RAG 最后一定会走向上下文工程:检索只是入口,真正的工作是选择、组织、约束和解释这些上下文。
回到最后
RAG 不是为了做一个“能搜索文章”的按钮,而是为了让 AI 助手能理解站内内容、尊重当前页面、保留来源证据,并在多轮对话中保持清晰边界。
当用户问“这个网站有什么文章”,它应该能给出真实列表和数量;当用户问“这篇文章讲了什么”,它应该基于当前文章回答;当用户继续问“还有吗”,它应该知道上一轮已经给了哪些资料;当知识库没有证据时,它应该明确收住,而不是编造一个看似合理的答案。
这就是我现在理解的 RAG:不是把资料塞给模型,而是把检索、证据、边界、上下文和回答责任串成一条可追踪的链路。
一句话收束:
RAG 的核心不是让 AI 多知道一点,而是让 AI 在该知道的时候知道正确的东西,在不知道的时候也能诚实地说清楚边界。但真的如今做得好不好,还得不断添加内容,检索和测试来得出结论。