RAG 与文本向量化详解
本文详细介绍了 RAG(检索增强生成)技术的核心概念、文本向量化的原理、以及完整的 RAG 流程,包括文档加载、文本分割、向量存储、相似度检索和生成回答等环节。
目录
-
[Embedding 模型介绍](#Embedding 模型介绍)
-
[RAG 解决什么问题](#RAG 解决什么问题)
-
[RAG 完整流程](#RAG 完整流程)
-
[RAG 链式调用](#RAG 链式调用)
-
[LangChain 代码实战](#LangChain 代码实战)
1. 什么是文本向量化
1.1 概念定义
文本向量化(Text Embedding) 是将文本数据转换为数学向量(数值数组)的过程。在自然语言处理中,计算机无法直接理解人类文字,需要将文字转换成数学表示才能进行计算和分析。
文本 → [0.123, -0.456, 0.789, ..., 0.321] (向量)
1.2 为什么需要文本向量化
| 特性 | 说明 |
|---|---|
| 数学运算 | 文字无法直接做加减乘除,向量可以 |
| 语义相似 | 语义相近的文本,向量距离也相近 |
| 可计算 | 可以用余弦相似度、欧氏距离等衡量文本关系 |
| 机器学习 | 机器学习模型只能处理数值数据 |
1.3 Embedding 的维度
现代 Embedding 模型通常生成 768 维 、1024 维 、1536 维 等高维向量。维度越高,理论上能表达的语义信息越丰富,但计算成本也越高。
text-embedding-v2 (阿里云): 1536 维
tongyi-embedding-vision-plus: 1024 维
2. Embedding 模型介绍
2.1 什么是 Embedding 模型
Embedding 模型是一种专门训练用于将文本转换为向量的神经网络模型。它能够理解文本的语义含义,将语义相似的文本映射到向量空间中相近的位置。
2.2 主流 Embedding 模型
| 模型 | 提供商 | 维度 | 特点 |
|---|---|---|---|
| text-embedding-v2 | 阿里云 DashScope | 1536 | 中文效果好,稳定 |
| text-embedding-v3 | 阿里云 DashScope | 1536 | 最新版本,效果更好 |
| text-embedding-v4 | 阿里云 DashScope | - | 最新版本 |
| text-embedding-3-small | OpenAI | 1536 | 体积小,速度快 |
| text-embedding-3-large | OpenAI | 3072 | 效果最好 |
2.3 Embedding 模型的工作原理
输入文本: "今天天气很好"
↓
分词/标记化
↓
神经网络编码
↓
输出向量: [0.123, -0.456, 0.789, ..., 0.321] (1536维)
2.4 在 LangChain 中使用
from langchain_community.embeddings import DashScopeEmbeddings
# 初始化 Embedding 模型
embeddings = DashScopeEmbeddings(
model="text-embedding-v2", # 或 text-embedding-v3
dashscope_api_key=os.getenv("aliQwen-api")
)
# 单文本向量化
vector = embeddings.embed_query("要转换的文本")
print(f"向量维度: {len(vector)}")
print(f"向量前5个值: {vector[:5]}")
3. 余弦相似度
3.1 什么是余弦相似度
余弦相似度(Cosine Similarity) 是衡量两个向量相似程度的指标,取值范围是 [-1, 1]。值越接近 1,表示两个向量越相似;值越接近 -1,表示两个向量越相反;值接近 0,表示两个向量无关联。
3.2 计算公式
Cosine Similarity = (A · B) / (|A| × |B|)
其中:
-
A · B:向量 A 和 B 的点积(各分量相乘后求和)
-
|A|:向量 A 的模(长度)
-
|B|:向量 B 的模(长度)
3.3 图解说明
向量A: [1, 1] ────────────→ 角度 θ = 0°,相似度 = 1.0(完全相同)
向量B: [1, 1] ────────────↗
向量A: [1, 1] ────────────→ 角度 θ = 90°,相似度 = 0.0(正交无关)
向量B: [0, 1] ────────────↑
向量A: [1, 1] ────────────→ 角度 θ = 180°,相似度 = -1.0(完全相反)
向量B: [-1, -1] ←──────────┘
3.4 余弦相似度与向量距离
余弦相似度 = 1 - 余弦距离
向量距离越小 → 相似度越高 → 语义越相近
4. 向量数据库
4.1 什么是向量数据库
向量数据库(Vector Database) 是专门用于存储和检索向量数据的数据库系统。它能够在大规模向量集合中快速找到与查询向量最相似的结果。
4.2 常见向量数据库
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| FAISS | Facebook 开源,本地文件存储,轻量 | 小规模数据、快速原型 |
| Redis | 内存数据库,支持向量检索 | 生产环境,高并发 |
| Milvus | 专门向量数据库,分布式 | 大规模向量检索 |
| Pinecone | 云服务,完全托管 | 不想维护基础设施 |
| Chroma | 轻量级,面向 LLM | 简单应用,本地开发 |
4.3 FAISS 本地向量存储
from langchain_community.vectorstores import FAISS
# 创建向量存储
vector_store = FAISS.from_documents(
documents=texts, # 分割后的文档列表
embedding=embeddings # Embedding 模型
)
# 保存到本地文件
vector_store.save_local("faiss_index")
# 从本地文件加载(下次使用)
vector_store = FAISS.load_local(
"faiss_index",
embeddings,
allow_dangerous_deserialization=True
)
4.4 FAISS 存储的文件结构
faiss_index/
├── index.faiss # 向量索引文件(存储数学向量)
└── index.pkl # 元数据文件(存储原始文本和对应关系)
| 文件 | 作用 |
|---|---|
index.faiss |
存储所有文档的向量表示 |
index.pkl |
存储原始文档内容、metadata,以及向量与文本的映射关系 |
5. RAG 解决什么问题
5.1 大语言模型的局限性
| 问题 | 说明 | 示例 |
|---|---|---|
| 知识截止日期 | 模型训练数据有时间限制 | GPT-4 不知道 2023 年后的新闻 |
| 幻觉问题 | 模型可能生成看似合理但错误的内容 | 把不存在的书名写进答案 |
| 专业知识缺乏 | 垂直领域知识不足 | 医疗、法律、金融等专业问题 |
| 信息更新 | 无法及时获取最新信息 | 股价、天气、赛事结果 |
5.2 RAG 的解决思路
RAG = Retrieval(检索)+ Augmented(增强)+ Generation(生成)
用户问题 → 检索相关文档 → 将文档作为上下文 → 大模型生成答案
5.3 RAG vs 无 RAG 对比
无 RAG(直接提问):
用户: "我们公司 Q3 利润是多少?"
模型: "作为 AI,我无法访问您公司的内部数据..."
有 RAG(检索增强):
用户: "我们公司 Q3 利润是多少?"
↓
检索: 找到《公司Q3财报.pdf》中相关段落
↓
注入: 将财报数据作为上下文提供给模型
↓
生成: "根据公司Q3财报,贵公司Q3净利润为XXX万元,同比增长XX%"
5.4 RAG 的优势
-
实时性:可以访问最新文档和数据
-
可解释性:答案有文档支撑,不是凭空生成
-
成本低:无需微调模型即可注入新知识
-
灵活性:可以随时更新知识库
-
安全性:敏感数据可私有化部署
6. RAG 完整流程
6.1 流程概览
┌─────────────────────────────────────────────────────────────────┐
│ RAG 工作流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 【离线阶段 - 知识库构建】 │
│ ┌──────────┐ ┌────────────┐ ┌─────────────┐ │
│ │ 文档加载 │ → │ 文本分割 │ → │ 向量嵌入 │ │
│ └──────────┘ └────────────┘ └─────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────┐ │
│ │ 向量数据库 │ │
│ └─────────────┘ │
│ │
│ 【在线阶段 - 问答查询】 │
│ ┌──────────┐ ┌────────────┐ ┌─────────────┐ ┌─────────┐ │
│ │ 用户问题 │ → │ 向量检索 │ → │ 上下文组装 │→│ 大模型生成│ │
│ └──────────┘ └────────────┘ └─────────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 各环节详细说明
环节 1: 文档加载(Document Loading)
将各种格式的文档(PDF、Word、TXT、HTML、Markdown 等)读取为统一格式。
环节 2: 文本分割(Text Splitting)
将长文档切割成适合检索的小块(chunks),控制每块的长度和重叠度。
环节 3: 向量化(Embedding)
使用 Embedding 模型将每个文本块转换为向量表示。
环节 4: 向量存储(Vector Storage)
将向量和原始文本存入向量数据库,建立索引以便快速检索。
环节 5: 相似度检索(Similarity Search)
将用户问题向量化,在向量数据库中查找最相似的 K 个文档块。
环节 6: 组装上下文(Context Assembly)
将检索到的文档块组装成提示词(Prompt)的一部分。
环节 7: 大模型生成(Generation)
将用户问题和组装好的上下文一起发送给大模型,生成最终答案。
7. 文档加载器
7.1 LangChain 文档加载器一览
| 加载器 | 支持格式 | 说明 |
|---|---|---|
TextLoader |
.txt | 纯文本文件 |
PyPDFLoader |
PDF 文档 | |
Docx2txtLoader |
.docx | Word 文档 |
UnstructuredMarkdownLoader |
.md | Markdown 文件 |
CSVLoader |
.csv | CSV 表格文件 |
JSONLoader |
.json | JSON 文件 |
PyMuPDFLoader |
更高质量的 PDF 解析 |
7.2 Document 对象结构
加载后的文档是 Document 对象:
class Document:
page_content: str # 文档内容(文本)
metadata: dict # 元数据(来源文件、页码等)
# 示例
doc = Document(
page_content="这是文档的正文内容...",
metadata={"source": "report.pdf", "page": 1}
)
8. 文本分割器
8.1 为什么需要文本分割
| 问题 | 说明 |
|---|---|
| 向量长度限制 | Embedding 模型有最大 token 限制 |
| 语义完整性 | 太小会丢失上下文,太大会有噪声 |
| 检索精度 | 精准的小块更容易找到相关内容 |
8.2 常用文本分割器
| 分割器 | 说明 | 适用场景 |
|---|---|---|
CharacterTextSplitter |
按字符数分割 | 简单场景 |
RecursiveCharacterTextSplitter |
按字符递归分割,保持语义完整 | 通用场景 |
TokenTextSplitter |
按 token 数分割 | 精确控制 token 数量 |
MarkdownTextSplitter |
按 Markdown 标题结构分割 | Markdown 文档 |
PythonCodeTextSplitter |
按 Python 代码结构分割 | 代码文件 |
8.3 关键参数
RecursiveCharacterTextSplitter(
chunk_size=100, # 每个块的最大字符数
chunk_overlap=30, # 块之间的重叠字符数
length_function=len # 计算文本长度的函数
)
8.4 chunk_size 和 chunk_overlap 的关系
原始文本: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" (26个字符)
chunk_size=10, chunk_overlap=5
分割结果:
块1: "ABCDEFGHIJ" (位置 0-9)
块2: "FGHIJKLMNOP" (位置 5-14) ← 与块1重叠 FGHIJ
块3: "LMNOPQRSTUV" (位置 10-19) ← 与块2重叠 LMNOP
块4: "QRSTUVWXYZ" (位置 15-24) ← 与块3重叠 QRSTUV
重叠的作用 :确保边界附近的上下文不会丢失。比如 "什么是机器学习" 可能跨两个块,有重叠就能保留完整语义。
8.5 代码示例
方式一:split_documents(直接分割 Document 对象):
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=30,
length_function=len
)
# 直接分割 Document 对象列表
documents = loader.load() # 加载文档
splitter_documents = text_splitter.split_documents(documents)
print(f"分割后的文档数量: {len(splitter_documents)}")
方式二:split_text + create_documents(先分割文本,再转 Document):
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=30,
length_function=len
)
# 先分割文本
texts = text_splitter.split_text(long_text)
# 再转换为 Document 对象
documents = text_splitter.create_documents(texts)
8.6 分割策略建议
| 场景 | chunk_size | chunk_overlap | 说明 |
|---|---|---|---|
| 问答系统 | 500-1000 | 50-100 | 答案通常在几句话内 |
| 对话总结 | 1000-2000 | 100-200 | 需要更多上下文 |
| 论文检索 | 500 | 50 | 精确检索段落 |
| 代码检索 | 200-500 | 20-50 | 按函数/类分割 |
9. 向量存储
9.1 创建向量存储
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import FAISS
# 1. 初始化 Embedding 模型
embeddings = DashScopeEmbeddings(
model="text-embedding-v2",
dashscope_api_key=os.getenv("aliQwen-api")
)
# 2. 创建向量存储
vector_store = FAISS.from_documents(
documents=splitter_documents, # 分割后的文档列表
embedding=embeddings # Embedding 模型
)
9.2 保存和加载
# 保存到本地
vector_store.save_local("faiss_index")
# 从本地加载
vector_store = FAISS.load_local(
"faiss_index",
embeddings,
allow_dangerous_deserialization=True # 允许反序列化 pickle 文件
)
9.3 添加新文档
# 添加单个文档
vector_store.add_documents(new_documents)
# 添加后重新保存
vector_store.save_local("faiss_index")
9.4 向量数据库对比
| 特性 | FAISS | Redis | Milvus |
|---|---|---|---|
| 存储位置 | 本地文件 | 内存/磁盘 | 分布式集群 |
| 部署难度 | ⭐ 简单 | ⭐⭐ 中等 | ⭐⭐⭐ 复杂 |
| 查询速度 | 快 | 极快 | 极快 |
| 数据规模 | 百万级 | 亿级 | 十亿级 |
| 成本 | 免费 | 需要 Redis | 需要服务器 |
10. 相似度检索
10.1 基础检索
# 相似度搜索(返回文档列表)
results = vector_store.similarity_search(
query="用户问题",
k=3 # 返回最相似的 3 个文档
)
for doc in results:
print(doc.page_content)
print(doc.metadata)
10.2 带相似度分数的检索
# 返回 (文档, 距离/分数) 元组
results = vector_store.similarity_search_with_score(
query="用户问题",
k=3
)
for doc, score in results:
similarity = 1 - score # 距离转相似度
print(f"相似度: {similarity:.4f}")
print(f"内容: {doc.page_content}")
10.3 As Retriever(转为检索器)
# 转换为 Retriever 对象,可用于 RAG 链
retriever = vector_store.as_retriever(
search_kwargs={"k": 2} # 指定返回数量
)
# 在链中使用
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
)
10.4 相似度分数解读
| 分数范围 (距离) | 相似度 (1-距离) | 语义关系 |
|---|---|---|
| 0.0 - 0.1 | 0.9 - 1.0 | 非常相似,几乎相同 |
| 0.1 - 0.3 | 0.7 - 0.9 | 比较相似 |
| 0.3 - 0.5 | 0.5 - 0.7 | 部分相似 |
| 0.5 - 0.7 | 0.3 - 0.5 | 有一定关联 |
| 0.7+ | 0.3 以下 | 不太相关 |
11. RAG 链式调用
11.1 LangChain LCEL 语法
LangChain 表达式语言(LangChain Expression Language)允许用 | 运算符组合多个组件:
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
)
等价于:
def rag_chain(question):
context = retriever.invoke(question) # 1. 检索相关文档
prompt_text = prompt.format(context=context, question=question) # 2. 组装 Prompt
response = llm.invoke(prompt_text) # 3. 大模型生成
return response
11.2 完整 RAG 链
from langchain.chat_models import init_chat_model
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
# 1. 初始化大模型
llm = init_chat_model(
model="qwen-plus",
model_provider="openai", # 这里只是调用方式,不是真正的 OpenAI
api_key=os.getenv("aliQwen-api"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 2. 定义 Prompt 模板
prompt_template = """
请根据以下提供的文本内容来回答问题。
仅使用提供的文本信息,如果文本中没有相关信息,请回答"抱歉,提供的文本中没有这个信息"。
文本内容:
{context}
问题:{question}
回答:
"""
prompt = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# 3. 构建 RAG 链
rag_chain = (
{
"context": retriever,
"question": RunnablePassthrough()
}
| prompt
| llm
)
# 4. 提问
question = "什么是机器学习?"
result = rag_chain.invoke(question)
print(result.content)
11.3 内部执行流程
用户: "什么是机器学习?"
│
▼
┌──────────────────────────────────────────────┐
│ Step 1: Retriever (向量检索) │
│ - 将问题向量化 │
│ - 在 FAISS 中搜索相似文档 │
│ - 返回 top-k 相关文档 │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Step 2: Prompt 组装 │
│ - 将检索到的文档填入 {context} │
│ - 将用户问题填入 {question} │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Step 3: LLM 生成 │
│ - 将组装好的 Prompt 发送给大模型 │
│ - 返回生成的回答 │
└──────────────────────────────────────────────┘
│
▼
输出: "根据提供的文本,机器学习是..."
12. LangChain 代码实战
12.1 完整示例:RAG 问答系统
"""
RAG 完整流程示例:使用本地文档进行问答
"""
import os
from dotenv import load_dotenv
load_dotenv(encoding='utf-8')
from langchain_community.document_loaders import TextLoader
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain.chat_models import init_chat_model
# ========== 第一步:准备文档 ==========
loader = TextLoader("knowledge.txt", encoding="utf-8")
documents = loader.load()
# ========== 第二步:分割文档 ==========
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
length_function=len
)
texts = text_splitter.split_documents(documents)
print(f"原始文档数: {len(documents)}")
print(f"分割后文档数: {len(texts)}")
# ========== 第三步:向量化并存储 ==========
embeddings = DashScopeEmbeddings(
model="text-embedding-v2",
dashscope_api_key=os.getenv("aliQwen-api")
)
vector_store = FAISS.from_documents(
documents=texts,
embedding=embeddings
)
# 保存向量库
vector_store.save_local("faiss_index")
print("向量库已保存")
# ========== 第四步:构建 RAG 链 ==========
# 初始化大模型
llm = init_chat_model(
model="qwen-plus",
model_provider="openai",
api_key=os.getenv("aliQwen-api"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 定义 Prompt
prompt_template = """请根据以下文本内容回答问题。
如果文本中没有相关信息,请如实告知。
文本内容:
{context}
问题:{question}
回答:"""
prompt = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# 创建检索器
retriever = vector_store.as_retriever(search_kwargs={"k": 2})
# 构建 RAG 链
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
)
# ========== 第五步:问答 ==========
question = "你的问题是什么?"
result = rag_chain.invoke(question)
print(f"\n问题: {question}")
print(f"回答: {result.content}")
12.2 不同文档格式的加载方式
PDF 文档:
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("document.pdf", extraction_mode="plain")
documents = loader.load()
Word 文档:
from langchain_community.document_loaders import Docx2txtLoader
loader = Docx2txtLoader("document.docx")
documents = loader.load()
CSV 表格:
from langchain_community.document_loaders import CSVLoader
loader = CSVLoader("data.csv", source_column="content")
documents = loader.load()
JSON 文件:
from langchain_community.document_loaders import JSONLoader
loader = JSONLoader("data.json", jq_schema=".content", text_content=False)
documents = loader.load()
13. 常见问题与解决方案
13.1 依赖问题
| 问题 | 解决方案 |
|---|---|
ModuleNotFoundError: No module named 'langchain_unstructured' |
pip install langchain-unstructured |
ModuleNotFoundError: No module named 'unstructured' |
pip install unstructured |
ModuleNotFoundError: No module named 'faiss' |
pip install faiss-cpu 或 pip install faiss-gpu |
ModuleNotFoundError: No module named 'langchain_text_splitters' |
pip install langchain-text-splitters |
13.2 路径问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
FileNotFoundError: no such file |
相对路径找不到文件 | 使用绝对路径或 Path(__file__).parent |
| UNC 路径不支持 | WSL 调用 Windows CMD | 切换到 Windows 路径格式 |
正确处理路径:
from pathlib import Path
# 获取脚本所在目录
current_dir = Path(__file__).parent.absolute()
# 拼接文件路径
file_path = current_dir / "rag.txt"
# 使用
loader = TextLoader(file_path, encoding="utf-8")
13.3 Embedding 模型问题
| 问题 | 解决方案 |
|---|---|
| API Key 无效 | 检查环境变量 aliQwen-api 是否正确设置 |
| 模型不存在 | 使用 text-embedding-v2 或 text-embedding-v3(v4 可能不稳定) |
| 向量维度不匹配 | 确保 Embedding 模型版本一致 |
13.4 向量数据库问题
| 问题 | 解决方案 |
|---|---|
| Redis 连接失败 | 检查 Redis 服务是否启动,或改用 FAISS 本地存储 |
| FAISS 文件损坏 | 删除 faiss_index 目录,重新生成 |
| 加载文件报错 | allow_dangerous_deserialization=True 允许反序列化 |
13.5 检索效果优化
| 问题 | 优化方法 |
|---|---|
| 检索不到相关内容 | 降低 chunk_size,增加 k 值 |
| 返回内容噪声太多 | 提高 chunk_size,减少重叠 |
| 上下文丢失 | 增加 chunk_overlap |
| 回答不准确 | 优化 Prompt 模板,添加"只使用提供的文本"等指令 |
附录:关键概念速查表
| 概念 | 说明 |
|---|---|
| Embedding | 将文本转换为向量的模型 |
| Vector | 文本的数学向量表示 |
| Cosine Similarity | 余弦相似度,衡量向量间的相似程度 |
| Chunk | 分割后的文本小块 |
| Retriever | 从向量数据库检索相关文档的组件 |
| RAG | Retrieval Augmented Generation,检索增强生成 |
| LCEL | LangChain Expression Language,链式调用语法 |
| FAISS | Facebook AI Similarity Search,向量检索库 |
| Document | LangChain 中的文档对象,包含内容和元数据 |