用 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