使用LangChain和Vertex AI PaLM实现大型文档的可扩展问答
Published on
关于使用LLMs进行大型文档问答的简介
问答(QA)是一个关键的自然语言处理任务,旨在自动回答人类用自然语言提出的问题。尽管像PaLM这样的大型语言模型已经展示了令人印象深刻的问答能力,但它们受到可以在其令牌限制范围内适应的上下文数量的限制(通常为几千个令牌)。这对于可能跨越多个页面的大型文档的问答构成了挑战。
本文将探讨如何通过将LangChain框架与Google的Vertex AI PaLM API相结合,为大型文档构建可扩展的问答系统。我们将涵盖多种方法,包括:
- Stuffing - 将完整文档作为上下文
- Map-Reduce - 将文档拆分为块并并行处理
- Refine - 迭代地在文档块上细化答案
- Similarity Search - 使用向量嵌入来查找相关块
我们将比较每种方法的优点和局限性。完整代码可在此Colab笔记本中找到。
让我们在我们的50页示例文档上比较各种方法的指标:
方法 | 相关文档 | LLM调用次数 | 总令牌数 | 答案质量 |
---|---|---|---|---|
Stuffing | 3页 | 1 | 8432 | 良好 |
Map-Reduce | 50页 | 51 | 63019 | 可行 |
Refine | 50页 | 50 | 71209 | 良好 |
Similarity Search | 4页 | 5 | 5194 | 极佳 |
相似性搜索方法能够在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排行榜!