Un enfoque agente para reducir las alucinaciones LLM | de Youness Mansar | diciembre 2024

Un enfoque agente para reducir las alucinaciones LLM | de Youness Mansar | diciembre 2024

Consejo 2: utilice resultados estructurados

Usar resultados estructurados significa obligar al LLM a producir texto JSON o YAML válido. Esto le ayudará a reducir las divagaciones innecesarias y obtener respuestas «claras» sobre lo que necesita del LLM. Esto también ayudará con los siguientes consejos, ya que facilitará la verificación de las respuestas del LLM.

A continuación se explica cómo hacerlo con la API de Gemini:

import json

import google.generativeai as genai
from pydantic import BaseModel, Field

from document_ai_agents.schema_utils import prepare_schema_for_gemini

class Answer(BaseModel):
answer: str = Field(..., description="Your Answer.")

model = genai.GenerativeModel("gemini-1.5-flash-002")

answer_schema = prepare_schema_for_gemini(Answer)

question = "List all the reasons why LLM hallucinate"

context = (
"LLM hallucination refers to the phenomenon where large language models generate plausible-sounding but"
" factually incorrect or nonsensical information. This can occur due to various factors, including biases"
" in the training data, the inherent limitations of the model's understanding of the real world, and the "
"model's tendency to prioritize fluency and coherence over accuracy."
)

messages = (
[context]
+ [
f"Answer this question: {question}",
]
+ [
f"Use this schema for your answer: {answer_schema}",
]
)

response = model.generate_content(
messages,
generation_config={
"response_mime_type": "application/json",
"response_schema": answer_schema,
"temperature": 0.0,
},
)

response = Answer(**json.loads(response.text))

print(f"{response.answer=}")

Donde «prepare_schema_for_gemini» es una función de utilidad que prepara el esquema para cumplir con los extraños requisitos de Gemini. Puedes encontrar su definición aquí: código.

Este código define un esquema Pydantic y envía ese esquema como parte de la solicitud en el campo «response_schema». Esto obliga al LLM a seguir este patrón en su respuesta y hace que su resultado sea más fácil de analizar.

Consejo 3: utilice una cadena de pensamientos y mejores indicaciones

A veces, darle al LLM espacio para desarrollar su respuesta, antes de comprometerse con una respuesta final, puede ayudar a producir respuestas de mayor calidad. Esta técnica se llama Cadena de pensamientos y es muy utilizada porque es eficaz y muy sencilla de implementar.

También podemos pedirle explícitamente al LLM que responda con «N/A» si no puede encontrar suficiente contexto para producir una respuesta de calidad. Esto le dará una salida fácil en lugar de intentar responder preguntas para las que no tiene respuesta.

Por ejemplo, veamos esta simple pregunta y contexto:

Contexto

Thomas Jefferson (13 de abril [O.S. April 2]1743 – 4 de julio de 1826) fue un estadista, plantador, diplomático, abogado, arquitecto, filósofo y padre fundador estadounidense que sirvió como tercer presidente de los Estados Unidos de 1801 a 1809.[6] Fue el autor principal de la Declaración de Independencia. Después de la Guerra de Independencia de los Estados Unidos y antes de convertirse en presidente en 1801, Jefferson fue el primer Secretario de Estado de los Estados Unidos bajo George Washington y luego el segundo vicepresidente bajo John Adams. Jefferson fue un destacado defensor de la democracia, el republicanismo y los derechos naturales, y produjo documentos y decisiones formativos a nivel estatal, nacional e internacional. (Fuente: Wikipedia)

Pregunta

¿En qué año murió Davis Jefferson?

Un enfoque ingenuo da:

Respuesta

respuesta = ‘1826’

Lo cual es obviamente falso ya que Jefferson Davis ni siquiera se menciona en el contexto. Thomas Jefferson murió en 1826.

Si cambiamos el patrón de la respuesta para usar una cadena de pensamientos para:

class AnswerChainOfThoughts(BaseModel):
rationale: str = Field(
...,
description="Justification of your answer.",
)
answer: str = Field(
..., description="Your Answer. Answer with 'N/A' if answer is not found"
)

También estamos agregando más detalles sobre lo que esperamos como resultado cuando la pregunta no se puede responder usando el contexto «Responder con ‘N/A’ si no se responde».

Con este nuevo enfoque obtenemos lo siguiente razonamiento (recuerde, cadena de pensamiento):

El texto proporcionado es sobre Thomas Jefferson, no sobre Jefferson Davis. No se incluye información sobre la muerte de Jefferson Davis.

y el final respuesta:

respuesta = ‘N/A’

Excelente ! Pero, ¿podemos utilizar un enfoque más general para detectar alucinaciones?

¡Podemos, con los agentes!

Consejo 4: utilice un enfoque agente

Construiremos un agente simple que implemente un proceso de tres pasos:

  • El primer paso es incluir el contexto y plantear la pregunta al LLM para obtener la primera respuesta del candidato y el contexto relevante que utilizó para su respuesta.
  • El segundo paso es reformular la pregunta y la respuesta del primer candidato como una declaración.
  • El tercer paso es pedirle al LLM que verifique si el contexto relevante implícito la respuesta del candidato. A esto se le llama “autoverificación”: https://arxiv.org/pdf/2212.09561

Para implementar esto, definimos tres nodos en LangGraph. El primer nodo hará la pregunta incluyendo el contexto, el segundo nodo la reformulará usando el LLM y el tercer nodo verificará la implicación de la declaración con el contexto de entrada.

El primer nodo se puede definir de la siguiente manera:

    def answer_question(self, state: DocumentQAState):
logger.info(f"Responding to question '{state.question}'")
assert (
state.pages_as_base64_jpeg_images or state.pages_as_text
), "Input text or images"
messages = (
[
{"mime_type": "image/jpeg", "data": base64_jpeg}
for base64_jpeg in state.pages_as_base64_jpeg_images
]
+ state.pages_as_text
+ [
f"Answer this question: {state.question}",
]
+ [
f"Use this schema for your answer: {self.answer_cot_schema}",
]
)

response = self.model.generate_content(
messages,
generation_config={
"response_mime_type": "application/json",
"response_schema": self.answer_cot_schema,
"temperature": 0.0,
},
)

answer_cot = AnswerChainOfThoughts(**json.loads(response.text))

return {"answer_cot": answer_cot}

Y el segundo como:

    def reformulate_answer(self, state: DocumentQAState):
logger.info("Reformulating answer")
if state.answer_cot.answer == "N/A":
return

messages = [
{
"role": "user",
"parts": [
{
"text": "Reformulate this question and its answer as a single assertion."
},
{"text": f"Question: {state.question}"},
{"text": f"Answer: {state.answer_cot.answer}"},
]
+ [
{
"text": f"Use this schema for your answer: {self.declarative_answer_schema}"
}
],
}
]

response = self.model.generate_content(
messages,
generation_config={
"response_mime_type": "application/json",
"response_schema": self.declarative_answer_schema,
"temperature": 0.0,
},
)

answer_reformulation = AnswerReformulation(**json.loads(response.text))

return {"answer_reformulation": answer_reformulation}

Al tercero le gusta:

    def verify_answer(self, state: DocumentQAState):
logger.info(f"Verifying answer '{state.answer_cot.answer}'")
if state.answer_cot.answer == "N/A":
return
messages = [
{
"role": "user",
"parts": [
{
"text": "Analyse the following context and the assertion and decide whether the context "
"entails the assertion or not."
},
{"text": f"Context: {state.answer_cot.relevant_context}"},
{
"text": f"Assertion: {state.answer_reformulation.declarative_answer}"
},
{
"text": f"Use this schema for your answer: {self.verification_cot_schema}. Be Factual."
},
],
}
]

response = self.model.generate_content(
messages,
generation_config={
"response_mime_type": "application/json",
"response_schema": self.verification_cot_schema,
"temperature": 0.0,
},
)

verification_cot = VerificationChainOfThoughts(**json.loads(response.text))

return {"verification_cot": verification_cot}

código completo en

Observe cómo cada nodo utiliza su propio esquema para la salida estructurada y su propio mensaje. Esto es posible gracias a la flexibilidad de Gemini API y LangGraph.

Trabajemos en este código usando el mismo ejemplo anterior ➡️
(Nota: no utilizamos una cadena de pensamiento en el primer mensaje para que se active la verificación de nuestras pruebas).

Contexto

Thomas Jefferson (13 de abril [O.S. April 2]1743 – 4 de julio de 1826) fue un estadista, plantador, diplomático, abogado, arquitecto, filósofo y padre fundador estadounidense que sirvió como tercer presidente de los Estados Unidos de 1801 a 1809.[6] Fue el autor principal de la Declaración de Independencia. Después de la Guerra de Independencia de los Estados Unidos y antes de convertirse en presidente en 1801, Jefferson fue el primer Secretario de Estado de los Estados Unidos bajo George Washington y luego el segundo vicepresidente bajo John Adams. Jefferson fue un destacado defensor de la democracia, el republicanismo y los derechos naturales, y produjo documentos y decisiones formativos a nivel estatal, nacional e internacional. (Fuente: Wikipedia)

Pregunta

¿En qué año murió Davis Jefferson?

Resultado del primer nodo (primera respuesta):

contexto_relevante=’Thomas Jefferson (13 de abril [O.S. April 2]1743 – 4 de julio de 1826) fue un estadista, plantador, diplomático, abogado, arquitecto, filósofo y padre fundador estadounidense que sirvió como tercer presidente de los Estados Unidos de 1801 a 1809.

respuesta = ‘1826’

Resultado del segundo nodo (Reformulación de la respuesta):

respuesta_declarativa=’Davis Jefferson murió en 1826′

Resultado del tercer nodo (verificación):

razonamiento=’El contexto dice que Thomas Jefferson murió en 1826. La declaración dice que Davis Jefferson murió en 1826. El contexto no menciona a Davis Jefferson, solo a Thomas Jefferson.’

enredo=’No’

Entonces el paso de verificación rechazado (No hay conexión entre los dos.) la respuesta inicial. Ahora podemos evitar devolverle una alucinación al usuario.

Consejo adicional: utilice modelos más potentes

Este consejo no siempre es fácil de aplicar debido a limitaciones de presupuesto o latencia, pero debes saber que los LLM más fuertes son menos propensos a sufrir alucinaciones. Entonces, si es posible, opte por un LLM más potente para sus casos de uso más delicados. Puedes ver un punto de referencia de alucinaciones aquí: . Podemos ver que los mejores modelos en este punto de referencia (menos alucinaciones) también ocupan un lugar destacado en las clasificaciones de PNL convencional.

Fuente: Licencia fuente: Apache 2.0

En este tutorial, exploramos estrategias para mejorar la confiabilidad de los resultados de LLM al reducir la tasa de alucinaciones. Las recomendaciones clave incluyen un formato cuidadoso y indicaciones para guiar las llamadas de LLM y el uso de un enfoque basado en el flujo de trabajo en el que los agentes están diseñados para verificar sus propias respuestas.

Esto implica varios pasos:

  1. Recupere los elementos de contexto exactos utilizados por el LLM para generar la respuesta.
  2. Reformule la respuesta para facilitar la verificación (en forma declarativa).
  3. Pídale al LLM que verifique la coherencia entre el contexto y la respuesta reformulada.

Si bien todos estos consejos pueden mejorar significativamente la precisión, recuerde que ningún método es infalible. Siempre existe el riesgo de rechazar respuestas válidas si el LLM es demasiado conservador al comprobar o si pasa por alto casos reales de alucinaciones. Por lo tanto, siempre es esencial una evaluación rigurosa de los flujos de trabajo específicos de su LLM.

código completo en