检索增强生成RAG

LLM是基于一定训练数据训练成的,如果输入的内容所涉及的知识并不在训练数据范围内,LLM通常是无法准确回答问题的,不过检索增强生成(RAG)方案可以解决这个问题。RAG简单来说就是在外部增加了一个知识库,当输入的内容所涉及的知识不在训练数据范围内时,可以通过检索知识库来获取相关知识,然后与输入的内容进行融合,再通过LLM生成最终的回答。

向量数据库和Embedding模型

环境准备

实现RAG的一个关键点是如何准确的找到和问题语义相关的知识,这通常需要结合向量数据库和Embedding模型来实现。Embedding模型负责将输入的内容映射成向量,向量数据库则负责存储和检索向量。向量数据库有很多种,这里我们以比较简单易用的Chroma为例进行介绍,Embedding模型选择对中文支持较好的bge-large-zh-v1.5

执行以下命令安装加载Embedding模型和Chroma相关的依赖。

pip install sentence_transformers langchain-chroma

其次我们还需要将bge-large-zh-v1.5模型下载到本地。

向量数据库存入和搜索

LangChain中,文档对应的封装对象是Document。下面例子中我们手动编写了一些文档并Embedding后存入了Chroma向量数据库中,之后我们又通过向量数据库来检索和问题相关的文档。

from langchain_core.documents import Document
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain_chroma import Chroma

embedding_model = HuggingFaceBgeEmbeddings(
    model_name='../bge-large-zh-v1.5/',
    model_kwargs={'device': 'cpu'},
    encode_kwargs={'normalize_embeddings': True}
)

documents = [
    Document(
        page_content='汤姆是一只有蓝色的毛发的英国短毛猫,它经常追逐杰瑞。',
        metadata={'source': '宠物介绍'},
    ),
    Document(
        page_content='杰瑞是一只褐色的家鼠,它非常聪明和敏捷。',
        metadata={'source': '宠物介绍'},
    ),
    Document(
        page_content='斯派克是一只又大又笨的狗,他是杰瑞的朋友。',
        metadata={'source': '宠物介绍'},
    )
]

vector_store = Chroma.from_documents(documents, embedding=embedding_model, persist_directory="./chroma_db")
result = vector_store.similarity_search('斯派克是谁?', k=1)
print(result)

上述代码中,我们首先加载了Embedding模型,随后将文档的特征使用Embedding模型处理后存入了Chroma向量数据库。documents是我们手写的一组文档,实际开发中,文档可能来源于其它数据源,并且可能是增量更新到向量数据库的,Chroma支持对文档数据的增删改查操作,具体可以参考LangChain和Chroma集成相关的文档。最后,我们调用了similarity_search方法并输入了一个问题,参数k=1意味着我们指定检索结果输出1个和问题最相关的文档。

在上面例子中,加入我们的问题和“猫”或“汤姆”有关,程序通常就会检索到第一条数据,因为这条数据和我们的问题是语义相关的,其它词条也是同理。

基于LangChain实现RAG

在LangChain中,将RAG整合到Chain还需要一个Retriever组件,下面我们使用LangChain编写一个完整的RAG应用程序。

from langchain.globals import set_debug, set_verbose
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain_chroma import Chroma
from langchain_community.chat_models import ChatOllama

set_debug(True)
set_verbose(True)

# 创建Embedding模型对象
embedding_model = HuggingFaceBgeEmbeddings(
    model_name='../bge-large-zh-v1.5/',
    model_kwargs={'device': 'cpu'},
    encode_kwargs={'normalize_embeddings': True}
)

# 加载文档数据
documents = [
    Document(
        page_content='汤姆是一只有蓝色的毛发的英国短毛猫,它经常追逐杰瑞。',
        metadata={'source': '宠物介绍'},
    ),
    Document(
        page_content='杰瑞是一只褐色的家鼠,它非常聪明和敏捷。',
        metadata={'source': '宠物介绍'},
    ),
    Document(
        page_content='斯派克是一只又大又笨的狗,他是杰瑞的朋友。',
        metadata={'source': '宠物介绍'},
    )
]

# 将文档存入Chroma
vector_store = Chroma.from_documents(documents, embedding=embedding_model, persist_directory="./chroma_db")

# 创建Retriever
retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 1},
)

# 组织Prompt
message = """
Answer this question using the provided context only.

{question}

Context:
{context}
"""
prompt = ChatPromptTemplate.from_messages([("human", message)])

# 创建LLM对象
llm = ChatOllama(model='qwen2:7b-instruct-q5_K_M', temperature=1)

# 拼接Chain
rag_chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser()

q = input()
a = rag_chain.invoke(q)
print(a)
  • RunnablePassthrough():该函数用于获取Chain上的数据数据

代码其实很简单,Chain的开头我们拼接了一个新的组件用于输入数据,其中context是通过Retriever从Chroma向量数据库中查询得到的结果,question是用户的输入,将这些参数传入Prompt模板并拼接为完整的Prompt,之后输入LLM并读取其输出,一个最简单的RAG应用我们就开发完成了。

Document加载器和文本分割器

前面我们编写的程序中,Document(文档)具有十分标准的格式,它是一个数组,数组的每个元素都是一个长度近似的词条,然而实际应用场景中我们可能并没有如此规整的数据,知识库数据可能来自一个大段的文本,数据甚至是包含在一些千奇百怪的格式中的,如CSV、Word、PDF等。加载这些数据我们需要两个组件,一个是对应格式的解析器,另外就是针对大段文本的文本分割器。

下面例子代码中,我们从微软Word(docx)文件中解析数据,并使用递归分割器将原始数据分割成固定大小的块。

from langchain.globals import set_debug, set_verbose
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_chroma import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.chat_models import ChatOllama
from langchain_community.embeddings import HuggingFaceBgeEmbeddings

set_debug(True)
set_verbose(True)

# 创建Embedding模型对象
embedding_model = HuggingFaceBgeEmbeddings(
    model_name='../bge-large-zh-v1.5/',
    model_kwargs={'device': 'cpu'},
    encode_kwargs={'normalize_embeddings': True}
)

# 加载文档数据
documents = Docx2txtLoader("./狗头人之世界创建指南_电子版.docx").load()

# 分割文档
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=20,
    length_function=len,
    is_separator_regex=False,
)
documents = text_splitter.split_documents(documents)

# 将文档存入Chroma
vector_store = Chroma.from_documents(documents, embedding=embedding_model, persist_directory="./chroma_db")

# 创建Retriever
retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 3},
)

# 组织Prompt
message = """
Answer this question using the provided context only.

{question}

Context:
{context}
"""
prompt = ChatPromptTemplate.from_messages([("human", message)])

# 创建LLM对象
llm = ChatOllama(model='qwen2:7b-instruct-q5_K_M', temperature=1)

# 拼接Chain
rag_chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser()

q = input('输入你的问题:')
a = rag_chain.invoke(q)
print(a)

当然,使用RecursiveCharacterTextSplitter效果一定不如之前的结构化数据,这种方式只是针对大段未分割文本处理的一种妥协,如果有条件我们还是尽量将数据创建为结构化的形式,这样RAG才能够达到最好的效果。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap