RAG

首先说说名字。RAG 是 retrieval-augmented generation(检索增强生成)的简称:你自己获取相关文本,在提问时把它交给模型,这样模型就能根据你提供的事实来回答,而不是依赖冻结的记忆。本章讲的就是这个获取-回答循环如何工作,以及它在哪里出了问题。
检索-生成模式
模型不了解你公司的帮助文档、产品详情,或任何训练后发生的事。问它这些问题,它要么承认无法帮助,要么更糟的是,编造一些听起来对的东西。RAG 通过在提问时把相关文本交给模型来解决这个问题。
有了嵌入,这个想法就成立了。嵌入是个技巧,把文本变成可以按接近度比较的向量(余弦相似度用来衡量两个向量的接近程度)。**检索增强生成**是在正常模型调用上附加的两个步骤:
- 检索。 拿用户的问题,在你的文档里搜索最相关的部分,收集排名靠前的匹配。
- 生成。 把这些部分放入提示作为上下文,然后让模型仅根据这个上下文回答问题。
模型的通用语言能力负责写作;你检索的文本提供事实。你得到的答案流利、基于你的内容,并且能保持最新,因为你控制了什么内容进来。
从预测循环的角度理解为什么这么有效很有帮助。没有 RAG,你是在要求模型从参数中冻结的模式里回忆一个事实,它可能根本没有,导致自信的猜测。有了 RAG,这个事实就摆在上下文里,所以模型的工作从"记住这个"变成"读你面前的内容并从中回答"。检索把工作从记忆移到阅读。清晰的画面是考试:闭卷考试会引发猜测,而 RAG 把它变成开卷考试,相关的页面就摊开在桌子上。
分块
你不嵌入整个文档。你先把它们分成更小的**分块**,一个段落或几个句子,然后嵌入那些。两个原因。首先,检索变得更精确:你拉取相关的一个段落而不是整个 40 页手册。其次,分块符合上下文窗口,一个巨大的文档可能不符合。
# 一个小分块器:按段落分割
def chunk(text):
return [c.strip() for c in text.split("\n\n") if c.strip()]分块大小是一个平衡,原因可追溯到嵌入。每个分块变成一个总结其整个意义的单个向量。分块太大了,它覆盖多个主题,所以它的一个向量是所有主题的浑浊平均,与任何特定问题都不匹配好。分块太小了,它失去周围意义,没有这个意义它就无法理解,所以向量指向一个碎片。段落大小的分块各持有一个连贯的想法。你根据检索表现从那里调整。
向量住在哪里
对于少数几个分块,你可以把向量保在内存里,用嵌入章节的余弦相似度函数比较它们,这是后面例子做的。对于数千或数百万分块,那变得慢,你会用到**向量存储**:一个建来在规模上快速找最近向量的数据库。
你不需要一个来学 RAG,你现在也不需要选一个。有几个作为托管服务和库存在;概念才是重点:某处存储嵌入并快速搜索它们。从内存开始,当收集超过它时移到存储。
基础化和引用
生成步骤是一个提示,你怎么写它决定 RAG 是否真的减少编造的答案。两个指令做重活:告诉模型仅从提供的上下文回答**only**,告诉它当上下文不包含答案时说。
context_text = "\n\n".join(retrieved_chunks)
system_prompt = f'''仅使用下面的上下文回答问题。
如果上下文不包含答案,说"我没有那个信息"。
在你的答案中引用上下文的相关部分。
上下文:
"""
{context_text}
"""'''这把两个早期课程拉到一起。分隔符和明确的"仅从这个回答"来自提示。允许说"我没有那个信息"是幻觉防守:一个有逃脱舱基础化模型编造远少于一个留下猜测的。基础化加逃脱舱是什么切掉编造的答案。要求它引用源也给用户什么验证。
超越最近邻:混合搜索和重排
纯向量搜索在意义上匹配,那大多数时候是你想要的。但它有个盲点:它能错过一个重要精确词。搜索一个错误代码像 E_4021 或一个特定产品名,基于意义的搜索可能交回关于相同主题的分块,跳过含精确字符串的那个。
修复有个名字,混合搜索:结合基于意义的搜索和老式关键字搜索,这样精确项和一般意义都得到一票。你不需要建这个来开始 RAG。当精确项重要且纯向量继续错过它们时伸手。
实践中
一个最小 RAG 答案,重用来自嵌入章的 search 和 embed 帮手:
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 管道**:嵌入搜索给模型调用养料,一个基础化提示把它们扣在一起。几乎所有质量住在检索正确的分块。
何时你不需要 RAG
RAG 不是免费的,它不总是答案。跳过它当:
- 数据小且装进提示。 如果你整个知识库是几个段落,直接粘进去。RAG 是当有太多一下都发送时的。
- 一个长上下文模型能全部装。 一些模型接受很大输入,足够一下取一整个文档,那能比建检索少工作。
记住真实限制:RAG 质量大多是检索质量。如果搜索步返回错的分块,即使一个完美提示也不能拯救答案。大多数现实 RAG 系统的努力进入检索正确的文本,不进最后的模型调用。
嵌入和 RAG 在提问时给模型知识,永不改变模型本身。下一章,微调,覆盖另一选项:用你的自己例子改变模型,和那何时值得对提示或 RAG。

