QA RAG con Autoevaluación II
Para esta variación, realizamos un cambio en el procedimiento de evaluación. Además del par pregunta-respuesta, también pasamos el contexto recuperado al evaluador LLM.
Para lograr esto, agregamos una función itemgetter adicional en el segundo RunnableParallel para recopilar la cadena de contexto y pasarla a la nueva plantilla de solicitud qa_eval_prompt_with_context.
rag_chain = (
RunnableParallel(context = retriever | format_docs, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question"), context = itemgetter("context") ) |
qa_eval_prompt_with_context |
llm_selfeval |
json_parser
)
Diagrama de flujo de implementación:
Uno de los puntos débiles comunes al utilizar una implementación en cadena como LCEL es la dificultad para acceder a las variables intermedias, lo cual es importante para depurar canalizaciones. Analizamos algunas opciones en las que aún podemos acceder a cualquier variable intermedia que nos interese mediante manipulaciones de LCEL.
Uso de RunnableParallel para transferir salidas intermedias
Como vimos anteriormente, RunnableParallel nos permite llevar múltiples argumentos al siguiente paso de la cadena. Entonces usamos esta capacidad de RunnableParallel para trasladar los valores intermedios requeridos hasta el final.
En el siguiente ejemplo, modificamos la cadena RAG de autoevaluación original para generar el texto de contexto recuperado junto con el resultado final de la autoevaluación. El cambio principal es que agregamos un objeto RunnableParallel a cada paso del proceso para llevar adelante la variable de contexto.
Además, también utilizamos la función itemgetter para especificar claramente las entradas para los pasos siguientes. Por ejemplo, para los dos últimos objetos RunnableParallel, usamos captador de elementos(‘entrada’) para garantizar que solo el argumento de entrada del paso anterior se pase a los objetos del analizador LLM/Json.
rag_chain = (
RunnableParallel(context = retriever | format_docs, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question"), context = itemgetter("context") ) |
RunnableParallel(input = qa_eval_prompt, context = itemgetter("context")) |
RunnableParallel(input = itemgetter("input") | llm_selfeval , context = itemgetter("context") ) |
RunnableParallel(input = itemgetter("input") | json_parser, context = itemgetter("context") )
)
El resultado de esta cadena se parece a lo siguiente:
Una variación más concisa:
rag_chain = (
RunnableParallel(context = retriever | format_docs, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question"), context = itemgetter("context") ) |
RunnableParallel(input = qa_eval_prompt | llm_selfeval | json_parser, context = itemgetter("context"))
)
Uso de variables globales para guardar pasos intermedios
Este método utiliza esencialmente el principio de un registrador. Introducimos una nueva función que guarda su entrada en una variable global, permitiéndonos así acceder a la variable intermedia a través de la variable global.
global contextdef save_context(x):
global context
context = x
return x
rag_chain = (
RunnableParallel(context = retriever | format_docs | save_context, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question") ) |
qa_eval_prompt |
llm_selfeval |
json_parser
)
Aquí definimos una variable global llamada contexto y una función llamada guardar_contexto que guarda su valor de entrada en el global contexto variable antes de devolver la misma entrada. En la cadena añadimos el guardar_contexto Funciona como el último paso del paso de recuperación de contexto.
Esta opción le permite acceder a cualquier paso intermedio sin realizar cambios importantes en la cadena.
Usando devoluciones de llamada
Adjuntar devoluciones de llamada a su cadena es otro método común utilizado para registrar valores de variables intermedias. Hay mucho que cubrir sobre el tema de las devoluciones de llamada en LangChain, por lo que cubriré esto en detalle en una publicación diferente.