开源 AI 食谱文档

使用 Hugging Face Zephyr 和 LangChain 为 GitHub Issues 构建简单的 RAG

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

Open In Colab

使用 Hugging Face Zephyr 和 LangChain 为 GitHub Issues 构建简单的 RAG

作者:Maria Khalusova

本笔记演示了如何使用 HuggingFaceH4/zephyr-7b-beta 模型和 LangChain,为一个项目的 GitHub issues 快速构建一个 RAG (检索增强生成) 系统。

什么是 RAG?

RAG 是一种流行的方法,用于解决强大的大语言模型(LLM)因特定内容未包含在其训练数据中而无法感知,或即使见过也可能产生幻觉的问题。这些特定内容可能是专有的、敏感的,或者像本例中一样,是近期且经常更新的。

如果你的数据是静态的并且不经常变化,你可能会考虑微调一个大模型。然而,在许多情况下,微调成本高昂,并且当重复进行时(例如为了解决数据漂移问题),会导致“模型漂移”。这是指模型的行为发生了不期望的改变。

RAG (检索增强生成) 不需要模型微调。相反,RAG 通过向 LLM 提供从相关数据中检索到的额外上下文来工作,以便它能生成一个信息更全面的响应。

这是一个简单的图示

RAG diagram

  • 外部数据通过一个单独的嵌入模型转换成嵌入向量,这些向量被保存在数据库中。嵌入模型通常较小,因此定期更新嵌入向量比微调一个模型更快、更便宜、更容易。

  • 同时,由于不需要微调,当有更强大的 LLM 可用时,你可以自由地替换它,或者如果你需要更快的推理速度,也可以切换到一个更小的蒸馏版本。

让我们用一个开源的 LLM、嵌入模型和 LangChain 来演示如何构建一个 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 个人访问令牌 来访问 GitHub API。

from getpass import getpass

ACCESS_TOKEN = getpass("YOUR_GITHUB_PERSONAL_TOKEN")

接下来,我们将加载 huggingface/peft 仓库中的所有 issues。

  • 默认情况下,pull requests 也被视为 issues,这里我们通过设置 include_prs=False 将它们排除在数据之外。
  • 设置 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()

单个 GitHub issue 的内容可能比嵌入模型能接受的输入要长。如果我们想嵌入所有可用的内容,我们需要将文档分块成适当大小的片段。

最常见和直接的分块方法是定义一个固定的块大小以及它们之间是否应该有重叠。在块之间保留一些重叠可以让我们保留块之间的某些语义上下文。对于通用文本,推荐的分词器是 RecursiveCharacterTextSplitter,我们这里就用它。

from langchain.text_splitter import RecursiveCharacterTextSplitter

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

chunked_docs = splitter.split_documents(docs)

创建嵌入向量和检索器

现在文档都已是适当的大小,我们可以用它们的嵌入向量创建一个数据库了。

为了创建文档块的嵌入向量,我们将使用 HuggingFaceEmbeddingsBAAI/bge-base-en-v1.5 嵌入模型。Hub 上有许多其他可用的嵌入模型,你可以通过查看 大规模文本嵌入基准(MTEB)排行榜来关注表现最好的模型。

为了创建向量数据库,我们将使用 FAISS,一个由 Facebook AI 开发的库。这个库为密集向量提供了高效的相似性搜索和聚类,这正是我们所需要的。FAISS 是目前在海量数据集中进行最近邻搜索最常用的库之一。

我们将通过 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,一个体积小但功能强大的模型。

由于每周都有许多模型发布,你可能想用最新最好的模型来替代这个模型。跟踪开源 LLM 的最佳方式是查看开源 LLM 排行榜

为了加快推理速度,我们将加载模型的量化版本。

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 链所需的所有部分。

首先,使用加载的模型及其分词器创建一个文本生成管道(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_chain 与检索器结合起来,创建一个 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 的下一次迭代,可能值得包括文档的嵌入向量。

< > 在 GitHub 上更新