Want to Become a Sponsor? Contact Us Now!🎉

LLM
使用LangChain和Vertex AI PaLM在大型文档上构建可扩展的问答系统

使用LangChain和Vertex AI PaLM实现大型文档的可扩展问答

Published on

本文探讨如何通过将LangChain框架与Google的Vertex AI PaLM API相结合,为大型文档构建可扩展的问答系统。

关于使用LLMs进行大型文档问答的简介

问答(QA)是一个关键的自然语言处理任务,旨在自动回答人类用自然语言提出的问题。尽管像PaLM这样的大型语言模型已经展示了令人印象深刻的问答能力,但它们受到可以在其令牌限制范围内适应的上下文数量的限制(通常为几千个令牌)。这对于可能跨越多个页面的大型文档的问答构成了挑战。

本文将探讨如何通过将LangChain框架与Google的Vertex AI PaLM API相结合,为大型文档构建可扩展的问答系统。我们将涵盖多种方法,包括:

  1. Stuffing - 将完整文档作为上下文
  2. Map-Reduce - 将文档拆分为块并并行处理
  3. Refine - 迭代地在文档块上细化答案
  4. Similarity Search - 使用向量嵌入来查找相关块

我们将比较每种方法的优点和局限性。完整代码可在此Colab笔记本中找到。

Anakin AI - The Ultimate No-Code AI App Builder

让我们在我们的50页示例文档上比较各种方法的指标:

方法相关文档LLM调用次数总令牌数答案质量
Stuffing3页18432良好
Map-Reduce50页5163019可行
Refine50页5071209良好
Similarity Search4页55194极佳

相似性搜索方法能够在10倍较少的页面、LLM调用次数和令牌数下找到高质量的答案,与完整文档方法相比。在更大的数据集上,这种差距将进一步扩大。

第1步:设置LangChain以实现大型文档的问答

首先,安装所需的依赖项,包括Vertex AI SDK、LangChain和ChromaDB:

!pip install google-cloud-aiplatform langchain==0.0.323 chromadb==0.3.26 pypdf

导入关键库:

from langchain.document_loaders import PyPDFLoader  
from langchain.llms import VertexAI
from langchain.chains.question_answering import load_qa_chain
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import Chroma
from langchain.embeddings import VertexAIEmbeddings

加载PaLM文本模型和嵌入模型:

vertex_llm_text = VertexAI(model_name="text-bison@001")
vertex_embeddings = VertexAIEmbeddings(model_name="textembedding-gecko@001")  

第2步:加载文档

对于本示例,我们将使用一份关于MLOps的PDF白皮书。下载并使用PyPDFLoader加载文本:

pdf_url = "https://services.google.com/fh/files/misc/practitioners_guide_to_mlops_whitepaper.pdf"
pdf_loader = PyPDFLoader(pdf_file)
pages = pdf_loader.load_and_split()

这将白皮书拆分为我们可以用作基本文档的页面。

第3步:Stuffing文档

最简单的方法是将全文本内容放入LLM的上下文窗口中。设置一个提示模板:

prompt_template = """尽可能准确地回答问题,使用提供的上下文。 
如果答案不在上下文中,请说“上下文中没有可用的答案”。\n\n
上下文:\n {context}?\n
问题:\n {question} \n
答案:
"""
 
prompt = PromptTemplate(
    template=prompt_template, input_variables=["context", "question"]
)

加载Stuffing QA链:

stuff_chain = load_qa_chain(vertex_llm_text, chain_type="stuff", prompt=prompt)

然后在一个问题上运行它:

question = "什么是实验?"
context = "\n".join(str(p.page_content) for p in pages[:7])
stuff_answer = stuff_chain(
    {"input_documents": pages[7:10], "question": question}, return_only_outputs=True
)

这样可以工作,但受到模型可以处理的上下文大小的限制(几千个令牌)。将完整的50页文档装箱时,将达到这一限制:

try:
    print(stuff_chain(
        {"input_documents": pages[7:], "question": question}, 
        return_only_outputs=True))
except Exception as e:  
    print("该代码失败,因为它无法在如此巨大的上下文上运行推断")

第4步:Map-Reduce

要扩展到更大的文档,我们可以将它们拆分为块,对每个块运行QA,然后整合结果。LangChain提供了一个map-reduce链来处理此过程。

首先定义单独的问题和合并提示:

question_prompt_template = """
尽可能准确地回答问题,使用提供的上下文。\n\n
上下文:\n {context} \n
问题:\n {question} \n  
答案:
"""
question_prompt = PromptTemplate(
    template=question_prompt_template, input_variables=["context", "question"]
)
 
combine_prompt_template = """根据提取的内容和问题,创建最终答案。\n
如果答案不在上下文中,请说“上下文中没有可用的答案”。\n\n
总结:\n {summaries}?\n
问题:\n {question} \n
答案:  
"""
combine_prompt = PromptTemplate(
    template=combine_prompt_template, input_variables=["summaries", "question"]
)

加载map-reduce链并指定问题和合并提示:

map_reduce_chain = load_qa_chain(
    vertex_llm_text, 
    chain_type="map_reduce",
    return_intermediate_steps=True,
    question_prompt=question_prompt,
    combine_prompt=combine_prompt,
)
map_reduce_outputs = map_reduce_chain({"input_documents": pages, "question": question})

这将对每个页面进行独立的QA,然后在最后一步中合并结果。我们可以检查中间结果:

for doc, out in zip(
    map_reduce_outputs["input_documents"], map_reduce_outputs["intermediate_steps"]
):
    print(f"页面: {doc.metadata['page']}")
    print(f"答案: {out}")

map-reduce方法适用于大型文档,并提供了一些关于信息来源的见解。然而,有时候在最后的合并步骤中会丢失信息。

第5步:优化

优化方法旨在通过迭代优化答案来减少信息丢失。它从第一个块上的初始答案开始,然后在每个后续块上进行优化。

定义一个包含现有答案和新上下文的优化提示:

refine_prompt_template = """
原始问题是:\n {question} \n
提供的答案是:\n {existing_answer}\n  
如果需要,请通过以下上下文来优化现有答案:\n {context_str} \n
给定提取的内容和问题,创建一个最终答案。
如果答案不在上下文中,请说“上下文中无法找到答案”。\n\n  
"""
refine_prompt = PromptTemplate(
    input_variables=["question", "existing_answer", "context_str"],  
    template=refine_prompt_template,
)

加载一个优化链:

refine_chain = load_qa_chain(
    vertex_llm_text,
    chain_type="refine", 
    return_intermediate_steps=True,
    question_prompt=initial_question_prompt,
    refine_prompt=refine_prompt,
)

在整个文档上运行它:

refine_outputs = refine_chain({"input_documents": pages, "question": question})

检查中间步骤以查看答案的优化情况:

for doc, out in zip(
    refine_outputs["input_documents"], refine_outputs["intermediate_steps"]
):
    print(f"页面: {doc.metadata['page']}")  
    print(f"答案: {out}")

优化方法有助于在整个文档中保留信息。但仍然需要线性地处理整个文档。

第6步:相似性搜索

为了提高效率,我们可以首先使用嵌入向量来找到与给定问题最相关的块。这样就避免了需要处理整个文档。

使用ChromaDB创建文档块的向量索引:

vector_index = Chroma.from_documents(pages, vertex_embeddings).as_retriever()

检索与问题最相关的块:

docs = vector_index.get_relevant_documents(question)

仅对这些相关块运行map-reduce链:

map_reduce_embeddings_outputs = map_reduce_chain(
    {"input_documents": docs, "question": question}
)
print(map_reduce_embeddings_outputs["output_text"])  

这样可以在仅处理完整个文档的一个小子集时找到高质量的答案。相似性搜索方法在准确性和效率之间提供了最佳平衡。

结论

在本文中,我们使用LangChain和Vertex AI PaLM展示了几种处理大型文档的问答方法。虽然简单的填充方法适用于小型文档,但需要使用map-reduce和优化方法来处理更大的数据。

然而,最高效和最有效的方法是首先使用向量相似性搜索,找到与给定问题最相关的段落。这样可以最小化LLM需要处理的文本量,同时仍然产生高质量的答案。

相似性搜索、LangChain的QA链以及像PaLM这样的强大LLM的结合,使得能够构建可扩展的大型文档集上的问答系统成为可能。您可以从本笔记本中的完整代码开始。

想了解最新的LLM相关新闻吗?请查看最新的LLM排行榜

Anakin AI - The Ultimate No-Code AI App Builder