Skip to content
This page has been auto-translated and may contain errors.View in English

RAG

docs.scrimba.com

首先说说名字。RAGretrieval-augmented generation(检索增强生成)的简称:你自己获取相关文本,在提问时把它交给模型,这样模型就能根据你提供的事实来回答,而不是依赖冻结的记忆。本章讲的就是这个获取-回答循环如何工作,以及它在哪里出了问题。

检索-生成模式

模型不了解你公司的帮助文档、产品详情,或任何训练后发生的事。问它这些问题,它要么承认无法帮助,要么更糟的是,编造一些听起来对的东西。RAG 通过在提问时把相关文本交给模型来解决这个问题。

有了嵌入,这个想法就成立了。嵌入是个技巧,把文本变成可以按接近度比较的向量(余弦相似度用来衡量两个向量的接近程度)。**检索增强生成**是在正常模型调用上附加的两个步骤:

  1. 检索。 拿用户的问题,在你的文档里搜索最相关的部分,收集排名靠前的匹配。
  2. 生成。 把这些部分放入提示作为上下文,然后让模型仅根据这个上下文回答问题。

模型的通用语言能力负责写作;你检索的文本提供事实。你得到的答案流利、基于你的内容,并且能保持最新,因为你控制了什么内容进来。

从预测循环的角度理解为什么这么有效很有帮助。没有 RAG,你是在要求模型从参数中冻结的模式里回忆一个事实,它可能根本没有,导致自信的猜测。有了 RAG,这个事实就摆在上下文里,所以模型的工作从"记住这个"变成"读你面前的内容并从中回答"。检索把工作从记忆移到阅读。清晰的画面是考试:闭卷考试会引发猜测,而 RAG 把它变成开卷考试,相关的页面就摊开在桌子上。

Juno检索-生成模式 RAG 在正常模型调用上加了两个步骤:检索与问题最相关的文档,然后用它们作为上下文生成答案。模型提供语言,你检索的文本提供事实。这样它就能从你的内容回答,而不是从记忆回答,这是什么阻止它编造答案的。我花了一段时间才信任它,但开卷模型确实比闭卷的猜测少。

模型不带你的帮助文档、产品详情,或训练截止后的任何内容。问它仍然会得到拒绝或听起来不错但编造的答案。**检索增强生成**通过在请求时获取相关文本并把它放在提示里来填补这个空白,这样模型读取事实而不是回忆它。这个机制基于嵌入,每一段文本都成为一个向量,相似性是向量之间的距离(余弦相似度,即它们之间角的余弦)。

循环分两个步骤:

  1. 检索。 把问题嵌入到与文档相同的向量空间中,然后按相似度拉取最接近的分块。
  2. 生成。 把这些分块连接到提示中作为上下文,并指示模型仅从它们回答。

检索步骤有一个你设置的刻度叫 top-k:你实际保留的最接近分块有多少个。k 是一个平衡。太小了,你会错过拥有答案的分块,模型就没有什么可用的。太大了,你用松散相关的文本填充上下文,这会稀释信号、增加成本,让中间迷失问题更糟。k 设为 3 到 5 是个合理的起点,你根据实际问题对它进行调整。

这里是要点:把索引结构化为记录列表,每个记录持有分块文本和其预计算的向量,并把提示结构化,使检索到的上下文坐在问题上方清晰分隔的块中。模型的语言能力写作;你检索的文本决定什么是真的。

python
def retrieve(question, index, k=4):
    query_vector = embed(question)
    scored = [
        {"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])}
        for item in index
    ]
    return sorted(scored, key=lambda x: x["score"], reverse=True)[:k]
Juno检索-生成模式 RAG 是检索然后生成:嵌入问题,拉取最接近的分块,把它们放进提示,从中回答。这里重要的刻度是 top-k,你保留的分块数:太少你会错过答案,太多你把它埋在噪声里并为此付费。从 3 到 5 开始,根据实际问题调整,不要靠直觉。

模型不保有你的私有语料库或截止后的任何内容,它不会告诉你是哪个;它以相同的自信语气回答两种情况。**检索增强生成**是在请求时获取相关文本并基于它的模式,使正确性依赖于你控制的源而不是参数回忆。它运行在嵌入上:文本映射到向量,按余弦相似度排名(两个向量之间角的余弦,所以无论长度如何都能衡量意义接近度)。

检索然后生成的两步框架是正确的,但隐藏了工作在哪里。检索是一个穿着生成外衣的搜索问题,整个系统的质量在那里设定,不在模型调用中。所以从一开始就把检索-生成当作检索-排名-生成,因为朴素版本,嵌入查询、取最近 k 个、粘贴进去,在下层没遇过的失败模式上就破了:精确匹配错过、接近重复分块挤出覆盖,还有陈旧文本自信回答。

早期值得培养的第一个习惯是这是个可测量的系统,不是直觉。拿出一组真实问题和应该回答它们的分块,然后跟踪 recall@k:应该被检索的分块中,有多少落在你的前 k 个。Recall@k 是你的天花板。如果正确的分块没进上下文,再多的提示工艺也救不了,生成步骤无法回答检索没交给它的东西。下游一切都受这个数字约束,所以这是第一个要测量的,第一个要改进的。

python
def retrieve(question, index, k=8):
    query_vector = embed(question)
    scored = [
        {"text": it["text"], "score": cosine_similarity(query_vector, it["vector"]), "meta": it["meta"]}
        for it in index
    ]
    # 这里过度获取,然后重新排名到更小的最终集合(见下文)
    return sorted(scored, key=lambda x: x["score"], reverse=True)[:k]
Juno检索-生成模式 RAG 是穿着生成外衣的搜索问题:质量在检索中设定,不在模型调用中,所以朴素的嵌入-取k-粘贴版本会在精确匹配和陈旧文本上让你失望。约束一切的数字是 recall@k,应该被检索的分块中有多少实际进入你的前 k 个。如果正确的分块从未进上下文,没有提示能救你。在调整其他任何东西之前先测量那个,否则你是在打磨错的一端。

分块

你不嵌入整个文档。你先把它们分成更小的**分块**,一个段落或几个句子,然后嵌入那些。两个原因。首先,检索变得更精确:你拉取相关的一个段落而不是整个 40 页手册。其次,分块符合上下文窗口,一个巨大的文档可能不符合。

python
# 一个小分块器:按段落分割
def chunk(text):
    return [c.strip() for c in text.split("\n\n") if c.strip()]

分块大小是一个平衡,原因可追溯到嵌入。每个分块变成一个总结其整个意义的单个向量。分块太大了,它覆盖多个主题,所以它的一个向量是所有主题的浑浊平均,与任何特定问题都不匹配好。分块太小了,它失去周围意义,没有这个意义它就无法理解,所以向量指向一个碎片。段落大小的分块各持有一个连贯的想法。你根据检索表现从那里调整。

Juno分块 嵌入前,把文档分成更小的分块,大约段落大小,然后嵌入那些。更小的分块使检索更精确并更好地适应上下文窗口。分块大小是一个平衡:太大是模糊和浪费,太小失去周围意义。当我的答案含糊时,分块几乎总是太大了。

你嵌入分块,不是整个文档,因为每个分块成为一个必须总结其整个意义的向量。把文档分成段落大小的片段并嵌入每一个。太大的分块把多个主题平均成与任何东西都不匹配锐利的向量;太小的分块失去使它有意义的上下文。

所以把**分块大小**当作调整刻度,不是固定规则。几百个标记的分块往往清晰地保有一个想法。这个刻度与 top-k 相互作用:更大的分块意味着你的上下文预算里装的少,所以每个请求覆盖少。更小的分块提高精度但需要更大的 k 来重组跨越它们的想法。

第二个刻度是重叠:让连续分块在它们的边界共享一片文本,比如 10 到 20 百分比。没有重叠,一个回答问题的句子可能会在边界上分裂,一半在一个分块,一半在下一个,所以两个分块都检索得不好。重叠让你承受一些重复和几个更多向量,它买回那些边界会否则分开的答案。

python
def chunk(text, size=800, overlap=120):
    # 这里大小和重叠以字符计;在真实系统中以标记预算
    out, start = [], 0
    while start < len(text):
        out.append(text[start:start + size])
        start += size - overlap
    return out
Juno分块 分块大小和重叠是调整刻度,不是你一次就对的设置。以保有一个想法的分块为目标,几百个标记,记住这个刻度权衡 top-k:更大的分块,装得少。加 10 到 20 百分比重叠,这样一个跨越边界的答案仍然完整地在至少一个分块里活着。边界是检索静悄悄地失去答案的地方。

分块是在任何查询运行前检索召回赢或输的地方。每一个**分块**成为一个必须代表其整个意义的向量,所以分割决定了什么是可找的。当文档有结构时,段落或节边界打败固定字符窗口,因为一个向量覆盖一个连贯节是比覆盖两个节的一半更清晰的查询目标。

分块大小和重叠耦合到你的整个预算,不是自由参数。大小权衡 top-k 对抗上下文窗口:大分块各自携带更多上下文但装得少,所以覆盖下降;小分块提高精度但碎片化想法,需要更高的 k 来重组,这花费标记并重新引入中间迷失。大约 10 到 20 百分比的重叠救回跨越边界的答案,成本是索引里的重复。重复有下游咬痕:接近重同的重叠分块能填满你几个 top-k 槽位同样的内容,饿死覆盖。那是后来重排和去重赚得一席之地的原因之一。

大多数人跳过直到它伤了的成本:分块策略烤进你的索引,所以改变它意味着重新嵌入和重新索引整个语料。那是真正的计算,真正的钱,和一个旧新向量不应混的迁移窗口。根据你的实际文档和查询早期决定分块大小,在留出的集合上测量 recall@k,把重新分块当作刻意重新索引,不是你周五扔出的配置调整。

Juno分块 有结构的地方在结构上分块,因为每个分块是一个代表其整个意义的向量,大小权衡 top-k 对抗窗口一次全部。10 到 20 百分比的重叠救回跨越边界的答案但生出接近重复挤出你的 top-k,这是重排存在的一部分原因。记住分块选择烤进索引:改变它是完整重新嵌入和重新索引,真正成本和迁移,不是配置翻转。

向量住在哪里

对于少数几个分块,你可以把向量保在内存里,用嵌入章节的余弦相似度函数比较它们,这是后面例子做的。对于数千或数百万分块,那变得慢,你会用到**向量存储**:一个建来在规模上快速找最近向量的数据库。

你不需要一个来学 RAG,你现在也不需要选一个。有几个作为托管服务和库存在;概念才是重点:某处存储嵌入并快速搜索它们。从内存开始,当收集超过它时移到存储。

Juno向量住在哪里 对于几个分块,把向量保在内存里直接比较它们。对于大收集,向量存储是一个在规模上快速找最近邻的数据库。你不需要一个来学 RAG:从内存开始,当收集超过它时移到向量存储。不需要在第一天就买数据库。

对于几百个分块,向量住在内存里,你给查询打分每一个。那是一个线性扫描:好在小规模,一旦收集跑到数千或数百万就慢,因为你在每个查询上对每个向量比较。那时你移到**向量存储**,一个为快速最近邻搜索索引向量的数据库。

值得保有的不对称是嵌入-一次对比嵌入-每查询。你嵌入和索引每个文档分块恰好一次,提前,那个工作对每个未来问题被重用。你在请求时嵌入查询一次每请求,那是热路径上的唯一嵌入成本。所以重索引成本提前付,摊销;每查询成本是一个嵌入调用加一个搜索。那就是什么让 RAG 便宜地服。

你不需要选一个存储来学模式。从内存里的记录列表开始,当规模或持久性要求时换进向量存储,保持接口相同:嵌入查询、搜索、返回 top-k。

Juno向量住在哪里 在内存中你扫描每个查询的每个向量,好直到收集长大,然后向量存储给你索引最近邻搜索。在你的头里保有不对称:文档嵌入和索引一次永远重用,而查询每请求新嵌入。提前成本付一次摊销,所以服装便宜。从内存开始,当规模或持久性强制时换进存储。

在内存中你做线性扫描,给查询打分对抗每个分块向量,那是精确但每查询 O(n) 并在数万某处停止扩展。一个**向量存储**用一个近似最近邻索引(ANN)替换扫描:它为一个大速度提升权衡一点召回,通过不检查每个向量。那个权衡是一个旋钮,生产规模你接受索引能偶尔错过真最近邻,那直接折回到你的 recall@k。

嵌入-一次不对称设定你的成本形状。索引嵌入整个语料一次,一个大提前批成本;服装嵌入一个查询,每请求运行一个搜索。所以你稳态延迟预算大约是:嵌入查询、搜索索引、然后生成调用。搜索通常是最小片;生成调用主宰延迟,查询嵌入是你在能检索前必须付的固定税。值得测量每条腿,因为人们在真延迟一直坐生成里时为更快向量存储伸手。

然后是陈旧,失败模式那个内存玩具从不给你看。你的索引是快照;源文档漂移。一个策略改变,文档编辑,一个旧分块仍坐索引里指向过去,检索全信心地服它。所以索引不是写一次:你需要一条重新索引路径重新嵌入改变的文档,理想上每个分块上的元数据(源 id、版本、时间戳)这样你能无效或过滤陈旧条目。决定刷新节奏对抗你的真速度多快移,把"索引是当前的"当作什么你验证,不假定。

Juno向量住在哪里 向量存储交换精确线性扫描为近似最近邻索引:更快,但它能错过真邻居,那落回到你的 recall@k。嵌入是一次语料和一次每查询,所以稳态延迟是嵌入查询加搜索加生成,生成调用通常主宰,所以在怪罪存储前测量。玩具从不显示的陷阱是陈旧:索引是快照,源漂移,所以建一条重新索引路径,戳分块用版本和时间戳代替信任索引是当前的。

基础化和引用

生成步骤是一个提示,你怎么写它决定 RAG 是否真的减少编造的答案。两个指令做重活:告诉模型仅从提供的上下文回答**only**,告诉它当上下文不包含答案时说。

python
context_text = "\n\n".join(retrieved_chunks)

system_prompt = f'''仅使用下面的上下文回答问题。
如果上下文不包含答案,说"我没有那个信息"。
在你的答案中引用上下文的相关部分。

上下文:
"""
{context_text}
"""'''

这把两个早期课程拉到一起。分隔符和明确的"仅从这个回答"来自提示。允许说"我没有那个信息"是幻觉防守:一个有逃脱舱基础化模型编造远少于一个留下猜测的。基础化加逃脱舱是什么切掉编造的答案。要求它引用源也给用户什么验证。

Juno基础化和引用 生成步骤是一个提示,它决定 RAG 是否工作。告诉模型仅从提供的上下文回答,当答案不在那里时说。那个基础化加允许承认它不知道,是什么切掉编造的答案。要求它引用源给用户什么检查,给你一条线索当检索错过。

检索能交给模型完美上下文,模型仍能忽视它。生成提示是什么把答案绑到上下文,所以故意写它:指示模型仅从提供的上下文回答**only**,当上下文缺少答案时承认,并引用它用的跨度。

python
context_text = "\n\n".join(f"[{i}] {c}" for i, c in enumerate(retrieved_chunks))

system_prompt = f'''仅使用下面的上下文回答。
如果答案不在上下文中,恰好回复:"我没有那个信息"。
引用你用的分块数字,像 [2]。

上下文:
"""
{context_text}
"""'''

编号分块把"引用源"变成一个你能以程序方式检查的引用:模型指向分块 [2],你能确认分块 [2] 实际支持声明。那是你在 RAG 中最错过的失败模式的手柄,正确答案错的分块检索,模型产生一个正确声音的答案,提供的上下文不支持。没有引用你不能分一个基础化答案从幸运猜测。

逃脱舱与指令一样重要。一个模型被告诉仅从上下文回答,但给无许可失败,仍会从薄弱检索拼凑什么东西。给它明确的出,精确的"我没有那个信息"字符串,检索错过表面作为清晰空隙代替自信发明。消息形状这里是 OpenAI 风格;系统然后上下文排列按提供商变化,但原则持有。

Juno基础化和引用 生成提示把答案绑到上下文:仅从它回答,当它不在那里时承认,引用分块。编号分块这样引用是可检查的,那抓住狡猾的失败其中答案是对的但检索的分块从不支持它。给模型精确逃脱字符串,或薄弱检索进来自信错的。形状按提供商变化,但系统然后上下文结构传结。

基础化是检索和模型之间的合约,它是你有的最便宜可靠性杠杆,所以在提示上花时间。指示模型仅从提供的上下文回答**only**,当上下文不支持答案时返回固定弃权字符串,引用它用的分块 id。

python
context_text = "\n\n".join(f"[{c['id']}] {c['text']}" for c in retrieved)

system_prompt = f'''仅使用下面的上下文回答。
如果上下文不支持答案,恰好回复:"我没有那个信息"。
对每个声明,用括号引用分块 id,比如 [doc12]。

上下文:
"""
{context_text}
"""'''

引用不是装饰;它们是你如何让基础化可测量。把分块 id 放在输出中你能自动检查一个引用的分块是否真的包含声明,那分开一个基础化答案从流利猜测。那也是怎样你检测打败朴素 RAG 的失败:答案似乎正确但检索的上下文从不支持它,所以检索错过,模型从参数记忆填空隙。没有 id 你在抽查;有它们你能在评估(一组问题用已知好答案,自动打分)里打分忠实,看忠实作为一个数字。

两个生产注。弃权路径必须便宜和被许可,因为一个模型无许可失败总是生成什么东西,薄弱检索然后成为自信错的而不是清晰空隙。消息结构这里是 OpenAI-SDK 形状;系统对比用户分割和上下文如何附加按提供商变化,所以把排列当作便携的,精确字段名当作不。下面的持久规则:从不让模型是源,只是源的表述者你能引用和检查。

Juno基础化和引用 基础化是合约:仅从上下文回答,用固定字符串弃权,引用分块 id 这样忠实成为什么你在评估里打分,不眼球。引用是怎样你抓住正确但无支持的答案,其中检索错过,模型从记忆修补空隙。保持弃权路径便宜和被许可或薄弱检索成为自信错的。形状按提供商变化;结构传结,字段名不。

超越最近邻:混合搜索和重排

纯向量搜索在意义上匹配,那大多数时候是你想要的。但它有个盲点:它能错过一个重要精确词。搜索一个错误代码像 E_4021 或一个特定产品名,基于意义的搜索可能交回关于相同主题的分块,跳过含精确字符串的那个。

修复有个名字,混合搜索:结合基于意义的搜索和老式关键字搜索,这样精确项和一般意义都得到一票。你不需要建这个来开始 RAG。当精确项重要且纯向量继续错过它们时伸手。

Juno超越最近邻:混合搜索和重排 向量搜索在意义上匹配,好直到有人搜索精确代码或产品名,正确分块被跳过。混合搜索混进纯关键字搜索,这样精确项也计数。你能开始 RAG 不用它,一旦你注意精确匹配错过堆起就加它。

最近邻向量搜索在意义上匹配,那也是它的弱点。它能排名一个在主题上接近的分块在你需要的精确标记的分块上,一个错误代码、一个 SKU、一个函数名、一个稀有专有名词。嵌入把那些涂进"粗糙这个意义区域",精确字符串在涂里丢失。

**混合搜索**运行两个检索器和合并它们:向量搜索为了意义,加关键字搜索(经典项匹配,通常 BM25,打分文档按他们有多少查询的精确词和那些词有多稀有)。你取两个候选和结合排名,这样一个赢在精确项或语义接近上的分块能表面。实际收获是纯向量摸清的查询的 recall。

第二个技术是**重排**:一个第二遍用一个更精确的、更昂贵模型重排你的第一遍候选。模式是过度获取然后窄。检索一个慷慨集廉价,说前 20,然后运行一个重排器读每个候选对抗查询并重新排序它们,保持前 4 真的发送。第一遍检索快和粗;重排器慢和尖,这样你只在短列表上运行它。结果是一个更好最终 k 不为重排器成本跨你的整个索引付费。

Juno超越最近邻:混合搜索和重排 纯向量搜索错过精确字符串像错误代码和 SKU,因为嵌入把它们模糊进一个意义区域。混合搜索加关键字匹配(BM25)这样精确项得到一票和语义学一样。重排是一个便宜然后尖的模式:用快检索过度获取一个宽集,然后用一个慢的、更精确模型重排短列表,保持最好的几个。你得到更好最终结果不为昂贵打分器跨一切付费。

纯近似最近邻是一个强大默认和已知失败表面。它优化为语义接近,所以它在查询铰链于一个精确标记、一个错误代码、一个标识符、一个 SKU、一个稀有专有名词时欠执行,因为嵌入把那个标记压到一个意义邻域精确字符串在那里模糊。那不是调整问题;它是稠密向量做什么。

**混合搜索**通过运行稠密检索(向量、意义)和稀疏检索(关键字项匹配,经常 BM25,打分在精确项频率加权按项稀有度)并融合结果列表,通常用互反排名融合,它结合两个排名每个项的位置而不是不可比较的原始分数。胜利可测量为 recall@k 在精确匹配查询上,那是纯稠密悄悄掉的片。成本是第二个索引和一个融合步,这样它付当你的流量包括标识符和稀有项时,少当它全是自然语言释义时。

**重排**攻击不同错过。第一遍检索(稠密、稀疏或混合)建为速度在大索引,这样它粗排。一个重排器是一个交叉编码器(一个模型读查询和候选分块一起,一起打分它们的相关性,而不是比较两个预计算向量)那是更精确,远太慢在整个语料运行。这样你用便宜检索过度获取,前 20 到 50,然后重排那个短列表,保持最后几个。

那是在最多生产 RAG 里最大收获的 recall 修复:它直接攻击正确答案错的分块失败,通过在单纯主题上促进真的支持答案的分块,你测量收获为 recall@k 重排阶段前对比之后。预算它:重排器在热路径加延迟和每查询成本,这样按你延迟目标调度过度获取,不是按你在抽象可以承担打分多少候选。

Juno超越最近邻:混合搜索和重排 稠密向量模糊精确标记,这样混合搜索运行稀疏关键字检索(BM25)在稠密旁并用互反排名融合融合列表,在标识符和稀有项上买回 recall@k。重排是更大胜利:廉价过度获取,然后用一个一起读查询和分块的交叉编码器重排短列表,直接固定正确答案错分块,通过促进支持答案的分块。测量 recall@k 重排前后,对你的热路径目标预算重排延迟和每查询成本。

实践中

一个最小 RAG 答案,重用来自嵌入章的 searchembed 帮手:

python
def answer_from_docs(question, index):
    # 1. 检索:问题的前匹配分块
    query_vector = embed(question)
    scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
    top = sorted(scored, key=lambda x: x["score"], reverse=True)[:3]

    # 2. 生成:仅使用那些分块回答
    context = "\n\n".join(t["text"] for t in top)
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": f'仅使用这个上下文回答。如果不在这里,说你不知道。\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

检索三个最接近的分块,把它们放进提示,让模型从它们回答。那是整个**RAG 管道**:嵌入搜索给模型调用养料,一个基础化提示把它们扣在一起。几乎所有质量住在检索正确的分块。

Juno实践中 一个最小 RAG 答案是嵌入搜索给模型调用养料:检索最接近的分块,把它们放进提示,问模型仅从它们回答。检索供事实,基础化提示保持答案锚定到它们。几乎所有质量住在检索正确的分块,这样那是哪里花你的时间。

一个工作 RAG 调用是检索、基础化、生成,早期引用手柄接线进去:

python
def answer_from_docs(question, index, k=4):
    top = retrieve(question, index, k)  # 从模式节
    context = "\n\n".join(f"[{i}] {t['text']}" for i, t in enumerate(top))
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
                f'仅使用这个上下文回答。如果不在这里,回复'
                f'"我没有那个信息。"引用分块像 [1]。\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

整个东西是嵌入搜索给模型调用养料,由一个基础化提示扣在一起。chat.completions 形状是 OpenAI 风格;消息角色和调用签名按提供商变化,但检索然后基础化然后生成流到处相同。

当一个答案错,按阶段顺序调试。首先检查检索的分块:相关文本落在 top 里吗?如果不,修复在检索里,提高 k,修复分块,加混合或重排,不在提示。如果正确分块被检索,答案仍然错,然后它是提示问题。这个顺序保你从对检索错误调整提示词,那从不汇聚。

Juno实践中 一个工作 RAG 调用是检索、引用基础化、生成,OpenAI 风格消息形状如果不在字段名在结构传到其他提供商。按固定顺序调试:首先看检索的分块。如果正确文本从不落,修复检索(k、分块、混合、重排),不提示。只有一旦正确分块在上下文,错答案是提示问题。

实践中检索-排名-生成管道是一个函数,你接通的部分是阶段间的接合处:

python
def answer_from_docs(question, index, k_fetch=20, k_final=4):
    candidates = retrieve(question, index, k=k_fetch)   # 稠密(或混合)
    top = rerank(question, candidates)[:k_final]        # 交叉编码器遍
    context = "\n\n".join(f"[{c['id']}] {c['text']}" for c in top)
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
                f'仅使用这个上下文回答。如果无支持,回复'
                f'"我没有那个信息。"引用 id 像 [doc12]。\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content, [c["id"] for c in top]

在答案旁返回分块 id 是让系统**可观察**的举动:你记录什么被检索,什么被引用,引用的 id 是否真的支持答案,那是你在评估里的忠实信号。chat.completions 形状是 OpenAI-SDK;当流便携时把字段名当提供商特定。

这个管道的延迟和成本图值得在你的头里保有,因为优化去哪里时间是:

  • 查询嵌入是一个固定小税。
  • 第一遍检索是便宜的。
  • 重排按你的过度获取大小加一个真每查询打击。
  • 生成调用同时主宰延迟和美元成本。

这样在你追一个更快向量存储前,确认搜索曾是瓶颈;通常它是生成。

通过阶段顺序调试失败:检索集、然后重排集、然后引用、然后提示。一个错答案其正确分块从不进 candidates 是检索错,没有提示改变修复从不在那里的分块。

Juno实践中 管道是检索、重排、基础化、生成,在答案旁返回分块 id 是什么让它可观察:记录检索对比引用,你能在评估里打分忠实代替猜。看成本图,生成主宰延迟和美元,这样搜索是瓶颈前确认交换向量存储。按阶段顺序调试,检索然后重排然后引用然后提示,因为没有提示修复从不进候选集的分块。形状是 OpenAI-SDK;流便携,字段名不。

何时你不需要 RAG

RAG 不是免费的,它不总是答案。跳过它当:

  • 数据小且装进提示。 如果你整个知识库是几个段落,直接粘进去。RAG 是当有太多一下都发送时的。
  • 一个长上下文模型能全部装。 一些模型接受很大输入,足够一下取一整个文档,那能比建检索少工作。

记住真实限制:RAG 质量大多是检索质量。如果搜索步返回错的分块,即使一个完美提示也不能拯救答案。大多数现实 RAG 系统的努力进入检索正确的文本,不进最后的模型调用。

嵌入和 RAG 在提问时给模型知识,永不改变模型本身。下一章,微调,覆盖另一选项:用你的自己例子改变模型,和那何时值得对提示或 RAG。

Juno何时你不需要 RAG 当你的数据小到足以粘进提示,或当长上下文模型能一下全部装时跳过 RAG。保持真实限制在脑子里:RAG 质量大多是检索质量,这样如果搜索返回错分块,没有提示能救答案。现实工作大多在检索正确文本,不在最后调用。

RAG 是**基础设施**,一个索引、一个嵌入步、一个检索步、一条重新索引路径,它赚那个负担只当数据太大无法发送且改变通常足够,重新训练不是选项。跳过它当一个便宜更形状做工作:

  • 语料装进提示。 几个段落或一个小策略文档直接在上下文。RAG 只赚它的保持当有超过你能一下发送。
  • 长上下文模型装整个源。 如果文档装一个大窗口,一下发送它能打一个你必须建和调的检索管道,虽然你为那些标记每个调用付费,仍然打中间迷失。

保有管理事实:RAG 质量由检索质量限界。一个完美提示在错分块仍然答错,这样索引和检索步是工作和胜利的地方,不最后模型调用。当你确实需要改变行为而不是供事实时,微调是下一章,它解决一个不同的问题比 RAG 做。

Juno何时你不需要 RAG RAG 是有真实负担的基础设施,这样当语料装进提示或长上下文模型能吞整个源时跳过它,记住长上下文路由每调用付标记,仍然在中间漂移。管理事实站:质量由检索限界,这样完美提示在错分块仍然答错。在索引和检索花,不最后调用。

RAG 为你买币和基础化,代价是一个你建、调、监视和重新索引的检索系统。这样决定是一个成本效益的,不是一个默认。跳过它当一个更轻形状清吧:

  • 小的、稳定的语料。 如果它装提示并很少改变,放上下文,倾靠提示缓存这样固定块在重复调用被廉价服。一条检索管道这里是无回报负担。
  • 长上下文、谦虚重用。 如果源装一个大窗口,一下发送它跳过建,但你在每个请求为那些输入标记付费,recall 仍在中间腐烂,这样它输给 RAG 一旦卷或文档大小爬。

管理约束是一样运行全章的:端到端质量由 recall@k 限界。没有提示、没有模型升级、没有重排救一个事实检索永不放上下文。这样工程投资进检索、分块、混合、重排、评估、重新索引,模型调用是便宜部分获对。保持 RAG 和微调在它们**分开的车道**:检索供什么是真实,停留当前当源改变,而微调形状如何模型回应,是一个陈旧的、无源的方式注入事实。为知识用检索,为形式用微调,不要要求任何一个做另一个的工作。

Juno何时你不需要 RAG RAG 为币和基础化买,代价是一个系统你建和重新索引,这样当语料是小和稳定时跳过它(粘进去,倾靠提示缓存)或装长窗口带谦虚重用。约束运行一切是 recall@k:没有东西下游救一个事实检索永不表面,这样投资在检索,不模型调用。保持车道干净:检索对什么是真实,微调对它听起来怎样,从不要求一个做另一个的工作。