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

作者:京东科技 蔡欣彤

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

内容背景

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

•什么是RAG

•上传的文档怎么就能检索了,中间是什么过程

•有的知道中间会进行向量化,会向量存储,那他们具体的含义和实际过程产生效果是什么

•还有RAG = 向量化 + 向量存储 + 检索 么?

•向量化 + 向量存储 就只是RAG 么?

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

最后代码都上传到coding了,地址是: github.com/XingtongCai...

一. 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为例尝试:

ini 复制代码
# 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用法,

ini 复制代码
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支持这个方式。

ini 复制代码
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 文件。在这些情况下,基于文档结构进行分割是有益的,因为它通常自然地分组了语义相关的文本。

ini 复制代码
# 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:用于嵌入单个文本(查询)。

python 复制代码
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
ini 复制代码
# 单个查询
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:搜索与给定查询相似的文档。

ini 复制代码
# 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)

额外补充一句:

scss 复制代码
向量化嵌入和向量存储不仅限于检索增强生成(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 参数决定了向量检索的具体算法和行为。以下是三种搜索类型的详细解释和对比:

makefile 复制代码
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等)

使用方法:

python 复制代码
# 构建一个高性能问答系统
from langchain.chains import RetrievalQA
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain.retrievers.multi_vector import MultiVectorRetriever
ini 复制代码
# 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做问答

代码地址github.com/XingtongCai...

目录结构

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

ini 复制代码
# 如果已经有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这个文件

bash 复制代码
cd translatorAssistant/basic_knowledge/员工手册的检索系统

python Langchain_QA.py

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

结果界面

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

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

技术点

•使用WebBaseLoader来加载网页内容

•使用rlm/rag-prompt来构建rag的prompt提示词模版

•使用ConversationBufferMemory来做记忆存储,进行多轮对话

•构建stream_generator支持流式输出

代码地址github.com/XingtongCai...

目录结构

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

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

运行结果

多轮对话的的prompt:

最后结果

四.企业级RAG

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

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

4.1 文本加载和文本清洗

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

•去除杂音:去掉文本中的无关信息或者噪声,比如多余的空格、HTML 标签和特殊符号等。

•标准化格式:将文本转换为统一的格式,如将所有字符转为小写,或者统一日期、数字的表示。

•处理标点符号和分词:处理或移除不必要的标点符号,并对文本进行适当的分词处理。

•去除停用词:移除那些对语义没有特别贡献的常用词汇,例如"的"、"在"、"而"等。

•拼写纠错:纠正文本中的拼写错误,以确保文本的准确性和一致性。

•词干提取和词形还原:将词语简化为词干或者词根形式,以减少词汇的多样性并保持语义的一致性。

4.2 文本分割

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

补充一下多级索引结构:

ini 复制代码
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. mp.weixin.qq.com/s/_tfc6qBIJ...

  2. mp.weixin.qq.com/s/jDSSt6MB1...

相关推荐
马可奥勒留6 小时前
我的管理日记(2)——招聘
程序员
小华同学ai11 小时前
1K star!这个开源项目让短信集成简单到离谱,开发效率直接翻倍!
后端·程序员·github
程序员鱼皮12 小时前
感觉程序员要被 AI 淘汰了?学什么才有机会?
计算机·ai·程序员·互联网·编程经验
掘金安东尼13 小时前
GPT-4.5 被 73% 的人误认为人类,“坏了?!我成替身了!”
人工智能·程序员
Apifox13 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
赣州云智科技的技术铺子14 小时前
【一步步开发AI运动APP】六、运动计时计数能调用
人工智能·程序员
重生之我在写代码18 小时前
如何进行apk反编译
android·程序员·编译器
Goboy19 小时前
从崩溃到升职:腾讯云EdgeOne Pages MCP拯救了我的996危机
后端·程序员·架构
网安刚哥1 天前
MCP Server 牛刀小试之雷池WAF MCP
程序员·github·ai编程