RAG 介绍
定义
RAG(Retrieval-Augmented Generation)的核心是将模型内部学到的“参数化知识”(模型权重中固化的、模糊的“记忆”),与来自外部知识库的“非参数化知识”(精准、可随时更新的外部数据)相结合。其运作逻辑就是在 LLM 生成文本前,先通过检索机制从外部知识库中动态获取相关信息。
技术原理
检索阶段:寻找“非参数化知识”
- 知识向量化:嵌入模型(Embedding Model) 将 外部知识库编码 为 向量索引(Index),存入向量数据库。
- 语义召回:当用户发起查询时,检索模块利用同样的嵌入模型将问题向量化,并通过相似度搜索(Similarity Search) ,从海量数据中精准锁定与问题最相关的文档片段。
生成阶段:融合两种知识
- 上下文整合:生成模块接收检索阶段送来的相关文档片段以及用户的原始问题。
- 指令引导生成:该模块会遵循预设的 Prompt 指令,将上下文与问题有效整合,并引导 LLM进行可控的、有理有据的文本生成。
技术分类
初级 RAG
- 离线(数据预处理):索引
- 在线(用户发起请求后):检索 -> 生成
基础线性流程,基础向量检索,效果不稳定,难以优化。
中级 RAG
- 离线:索引
- 在线:... -> 检索前 -> ... -> 检索后 -> ...
增加检索前后的优化步骤,查询重写(Query Rewrite) ,结果重排(Rerank),流程相对固定,优化点有限。
高级 RAG
积木式可编排流程,模块化、可组合、可动态调整,动态路由(Routing),查询转换(Query Transformation),多路融合(Fusion),系统复杂性高
为什么要使用 RAG?
提示词工程 RAG 微调
- 先尝试提示工程:通过精心设计提示词来引导模型,适用于任务简单、模型已有相关知识的场景。
- 再选择 RAG:如果模型缺乏特定或实时知识而无法回答,则使用 RAG,通过外挂知识库为其提供上下文信息。
- 最后考虑微调:当目标是改变模型“如何做”(行为/风格/格式)而不是“知道什么”(知识)时,微调是最终且最合适的选择。例如,让模型学会严格遵循某种独特的输出格式、模仿特定人物的对话风格,或者将极其复杂的指令“蒸馏”进模型权重中。
RAG的优点:
RAG 的出现填补了通用模型与专业领域之间的鸿沟。
- 准确性与可信度的双重提升:RAG 最核心的价值在于突破了模型预训练知识的限制。它不仅能补充专业领域的知识盲区,还能通过提供具体的参考材料,有效抑制“一本正经胡说八道”的幻觉现象。
- 时效性保障:RAG 允许知识库独立于模型进行动态更新——新政策或新数据一旦入库,立刻就能被检索到。
- 显著的综合成本效益:RAG 是一种高性价比的方案。它避免了高频微调带来的巨额算力成本; 有了外部知识的强力辅助,我们在处理特定领域问题时,往往可以使用参数量更小的基础模型来达到类似的效果,从而直接降低了推理成本。
- 灵活的模块化可扩展性:RAG 的架构具备极强的包容性,支持多源集成,无论是 PDF、Word 还是网页数据,都能统一构建进知识库中。模块化设计实现了检索与生成的解耦。
使用RAG
工具
开发模式
- 成熟框架:LangChain 或 LlamaIndex
- 原生开发:不依赖现成的框架
记忆载体(向量数据库)
- 大规模数据:Milvus、Pinecone
- 轻量级:FAISS、Chroma
- 自动化评估工具:RAGAS、TruLens
构建最小可行系统(MVP)
- 数据准备与清洗:将 PDF、Word 等多源异构数据标准化,并采用合理的分块策略。
- 索引构建:将切分好的文本通过嵌入模型转化为向量,并存入数据库。在此阶段关联元数据(如来源、页码),这对后续的精确引用很有帮助。
- 检索策略优化:采用混合检索(向量+关键词)等方式来提升召回率,并引入重排序模型对检索结果进行二次精选,确保 LLM 看到的都是精华。
- 生成与提示工程:最后,设计一套清晰的 Prompt 模板,引导 LLM 基于检索到的上下文回答用户问题,并明确要求模型“不知道就说不知道”,防止幻觉。
进阶
评估维度与挑战
通常会从几个维度进行量化评估一套 RAG 系统的好坏:
- 检索相关性:找到的内容是否包含答案
- 语义准确性:回答的意思是否正确
- 词汇匹配度:专业术语是否使用得当
优化方向与架构演进
性能层面:
- 索引分层:对高频数据启用缓存
- 多模态扩展:支持图像/表格检索
架构层面:
- 分支模式:并行处理多路检索
- 循环模式:进行自我修正
使用 langchain 四步构建 RAG
四步构建最小可行系统分别是数据准备、索引构建、检索优化和生成集成。下面将围绕这四个方面来实现一个基于 LangChain 框架的 RAG 应用。
设置初始化
导入必要的库、加载环境变量以及下载嵌入模型。
import os
# os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
from dotenv import load_dotenv
# TextLoader:加载器,读取文件
from langchain_community.document_loaders import TextLoader
# RecursiveCharacterTextSplitter:切分器,会把几万字的文档切成一小块一小块
from langchain_text_splitters import RecursiveCharacterTextSplitter
# HuggingFaceEmbeddings:嵌入模型,把文本变成向量
from langchain_huggingface import HuggingFaceEmbeddings
# InMemoryVectorStore :向量数据库,存储这些向量的地方
from langchain_core.vectorstores import InMemoryVectorStore
# ChatPromptTemplate` :提示词模版: 它是你跟 AI 沟通的剧本
from langchain_core.prompts import ChatPromptTemplate
# ChatOpenAI:语言模型驱动)调用 DeepSeek 的模型作为底层推理工具
from langchain_deepseek import ChatOpenAI
# 加载环境变量
# 读取项目根目录下的 `.env` 文件
# API Key(比如 DeepSeek 或 OpenAI 的密钥)通常写在里面
load_dotenv()
数据准备
- 加载原始文档:定义文件的路径,使用
TextLoader加载该文件。 - 文本分块 (Chunking):长文档被分割成较小的、可管理的文本块(chunks),便于后续的嵌入和检索。
- 默认分隔符与语义保留:使用一系列预设的分隔符
["\n\n" (段落), "\n" (行), " " (空格), "" (字符)]来递归分割文本,尽可能保持段落、句子和单词的完整性。 - 保留分隔符:
keep_separator=True,分隔符本身会被保留在分割后的文本块中。 - 默认块大小与重叠: 默认参数 chunk_size=4000(块大小)和 chunk_overlap=200(块重叠),确保文本块符合预定的大小限制,通过重叠来减少上下文信息的丢失。
- 默认分隔符与语义保留:使用一系列预设的分隔符
markdown_path = "../../data/C1/markdown/easy-rl-chapter1.md"
loader = TextLoader(markdown_path)
# 返回 list[Document]
# Document包含page_content(文件的实际文字内容)和metadata(文件的元数据,比如文件路径、文件名、页码等)
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter()
# 依然返回list[Document],但现在的 Document 变得很短
texts = text_splitter.split_documents(docs)
索引构建
- 初始化中文嵌入模型:
embeddings = HuggingFaceEmbeddings(
# 选择中文 Embedding 模型
model_name="BAAI/bge-small-zh-v1.5",
# 告诉电脑使用哪个设备运行计算,例如cpu、cuda
model_kwargs={'device': 'cpu'},
# 将生成的向量进行归一化处理,计算两个向量的相似度(余弦相似度)会变得极其简单和快速
encode_kwargs={'normalize_embeddings': True})
- 构建向量存储: 将分割后的文本块 (
texts) 通过初始化好的嵌入模型转换为向量表示使用InMemoryVectorStore将这些向量及其对应的原始文本内容添加进去,从而在内存中构建出一个向量索引。
# 创建了一个运行在内存中的向量数据库
# 向量数据库能够进行“模糊的语义搜索”
vectorstore = InMemoryVectorStore(embeddings)
# 把chunks全部转换成向量,并存入数据库
vectorstore.add_documents(texts)
查询与检索
针对用户问题进行查询与检索:
- 定义用户查询: 设置一个具体的用户问题字符串。
- 在向量存储中查询相关文档: 使用向量存储的
similarity_search方法,根据用户问题,在索引中查找最相关的k(此处示例中k=3) 个文本块。 - 准备上下文: 将检索到的多个文本块的页面内容 (
doc.page_content) 合并成一个单一的字符串,形成最终的上下文信息 (docs_content) 供大语言模型参考。
question = "文中举了哪些例子?"
# 会用 HuggingFaceEmbeddings 把问题转换成一个向量
# 测量这个 问题向量 与 数据库中所有 chunk向量 的距离
# 把距离最近的 3个chunk 抽出来
retrieved_docs = vectorstore.similarity_search(question, k=3)
# LLM 只认识纯文本。所以需要把搜索到的chunks转变成字符串
# 双换行符通常代表段落的结束和新段落的开始,这种格式有助于LLM将每个块视为一个独立的上下文来源
docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs)
生成集成
将检索到的上下文与用户问题结合,利用大语言模型(LLM)生成答案。
- 构建提示词模板: 使用
ChatPromptTemplate.from_template创建一个结构化的提示模板。此模板指导LLM根据提供的上下文 (context) 回答用户的问题 (question),并明确指出在信息不足时应如何回应:
prompt = ChatPromptTemplate.from_template("""请根据下面提供的上下文信息来回答问题。
请确保你的回答完全基于这些上下文。
如果上下文中没有足够的信息来回答问题,请直接告知:“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。”
上下文:
{context}
问题: {question}
回答:"""
- 配置大语言模型:初始化
ChatOpenAI客户端,配置所用模型(glm-4.7-flash-free)、生成答案的温度参数(temperature=0.7)、最大Token数 (max_tokens=2048) 以及API密钥(从环境变量加载)和 url。
llm = ChatOpenAI(
# 指定模型
model="glm-4.7-flash-free",
# temperature:控制模型回答的 发散性 / 创造力
temperature=0.7,
# max_tokens:限制模型这次回答最多能“吐”出多少个字/词元。
max_tokens=2048,
api_key=os.getenv("DEEPSEEK_API_KEY")
base_url="https://aihubmix.com/v1"
)
- 调用LLM生成答案并输出:
answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)