导读
环境:OpenEuler、Windows 11、WSL 2、Python 3.12.3 langchain 0.3

背景:前期忙碌的开发阶段结束,需要沉淀自己的应用知识,过一遍LangChain

时间:20250223

说明:技术梳理,使用LangChain实现rag,当前主流实现均使用LangGraph,此处使用LangChain的目的是加强LangChain的熟练度

原理与问题

背景

最初源于2020年Facebook的一篇论文——《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》

概念

RAG(Retrieval-Augmented Generation)检索增强生成,即大模型LLM在回答问题或生成文本时,会先从大量的文档中检索出相关信息,然后基于这些检索出的信息进行回答或生成文本,从而可以提高回答的质量,而不是任由LLM来发挥。

解决问题

在大模型的快速发展过程中,以下四点比较突出:

幻觉问题:大语言模型基于概率推理,所以LLMs有时候会一本正经的胡说八道,编造看似合理的答案。
知识缺乏问题:大模型都是预训练,就拿ChatGPT3.5来说训练数据是2021年,但是对于2021年之后的事情,它将一无所知。另外还可能会产生过时的知识和缺乏一些特定领域的知识。
数据安全问题:对于企业来说,企业的经营数据,商业机密数据都是非常重要的,直接使用大模型可能会有数据安全泄露风险。
可信度问题:不透明、无法追踪的推理过程,导致回答问题可信度问题。

与微调对比

以上问题,微调也可以解决,下面是对比信息

RAG、微调优缺点对比
微调    RAG
有点 针对特定任务调整预训练模型。优点是可针对特定任务优化 结合检索系统和生成模型。优点是能利用最新信息,提高答案质量,具有更好的可解释性和适应性
缺点 但缺点是更新成本高,对新信息适应性较差 是可能面临检索质量问题和曾加额外计算资源需求
RAG、微调特性对比
特性 RAG 微调
知识更新 实时更新检索库,适合动态数据,无需频繁重训 存储静态信息,更新知识需要重新训练
外部知识 高效利用外部资源,适合各类数据库 可对齐外部知识,但对动态数据源不够灵活
数据处理 数据处理需求低  需构建高质量数据集,数据限制可能影响性能
模型定制化 专注于信息检索和整合,定制化程度低 可定制行为,风格及领域知识
可解释性 答案可追溯,解释性高 解释性相对低
计算资源 需要支持检索的计算资源,维护外部数据源 需要训练数据集和微调资源
延迟要求 数据检索可能增加延迟 微调后的模型反应更快
减少幻觉 基于实际数据,幻觉减少 通过特定域训练可减少幻觉,但仍然有限
道德与隐私 处理外部文本数据时需要考虑隐私和道德问题 训练数据的敏感内容可能引发隐私问题

原理图详解

图片地址:Retrieval-Augmented Generation for Large Language Models: A Survey

该论文综述中,将RAG技术按照复杂度分为Naive RAGAdvanced RAGModular RAG

初级 RAG 在检索质量、响应生成质量以及增强过程中存在多个挑战,高级 RAG 范式随后被提出,高级RAG在数据索引、检索前和检索后都进行了额外处理。通过更精细的数据清洗、设计文档结构和添加元数据等方法提升文本的一致性、准确性和检索效率。随着 RAG 技术的进一步发展和演变,新的技术突破了传统的 初级 RAG 检索 — 生成框架,基于此我们提出模块化 RAG 的概念。在结构上它更加自由的和灵活,引入了更多的具体功能模块,例如查询搜索引擎、融合多个回答。技术上将检索与微调、强化学习等技术融合。流程上也对 RAG 模块之间进行设计和编排,出现了多种的 RAG 模式。

本文主讲LangChain中的RAG,故而此处简要介绍原始RAG的原理,高级和模块化的均基于naive RAG

Naive RAG

该RAG的流程包括索引、检索和生成三个步骤,既把问答内容输入到数据库中,给定query,可以直接去数据库中搜索,搜索完成后把查询结果和query拼接起来送给模型去生成内容。图示如下:

索引

索引阶段是将文本、图片、音视频等格式的内容进行解析、分割、向量化处理,并最终向量化后的内容存储到向量数据库,具体包含四个步骤:数据加载、文本分块、文本嵌入、创建索引

数据加载

将外部数据进行清理和提取,将CSV、 PDF、HTML、Word、Markdown 等不同格式的文件转换成纯文本,这里可以借助LangChain内置的加载器来实现。LangChain内置的加载器是LangChain中最有用的部分之一,例如加载CSV的CSVLoader,加载PDF的PyPDFLoader,加载HTML的UnstructuredHTMLLoader,加载Word的:UnstructuredWordDocumentLoader,加载MarkDown的:UnstructuredMarkdownLoader等

文本分块

一方面Transformer模型有固定的输入序列长度,即使输入context很大,一个句子或几个句子的向量也比几页文本的平均向量更好地代表它们的语义含义,另一方面,我们将文档分割成适合搜索的小块,使其更适合进行嵌入搜索,从而提升片段召回的准确性。

LangChain中集成了不少分块工具,如下:

from langchain import text_splitter
文本嵌入

亦称之为向量化,是将文本内容通过embedding嵌入模型转化为多维向量的过程

可以这样认为:将不同的人类语言生成的向量绘制到多维坐标系中,发现在这个假设的语言空间中,两个点越接近,它们所表达的语义就越相似。

创建索引

将原始语料块和嵌入以键值对形式存储到向量数据库,以便于未来进行快速且频繁的搜索

常用的向量数据库有:ChromaWeaviate、 FAISSES、Milvus

检索

检索是RAG框架中的重要组成部分,根据用户的查询,快速检索到与之最相关的知识,并将其融入提示词(Prompt)中。这个过程一般分两步:

1、根据用户的输入,采用与索引创建相同的编码模型将查询内容转换为向量。
2、计算问题向量与语料库中文档块向量之间的相似性,并根据相似度水平选出最相关的前 K 个文档块作为当前问题的补充背景信息。

常见的检索方法:分层索引检索混合检索HyDE方案

生成

将用户的问题与知识库被检索出的文本块相结合, 用prompt的形式传递给大语言模型的上下文,使大模型更好理解用户意图,生成用户想要的结果

以上就是RAG框架的整体流程,接下来将用代码实现一个完整的RAG Demo。

代码实现 

1、索引

创建索引需要执行以下四个步骤

数据加载

将如下内容写入到pdf中,后续使用pdf实现数据加载

王二狗,1990年出生于中国西南部一个风景秀丽的小山村——云南省大理白族自治州的一个小村庄。从小,他就对周围的世界充满了好奇心,尤其对电子设备和计算机表现出浓厚的兴趣。尽管家庭条件并不富裕,但父母的支持与鼓励为他的成长奠定了坚实的基础。

2008年,18岁的王二狗以优异的成绩考入了清华大学计算机科学与技术专业,成为了村里第一个进入这所著名学府的学生。大学期间,他不仅在学业上取得了显著成就,还积极参与各种学术研究和社会实践活动。他曾作为团队核心成员参加了全国大学生智能车竞赛,并荣获一等奖。

毕业后,王二狗加入了一家知名的互联网公司,专注于人工智能领域的发展。在这里,他迅速崭露头角,凭借出色的编程能力和创新思维,参与了多个重要项目,为公司的技术进步做出了突出贡献。

工作之余,王二狗也不忘回馈社会。他经常回到家乡,给村里的孩子们讲述外面世界的精彩,鼓励他们勇敢追求梦想。如今,35岁的王二狗已经成为行业内的一名杰出专家,但他依旧保持着谦逊和学习的态度,继续探索科技的无限可能。他的故事激励着无数年轻人勇敢追梦,不畏艰难,用知识改变命运。

 实现数据加载的代码

from PyPDF2 import PdfReader

# 数据加载
pdf_path = "/home/jack/langchain_test/langchain_rag/static/test_xx.pdf"
text = ""
pdf_reader = PdfReader(pdf_path)
for page in pdf_reader.pages:
    text += page.extract_text()
print(text)

输出 

 '王二狗,1990 年出生于中国西南部一个风景秀丽的小山村 ——云南省大理白族自治州\n的一个小村庄。从小,他就对周围的世界充满了好奇心,尤其对电子设备和计算机表\n现出浓厚的兴趣。尽管家庭条件并不富裕,但父母的支持与鼓励为他的成长奠定了坚\n实的基础。 \n \n2008年,18岁的王二狗以优异的成绩考入了清华大学计算机科学与技术专业,成为了\n村里第一个进入这所著名学府的学生。大学期间,他不仅在学业上取得了显著成就,\n还积极参与各种学术研究和社会实践活动。他曾作为团队核心成员参加了全国大学生\n智能车竞赛,并荣获一等奖。  \n \n毕业后,王二狗加入了一家知名的互联网公司,专注于人工智能领域的发展。在这\n里,他迅速崭露头角,凭借出色的编程能力和创新思维,参与了多个重要项目,为公\n司的技术进步做出了突出贡献。 \n \n工作之余,王二狗也不忘回馈社会。他经常回到家乡,给村里的孩子们讲述外面世界\n的精彩,鼓励他们勇敢追求梦想。如今,35岁的王二狗已经成为行业内的一名杰出专\n家,但他依旧保持着谦逊和学习的态度,继续探索科技的无限可能。他的故事激励着\n无数年轻人勇敢追梦,不畏艰难,用知识改变命运。 '

显然,之前换行的地方都加上\n,读取出来就是单纯的字符串

此处示例是pdf,使用的是PdfReader方式获取,还可以使用如下方式

from langchain_community.document_loaders import PyPDFLoader

pdf_path = "/home/jack/langchain_test/langchain_learn/static/test_cui.pdf"
pdf_reader = PyPDFLoader(file_path=pdf_path)
document_obj = pdf_reader.load()
text = document_obj[0].page_content

 该种方式是集成自langchain_community,当然不止PDF,例如:

text -- TextLoader

outlook -- OutlookMessageLoader

csv -- CSVLoader

json -- JSONLoader

此处就不一一列举了,常见均给封装了方法,

文本分块

参数说明

separator:块与块之间的分隔符

chunk_size:每个块(字符串)的大小

chunk_overlap:块与块重叠部分的大小

length_function:计算长度的函数

text_spliter = CharacterTextSplitter(separator="\n", chunk_size=100, chunk_overlap=40, length_function=len)
content_chunks = text_spliter.split_text(text)

输出结果

['王二狗,1990 年出生于中国西南部一个风景秀丽的小山村 ——云南省大理白族自治州\n的一个小村庄。从小,他就对周围的世界充满了好奇心,尤其对电子设备和计算机表', '的一个小村庄。从小,他就对周围的世界充满了好奇心,尤其对电子设备和计算机表\n现出浓厚的兴趣。尽管家庭条件并不富裕,但父母的支持与鼓励为他的成长奠定了坚\n实的基础。', '实的基础。 \n \n2008年,18岁的王二狗以优异的成绩考入了清华大学计算机科学与技术专业,成为了\n村里第一个进入这所著名学府的学生。大学期间,他不仅在学业上取得了显著成就,', '村里第一个进入这所著名学府的学生。大学期间,他不仅在学业上取得了显著成就,\n还积极参与各种学术研究和社会实践活动。他曾作为团队核心成员参加了全国大学生\n智能车竞赛,并荣获一等奖。', '智能车竞赛,并荣获一等奖。  \n \n毕业后,王二狗加入了一家知名的互联网公司,专注于人工智能领域的发展。在这\n里,他迅速崭露头角,凭借出色的编程能力和创新思维,参与了多个重要项目,为公', '里,他迅速崭露头角,凭借出色的编程能力和创新思维,参与了多个重要项目,为公\n司的技术进步做出了突出贡献。 \n \n工作之余,王二狗也不忘回馈社会。他经常回到家乡,给村里的孩子们讲述外面世界', '工作之余,王二狗也不忘回馈社会。他经常回到家乡,给村里的孩子们讲述外面世界\n的精彩,鼓励他们勇敢追求梦想。如今,35岁的王二狗已经成为行业内的一名杰出专', '的精彩,鼓励他们勇敢追求梦想。如今,35岁的王二狗已经成为行业内的一名杰出专\n家,但他依旧保持着谦逊和学习的态度,继续探索科技的无限可能。他的故事激励着', '家,但他依旧保持着谦逊和学习的态度,继续探索科技的无限可能。他的故事激励着\n无数年轻人勇敢追梦,不畏艰难,用知识改变命运。']

返回值为列表,每个元素的结尾和下一个元素的开头都有重复的部分,但是并非(肉眼看到的)是40个字符,实际存在类似于\n或是其他不可见的元素在其中 

此处使用了CharacterTextSplitter方法,当然还封装了其他方法,例如:

    "TokenTextSplitter",

    "TextSplitter",

    "Tokenizer",

    "Language",

    "RecursiveCharacterTextSplitter",

    "RecursiveJsonSplitter",

    "LatexTextSplitter",

    "PythonCodeTextSplitter",

    "KonlpyTextSplitter",

    "SpacyTextSplitter",

    "NLTKTextSplitter",

    "split_text_on_tokens",

    "SentenceTransformersTokenTextSplitter",

    "ElementType",

    "HeaderType",

    "LineType",

    "HTMLHeaderTextSplitter",

    "MarkdownHeaderTextSplitter",

    "MarkdownTextSplitter",

    "CharacterTextSplitter",

 上述方法复制自源码,可能存在版本不同,方法不同的问题,可根据自己的需求选用合适的方法

文本嵌入并创建索引

# 文本嵌入并创建索引
vectorstore = Chroma.from_texts(texts=content_chunks, embedding=embedding_model)

 一行代码实现了两个功能,文本向量化、创建索引。下面为from_text的源码注释

Create a Chroma vectorstore from a raw documents.If a persist_directory is specified, the collection will be persisted there.Otherwise, the data will be ephemeral in-memory.

译为中文

从原始文档创建 Chroma 向量存储。如果指定了 persist_directory,则集合将保留在那里。否则,数据将在内存中短暂显示。

为什么使用chroma

使用该向量数据库,较为方便,仅需安装、引用即可直接使用,默认使用内存存储。当然,生产环境尽量不要使用 

创建检索器

retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

search_kwargs (Optional[Dict]):要传递给搜索功能。可以包括以下内容:
k:要返回的文档数量(默认值:4) 

 使用检索器检索上下文

通过检索器从向量数据库中获取与问题相关的chunks,上文设置获取两个,并将每个chunk使用换行符连接。

context_docs = retriever.invoke(question)
context = "\n".join([doc.page_content for doc in context_docs])

格式化提示词

将问题与查询出与问题相关的向量数据库中的文本整合为一个提示词

formatted_prompt = prompt.format(text=question, context=context)

调用大模型 

将上面整合后的提示词作为输入提交到大模型

llm_response = qa.invoke({"query": formatted_prompt})
answer = llm_response["result"]

输出

'王二狗出生于1990年,所以如果按当前年份2023年计算,他应为33岁。但实际上,具体年龄会根据他的出生月份和当前月份来确切决定。不过,基于提供的信息,我们可以推测他大约是33岁。' 

整体代码 

from langchain_community.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from PyPDF2 import PdfReader
from langchain_openai import AzureOpenAIEmbeddings, ChatOpenAI
from langchain.chains import retrieval_qa, RetrievalQA
from langchain.prompts import ChatPromptTemplate

embedding_model = AzureOpenAIEmbeddings(
    “请输入自己的新”
)

llm = ChatOpenAI(
    “请使用自己的信息”
    )

def rag_test_pdf(question):
    # 数据加载
    pdf_path = "/home/jack/langchain_test/langchain_learn/static/test_cui.pdf"
    text = ""
    pdf_reader = PdfReader(pdf_path)
    for page in pdf_reader.pages:
        text += page.extract_text()

    # 文本分块
    text_spliter = CharacterTextSplitter(separator="\n", chunk_size=100, chunk_overlap=20, length_function=len)
    content_chunks = text_spliter.split_text(text)
    
    # 文本嵌入并创建索引
    vectorstore = Chroma.from_texts(texts=content_chunks, embedding=embedding_model)

    # 创建检索器
    retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

    # 创建提示词
    prompt = ChatPromptTemplate.from_messages(
        [("system", "你是一个智能助手"), ("user","{text}")]
    )
    qa = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=retriever)

    # 使用检索器检索上下文
    context_docs = retriever.invoke(question)
    context = "\n".join([doc.page_content for doc in context_docs])

    # 使用问题和上下文设置提示的格式
    formatted_prompt = prompt.format(text=question, context=context)

    # 使用格式化提示调用 LLM
    llm_response = qa.invoke({"query": formatted_prompt})
    answer = llm_response["result"]
    print(answer)
rag_test_pdf("王二狗今年多大")

LangChain中有几种rag,此处以简单的示例来做说明
其实,RAG在LangChain中已经推荐使用LangGraph实现,所以此处仅为熟悉LangChain,建议项目还是要使用LangGraph来实现该功能,后续也会有LangGraph版本的RAG,敬请期待

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐