#
用 Hugging Face Zephyr 和 LangChain 针对 Github issues 构建简单的 RAG

作者: Maria Khalusova

本 notebook 展示了如何使用 `HuggingFaceH4/zephyr-7b-beta` 模型和 LangChain 快速构建一个针对项目 GitHub issues 的简单 RAG。

什么是 RAG

RAG 是一个很流行的方法，用来解决强大的 LLM 不知道具体内容的问题，因为具体内容不在其训练数据中，或者当它看到它之前时产生幻觉。这样的具体内容可能是专有的、敏感的，或者，就像这个例子中一样，是最近的和更新的。

如果你的数据集是静态的和不需要定期更新的，那么你可能会考虑微调一个大模型。但在大多数情况下，微调模型花费巨大并且重复去微调的话(比如，处理数据漂移的时候)，可能会导致“模型偏移”。这种情况模型行为的变换就不是设计的那样了。

RAG (检索增强生成) 并不需要模型微调。相反， RAG 通过提供检索到的额外的相关内容喂给 LLM 以此来获得更好的回答。

这里是一个简单说明：

- 额外的数据通过独立的嵌入模型会被转化为嵌入向量，这些向量会储存在向量数据库里。嵌入模型通常都比较小，因此在常规偏差上更新嵌入向量相比于微调模型会更快，便宜，和简单。

- 与此同时，由于不需要微调，给了你极大的自由度去切换选择你自己的更强的 LLM，或者对于更快速的推理去切换更小的蒸馏模型。

让我们用开源的 LLM ，嵌入模型，和 LangChain 快速构建一个针对项目 GitHub issues 的简单 RAG。

首先安装相关依赖：

    !pip install -q torch transformers accelerate bitsandbytes transformers sentence-transformers faiss-gpu

    # If running in Google Colab, you may need to run this cell to make sure you're using UTF-8 locale to install LangChain
    import locale

    locale.getpreferredencoding = lambda: "UTF-8"

    !pip install -q langchain langchain-community

##
准备数据

在这个例子中，我们会从PEFT 库的仓库加载所有的 issues（包括现在开放的和已经关闭的）。

首先，你需要获取一个 GitHub 个人权限 token 来访问 GitHub API。

    from getpass import getpass

    ACCESS_TOKEN = getpass("YOUR_GITHUB_PERSONAL_TOKEN")

下一步，我们将会加载 huggingface/peft 仓库中所有的 issues:

- 默认情况下， PR 也被认定为 issues，这里我们要设置 `include_prs=False` 来排除 PR。
- 设置 `state = "all"` 意味着我们会把开放和已经关闭的 issues 都加载了。

    from langchain.document_loaders import GitHubIssuesLoader

    loader = GitHubIssuesLoader(repo="huggingface/peft", access_token=ACCESS_TOKEN, include_prs=False, state="all")

    docs = loader.load()

个人仓库的 issues 内容可能会长于一个嵌入模型可以最为输入处理的长度。如果我们想要嵌入所有可用的内容，我们需要把文档分割成适当大小的块。

最普通直接的切块方法就是定义一个固定的块大小，以及判断块之间是否加入重叠。保存一些块之间的重叠允许我们去保存一些语义上下文。

其他方法通常更复杂，会考虑到文档的结构和上下文。例如，人们可能希望根据句子或段落来分割文档，然而，固定大小的分块在大多数常见情况下都表现得很好，所以我们将在这里采用这种方法。

    from langchain.text_splitter import RecursiveCharacterTextSplitter

    splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=30)

    chunked_docs = splitter.split_documents(docs)

##
创建嵌入和检索器

现在所有的文档都设置成立合适的大小，我们可以用他们的嵌入创建一个数据集了。

为了创建文档块嵌入，我们将会使用 `HuggingFaceEmbeddings` 和 `BAAI/bge-base-en-v1.5` 嵌入模型。在 Hub 上有许多其他的嵌入模型可用，你也可以查看 Massive Text Embedding Benchmark (MTEB) Leaderboard 关注表现最好的模型。

为了创建向量数据库，我们将会使用 `FAISS` 库。这个库提供高效的相似度搜索和稠密向量的聚类，正是我们需要的。FAISS 目前是大规模数据集上 NN 搜索最常用的库之一。

我们通过 LangChain 的 API 来获取嵌入模型和 FAISS 向量数据库。

    from langchain.vectorstores import FAISS
    from langchain.embeddings import HuggingFaceEmbeddings

    db = FAISS.from_documents(chunked_docs, HuggingFaceEmbeddings(model_name="BAAI/bge-base-en-v1.5"))

我们需要一种方式，来返回给定无结构的查询所需要的文档。针对这个，我们会使用 `as_retriever` 方法，使用 `db` 作为支柱：

- `search_type="similarity"` 意味着我们会执行查询和文档之间的相似度搜索
- `search_kwargs={'k': 4}` 指示我们指定返回的最高的 4 个结果

    retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": 4})

向量数据库和检索器现在设置好了，下一步我们需要设置好链中的下一块 - 模型。

##
加载量化模型

针对本例，我们选择 `HuggingFaceH4/zephyr-7b-beta`, 一个小而强大的模型。

随着每周都会出好多模型，你可能会想要替换这个模型到最新的最好的模型。最好的方式是查看 Open-source LLM leaderboard。

为了推理更快，我们将加载模型的量化版本：

    import torch
    from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

    model_name = "HuggingFaceH4/zephyr-7b-beta"

    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16
    )

    model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=bnb_config)
    tokenizer = AutoTokenizer.from_pretrained(model_name)

##
设置 LLM 链

最后，我们有了所有的需要设置的 LLM 链的部分。

首先，使用加载的模型和他的tokenizer创建一个文本生成的流水线(pipeline)

下一步，创建一个提示模板-这个应该遵循模型的格式，所以如果你替换了模型检查点，确保使用合适的格式。

    from langchain.llms import HuggingFacePipeline
    from langchain.prompts import PromptTemplate
    from transformers import pipeline
    from langchain_core.output_parsers import StrOutputParser

    text_generation_pipeline = pipeline(
        model=model,
        tokenizer=tokenizer,
        task="text-generation",
        temperature=0.2,
        do_sample=True,
        repetition_penalty=1.1,
        return_full_text=True,
        max_new_tokens=400,
    )

    llm = HuggingFacePipeline(pipeline=text_generation_pipeline)

    prompt_template = """
    <|system|>
    Answer the question based on your knowledge. Use the following context to help:

    {context}

    </s>
    <|user|>
    {question}
    </s>
    <|assistant|>

     """

    prompt = PromptTemplate(
        input_variables=["context", "question"],
        template=prompt_template,
    )

    llm_chain = prompt | llm | StrOutputParser()

注意：你也可以使用 `tokenizer.apply_chat_template` 转换列表消息为合适聊天格式的字符串（字典也行 `{'role': 'user', 'content': '(...)'}`）

最后，我们需要将 LLM 链与检索器(retriever)结合起来创建一个 RAG 链。我们将原始问题以及检索到的文档上下文传递到最后生成步骤：

    from langchain_core.runnables import RunnablePassthrough

    retriever = db.as_retriever()

    rag_chain = {"context": retriever, "question": RunnablePassthrough()} | llm_chain

##
比较结果

让我们看看对于特定领域库的问题不同的 RAG 的生成的回答。

    question = "How do you combine multiple adapters?"

首先，让我们看看仅仅通过模型自身不加检索内容能得到什么答案:

    llm_chain.invoke({"context": "", "question": question})

可以看到，模型将这个问题解释为关于物理电脑适配器的问题，而在 PEFT 的背景下，“适配器”指的是 LoRA 适配器。 让我们看看添加 GitHub issues 的上下文是否有助于模型给出更相关的答案：

    rag_chain.invoke(question)

我们可以看到，加入检索的信息后，同一个模型能够对于特定库的问题给出更准确、更相关的答案。

值得注意的是，将多个适配器结合用于推理的功能已经被添加到库中，人们可以在文档中找到这些信息，因此在下一个迭代的RAG中，包含文档嵌入可能是有价值的。

< > Update on GitHub