5分钟理解 RAG:从零开始构建你的第一个检索增强生成系统
5分钟理解 RAG:从零开始构建你的第一个检索增强生成系统
目录
用 130 行代码,带你快速理解 RAG 的核心原理
前言:什么是 RAG?
你有没有遇到过这种情况:问 ChatGPT 一些新技术或最新动态时,它回答得头头是道,但仔细一看全是错的?
这就是大语言模型的"幻觉"问题——模型可能编造不存在的信息来回答问题。
RAG(Retrieval Augmented Generation,检索增强生成) 提供了一个解决方案:让 AI 先检索相关文档,再基于这些准确的信息回答问题。在较早之前LLM模型还没有大规模应用时,一些研究,如Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks 就通过这种方案解决模型生成文本偏离实际较大的问题。
打个比方:
- 普通 LLM:闭卷考试,只能靠记忆回答,容易出错
- RAG:开卷考试,可以查阅资料,答案更可靠
RAG 的核心就是两件事:
- 从知识库中检索相关文档
- 基于检索到的文档生成答案
为什么需要 RAG?
传统大语言模型面临几个实际问题:
| 问题 | 传统 LLM | RAG |
|---|---|---|
| 知识过时 | 无法获取训练后的新知识 | 更新知识库即可 |
| 幻觉问题 | 可能编造信息 | 基于文档事实回答 |
| 私有数据 | 无法访问企业内部数据 | 可以接入内部文档 |
| 可追溯性 | 无法说明答案来源 | 可以展示参考文档 |
本文目标
通过这篇文章,你将:
- 理解 RAG 的完整工作流程
- 掌握文本嵌入和向量搜索的基本概念
- 动手实现一个可运行的 RAG 系统
- 为后续深入学习打下基础
不用担心,只有 130 行代码,我们一步步来。
项目准备:极简 RAG Demo
为了帮助你快速理解 RAG,我们准备了一个超简单的 Demo 项目:
项目结构
demo1/
├── docs/
│ └── knowledge.txt # 知识库文档
├── rag_demo.py # 主程序(所有逻辑,约130行)
├── requirements.txt # 依赖包列表
└── README.md # 项目说明
设计理念
这个 Demo 遵循以下设计原则:
- 极简:所有逻辑在一个文件中,代码约 130 行
- 清晰:每一步都有详细注释
- 可运行:复制代码、配置环境变量即可运行
- 教学友好:适合 5 分钟快速理解
技术栈
| 组件 | 技术选择 | 原因 |
|---|---|---|
| 编程语言 | Python 3.10+ | 简单易学,生态丰富 |
| 嵌入模型 | OpenAI API (BAAI/bge-large-zh-v1.5) | 易于调用 |
| 向量计算 | numpy | 高效的数值计算 |
| LLM | OpenAI API | (任意 稳定可靠,易于调用 |
RAG 核心流程详解
完整流程图
流程步骤详解
我们将 RAG 流程分为 7 个步骤,每个步骤都有明确的目标和实现方法。
步骤一:准备知识库
概念解释
知识库是 RAG 系统的基础,包含了我们要检索的所有文档。在本 Demo 中,知识库是一个简单的文本文件。
实现代码
def load_knowledge(filepath):
"""
读取知识库文件
Args:
filepath: 知识库文件路径
Returns:
list: 文档块列表
"""
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 按空行切分成段落,每个段落作为一个文档块
chunks = [chunk.strip() for chunk in content.split('\n\n') if chunk.strip()]
return chunks
实践示例
我们的知识库文件 docs/knowledge.txt 包含:
RabbitMQ 是一个消息队列系统。
它用于系统之间的异步通信。
RabbitMQ 支持 AMQP 协议。
RabbitMQ 支持多种消息模型,包括点对点和发布订阅。
RabbitMQ 提供消息持久化和确认机制。
Kafka 是一个分布式流处理平台。
它适合处理高吞吐量的数据流。
Kafka 基于日志存储模型。
Kafka 支持多生产者和多消费者。
RAG 是检索增强生成技术。
它结合信息检索和大语言模型生成。
RAG 可以减少模型幻觉。
RAG 提供更准确的答案。
文档切分结果
按空行切分后,得到 3 个文档块:
| 块编号 | 内容概要 |
|---|---|
| 1 | RabbitMQ 相关知识 |
| 2 | Kafka 相关知识 |
| 3 | RAG 相关知识 |
为什么要切分?
- 精细粒度:更小的片段更容易精确匹配
- 上下文控制:避免一次性输入过多内容
- 灵活检索:可以组合多个相关片段
步骤二:文本嵌入(Embedding)
概念解释
计算机无法直接理解文本,只能处理数字。嵌入(Embedding) 就是将文本转换为数字向量的技术。
什么是向量?
向量是一个有序的数字列表,例如:
"RabbitMQ 是消息队列" → [0.23, -0.15, 0.34, 0.12, -0.08, ...]
"Kafka 是流处理平台" → [0.18, -0.22, 0.41, 0.19, -0.05, ...]
这个向量有成百上千个数字,每个数字都代表了文本的某种语义特征。
为什么向量能表示语义?
神经网络通过在海量文本上训练,学会了将语义相似的文本映射到相似的向量空间位置:
实现代码
def embed(text, client):
"""将文本转换为向量"""
# return model.encode(text, convert_to_numpy=True)
"""调用 OpenAI Embedding API"""
response = client.embeddings.create(
model="BAAI/bge-large-zh-v1.5",
input=text
)
return np.array(response.data[0].embedding)
实践示例
# 加载预训练模型
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("BASE_URL")
client = OpenAI(api_key=api_key, base_url=base_url)
# 文本嵌入
text = "RabbitMQ 是一个消息队列系统"
vector = embed(text, client)
print(f"文本: {text}")
print(f"向量维度: {vector.shape}") # (1024,)
print(f"向量前5个值: {vector[:5]}")
# 输出: [ 0.02450614 -0.00108401 -0.00753182 0.03007796 -0.00668125]
步骤三:计算相似度
概念解释
有了向量后,如何判断两个文本是否相似?答案是余弦相似度(Cosine Similarity)。
余弦相似度公式
Cosine Similarity = A ⋅ B ∥ A ∥ × ∥ B ∥ \text{Cosine Similarity} = \frac{A \cdot B}{\|A\| \times \|B\|} Cosine Similarity=∥A∥×∥B∥A⋅B
其中:
- A ⋅ B A \cdot B A⋅B 是两个向量的点积
- ∥ A ∥ \|A\| ∥A∥ 是向量 A 的范数(长度)
- ∥ B ∥ \|B\| ∥B∥ 是向量 B 的范数
相似度值的含义
| 相似度 | 含义 |
|---|---|
| 1.0 | 完全相同 |
| 0.7-1.0 | 高度相关 |
| 0.4-0.7 | 中度相关 |
| 0.1-0.4 | 低度相关 |
| 0.0 或负值 | 基本无关 |
实现代码
import numpy as np
def cosine_similarity(a, b):
"""
计算两个向量的余弦相似度
Args:
a: 向量 A
b: 向量 B
Returns:
float: 相似度分数,范围 [-1, 1]
"""
# 计算点积
dot_product = np.dot(a, b)
# 计算向量范数
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
# 计算余弦相似度
return dot_product / (norm_a * norm_b)
实践示例
# 示例向量
v1 = np.array([0.9, 0.2]) # "消息队列"
v2 = np.array([0.85, 0.25]) # "异步通信"
v3 = np.array([0.2, 0.8]) # "流处理"
# 计算相似度
sim_12 = cosine_similarity(v1, v2) # 0.9989(高度相关)
sim_13 = cosine_similarity(v1, v3) # 0.5664(中度相关)
print(f"消息队列 vs 异步通信: {sim_12:.4f}")
print(f"消息队列 vs 流处理: {sim_13:.4f}")
步骤四:向量搜索
概念解释
向量搜索的目标是:在知识库中找到与用户问题最相关的文档。
搜索流程:
实现代码
def search(query_vector, doc_vectors, docs, top_k=1):
"""
搜索最相关的文档
Args:
query_vector: 问题的向量表示
doc_vectors: 文档向量列表
docs: 文档文本列表
top_k: 返回最相关的前 k 个文档
Returns:
list: 最相关的文档列表
"""
# 计算问题与每个文档的相似度
similarities = [cosine_similarity(query_vector, v) for v in doc_vectors]
# 获取相似度最高的 top_k 个文档索引
# argsort 按升序排序,所以取后 top_k 个并反转
top_indices = np.argsort(similarities)[-top_k:][::-1]
# 返回对应的文档
return [docs[i] for i in top_indices]
实践示例
# 假设有 3 个文档及其向量
docs = [
"RabbitMQ 是消息队列系统",
"Kafka 是流处理平台",
"RAG 是检索增强技术"
]
doc_vectors = [embed(doc, model) for doc in docs]
# 用户问题
question = "什么是消息队列?"
query_vector = embed(question, model)
# 搜索最相关的文档
top_docs = search(query_vector, doc_vectors, docs, top_k=2)
print("最相关的文档:")
for i, doc in enumerate(top_docs, 1):
print(f"{i}. {doc}")
# 输出:
# 最相关的文档:
# 1. RabbitMQ 是消息队列系统
# 2. Kafka 是流处理平台
Top-K 的选择
| Top-K 值 | 适用场景 |
|---|---|
| 1 | 简单事实性问题 |
| 2-3 | 一般问答场景 |
| 5+ | 复杂分析类问题 |
本 Demo 使用 top_k=2,平衡了相关性和上下文完整性。
步骤五:构造上下文
概念解释
上下文(Context)是传递给 LLM 的文档信息,它告诉模型"请基于这些信息回答问题"。
实现代码
# 检索到的文档
top_docs = search(query_vector, doc_vectors, docs, top_k=2)
# 构造上下文
context = "\n\n".join([
f"Document {i+1}: {doc}"
for i, doc in enumerate(top_docs)
])
实践示例
# 检索到的文档
top_docs = [
"RabbitMQ 是一个消息队列系统。",
"它用于系统之间的异步通信。"
]
# 构造的上下文
context = """Document 1: RabbitMQ 是一个消息队列系统。
Document 2: 它用于系统之间的异步通信。"""
提示词咋写才好
好的提示词要做到:
- 把文档分清楚(Document 1, Document 2…)
- 别太长,不然 AI 看不完
- 格式清晰,AI 好理解
第六步:让 AI 生成答案
找到相关文档后,让大模型(LLM)根据这些资料生成答案。关键是提示词要写好。
代码示例
import os
from openai import OpenAI
# 初始化 OpenAI 客户端
api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=api_key)
# 让 AI 生成答案
response = client.chat.completions.create(
model="deepseek-ai/DeepSeek-V3",
messages=[
{
"role": "system",
"content": """你是一个基于知识库的问答助手。
回答规则:
1. 只能根据提供的上下文回答问题
2. 如果上下文中没有答案,请回答:无法从提供的资料中找到答案
3. 不要编造事实
4. 回答尽量简洁清晰
"""
},
{
"role": "user",
"content": f"""知识库内容:
{context}
用户问题:
{question}
请根据知识库内容回答问题。"""
}
],
temperature=0.2,
)
answer = response.choices[0].message.content
提示词是啥
系统提示词(System Prompt)
你是一个基于知识库的问答助手。
回答规则:
1. 只能根据提供的上下文回答问题
2. 如果上下文中没有答案,请回答:无法从提供的资料中找到答案
3. 不要编造事实
4. 回答尽量简洁清晰
这告诉 AI:
- 你的角色是啥
- 要根据给的资料回答
- 别瞎编
用户提示词(User Prompt)
知识库内容:
{context}
用户问题:
{question}
请根据知识库内容回答问题。
关键是:
- Context 部分要清楚
- Question 部分要明确
- 强调"根据资料回答"
Temperature 是个啥
| 值大小 | 效果 | 啥时候用 |
|---|---|---|
| 0.0-0.3 | 稳定,不变来变去 | 问答类 |
| 0.4-0.6 | 刚好 | 一般对话 |
| 0.7-1.0+ | 随机,爱发挥 | 写东西 |
咱们用 0.2,因为:
- RAG 要按事实回答
- 不想让 AI 乱发挥
- 答案要稳定点
完整代码解析
主程序流程
"""
极简 RAG Demo - 5分钟理解RAG核心流程
"""
import os
import numpy as np
from openai import OpenAI
def load_knowledge(filepath):
"""读取知识库文件"""
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 按空行切分成段落
chunks = [chunk.strip() for chunk in content.split('\n\n') if chunk.strip()]
return chunks
def embed(text, client):
"""将文本转换为向量"""
# return model.encode(text, convert_to_numpy=True)
"""调用 OpenAI Embedding API"""
response = client.embeddings.create(
model="BAAI/bge-large-zh-v1.5",
input=text
)
return np.array(response.data[0].embedding)
def cosine_similarity(a, b):
"""计算余弦相似度: (A·B) / (||A|| * ||B||)"""
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def search(query_vector, doc_vectors, docs, top_k=1):
"""搜索最相关的文档"""
similarities = [cosine_similarity(query_vector, v) for v in doc_vectors]
# 获取相似度最高的 top_k 个文档索引
top_indices = np.argsort(similarities)[-top_k:][::-1]
return [docs[i] for i in top_indices]
def main():
print("=" * 60)
print("极简 RAG Demo - 检索增强生成")
print("=" * 60)
# 初始化 环境变量
print("\n[1/6] 初始化 环境变量...")
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("BASE_URL")
if not api_key:
print("警告: 未设置 OPENAI_API_KEY 环境变量")
return
if not base_url:
print("警告: 未设置 BASE_URL 环境变量")
return
print("配置初始化完成")
# 初始化 LLM 客户端
print("\n[2/6] 初始化 LLM 客户端...")
client = OpenAI(api_key=api_key, base_url=base_url)
print("LLM 客户端初始化完成")
# 读取知识库
print("\n[3/6] 读取知识库...")
docs_path = os.path.join(os.path.dirname(__file__), "docs", "knowledge.txt")
docs = load_knowledge(docs_path)
print(f"已加载 {len(docs)} 个文档块")
# 文档向量化
print("\n[4/6] 文档向量化...")
doc_vectors = [embed(doc, client) for doc in docs]
print(f"已生成 {len(doc_vectors)} 个向量,维度: {doc_vectors[0].shape[0]}")
# 用户输入问题
print("\n[5/6] 等待用户输入...")
print("=" * 60)
question = input("\n请输入你的问题(输入 'q' 退出): ")
if question.lower() == 'q':
print("\n再见!")
return
# 问题向量化
print("\n[6/6] 执行 RAG 查询...")
query_vector = embed(question, client)
# 向量相似度搜索
top_docs = search(query_vector, doc_vectors, docs, top_k=2)
# 构造上下文
context = "\n\n".join([f"Document {i+1}: {doc}" for i, doc in enumerate(top_docs)])
# 调用 LLM 生成答案
print("\n检索到的文档:")
print("-" * 60)
for i, doc in enumerate(top_docs, 1):
print(f"\n[{i}] {doc}")
print("\n" + "-" * 60)
print("正在生成答案...\n")
response = client.chat.completions.create(
model="deepseek-ai/DeepSeek-V3",
messages=[
{
"role": "system",
"content": """你是一个基于知识库的问答助手。
回答规则:
1. 只能根据提供的上下文回答问题
2. 如果上下文中没有答案,请回答:无法从提供的资料中找到答案
3. 不要编造事实
4. 回答尽量简洁清晰
"""
},
{
"role": "user",
"content": f"""知识库内容:
{context}
用户问题:
{question}
请根据知识库内容回答问题。"""
}
],
temperature=0.2,
)
answer = response.choices[0].message.content
# 打印答案
print("-" * 60)
print("生成的答案:")
print("-" * 60)
print(f"\n{answer}")
print("\n" + "=" * 60)
if __name__ == "__main__":
main()
运行示例和结果展示
安装依赖
pip install -r requirements.txt
设置 API Key
# Windows (PowerShell)
$env:OPENAI_API_KEY='sk-your-api-key-here'
$env:BASE_URL='your-base-url-here'
# Linux / Mac
export OPENAI_API_KEY='sk-your-api-key-here'
export BASE_URL='your-base-url-here'
运行程序
python rag_demo.py
运行示例 :询问 RAG
============================================================
极简 RAG Demo - 检索增强生成
============================================================
[1/6] 初始化 环境变量...
配置初始化完成
[2/6] 初始化 LLM 客户端...
LLM 客户端初始化完成
[3/6] 读取知识库...
已加载 3 个文档块
[4/6] 文档向量化...
已生成 3 个向量,维度: 1024
[5/6] 等待用户输入...
============================================================
请输入你的问题(输入 'q' 退出): 什么是RAG
[6/6] 执行 RAG 查询...
检索到的文档:
------------------------------------------------------------
[1] RAG 是检索增强生成技术。
它结合信息检索和大语言模型生成。
RAG 可以减少模型幻觉。
RAG 提供更准确的答案。
[2] RabbitMQ 是一个消息队列系统。
它用于系统之间的异步通信。
RabbitMQ 支持 AMQP 协议。
RabbitMQ 支持多种消息模型,包括点对点和发布订阅。
RabbitMQ 提供消息持久化和确认机制。
------------------------------------------------------------
正在生成答案...
------------------------------------------------------------
生成的答案:
------------------------------------------------------------
RAG 是检索增强生成技术,它结合信息检索和大语言模型生成,可以减少模型幻觉并提供更准确的答案。
============================================================
核心概念总结
通过这个 Demo,我们掌握了 RAG 的核心概念:
1. 文本嵌入(Embedding)
| 概念 | 说明 |
|---|---|
| 定义 | 将文本转换为数字向量 |
| 目的 | 让计算机"理解"文本语义 |
| 模型 | BAAI/bge-large-zh-v1.5 |
| 维度 | 1024 |
2. 余弦相似度
| 概念 | 说明 |
|---|---|
| 定义 | 衡量两个向量相似度的方法 |
| 公式 | (A·B) / ( |
| 范围 | [-1, 1] |
| 应用 | 文档相似度搜索 |
3. 向量搜索
| 概念 | 说明 |
|---|---|
| 目的 | 找到最相关的文档 |
| 方法 | 计算相似度 → 排序 → 取Top-K |
| 输出 | 相似度分数排序的文档列表 |
4. 上下文构造
| 概念 | 说明 |
|---|---|
| 目的 | 为 LLM 提供准确信息 |
| 格式 | 清晰标注文档边界 |
| 限制 | 避免过长超出模型输入限制 |
5. LLM 生成
| 概念 | 说明 |
|---|---|
| 模型 | deepseek-ai/DeepSeek-V3 |
| Temperature | 0.2(确定性) |
| 输入 | 上下文 + 问题 |
| 输出 | 基于事实的答案 |
小结
本篇要点回顾
通过这篇文章,我们实现了:
| 步骤 | 实现内容 | 关键技术 |
|---|---|---|
| 1 | 读取知识库 | 文件 I/O |
| 2 | 文本切分 | 按空行分割 |
| 3 | 文本嵌入 | BAAI/bge-large-zh-v1.5 |
| 4 | 相似度计算 | 余弦相似度 |
| 5 | 向量搜索 | Top-K 检索 |
| 6 | 上下文构造 | 文档拼接 |
| 7 | LLM 生成 | OpenAI API |
核心收获
理解了 RAG 的完整流程:从问题到答案的 7 个步骤
掌握了文本嵌入原理:如何将文本转为向量
学会了相似度搜索:如何找到最相关文档
实现了可运行的 Demo:130 行代码理解 RAG
RAG 的优势
| 优势 | 说明 |
|---|---|
| 减少幻觉 | 基于事实回答,不编造信息 |
| 知识更新 | 无需重新训练,更新知识库即可 |
| 可追溯 | 可以查看答案来源 |
| 私有数据 | 可以使用企业内部文档 |
结语
通过这篇 5 分钟的教程,你应该不仅理解了 RAG 的核心原理,还亲手实现了一个可运行的系统。
记住:
- RAG 的核心是"检索 + 生成"
- 向量是连接文本和语义的桥梁
- 好的上下文是准确回答的基础
在接下来的文章中,我们将深入探索文档向量化系统和更完整的 RAG 问答系统。
参考文章:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)