沉淀一下 RAG :从资料检索到上下文工程

这篇文章记录 RAG 的完整工程演进:从基础知识库、Embedding、RRF 混合检索,到语义分块、HyDE、Reranker、动态上下文、Answerability、多轮 Evidence、引用溯源和 AI 观测。RAG 真正难的不是把资料搜出来,而是知道什么时候搜、搜什么、该注入什么、证据是否足够,以及下一轮对话如何延续这些证据。

做 ZarkOne 的 AI 助手时,RAG 不是一个孤立功能,而是一条把站内资料、用户问题、证据和模型回答连接起来的链路。它从最基础的“先查资料再回答”开始,逐步扩展到分块、混合检索、查询增强、重排、上下文选择、多轮证据、引用溯源和观测测试。

一、为什么需要 RAG

网站的内容不是静止的。文章会更新,项目会补充,页面会调整,照片、视频、AI 创作记录也会不断沉淀。用户问 AI 的时候,很多问题并不是通用知识,而是和这个网站当前已有的资料有关。

比如:

  • 这个网站写过哪些文章?

  • 某篇文章到底讲了什么?

  • 这个项目之前做到了哪一步?

  • 当前页面里的内容,能不能作为回答依据?

如果 AI 只靠模型原本的知识回答,它并不知道这些资料的最新状态。模型可以说出一段流畅的话,但这段话未必来自 ZarkOne 当前真实存在的内容。

所以这里需要 RAG。它的作用不是让 AI 显得更聪明,而是让 AI 在回答前先看一眼站内资料。回答应该尽量来自已有证据,而不是只靠模型自己的泛化能力。

简单说,ZarkOne 做 RAG,是为了让 AI 回答三个更实际的问题:

  • 当前网站里到底有什么资料?

  • 这些资料里哪些和用户的问题有关?

  • 回答时应该引用哪些资料作为依据?

二、RAG 到底在做什么

RAG 在一次回答里的工作流程图
RAG 的基本链路:用户问题进入系统后,先检索资料、整理上下文,再让模型基于证据回答,并保留引用来源。

RAG 的全称是 Retrieval-Augmented Generation,中文通常叫“检索增强生成”它不是一个独立按钮,而是 AI 回答前的一段工作流程。

用户提出问题后,系统先判断这个问题是否需要站内资料。如果需要,就去知识库里找相关内容,再把这些内容整理成模型能阅读的上下文。模型最后基于这份上下文组织答案。

可以把它看成一条回答链路:

  1. 用户提出问题

  2. 系统检索相关资料

  3. 系统整理可用上下文

  4. 模型基于上下文回答

  5. 答案保留可追溯来源

既然想让 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 闭环:资料入库、问题检索、回答生成三段链路共同完成一次可追溯回答。

到这里,一个基础 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.03252

  • B = 1/(60+1) + 1/(60+4) ≈ 0.03202

  • C = 1/(60+6) + 1/(60+20) ≈ 0.02765

这个例子想说明:RRF 会奖励“多路都比较靠前”的资料,也允许某一路检索特别强的结果被保留下来。


这些检索方式最终怎么组合

前面把关键词检索、TF-IDF、BM25、pg_trgm、向量检索和 RRF 分开讲,是为了先弄清每个概念的职责。真正落地时,它们不是孤立存在,而是一起组成一条检索流水线。

混合检索组合流程图
混合检索:同一个问题并行走语义线、关键词线、字面线,再用 RRF 把多个候选排名融合成统一候选池。

可以把混合检索理解成一个“多路召回 + 排名融合”的过程:

  1. 用户问题进入检索模块。

  2. 向量检索负责找语义相近的知识块。

  3. BM25 负责把关键词重要性算进排序里。

  4. pg_trgm 负责补充标题、路径、短文本的近似字面匹配。

  5. 每一路先得到自己的候选排名。

  6. RRF 根据名次把多个排名融合成一个候选池。

  7. 候选池再交给后面的重排、去重、上下文选择。

所以这些术语的关系可以这样看:

  • 向量检索:解决“意思像不像”。

  • 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 在该知道的时候知道正确的东西,在不知道的时候也能诚实地说清楚边界。但真的如今做得好不好,还得不断添加内容,检索和测试来得出结论。