RAG学习记录
前言:
为了快速掌控LLM中常见技术RAG,这里我参考datawhale的开源学习资料,进行相关知识的学习
【项目地址】https://github.com/datawhalechina/all-in-rag
Task1 简介与环境配置
任务一参考链接:https://github.com/datawhalechina/all-in-rag/blob/main/docs/chapter1/01_RAG_intro.md
一、RAG 核心定义
RAG(Retrieval-Augmented Generation)是一种融合信息检索与文本生成的技术范式。其核心逻辑是:在大型语言模型(LLM)生成文本前,先通过检索机制从外部知识库中动态获取相关信息,并将检索结果融入生成过程,从而提升输出的准确性和时效性。
当然RAG的定义会随着技术的发展而拓展,所以当前定义仅作为基本框架的确立。
💡 RAG本质:在LLM生成文本之前,先从外部知识库中检索相关信息,作为上下文辅助生成更准确的回答。
二、基础的流水线:
离线构建(Ingestion)
- 文档采集 → 清洗(去噪、去重) → 切片 chunking(如 200~500 tokens,带 10~ 20% overlap)
- 对每个 chunk 生成向量(Embedding 或 TF-IDF)→ 建索引(向量库 / 倒排索引)→ 存储(含元数据、来源)
在线检索(Retrieval)
- 解析用户 Query → 检索 top-k 相关 chunk(可以先粗检,再交叉编码器重排)
提示拼装(Prompt Compose)
- 把检索到的片段与来源链接、约束(风格、语气、是否必须引用等)拼成提示词
生成与引用(Generation & Grounded Answer)
- 调用 LLM 输出答案,最好带引用
- 可做答案压缩/反事实检查/过滤
接下来我将结合代码具体介绍其实现过程
1. 基于LangChain框架的RAG实现
完整代码如下:
import os
# hugging face镜像设置,如果国内环境无法使用启用该设置
# os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
from dotenv import load_dotenv
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_deepseek import ChatDeepSeek
DEEPSEEK_API_KEY = '你的api'
load_dotenv()
# 加载本地markdown文件
# 加载后以文本类型存储在docs中
markdown_path = "../../data/C1/markdown/easy-rl-chapter1.md"
loader = UnstructuredMarkdownLoader(markdown_path)
docs = loader.load()
# 文本分块
# 将存储文本按照固定大小划分chunks(保留句子完整性的前提下)
text_splitter = RecursiveCharacterTextSplitter()
chunks = text_splitter.split_documents(docs)
# 中文嵌入模型
# 嵌入模型作用在于将文本句子转为固定长度的embedding向量
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
# 构建向量存储
# 将每个chunks也转化为固定长度的向量
vectorstore = InMemoryVectorStore(embeddings)
vectorstore.add_documents(chunks)
# 提示词模板
prompt = ChatPromptTemplate.from_template("""请根据下面提供的上下文信息来回答问题。
请确保你的回答完全基于这些上下文。
如果上下文中没有足够的信息来回答问题,请直接告知:“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。”
上下文:
{context}
问题: {question}
回答:"""
)
# 配置大语言模型
llm = ChatDeepSeek(
model="deepseek-chat",
temperature=0.7,
max_tokens=2048,
api_key=DEEPSEEK_API_KEY
)
# 用户查询
question = "文中举了哪些例子?"
# 在向量存储中查询相关文档
# 默认先将question文本也转化为向量,接着按照向量间的相似度排序,这里是取前3个,接着再将对应向量的本文拼接在一起作为提示的上下文内容
retrieved_docs = vectorstore.similarity_search(question, k=3)
docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs)
answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)
运行结果:
content='根据上下文信息,文中举了以下例子:\n\n1. **自然界中的羚羊**:羚羊出生后通过试错(强化学习)学会站立和快速奔跑以适应环境。\n2. **股票交易**:将买卖股票的过程视为强化学习,根据市场反馈(奖励)学会如何最大化收益。\n3. **玩雅达利游戏(如Breakout, Pong)**:通过不断试错来学习如何玩游戏并通关。\n4. **选择餐馆**:\n * **利用**:去已知的最喜欢的餐馆。\n * **探索**:尝试一家新的餐馆。\n5. **做广告**:\n * **利用**:采取已知最优的广告策略。\n * **探索**:尝试一种新的广告策略。\n6. **挖油**:\n * **利用**:在已知有油的地方挖。\n * **探索**:在一个新的地方挖油。\n7. **玩游戏(如《街头霸王》)**:\n * **利用**:总是采取一种已知有效的策略(如蹲角落出脚)。\n * **探索**:尝试一些新的招式或策略。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 246, 'prompt_tokens': 5550, 'total_tokens': 5796, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 5504}, 'prompt_cache_hit_tokens': 5504, 'prompt_cache_miss_tokens': 46}, 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_feb633d1f5_prod0820_fp8_kvcache', 'id': '416139b1-da25-4d81-9aa6-3c3354e442f2', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None} id='run--39bc7051-0777-4d88-a00a-df8342e7db70-0' usage_metadata={'input_tokens': 5550, 'output_tokens': 246, 'total_tokens': 5796, 'input_token_details': {'cache_read': 5504}, 'output_token_details': {}}
2.基于LlamaIndex的RAG实现
import os
# os.environ['HF_ENDPOINT']='https://hf-mirror.com'
from dotenv import load_dotenv
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.llms.deepseek import DeepSeek
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
load_dotenv()
# 定义大模型
Settings.llm = DeepSeek(model="deepseek-chat", api_key=os.getenv("DEEPSEEK_API_KEY"))
# 定义嵌入模型
Settings.embed_model = HuggingFaceEmbedding("BAAI/bge-small-zh-v1.5")
# 将本地对应md文件读入并以文本形式存储
documents = SimpleDirectoryReader(input_files=["../../data/C1/markdown/easy-rl-chapter1.md"]).load_data()
# 默认先分块,然后再对应每个块生成对应的嵌入层向量
index = VectorStoreIndex.from_documents(documents)
# 这一步把索引包装成一个 查询引擎。
query_engine = index.as_query_engine()
print(query_engine.get_prompts())
print(query_engine.query("文中举了哪些例子?"))
结果:
文中举了选择餐馆、做广告、挖油和玩游戏等例子。
总结
根据上面的基于两种不同库的简单RAG实现方法,不难发现上面LlamaIndex
的功能模块都是高度封装好的,对应代码也比LangChain
实现的要短很多。但两种代码的主要逻辑都是
1.完成文件读取,将不同格式的文件内容统一按照文本格式读入存储,接着分chunk
2.定义embedding模型,同时将本地已经处理好的文本chunk生成对应embedding向量存储,同时存储好(向量——源文本)对应数据库/字典。
3.接着输入检索查询文本query,embedding模型将会先将该文本query转化为对应的embedding向量,然后将这个embedding向量和本次已经存储的chunk向量进行相似度对比,选取top-k的chunk,接着我们将对应文本chunk拼接起来同时结合我们query添加进我们准备的prompt中
4.将提示词喂给大模型即可得到我们预期的结果,而无需使用新的数据集在大模型上进行微调任务。