Responder Perguntas Escaláveis sobre Documentos Grandes com LangChain e Vertex AI PaLM
Published on
![Este artigo explora como construir um sistema de resposta a perguntas escalável para documentos grandes combinando o framework LangChain com a API Vertex AI PaLM do Google.](/_next/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Flynn-mikami%2FImages%2Fmain%2Fquestion-answering-document-retrieval-llm.jpeg%3Fx-oss-process%3Dimage%2Fresize%2Climit_0%2Cm_fill%2Cw_1536%2Ch_1024%2Fquality%2Cq_100&w=3840&q=75)
Introdução ao Responder Perguntas sobre Documentos Grandes com LLMs
Responder perguntas (QA) é uma tarefa fundamental de processamento de linguagem natural que tem como objetivo responder automaticamente as perguntas feitas por seres humanos em linguagem natural. Embora os grandes modelos de linguagem (LLMs) como o PaLM tenham mostrado capacidades impressionantes de QA, eles são limitados pela quantidade de contexto que pode ser incluída em seu limite de tokens (geralmente alguns milhares de tokens). Isso apresenta um desafio para o QA em documentos grandes que podem abranger várias páginas.
Neste artigo, exploraremos como construir um sistema de QA escalável para documentos grandes combinando o framework LangChain com a API Vertex AI PaLM do Google. Abordaremos vários métodos, incluindo:
- Stuffing - Inserir o documento completo como contexto
- Map-Reduce - Dividir documentos em partes e processar em paralelo
- Refinamento - Refinar iterativamente uma resposta em partes do documento
- Busca por Similaridade - Utilizar incorporações vetoriais para encontrar partes relevantes
Compararemos as vantagens e limitações de cada abordagem. O código completo está disponível neste notebook Colab.
Vamos comparar as métricas de cada método em nosso documento de amostra de 50 páginas:
Método | Documentos Relevantes | Chamadas LLM | Total de Tokens | Qualidade da Resposta |
---|---|---|---|---|
Stuffing | 3 páginas | 1 | 8432 | Boa |
Map-Reduce | 50 páginas | 51 | 63019 | Ok |
Refinamento | 50 páginas | 50 | 71209 | Boa |
Busca por Similaridade | 4 páginas | 5 | 5194 | Excelente |
A abordagem de busca por similaridade é capaz de encontrar uma resposta de alta qualidade com 10 vezes menos páginas, chamadas LLM e tokens em comparação com os métodos que utilizam o documento completo. Essa diferença se ampliaria ainda mais em conjuntos de dados maiores.
Passo 1. Configurar o LangChain para Responder Perguntas sobre Documentos Grandes
Primeiro, instale as dependências necessárias, incluindo o SDK Vertex AI, LangChain e ChromaDB:
!pip install google-cloud-aiplatform langchain==0.0.323 chromadb==0.3.26 pypdf
Importe as bibliotecas principais:
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
Carregue o modelo de texto PaLM e o modelo de incorporações:
vertex_llm_text = VertexAI(model_name="text-bison@001")
vertex_embeddings = VertexAIEmbeddings(model_name="textembedding-gecko@001")
Passo 2. Carregamento dos Documentos
Para este exemplo, usaremos um whitepaper em PDF sobre MLOps. Faça o download e carregue o texto usando o 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()
Isso divide o PDF em páginas que podemos usar como base para os documentos.
Passo 3. Stuffing dos Documentos
A abordagem mais simples é inserir o texto completo do documento como contexto na janela do LLM. Configure um modelo de prompt:
prompt_template = """Responda a pergunta o mais preciso possível usando o contexto fornecido.
Se a resposta não estiver contida no contexto, diga "resposta não disponível no contexto" \n\n
Contexto: \n {context}?\n
Pergunta: \n {question} \n
Resposta:
"""
prompt = PromptTemplate(
template=prompt_template, input_variables=["context", "question"]
)
Carregue uma cadeia de QA de stuffing:
stuff_chain = load_qa_chain(vertex_llm_text, chain_type="stuff", prompt=prompt)
Em seguida, execute-a em uma pergunta:
question = "O que é Experimentação?"
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
)
Isso funciona, mas é limitado pelo tamanho do contexto que o modelo pode manipular (alguns milhares de tokens). Inserir o documento completo de 50 páginas atinge esse limite:
try:
print(stuff_chain(
{"input_documents": pages[7:], "question": question},
return_only_outputs=True))
except Exception as e:
print("O código falhou porque não poderá executar inferência em um contexto tão grande")
Passo 4. Map-Reduce
Para dimensionar a documentos maiores, podemos dividi-los em partes, executar o QA em cada parte e, em seguida, agregar os resultados. O LangChain fornece uma cadeia de map-reduce para lidar com isso.
Primeiro, defina modelos de prompt separados para a pergunta e a combinação:
question_prompt_template = """
Responda a pergunta o mais preciso possível usando o contexto fornecido. \n\n
Contexto: \n {context} \n
Pergunta: \n {question} \n
Resposta:
"""
question_prompt = PromptTemplate(
template=question_prompt_template, input_variables=["context", "question"]
)
combine_prompt_template = """Dadas o conteúdo extraído e a pergunta, crie uma resposta final.
Se a resposta não estiver contida no contexto, diga "resposta não disponível no contexto". \n\n
Sumários: \n {summaries}?\n
Pergunta: \n {question} \n
Resposta:
"""
combine_prompt = PromptTemplate(
template=combine_prompt_template, input_variables=["summaries", "question"]
)
Carregue a cadeia de map-reduce especificando os modelos de prompt para a pergunta e a combinação:
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,
)
Execute-o no conjunto completo de documentos:
map_reduce_outputs = map_reduce_chain({"input_documents": pages, "question": question})
Isso roda o QA em cada página individualmente e depois combina os resultados em uma etapa final. Podemos inspecionar os resultados intermediários:
for doc, out in zip(
map_reduce_outputs["input_documents"], map_reduce_outputs["intermediate_steps"]
):
print(f"Página: {doc.metadata['page']}")
print(f"Resposta: {out}")
A abordagem map-reduce escalonada para grandes documentos e fornece uma visão de onde a informação está vindo. No entanto, às vezes, a informação pode ser perdida na etapa de combinação final.
Etapa 5. Refinar
A abordagem de refinar tem como objetivo mitigar a perda de informação refinindo iterativamente uma resposta. Ela começa com uma resposta inicial no primeiro chunk e depois a refina com cada chunk subsequente.
Defina uma prompt de refinação que incorpora a resposta existente e o novo contexto:
refine_prompt_template = """
A pergunta original é: \n {question} \n
A resposta fornecida é: \n {existing_answer}\n
Refine a resposta existente, se necessário, com o seguinte contexto: \n {context_str} \n
Dado o conteúdo extraído e a pergunta, crie uma resposta final.
Se a resposta não estiver contida no contexto, diga "resposta não disponível no contexto". \n\n
"""
refine_prompt = PromptTemplate(
input_variables=["question", "existing_answer", "context_str"],
template=refine_prompt_template,
)
Carregue uma cadeia de refinamento:
refine_chain = load_qa_chain(
vertex_llm_text,
chain_type="refine",
return_intermediate_steps=True,
question_prompt=initial_question_prompt,
refine_prompt=refine_prompt,
)
Execute-o no documento completo:
refine_outputs = refine_chain({"input_documents": pages, "question": question})
Inspecione as etapas intermediárias para ver a resposta sendo refinada:
for doc, out in zip(
refine_outputs["input_documents"], refine_outputs["intermediate_steps"]
):
print(f"Página: {doc.metadata['page']}")
print(f"Resposta: {out}")
A abordagem de refinar ajuda a preservar informações em todo o documento. Mas ainda requer o processamento linear do documento inteiro.
Etapa 6. Busca de Similaridade
Para melhorar a eficiência, podemos primeiro usar embeddings para encontrar apenas os chunks mais relevantes para uma determinada pergunta. Isso evita a necessidade de processar o documento completo.
Crie um índice vetor dos chunks do documento usando o ChromaDB:
vector_index = Chroma.from_documents(pages, vertex_embeddings).as_retriever()
Recupere os chunks mais relevantes para a pergunta:
docs = vector_index.get_relevant_documents(question)
Execute a cadeia de map-reduce apenas nesses chunks relevantes:
map_reduce_embeddings_outputs = map_reduce_chain(
{"input_documents": docs, "question": question}
)
print(map_reduce_embeddings_outputs["output_text"])
Isso encontra uma resposta de alta qualidade precisando apenas processar um pequeno subconjunto do documento completo. A abordagem de busca de similaridade fornece o melhor equilíbrio entre precisão e eficiência.
Conclusão
Neste artigo, demonstramos várias abordagens para responder perguntas em documentos grandes usando o LangChain e o Vertex AI PaLM. Embora o preenchimento simples possa funcionar para documentos pequenos, as abordagens de map-reduce e refinar são necessárias para escalar para dados maiores.
No entanto, o método mais eficiente e eficaz é usar primeiro a busca por similaridade de vetores para encontrar apenas os trechos mais relevantes para uma determinada pergunta. Isso minimiza a quantidade de texto que o LLM precisa processar enquanto ainda produz respostas de alta qualidade.
A combinação de busca por similaridade, cadeias de QA do LangChain e poderosos LLMs como o PaLM permitem a construção de sistemas de resposta a perguntas escaláveis em coleções de documentos grandes. Você pode começar com o código completo neste notebook.
Quer saber as últimas notícias do LLM? Confira as últimas classificações do LLM!