本地知识库QA与RAG | langchain实战课

221 阅读7分钟

相关实战课程理论概括

课程主要介绍的是本地知识库构建与本地检索,关于RAG的介绍比较片面。

Reference:

3. 用LangChain快速构建基于“易速鲜花”本地知识库的智能问答系统

16. 检索增强生成:通过RAG助力鲜花运营

1. langchain基于文档的问答功能实现

具体流程分为下面5步。

  1. Loading:文档加载器把Documents 加载为以LangChain能够读取的形式。
  2. Splitting:文本分割器把Documents 切分为指定大小的分割,我把它们称为“文档块”或者“文档片”。
  3. Storage:将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
  4. Retrieval:应用程序从存储中检索分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片)。
  5. Output:把问题和相似的嵌入片传递给语言模型(LLM),使用包含问题和检索到的分割的提示生成答案

2. RAG 的工作原理

RAG 的工作原理可以概括为几个步骤。

  1. 检索:对于给定的输入(问题),模型首先使用检索系统从大型文档集合中查找相关的文档或段落。这个检索系统通常基于密集向量搜索,例如ChromaDB、Faiss这样的向量数据库。
  2. 上下文编码:找到相关的文档或段落后,模型将它们与原始输入(问题)一起编码。
  3. 生成:使用编码的上下文信息,模型生成输出(答案)。这通常当然是通过大模型完成的。

本地知识库QA实现

0. 导入包

import os
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.document_loaders import TextLoader
from typing import Dict, List, Any
from langchain.embeddings.base import Embeddings
from pydantic.v1 import BaseModel
from volcenginesdkarkruntime import Ark
from langchain.text_splitter import RecursiveCharacterTextSplitter
import numpy as np
from langchain_community.vectorstores import Qdrant

1. 加载数据:导入文档

# 加载Documents
base_dir = "./OneFlower"  # 文档的存放目录
documents = []
for file in os.listdir(base_dir):
    # 构建完整的文件路径
    file_path = os.path.join(base_dir, file)
    if file.endswith(".pdf"):
        loader = PyPDFLoader(file_path)
        documents.extend(loader.load())
    elif file.endswith(".docx"):
        loader = Docx2txtLoader(file_path)
        documents.extend(loader.load())
    elif file.endswith(".txt"):
        loader = TextLoader(file_path)
        documents.extend(loader.load())

2. 分割文档

# 2.Split 将Documents切分成块以便后续进行嵌入和向量存储
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=10)
chunked_documents = text_splitter.split_documents(documents)

3. 向量数据库存储

  • 文本嵌入

    • 豆包嵌入模型
    class DoubaoEmbeddings(BaseModel, Embeddings):
        client: Ark = None
        api_key: str = ""
        model: str
        def __init__(self, **data: Any):
            super().__init__(**data)
            if self.api_key == "":
                self.api_key = os.environ["OPENAI_API_KEY"]
            self.client = Ark(
                base_url=os.environ["OPENAI_BASE_URL"],
                api_key=self.api_key
            )
        def embed_query(self, text: str) -> List[float]:
            """
            生成输入文本的 embedding.
            Args:
                texts (str): 要生成 embedding 的文本.
            Return:
                embeddings (List[float]): 输入文本的 embedding,一个浮点数值列表.
            """
            embeddings = self.client.embeddings.create(model=self.model, input=text)
            return embeddings.data[0].embedding
    
        def embed_documents(self, texts: List[str]) -> List[List[float]]:
            return [self.embed_query(text) for text in texts]
    
        class Config:
            arbitrary_types_allowed = True
    
    • 模型初始化
    '''嵌入模型初始化'''
    embeddingModel=DoubaoEmbeddings(
        model=os.environ["EMBEDDING_MODELEND"],
    )
    
    • (调用测试)测试文本嵌入向量的相似度
    '''测试embedding文本相似度'''
    # 生成文本嵌入
    text1 = "Hello, world! This is an example sentence."
    text2 = "Hello, world! This are some example sentences."
    embedding_1 = embeddingModel.embed_query(text1)
    embedding_2 = embeddingModel.embed_query(text2)
    
    print(f"Embedding dimension: {len(embedding_2)}")
    print(f"First few values: {embedding_2[:5]}")
    
    import numpy as np
    embedding_1 = np.array(embedding_1)
    embedding_2 = np.array(embedding_2)
    similarity = np.dot(embedding_1, embedding_2)
    print(similarity)
    
    
    • (进阶)缓存嵌入
    # 导入内存存储库,该库允许我们在RAM中临时存储数据
    from langchain.storage import InMemoryStore
    
    # 创建一个InMemoryStore的实例
    store = InMemoryStore()
    
    # 导入与嵌入相关的库。OpenAIEmbeddings是用于生成嵌入的工具,而CacheBackedEmbeddings允许我们缓存这些嵌入
    from langchain.embeddings import CacheBackedEmbeddings
    
    # 创建一个OpenAIEmbeddings的实例,这将用于实际计算文档的嵌入
    underlying_embeddings = DoubaoEmbeddings(
        model=os.environ["EMBEDDING_MODELEND"],
    )
    # 创建一个CacheBackedEmbeddings的实例。
    # 这将为underlying_embeddings提供缓存功能,嵌入会被存储在上面创建的InMemoryStore中。
    # 我们还为缓存指定了一个命名空间,以确保不同的嵌入模型之间不会出现冲突。
    embedder = CacheBackedEmbeddings.from_bytes_store(
        underlying_embeddings,  # 实际生成嵌入的工具
        store,  # 嵌入的缓存位置
        namespace=underlying_embeddings.model,  # 嵌入缓存的命名空间
    )
    
    # 使用embedder为两段文本生成嵌入。
    # 结果,即嵌入向量,将被存储在上面定义的内存存储中。
    embeddings = embedder.embed_documents(["你好", "智能鲜花客服"])
    print(len(embeddings[1]))
    
  • 存储在向量数据库中

    location=":memory:" 与前面缓存嵌入原理类似

    vectorstore = Qdrant.from_documents(
        documents=chunked_documents,  # 以分块的文档
        embedding=DoubaoEmbeddings(
            model=os.environ["EMBEDDING_MODELEND"],
        ),  # 用OpenAI的Embedding Model做嵌入
        location=":memory:",  # in-memory 存储
        collection_name="my_documents",
    )  # 指定collection_name
    

4. 相关信息的获取:Retrieval

使用多查询检索器MultiQueryRetriever

MultiQueryRetriever通过使用 LLM 从不同角度为给定的用户输入查询生成多个查询,可以自动执行提示调整过程

# 4. Retrieval 准备模型和Retrieval链
import logging  # 导入Logging工具
from langchain_openai import ChatOpenAI  # ChatOpenAI模型
from langchain.retrievers.multi_query import (
    MultiQueryRetriever,
)  # MultiQueryRetriever工具
from langchain.chains import RetrievalQA  # RetrievalQA链

# 设置Logging
logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)

# 实例化一个大模型工具 - OpenAI的GPT-3.5
llm = ChatOpenAI(model=os.environ["LLM_MODELEND"], temperature=0)

# 实例化一个MultiQueryRetriever
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(), llm=llm
)

5. 生成问答:构建QA链调用retriever,检索提问

# 实例化一个RetrievalQA链
qa_chain = RetrievalQA.from_chain_type(llm, retriever=retriever_from_llm)
# RetrievalQA链 - 读入问题,生成答案
result = qa_chain({"query": question})
print(result)

最终结果示例

INFO:langchain.retrievers.multi_query:Generated queries: ['1. What are the characteristics of a rose?', '2. How is a rose defined?', '3. What constitutes a rose?']
{'query': 'What is a rose?', 'result': "I don't know."}
INFO:langchain.retrievers.multi_query:Generated queries: ['1. What is a rose?', '2. Different types of roses.', '3. Characteristics of roses.']
{'query': 'rose', 'result': "I don't know."}

结论:并不理想。

解决方案:

  1. 减少载入数据库的文档,缩小检索范围后能回答到。

    • 修改前面文档载入代码,只载入“花语大全.txt”

    • 快速载入txt完成整个检索的demo

    embeddings = DoubaoEmbeddings(
        model=os.environ["EMBEDDING_MODELEND"],
    )
    
    # 导入文档加载器模块,并使用TextLoader来加载文本文件
    from langchain_community.document_loaders import TextLoader
    from langchain_openai import ChatOpenAI  # ChatOpenAI模型
    
    loader = TextLoader("./OneFlower/花语大全.txt", encoding="utf8")
    
    # 使用VectorstoreIndexCreator来从加载器创建索引
    from langchain.indexes import VectorstoreIndexCreator
    
    index = VectorstoreIndexCreator(embedding=embeddings).from_loaders([loader])
    
    llm = ChatOpenAI(model=os.environ["LLM_MODELEND"], temperature=0)
    
    # 定义查询字符串, 使用创建的索引执行查询
    query = "玫瑰花的花语是什么?"
    result = index.query(llm=llm, question=query)
    print(result)  # 打印查询结果
    
    ##显示调用,使用不同的text splitter
    ##text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
    ##from langchain_community.vectorstores import Qdrant
    
    ##index_creator = VectorstoreIndexCreator(
    ##    # vectorstore_cls=Qdrant,
    ##    embedding=embeddings,
    ##    text_splitter=CharacterTextSplitter(chunk_size=1000, chunk_overlap=0),
    ##)
    
INFO:langchain.retrievers.multi_query:Generated queries: ['1. What is a rose?', '2. Different types of roses.', '3. Characteristics of roses.']
{'query': 'rose', 'result': 'Love, Passion, Beauty'}
  1. 更换嵌入模型

使用开源嵌入模型完成本地知识库QA

BAAI的嵌入模型性能较优,有多种版本的模型:不同体量、支持中英文

本次使用bge-small-zh-v1.5

尝试使用bge-large在当前2core cpu设备不是很好运行

image.png

  • 下载并使用示例

    from langchain_community.embeddings import HuggingFaceBgeEmbeddings
    # 选择模型
    model_name = "BAAI/bge-small-zh-v1.5"
    model_kwargs = {"device": "cpu"}
    encode_kwargs = {"normalize_embeddings": True}
    
    # 初始化嵌入模型
    embeddingModel = HuggingFaceBgeEmbeddings(
        model_name=model_name,
        model_kwargs=model_kwargs,
        encode_kwargs=encode_kwargs,
        query_instruction="为这个句子生成表示以用于检索相关文章:"
    )
    
    
    # 生成文本嵌入
    text1 = "Hello, world! This is an example sentence."
    text2 = "Hello, world! This are some example sentences."
    embedding_1 = embeddingModel.embed_query(text1)
    embedding_2 = embeddingModel.embed_query(text2)
    
    print(f"Embedding dimension: {len(embedding_2)}")
    print(f"First few values: {embedding_2[:5]}")
    
    import numpy as np
    embedding_1 = np.array(embedding_1)
    embedding_2 = np.array(embedding_2)
    similarity = np.dot(embedding_1, embedding_2)
    print(similarity)
    
  • 修改前文整体QA流程embbeding载入代码

print("start embedding to vetcorstore")
# 3.Store 将分割嵌入并存储在矢量数据库Qdrant中
vectorstore = Qdrant.from_documents(
    documents=chunked_documents,  # 以分块的文档
    embedding=HuggingFaceBgeEmbeddings(
        model_name="BAAI/bge-small-zh-v1.5",
        # model_kwargs=model_kwargs,
        encode_kwargs={"normalize_embeddings": True},
        # query_instruction="为这个句子生成表示以用于检索相关文章:"
        ),
    location=":memory:",  # in-memory 存储
    collection_name="my_documents",
)  # 指定collection_name
print("finish embedding to vetcorstore")
  • 结果展示

在完整包含不同文件的知识库中,能够正确检索。

INFO:langchain.retrievers.multi_query:Generated queries: ['What is a rose?', 'Describe a rose.', 'How is a rose different from other flowers?']
{'query': 'Rose', 'result': 'Love, Passion, Beauty'}

待更新:RAG 进阶

从本地QA到RAG

外部知识增强RAG(google search)