Open-Source AI Cookbook documentation

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

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Open In Colab

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

作者: Maria Khalusova

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

什么是 RAG

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

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

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

这里是一个简单说明:

RAG diagram

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

  • 与此同时,由于不需要微调,给了你极大的自由度去切换选择你自己的更强的 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

准备数据

在这个例子中,我们会从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 CharacterTextSplitter

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

chunked_docs = splitter.split_documents(docs)

创建嵌入和检索器

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

为了创建文档块嵌入,我们将会使用 HuggingFaceEmbeddingsBAAI/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