【LangChain】一文读懂RAG基础以及基于langchain的RAG实战

本文参与神灯创作者计划 - 前沿技术探索与应用赛道

内容背景

随着大模型应用不断落地,知识库,RAG是现在绕不开的话题,但是相信有些小伙伴和我一样,可能会一直存在一些问题,例如:

什么是RAG
上传的文档怎么就能检索了,中间是什么过程
有的知道中间会进行向量化,会向量存储,那他们具体的含义和实际过程产生效果是什么
还有RAG = 向量化 + 向量存储 + 检索 么?
向量化 + 向量存储 就只是RAG 么?

为了解决这些困惑,我查找了langchain的官方文档,并利用文档中提供的方法进行了实际操作。这篇文章是我的学习笔记,也希望为同样存在相同困惑的伙伴们能提供一些帮助。

最后代码都上传到coding了,地址是: https://github.com/XingtongCai/langchain_project/tree/main/translatorAssistant/basic_knowledge

一. RAG的基本概念

检索增强生成(Retrieval Augmented Generation, RAG) 通过将语言模型与外部知识库结合来增强模型的能力。RAG 解决了模型的一个关键限制:模型依赖于固定的训练数据集,这可能导致信息过时或不完整。



 

RAG大致过程如图:

接收用户查询 (Receive an input query
使用检索系统根据查询寻找相关信息Use the retrieval system to search for relevant information based on the query.
将检索到的信息合并到提示词,然后发送给LLM(Incorporate the retrieved information into the prompt sent to the LLM.
模型利用提供的上下文生成对查询的响应(Generate a response that leverages the retrieved context.

二. 关键技术

示例代码地址: rag.ipynb ,用jupyter工具可以直接看并且调试

一个完整的 RAG流程 通常包含以下核心环节,每个环节都有明确的技术目标和实现方法

文档加载和预处理
文本分割
数据向量化
向量存储与索引构建
内容检索


2.1 Document Loaders 数据加载

具体文档: document_loaders

整个langchain支持多种形式的数据源加载,这里面以加载pdf为例尝试:

# document loader
from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("./DeepSeek_R1.pdf")

pages = []
# 异步加载前 11 页
i = 0  # 手动维护计数器
async for page in loader.alazy_load():
    if i >= 11:  # 只加载前 11 页
        break
    pages.append(page)
    i+=1
    
print(f"{pages[0].metadata}\n") // pdf的信息
print(pages[0].page_content) // 第一页的内容

2.2 Text splitters 文本分割

参考文档: Text splitters

文本分割器将文档分割成更小的块以供下游应用程序使用。

 

2.2.1 分割策略 - 基于长度(Length-based)

基于长度的分割是最直观的策略是根据文档的长度进行拆分。它的分割类型如下:

Token-based :Splits text based on the number of tokens, which is useful when working with language models.
Character-based: Splits text based on the number of characters, which can be more consistent across different types of text

常见CharacterTextSplitter用法,

from langchain_text_splitters import CharacterTextSplitter
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", chunk_size=20, chunk_overlap=0
)
texts = text_splitter.split_text(pages[0].page_content)
print(texts[0])
扩展补充: token和character的区别: token 是语言模型处理文本时的基本单位,通常是一个词、子词或符号。 Character 是文本的最小单位,例如英文字母、中文字符、标点符号等。 # 示例 from transformers import GPT2Tokenizer tokenizer = GPT2Tokenizer.from_pretrained("gpt2") text = "Hello,world!" tokens = tokenizer.tokenize(text) print(tokens) # ['Hello', ',', 'world', '!'] chunk_size = 5 chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)] print(chunks) # ['Hello', ',worl', 'd!']

2.1.2 分割策略 - 基于文本结构(Text-structured based )

文本自然地被组织成层次化的单元,例如段落、句子和单词。可以利用这种固有结构来指导分割策略,创建能够保持自然语言流畅性、在分割中保持语义连贯性并适应不同文本粒度级别的分割。RecursiveCharacterTextSplitter 支持这个方式。

from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,  # 每个片段的最大字符数
    chunk_overlap=100,  # 片段之间的重叠字符数
    separators=["\n\n", "\n"]  # 分割符(按段落、句子、标点等)
)
chunks = text_splitter.split_documents(pages)
print(f"文档被分割成 {len(chunks)} 个块") # 文档被分割成 37 个块

2.1.3 分割策略 - 基于文档结构(Document-structured based)

某些文档具有固有的结构,例如 HTMLMarkdownJSON 文件。在这些情况下,基于文档结构进行分割是有益的,因为它通常自然地分组了语义相关的文本。

# Markdown 文档的分割
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain.text_splitter import MarkdownHeaderTextSplitter

# 加载 Markdown 文档
loader = UnstructuredMarkdownLoader("example.md")
documents = loader.load()

# 使用 MarkdownHeaderTextSplitter 分割
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=["#", "##", "###"])
chunks = splitter.split_text(documents[0].page_content)

# 打印分割结果
for chunk in chunks:
    print(chunk)

2.1.4 分割策略 - 基于语义的分割(semantic-based splitting)

与之前的方法不同,基于语义的分割实际上考虑了文本的内容。虽然其他方法使用文档或文本结构作为语义的代理,但这种方法直接分析文本的语义。核心方法是基于NLP模型,使用句子嵌入(Sentence-BERT)计算语义边界,通过文本相似度突变检测分割点

2.3 Embedding 向量化

参考文档: Embedding models

嵌入模型(embedding models)是许多检索系统的核心。嵌入模型将人类语言转换为机器能够理解并快速准确比较的格式。这些模型以文本作为输入,生成一个固定长度的数字数组。嵌入使搜索系统不仅能够基于关键词匹配找到相关文档,还能够基于语义理解进行检索。主要应用之一是rag.

 

LangChain 提供了一个通用接口,用于与嵌入模型交互,并为常见操作提供了标准方法。这个通用接口通过两个核心方法简化了与各种嵌入提供商的交互:

embed_documents:用于嵌入多个文本(文档)。
embed_query:用于嵌入单个文本(查询)。
from langchain_openai import OpenAIEmbeddings

# 初始化 OpenAI 嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")

# 这里取第一个片段进行embedding,省token
document_embeddings = embeddings.embed_documents(chunks[0].page_content)
print("文档嵌入:", document_embeddings[0]) # 文档嵌入: [0.0063153719529509544, -0.00667550228536129,...]
print(len(document_embeddings)) # 935
# 单个查询
query = "这篇文章介绍DeepSeek的什么版本"
# 使用 embed_query 嵌入单个查询
query_embedding = embeddings.embed_query(query)
print("查询嵌入:", query_embedding)
# 查询嵌入:[-0.0036168822553008795, 0.0056339893490076065,...]



2.4 Vector stores 向量存储

参考文档: Vector stores

其实langchain支持embedding和vector 的种类有很多,具体支持类型见 full list of LangChain vectorstore integrations.

其中, 向量存储(Vector Stores) 是一种专门的数据存储,支持基于向量表示的索引和检索。这些向量捕捉了被嵌入数据的语义意义。

向量存储通常用于搜索非结构化数据(如文本、图像和音频),以基于语义相似性而非精确的关键词匹配来检索相关信息。主要应用场景之一是rag。



 


LangChain 提供了一个标准接口,用于与向量存储交互,使用户能够轻松切换不同的向量存储实现。

该接口包含用于在向量存储中写入、删除和搜索文档的基本方法。

关键方法包括:

1. add_documents:将一组文本添加到向量存储中。
2. delete:从向量存储中删除一组文档。
3. similarity_search:搜索与给定查询相似的文档。
# vector store
from langchain_core.vectorstores import InMemoryVectorStore
# 实例化向量存储
vector_store = InMemoryVectorStore(embeddings)
# 将已经转为向量的文档存储到向量存储中
ids =vector_store.add_documents(documents=chunks)
print(ids) # ['b5b11014-7702-45f6-aa0d-7272042c5d4d', '93ed319a-5110-40ac-8bf4-117a220ed0cb', '63b92a2f-bf47-4a10-b66b-cd39776995f7',...]

vector_store.delete(ids=["93ed319a-5110-40ac-8bf4-117a220ed0cb"])
vector_store.similarity_search('What is the model introduced in this paper?',k=4)

额外补充一句:

向量化嵌入和向量存储不仅限于检索增强生成(RAG)系统,它们有更广泛的应用场景:
其他应用领域包括:
1.语义搜索系统 - 基于内容含义而非关键词匹配的搜索
2.推荐系统 - 根据内容或用户行为的相似性推荐项目
3.文档聚类 - 自动组织和分类大量文档
4.异常检测 - 识别与正常模式偏离的数据点
5.多模态系统 - 连接文本、图像、音频等不同模态的内容
6.内容去重 - 识别相似或重复的内容
7.知识图谱 - 构建实体之间的语义关联
8.情感分析 - 捕捉文本的情感特征
RAG是向量嵌入技术的一个重要应用,但这些技术的价值远不止于此,它们为各种需要理解内容语义关系的AI系统提供了基础。

2.5 Retriever 检索

参考文档: retrievers

目前存在许多不同类型的检索系统,包括 向量存储(vectorstores)图数据库(graph databases)关系数据库(relational databases)



 

LangChain 提供了一个统一的接口,用于与不同类型的检索系统交互。LangChain 的检索器接口非常简单:

输入:一个查询(字符串)。
输出:一组文档(标准化的 LangChain Document 对象)。

而且LangChain的retriever是runnable类型,runnable可用方法他都可以调用,比如:

docs = retriever.invoke(query)

2.5.1 基础检索器的几种类型说明

在 LangChain 中,as_retriever() 方法的 search_type 参数决定了向量检索的具体算法和行为。以下是三种搜索类型的详细解释和对比:

retriever = vector_store.as_retriever(
    search_type="similarity",  # 可选 "similarity"|"mmr"|"similarity_score_threshold"
    search_kwargs={
        "k": 5,  # 返回结果数量
        "score_threshold": 0.7,  # 仅当search_type="similarity_score_threshold"时有效
        "filter": {"source": "重要文档.pdf"},  # 元数据过滤
        "lambda_mult": 0.25  # 仅MMR搜索有效(控制多样性)
    }
)

search_kwargs: 搜索条件

search_type可选 "similarity"|"mmr"|"similarity_score_threshold"

similarity(默认)-标准相似度搜索

原理:直接返回与查询向量最相似的 k 个文档(基于余弦相似度或L2距离)

特点

结果完全按相似度排序
适合精确匹配场景



mmr-最大边际相关性

原理:在相似度的基础上增加多样性控制,避免返回内容重复的结果

核心参数

lambda_mult:0-1之间的值,越小结果越多样
接近1:更偏向相似度(类似similarity)
接近0:更偏向多样性

特点

检索结果需要覆盖不同子主题时
避免返回内容雷同的文档



similarity_score_threshold-带分数阈值的相似度搜索

原理:只返回相似度超过设定阈值的文档
关键参数
score_threshold:相似度阈值(余弦相似度范围0-1)
k:最大返回数量(但实际数量可能少于k)
特点:
适合质量优先的场景
结果数量不固定(可能返回0个或多个)



类型 核心目标 结果数量 是否控制多样性 典型应用场景
similarity 精确匹配 固定k个 事实性问题回答
mmr 平衡相关性与多样性 固定k个 生成综合性报告
similarity_score_threshold 质量过滤 动态数量 高精度筛选

2.5.2 高级检索工具

1.MultiQueryRetriever - 提升模糊查询召回率

设计目标:通过查询扩展提升检索质量 核心思想

对用户输入问题生成多个相关查询(如同义改写、子问题拆解)
合并多查询结果并去重

关键特点

提升模糊查询的召回率
依赖LLM生成优质扩展查询(需控制生成成本)



2.MultiVectorRetriever -多向量检索

设计目标:处理单个文档的多种向量表示形式 核心思想

对同一文档生成多组向量(如全文向量、摘要向量、关键词向量等)
通过多角度表征提升召回率

关键特点

适用于文档结构复杂场景(如技术论文含正文/图表/代码)
存储开销较大(需存多组向量)



3.RetrievalQA

设计目标:端到端的问答系统构建 核心思想

将检索器与LLM答案生成模块管道化
支持多种链类型(stuff/map_reduce/refine等)

使用方法:

# 构建一个高性能问答系统
from langchain.chains import RetrievalQA
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain.retrievers.multi_vector import MultiVectorRetriever
# 1. 多向量存储
# 假设已生成不同视角的向量
vectorstore = FAISS.from_texts(documents, embeddings)

mv_retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    docstore=standard_docstore,  # 存储原始文档
    id_key="doc_id"  # 关联不同向量与原始文档
)

# 2. 多查询扩展
mq_retriever = MultiQueryRetriever.from_llm(
    retriever=mv_retriever,
    llm=ChatOpenAI(temperature=0.3)
)

# 3. 问答链
qa = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(temperature=0),
    chain_type="refine",
    retriever=mq_retriever,
    chain_type_kwargs={"verbose": True}
)

# 执行
answer = qa.run("请对比Transformer和CNN的优缺点")



4.TimeWeightedVectorStoreRetriever - 时间加权检索

设计目标专门用于在向量检索中引入时间衰减因子,使系统更倾向于返回近期文档

典型使用场景

1. 新闻/社交媒体应用 -优先展示最新报道而非历史文章

2. 技术文档系统 - 优先推荐最新API文档版本

3. 实时监控告警 - 使最近的事件告警排名更高



2.5.3 混合检索

EnsembleRetriever - 集合检索器

场景:结合语义+关键词搜索

ContextualCompressionRetriever-上下文压缩

场景:过滤低相关性片段

三. 代码实战

说完了技术,接下来就真正尝试两个个例子,走一遍RAG全流程。

3.1 构建一个简单的员工工作指南检索系统

需求背景:我现在要给一个企业做员工工作指南的检索系统,方便员工查阅最新资讯。里面包含.pdf,.docx,.txt三种文件。

技术点

使用FAISS做向量存储,MultiQueryRetriever和RetrievalQA做增强检索
构建一个chain做问答

代码地址https://github.com/XingtongCai/langchain_project/tree/main/translatorAssistant/basic_knowledge/%E5%91%98%E5%B7%A5%E6%89%8B%E5%86%8C%E7%9A%84%E6%A3%80%E7%B4%A2%E7%B3%BB%E7%BB%9F

目录结构



 

三个需要加载的文档, index.faiss和index.pkl是我已经做好向量化的文件,如果不想自己做向量化或者想省一些token,可以直接读取这两个文件。代码段如下:

# 如果已经有embedding文件,直接加载,不要重新处理了,节省token
embedding = OpenAIEmbeddings(model="text-embedding-ada-002",chunk_size=1000)
vector_store_1 = FAISS.load_local(
    folder_path="./docs",       # 存放index.faiss和index.pkl的目录路径
    embeddings=embedding,      # 必须与创建时相同的嵌入模型
    index_name="index",          # 可选:若文件名不是默认的"index",需指定前缀
    allow_dangerous_deserialization=True  # 显式声明信任
)
print(f"已加载 {len(vector_store_1.docstore._dict)} 个文档块")
print(vector_store_1)

运行方式2种:

1.用 jupyter工具,直接运行Langchain_QA.ipynb 这个文件,可以单步调试。我运行的结果也可以直接看到

2.用python脚本,直接运行Langchain_QA.py这个文件

cd translatorAssistant/basic_knowledge/员工手册的检索系统

python Langchain_QA.py

示例问题可以尝试:
"应聘人员须提供什么资料"
"Rose的花语是什么"

结果界面



 



 

3.2 读取网页的blog并进行多轮对话来查询

需求背景:我现在要读取 https://lilianweng.github.io/posts/2023-06-23-agent/ 这个blog的内容,并对里面内容进行多轮对话的检索

技术点

使用WebBaseLoader来加载网页内容
使用rlm/rag-prompt来构建rag的prompt提示词模版
使用ConversationBufferMemory来做记忆存储,进行多轮对话
构建stream_generator支持流式输出

代码地址https://github.com/XingtongCai/langchain_project/tree/main/translatorAssistant/basic_knowledge/%E5%A4%9A%E8%BD%AE%E5%AF%B9%E8%AF%9D%E7%9A%84%E6%A3%80%E7%B4%A2%E7%B3%BB%E7%BB%9F

目录结构

和上面结构差不多,docs里面是向量化好的文件,.ipynb可以单步调试并且能看我运行的每一步结果,.py是最后整个python文件

运行方式:和上面示例一样

运行结果

多轮对话的的prompt:

最后结果



 



四.企业级RAG

上面的内容是作为RAG的基础知识,但是想做企业级别的RAG,尤其是要做 产品资讯助手 这种基于知识库和用户进行问答式的交流还远远不够。总流程没有区别,但是每一块都有自己特殊处理地方:

我通过读取一些企业级RAG的文章,大致整理一下这种流程,但是真实使用时候,还是需要根据我们自己业务文档,使用的场景和对于回答准确率要求的情况来酌情处理。

4.1 文本加载和文本清洗

企业级知识库构建在 预处理 这部分的处理是重头戏。因为我们文档是多样性的,有不同类型文件,文件里面还有图片,图片里面内容有时候也是需要识别的。另外文档基本都是产品或者运营来写,里面有很多口头语的表述,过多不合适的分段/语气词,多余的空格,这些问题都会影响最后分割的效果。一般文本清洗需要做一下几步:

去除杂音:去掉文本中的无关信息或者噪声,比如多余的空格、HTML 标签和特殊符号等。
标准化格式:将文本转换为统一的格式,如将所有字符转为小写,或者统一日期、数字的表示。
处理标点符号和分词:处理或移除不必要的标点符号,并对文本进行适当的分词处理。
去除停用词:移除那些对语义没有特别贡献的常用词汇,例如“的”、“在”、“而”等。
拼写纠错:纠正文本中的拼写错误,以确保文本的准确性和一致性。
词干提取和词形还原:将词语简化为词干或者词根形式,以减少词汇的多样性并保持语义的一致性。

4.2 文本分割

除了采取上面的基本分割方式,然后还可以结合层次分割优化不同粒度的索引结构,提高检索匹配度。

补充一下多级索引结构:



 
from langchain.text_splitter import MarkdownHeaderTextSplitter

headers = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3")
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers)
chunks = splitter.split_text(markdown_doc)

4.3 向量存储

在企业级存储一般数据量都很大,用内存缓存是不现实的,基本都是需要存在数据库中。至于用什么数据库,怎么存也根据文本类型有变化,例如:

向量数据库:向量存储库非常适合存储文本、图像、音频等非结构化数据,并根据语义相似性搜索数据。
图数据库:图数据库以节点和边的形式存储数据。它适用于存储结构化数据,如表格、文档等,并使用数据之间的关系搜索数据。

4.4 向量检索

在很多情况下,用户的问题可能以口语化形式呈现,语义模糊,或包含过多无关内容。将这些模糊的问题进行向量化后,召回的内容可能无法准确反映用户的真实意图。此外,召回的内容对用户提问的措辞也非常敏感,不同的提问方式可能导致不同的检索结果。因此,如果计算能力和响应时间允许,可以先利用LLM对用户的原始提问进行改写和扩展,然后再进行相关内容的召回。

4.5 内容缓存

在一些特定场景中,对于用户的问答检索需要做缓存处理,以提高响应速度。可以以用户id,时间顺序,信息权重作为标识进行存储。

还有一些关于将检索信息和prompt结合时候,如何处理等其他流程,这里不多讲,讲多了头疼。。。

总结

本文介绍了RAG的基础过程,在langchain框架下如何使用,再到提供两个个例子进行了代码实践,最后又简要介绍了企业级别RAG构建的内容。最后希望大家能有所收获。

码字不易,如果喜欢,请给作者一个小小的赞做鼓励吧~~

参考文献

1. https://mp.weixin.qq.com/s/_tfc6qBIJkWXHe40lrbSbw

2. https://mp.weixin.qq.com/s/jDSSt6MB1QeQuft0ZNeGDg

开源 Java 工具 - Hutool 致大家的一封信 Visual Studio Code 1.99 发布,引入 Agent 和 MCP 亚马逊在最后一刻提交了收购 TikTok 的报价 FFmpeg 愚人节整活:加入 DOGE 团队,用汇编重写美国社保系统 龙芯 2K3000(3B6000M)处理器流片成功 中国首款全自研高性能 RISC-V 服务器芯片发布 清华大学开源软件镜像站的愚人节彩蛋 Linus 口吐芬芳:怒斥英特尔工程师提交的代码是“令人作呕的一坨” 比尔·盖茨公开自己写过的“最酷的代码” CDN 服务商 Akamai 宣布托管 kernel.org 核心基础设施
{{o.name}}
{{m.name}}

猜你喜欢

转载自my.oschina.net/u/4090830/blog/18059066
今日推荐