Respuestas a preguntas escalables sobre documentos grandes con LangChain y Vertex AI PaLM
Published on
Introducción a las respuestas a preguntas sobre documentos grandes con modelos de lenguaje
La respuesta a preguntas (QA, por sus siglas en inglés) es una tarea importante en el procesamiento del lenguaje natural que tiene como objetivo responder automáticamente preguntas formuladas por los seres humanos en lenguaje natural. Si bien los modelos de lenguaje grandes (LLMs, por sus siglas en inglés) como PaLM han demostrado capacidades impresionantes de QA, están limitados por la cantidad de contexto que pueden contener dentro de su límite de tokens (normalmente unos pocos miles de tokens). Esto presenta un desafío para el QA en documentos grandes que pueden abarcar muchas páginas.
En este artículo, exploraremos cómo construir un sistema de QA escalable para documentos grandes combinando el marco de trabajo LangChain con la API PaLM de Vertex AI de Google. Cubriremos varios métodos, incluyendo:
- Stuffing: incluir el documento completo como contexto
- Map-Reduce: dividir los documentos en fragmentos y procesar en paralelo
- Refine: refinar iterativamente una respuesta en fragmentos de documentos
- Búsqueda de similitudes: usar incrustaciones vectoriales para encontrar fragmentos relevantes
Compararemos las fortalezas y limitaciones de cada enfoque. El código completo está disponible en este cuaderno de Colab.
Comparemos las métricas de cada método en nuestro documento de muestra de 50 páginas:
Método | Documentos relevantes | Llamadas LLM | Tokens totales | Calidad de respuesta |
---|---|---|---|---|
Stuffing | 3 páginas | 1 | 8432 | Buena |
Map-Reduce | 50 páginas | 51 | 63019 | Aceptable |
Refine | 50 páginas | 50 | 71209 | Buena |
Búsqueda de similitudes | 4 páginas | 5 | 5194 | Excelente |
El enfoque de búsqueda de similitudes es capaz de encontrar una respuesta de alta calidad con 10 veces menos páginas, llamadas LLM y tokens en comparación con los métodos de documento completo. Esta brecha se ampliaría aún más en conjuntos de datos más grandes.
Paso 1. Configuración de LangChain para la respuesta a preguntas sobre documentos grandes
Primero, instale las dependencias requeridas, incluido el SDK de Vertex AI, LangChain y ChromaDB:
!pip install google-cloud-aiplatform langchain==0.0.323 chromadb==0.3.26 pypdf
Importe las bibliotecas clave:
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
Cargue el modelo de texto PaLM y el modelo de incrustaciones:
vertex_llm_text = VertexAI(model_name="text-bison@001")
vertex_embeddings = VertexAIEmbeddings(model_name="textembedding-gecko@001")
Paso 2. Carga de documentos
Para este ejemplo, utilizaremos un documento técnico en PDF sobre MLOps. Descárguelo y cargue el texto utilizando 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()
Esto divide el PDF en páginas que podemos utilizar como documentos base.
Paso 3. Inclusión de documentos completos
El enfoque más simple es incluir el texto completo del documento en la ventana de contexto del LLM. Configure una plantilla de pregunta:
prompt_template = """Responda la pregunta de la forma más precisa posible utilizando el contexto proporcionado.
Si la respuesta no se encuentra en el contexto, diga "respuesta no disponible en el contexto" \n\n
Contexto: \n {contexto}?\n
Pregunta: \n {pregunta} \n
Respuesta:
"""
prompt = PromptTemplate(
template=prompt_template, input_variables=["contexto", "pregunta"]
)
Cargue una cadena de QA para la inclusión:
stuff_chain = load_qa_chain(vertex_llm_text, chain_type="stuff", prompt=prompt)
Luego, ejecútelo en una pregunta:
pregunta = "¿Qué es la experimentación?"
contexto = "\n".join(str(p.page_content) for p in pages[:7])
stuff_answer = stuff_chain(
{"documentos_entrada": pages[7:10], "pregunta": pregunta}, return_only_outputs=True
)
Esto funciona, pero está limitado por el tamaño del contexto que el modelo puede manejar (unos pocos miles de tokens). La inclusión del documento completo de 50 páginas alcanza este límite:
try:
print(stuff_chain(
{"documentos_entrada": pages[7:], "pregunta": pregunta},
return_only_outputs=True))
except Exception as e:
print("El código falló porque no podrá realizar inferencias en un contexto tan grande")
Paso 4. Map-Reduce
Para escalar a documentos más grandes, podemos dividirlos en fragmentos, ejecutar QA en cada fragmento y luego agregar los resultados. LangChain proporciona una cadena de map-reduce para manejar esto.
Primero, defina plantillas de pregunta y combinación separadas:
question_prompt_template = """
Responda la pregunta de la forma más precisa posible utilizando el contexto proporcionado. \n\n
Contexto: \n {contexto} \n
Pregunta: \n {pregunta} \n
Respuesta:
"""
question_prompt = PromptTemplate(
template=question_prompt_template, input_variables=["contexto", "pregunta"]
)
combine_prompt_template = """Dada la información extraída y la pregunta, cree una respuesta final.
Si la respuesta no se encuentra en el contexto, diga "respuesta no disponible en el contexto. \n\n
Resúmenes: \n {resumenes}?\n
Pregunta: \n {pregunta} \n
Respuesta:
"""
combine_prompt = PromptTemplate(
template=combine_prompt_template, input_variables=["resumenes", "pregunta"]
)
Cargue la cadena de map-reduce especificando las plantillas de pregunta y combinación:
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,
)
Ejecute esto en el conjunto completo de documentos:
map_reduce_outputs = map_reduce_chain({"input_documents": pages, "question": question})
Esto ejecuta la consulta de preguntas y respuestas en cada página de manera individual, y luego combina los resultados en un paso final. Podemos inspeccionar los resultados intermedios:
for doc, out in zip(
map_reduce_outputs["input_documents"], map_reduce_outputs["intermediate_steps"]
):
print(f"Página: {doc.metadata['page']}")
print(f"Respuesta: {out}")
El enfoque del map-reduce se escala para documentos grandes y proporciona información sobre de dónde proviene la información. Sin embargo, a veces se puede perder información en el paso de combinación final.
Paso 5. Refinar
El enfoque de refinar tiene como objetivo mitigar la pérdida de información refinando iterativamente una respuesta. Comienza con una respuesta inicial en el primer fragmento y luego la refina con cada fragmento subsiguiente.
Defina una consulta de refinamiento que incorpore la respuesta existente y el nuevo contexto:
refine_prompt_template = """
La pregunta original es: \n {question} \n
La respuesta proporcionada es: \n {existing_answer}\n
Refine si es necesario la respuesta existente con el siguiente contexto: \n {context_str} \n
Dada la información extraída y la pregunta, cree una respuesta final.
Si la respuesta no se encuentra en el contexto, diga "respuesta no disponible en el contexto. \n\n
"""
refine_prompt = PromptTemplate(
input_variables=["question", "existing_answer", "context_str"],
template=refine_prompt_template,
)
Cargue una cadena de refinamiento:
refine_chain = load_qa_chain(
vertex_llm_text,
chain_type="refine",
return_intermediate_steps=True,
question_prompt=initial_question_prompt,
refine_prompt=refine_prompt,
)
Úselo en el documento completo:
refine_outputs = refine_chain({"input_documents": pages, "question": question})
Inspeccione los pasos intermedios para ver cómo se refinó la respuesta:
for doc, out in zip(
refine_outputs["input_documents"], refine_outputs["intermediate_steps"]
):
print(f"Página: {doc.metadata['page']}")
print(f"Respuesta: {out}")
El enfoque de refinamiento ayuda a preservar la información en todo el documento completo. Pero aún requiere procesar todo el documento de manera lineal.
Paso 6. Búsqueda de similitud
Para mejorar la eficiencia, podemos usar primero las incrustaciones (embeddings) para encontrar solo los fragmentos más relevantes para una determinada pregunta. Esto evita tener que procesar el documento completo.
Cree un índice vectorial de fragmentos del documento usando ChromaDB:
vector_index = Chroma.from_documents(pages, vertex_embeddings).as_retriever()
Recupere los fragmentos más relevantes para la pregunta:
docs = vector_index.get_relevant_documents(question)
Ejecute la cadena de map-reduce solo en estos fragmentos relevantes:
map_reduce_embeddings_outputs = map_reduce_chain(
{"input_documents": docs, "question": question}
)
print(map_reduce_embeddings_outputs["output_text"])
Esto encuentra una respuesta de alta calidad que solo necesita procesar un subconjunto pequeño del documento completo. El enfoque de búsqueda de similitud proporciona el mejor equilibrio entre precisión y eficiencia.
Conclusión
En este artículo, demostramos varios enfoques para responder preguntas en documentos grandes utilizando LangChain y Vertex AI PaLM. Si bien el enfoque simple puede funcionar para documentos pequeños, se necesitan enfoques de map-reduce y refinamiento para escalar a datos más grandes.
Sin embargo, el método más eficiente y efectivo es utilizar primero la búsqueda de similitud vectorial para encontrar solo los fragmentos más relevantes para una determinada pregunta. Esto minimiza la cantidad de texto que el LLM necesita procesar sin dejar de producir respuestas de alta calidad.
La combinación de búsqueda de similitud, la cadena de preguntas y respuestas de LangChain y poderosos LLM como PaLM permiten construir sistemas escalables de pregunta y respuesta sobre colecciones de documentos grandes. Puede comenzar con el código completo en este cuaderno.
¿Quiere estar al día con las últimas noticias sobre LLM? ¡Consulte la última tabla de clasificación de LLM!