什么是 RAG?通过工作示例解释检索增强生成

  1. 首页
  2. 博客
  3. 人工智能

人工智能

什么是 RAG? 通过工作示例解释检索增强生成

RAG 让人工智能模型根据您自己的文档而不是仅根据记忆来回答。了解管道的工作原理,用简单的 Python 运行一个小型版本,并了解真实系统失败的原因以及如何测试它们。

阅读约 9 分钟
什么是RAG?通过一个工作示例解释检索增强生成

要点速览

  • RAG 首先找到相关信息,将其添加到提示中,然后要求模型从中回答。
  • 这是将语言模型连接到私有或经常更改的数据的常用方法。
  • 大多数质量问题始于检索和数据准备,而不是模型。
  • RAG 减少了虚构的答案,但并没有消除它们,因此您需要对其进行测试。

向一般的AI助手询问你们公司的退款政策,它只能猜测。它从未见过您的文档。 RAG 是检索增强生成的缩写,通过查找正确的信息并将其与问题一起传递给模型来解决这个问题。

本指南用简单的语言解释了每个阶段,然后仅使用 Python 的标准库构建了一个小型版本。我们在 Python 3.13 上运行该代码,显示的分数来自该运行。它不使用语言模型,因此它演示了检索和提示构建,并且我们没有测试最终的答案步骤。

什么是RAG?

检索增强生成是一种将搜索步骤与语言模型相结合的技术。当出现问题时,系统会搜索知识源,将最佳结果添加到提示中,并要求模型根据该材料进行回答。

想想开卷考试。闭卷学习的学生依赖记忆,可能会自信地记错。开卷的学生可以先查找正确的页面。 RAG 为模型提供那本书,并让它指向它使用的页面。

RAG解决的问题

语言模型本身对业务使用有三个限制:

  • 它不知道您的私人数据。 您的保单、票据和合同不是其培训的一部分。
  • 它的知识有一个界限。它无法知道您今天早上更改的价格。
  • 它可能会发明答案。 当不确定时,它可能会产生一些看似合理但错误的东西。

RAG 可以解决所有三个问题,而且您无需重新训练任何内容。更新文档,重新索引它,下一个答案就可以使用它。

RAG 的工作原理(一步一步)

RAG 系统有两条管道。一个提前准备数据,另一个针对每个问题运行。

索引:准备数据

  1. 加载:从 PDF、wiki、数据库或票证中收集内容。
  2. 清理和分块:删除垃圾,然后将文档分割成小段落。
  3. 嵌入: 将每个段落转换为数字列表(称为嵌入),以捕获其含义。
  4. 存储:将嵌入保存在可搜索索引中,通常是向量数据库。

查询:回答问题

  1. 检索:嵌入问题并找到含义最接近的段落。
  2. 增强:将这些段落插入提示中。
  3. 生成:模型使用该上下文编写答案。

检索不必使用向量。它可以是关键字搜索,甚至是 SQL 查询,只要能为您的数据找到正确的信息即可。

一个微小的工作示例

为了清楚地看到运动部件,这是一个没有库的小型猎犬。它将文本表示为字数,这是真实嵌入的更简单的替代品,并且使用余弦度量对相似性进行评分。

import math, re
from collections import Counter

DOCS = {
    "refunds": "Refunds are paid within 7 days to the original payment method.",
    "shipping": "Standard shipping takes 3 to 5 working days across India.",
    "warranty": "All keyboards include a 2 year warranty against defects.",
}

def vectorize(text):
    return Counter(re.findall(r"[a-z0-9]+", text.lower()))

def similarity(a, b):
    dot = sum(a[word] * b[word] for word in a)
    size = math.sqrt(sum(v * v for v in a.values()))
    size *= math.sqrt(sum(v * v for v in b.values()))
    return dot / size if size else 0

def retrieve(question, k=1):
    q = vectorize(question)
    scored = [(similarity(q, vectorize(t)), name) for name, t in DOCS.items()]
    return sorted(scored, reverse=True)[:k]

def build_prompt(question):
    (score, name), = retrieve(question)
    return (
        "Answer using only the context. If it is not there, say you do not know.\n"
        f"Context: {DOCS[name]}\n"
        f"Question: {question}"
    )

询问退款会找到正确的段落并构建此提示:

print(retrieve("How many days until refunds are paid?"))
# [(0.4558..., 'refunds')]

print(build_prompt("How many days until refunds are paid?"))
# Answer using only the context. If it is not there, say you do not know.
# Context: Refunds are paid within 7 days to the original payment method.
# Question: How many days until refunds are paid?

在完整的系统中,您现在可以将该提示发送到语言模型。重要的细节是当答案缺失时承认的指示,您可以在我们的提示工程指南中了解更多信息。

我们的例子遗漏了什么

现在尝试一个含义相同但使用不同词语的问题:

print(retrieve("When can I get my money back?", k=3))
# [(0.0, 'warranty'), (0.0, 'shipping'), (0.0, 'refunds')]

每个分数都是零,因为该问题与任何文档都不共享任何单词。 “退款”意味着退款给人工,但字数无法说明。这个单一输出解释了为什么真实系统使用嵌入,嵌入的设计目的是使具有相似含义的文本最终得到相似的数字,即使没有共同的单词。

但不要认为嵌入可以解决所有问题。他们可能会难以理解产品代码和错误号等精确术语,而关键字搜索可以很好地处理这些术语。这就是为什么许多生产系统使用混合搜索,将两种方法结合起来。

分块:人们低估的步骤

文档被分成多个块,因为整个文件太长并且检索不精确。您选择的尺寸涉及真正的权衡:

块大小 优势 风险
太小了 精准匹配 丢失上下文,例如规则适用于哪个产品
太大 保留上下文 淡化意义,并使用更多的提示
按结构划分 尊重标题和章节 需要更清晰的源文件

块之间有一些重叠的几百个单词是一个常见的起点,但将其视为要测试的第一个猜测。如果可以的话,沿着自然边界(例如标题)进行拆分。并附加元数据,例如文档标题、日期和部门,以便您可以过滤结果并引用来源。

改进检索

当答案很差时,这些是最值得尝试的升级,大致按以下顺序:

  • 清理数据。首先删除过时和重复的文档。没有算法可以修复相互矛盾的来源。
  • 使用混合搜索。 结合基于含义的结果和关键字结果。
  • 添加重新排序器。 获取更多候选者,然后使用专门的模型按相关性对它们重新排序。
  • 按元数据过滤。在搜索前按产品、地区或日期限制结果。
  • 重写查询。 提出一个模糊的后续问题,例如“第二个怎么样?”变成一个完整的问题。

提示: 在指责模型之前,先看看检索实际返回了什么。如果上下文中没有正确的段落,则任何模型都无法从中正确回答。

RAG 与微调

问题 RAG 微调
它添加了什么? 提问时的知识 训练有素的行为或风格
更新信息 重新索引文档 再次训练
可以引用来源吗? 是的 不可靠
最适合 改变的事实 一致的格式或语气

它们并不相互排斥。我们对 RAG 与微调与提示工程 的比较涵盖了如何更深入地选择。

如何测试 RAG 系统

RAG 是软件,它像任何其他软件一样需要测试。创建一组真实问题以及您期望的段落和答案,然后分别测量两个阶段:

舞台 要问的问题 通用名
检索 我们是否找到了正确的通道? 情境回忆
检索 我们获取的东西中有多少是有用的? 上下文精度
一代 答案是否得到上下文的支持? 忠诚
一代 它真的回答了问题吗? 回答相关性

还包括尴尬的情况:数据中没有答案的问题、使用不同措辞的问题以及来源冲突的问题。每当您更改分块、模型或提示时,请重新运行此集,就像回归套件。我们的 pytest 教程 展示了如何通过单个测试运行多个输入。

RAG 系统为何失败

  • 答案并不在数据中。 该模型可能仍会尝试一个,因此请告诉它如何拒绝。
  • 未检索到正确的块。这通常是分块或搜索问题。
  • 上下文太多。 不相关的段落可能会分散模型的注意力并增加成本。
  • 过时的文档。 可以像当前策略一样自信地检索旧策略。
  • 表格和 PDF 解析得很糟糕。 如果提取扰乱了文本,检索将永远无法恢复。
  • 仅使用演示问题进行测试。 真实用户的表述方式非常不同。

安全和权限

RAG 带来了两个很容易被忽视的风险。首先,访问控制:如果索引包含 HR 文件,初级员工的问题不得检索它们。在检索期间应用每个用户的权限,因为指示模型保密并不是可靠的保护措施。

其次,及时注射。检索到的网页或文档可能包含诸如“忽略您的指示”之类的文本。将检索到的内容视为不可信数据,限制模型可以采取的操作,并要求人员批准敏感操作。

最后的话

RAG 是一个简单的想法,具有大量实用细节:找到正确的信息,然后要求模型使用它。从一小部分干净的文档和大约二十个实际问题开始。在增加复杂性之前先让检索工作正常,因为具有良好数据的适度管道通常会击败具有不良数据的复杂管道。

常见问题

RAG在人工智能中代表什么?

RAG 代表检索增强生成。系统检索相关信息,将其添加到提示中,然后模型从中生成答案。

RAG 能阻止 AI 幻觉吗?

它通过为模型提供真实来源来减少它们,但不会删除它们。模型仍然可能误读上下文,因此测试答案并显示引用。

我需要 RAG 的矢量数据库吗?

并非总是如此。小型项目可以使用内存索引、关键字搜索或 SQL。矢量数据库有助于大型集合和基于含义的搜索。

RAG 和微调有什么区别?

RAG 在提问时提供信息,因此更新很快。微调训练模型改变其行为或风格,它不太适合改变事实。

什么是嵌入?

嵌入是表示文本含义的数字列表。具有相似含义的段落具有相似的编号,这使得基于含义的搜索成为可能。

继续学习

与Stack Quora合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件测试

用户验收测试 (UAT):流程、清单和真实示例

用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。

阅读文章 →
数据库与 SQL

SQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱

通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。

阅读文章 →
数据库与 SQL

SQL CASE WHEN 语句:语法、实例和常见错误

SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。

阅读文章 →
软件开发

什么是智威汤逊?通过工作示例解释 JSON Web 令牌

什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。

阅读文章 →
软件开发

JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例

JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。

阅读文章 →
软件开发

Python 生成器和 Yield 解释:通过真实示例节省内存

Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。

阅读文章 →
人工智能

什么是嵌入?向量和余弦相似度简单解释

什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。

阅读文章 →
DevOps

蓝绿部署与金丝雀部署:差异、权衡以及如何选择

蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。

阅读文章 →
测试自动化

不稳定的测试:造成这些问题的原因以及如何彻底解决它们

我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。

阅读文章 →
软件测试

负载测试与压力测试:差异、指标和 k6 示例

负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。

阅读文章 →
软件测试

测试用例与测试场景:主要区别、示例以及何时使用它们

测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。

阅读文章 →
数据库与 SQL

SQL 查询优化:读取执行计划并修复慢速查询(带计时)

对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。

阅读文章 →
数据库与 SQL

SQL UNION 与 UNION ALL:差异、示例和性能测试

SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。

阅读文章 →
软件开发

JavaScript Fetch API 教程:GET、POST、错误处理和超时

针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。

阅读文章 →
软件开发

Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例

使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。

阅读文章 →
软件开发

Python 类和对象:带有实际示例的 OOP 教程

用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。

阅读文章 →
人工智能

什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释

什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。

阅读文章 →
DevOps

适合初学者的 Linux 命令:带有示例的基本终端指南

适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。

阅读文章 →
测试自动化

Selenium 与 Playwright:主要区别、代码比较以及选择

Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。

阅读文章 →
软件测试

等价划分:通过实例解释测试设计技术

等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。

阅读文章 →
软件测试

软件测试中的严重性与优先级:差异、矩阵和实例

软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。

阅读文章 →
数据库与 SQL

SQL 子查询教程:类型、相关子查询和 EXISTS 以及示例

SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。

阅读文章 →
数据库与 SQL

SQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别

SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。

阅读文章 →
软件开发

Git 命令备忘单:您实际使用的命令,以及示例

Git 命令备忘单按任务分组:设置、暂存、分支、远程、存储、历史记录和撤消错误,以及来自真实演示存储库的输出。

阅读文章 →
软件开发

JavaScript Promise 解释:then、catch、Promise.all 和常见错误

JavaScript 承诺用我们运行的代码进行了解释:状态、then 和 catch 链、错误处理、Promise.all、allSettled、race 和 any、事件循环和常见错误。

阅读文章 →
软件开发

Python 字典教程:方法、示例和常见模式

Python 字典教程,包含我们运行的代码:创建、读取和更新字典、关键方法、合并、理解、计数和分组模式以及常见错误。

阅读文章 →
人工智能

什么是 RAG?通过工作示例解释检索增强生成

什么是RAG?检索增强生成逐步解释:索引、分块、嵌入、检索、一个小型的 Python 工作示例、评估和常见故障。

阅读文章 →
DevOps

Terraform 初学者教程:从第一个文件到模块的基础设施即代码

适合初学者的 Terraform 教程:从没有云帐户开始,学习初始化、计划和应用,然后是变量、输出、状态、模块和安全的团队工作流程。

阅读文章 →
DevOps

Docker 与 Kubernetes:主要区别、它们如何协同工作以及您需要哪些

Docker 与 Kubernetes 的解释:各自的作用、同一个应用程序同时运行、Docker Compose 和 Swarm 的比较、dockershim 的变化以及如何决定您需要什么。

阅读文章 →
测试自动化

Pytest 初学者教程:夹具、参数化和真实示例

面向初学者的 pytest 教程,其中包含我们在 pytest 9.2 上运行的测试:断言、固定装置、参数化、conftest、标记、tmp_path、monkeypatch、有用的标志和 CI。

阅读文章 →
软件测试

软件测试生命周期 (STLC):6 个阶段、进入和退出标准、示例

软件测试生命周期解释:六个 STLC 阶段,包括进入和退出标准、可交付成果、有效的登录功能示例、STLC 与 SDLC 以及敏捷中的 STLC。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →