Generación mejorada por recuperación (RAG)

Firebase Genkit proporciona abstracciones que te ayudan a compilar flujos de generación mejorada de recuperación (RAG), así como complementos que ofrecen integraciones en herramientas relacionadas.

¿Qué es RAG?

La generación de recuperación mejorada es una técnica que se usa para incorporar fuentes de información en las respuestas de un LLM. Es importante poder hacerlo, ya que, si bien los LLMs suelen entrenarse con un cuerpo amplio de material, el uso práctico de los LLMs suele requerir conocimientos específicos del dominio (por ejemplo, es conveniente usar un LLM para responder las preguntas de los clientes sobre los productos de la empresa).

Una solución es ajustar el modelo con datos más específicos. Sin embargo, puede resultar caro en términos de costos de procesamiento y de esfuerzos necesarios para preparar los datos de entrenamiento adecuados.

Por el contrario, el modelo RAG incorpora fuentes de datos externas en una instrucción el tiempo que se pasa al modelo. Por ejemplo, imagina que la instrucción "¿Cuál es la relación de Bart con Lisa?" pudiera expandirse ("mejorarse") anteponiendo información relevante, dando como resultado el mensaje, "Los hijos de Homer y Marge se llaman Bart, Lisa y Maggie. ¿Cuál es la relación de Bart con a Lisa?"

Este enfoque tiene varias ventajas:

  • Puede ser más rentable porque no tienes que volver a entrenar el modelo.
  • Puedes actualizar tu fuente de datos continuamente y el LLM puede aprovechar la información actualizada.
  • Ahora tienes la posibilidad de citar referencias en las respuestas de tu LLM.

Por otro lado, usar RAG naturalmente implica instrucciones más largas y ciertos cargos por servicios de la API de LLM por cada token de entrada que envías. En última instancia, debes evaluar las compensaciones de costos para tus aplicaciones.

RAG es un área muy amplia y se usan muchas técnicas diferentes para lograr la mejor calidad de RAG. El framework principal de Genkit ofrece dos abstracciones principales para ayudarte con la RAG:

  • Indexadores: agregan documentos a un "índice".
  • Incorporadores: transforman documentos en una representación vectorial.
  • Recuperadores: recuperan documentos de un "índice", según una consulta.

Estas definiciones son amplias a propósito porque Genkit no ofrece recomendaciones sobre qué es un "índice" o la forma exacta en que se recuperan los documentos. Genkit solo proporciona un formato Document, y el recuperador o el proveedor de implementación del indexador.

Indexadores

El índice es responsable de realizar un seguimiento de tus documentos para que puedas recuperar rápidamente documentos relevantes en función de una consulta específica. Esto es logra de la forma más habitual mediante una base de datos de vectores, que indexa tus documentos usando vectores multidimensionales llamados embeddings. Un embedding de texto (de manera opaca) representa los conceptos expresados en un fragmento de texto; estos se generan usando modelos de AA con propósitos especiales. Mediante la indexación de texto con su embedding, una base de datos de vectores puede agrupar texto relacionado de forma conceptual y recuperar documentos relacionadas con una nueva cadena de texto (la consulta).

Antes de que puedas recuperar documentos para generarlos, debes transferirlos al índice del documento. Un flujo de transferencia típico realiza lo siguiente:

  1. Divide documentos grandes en documentos más pequeños para que solo se utilicen las porciones relevantes para mejorar las instruciones: "fragmentación". Esto es necesario, ya que muchos LLMs tienen una ventana de contexto limitada, por lo que no es práctico incluir documentos completos con una instrucción.

    Genkit no proporciona bibliotecas de fragmentación integradas. Sin embargo, existen bibliotecas fuente abiertas que son compatibles con Genkit.

  2. Genera embeddings para cada fragmento Según la base de datos que utilices, podrías hacerlo de forma explícita con un modelo de generación de embeddings o podrías usar el generador de embeddings proporcionado por la base de datos.

  3. Agrega el bloque de texto y su índice a la base de datos.

Puedes ejecutar el flujo de transferencia con poca frecuencia o solo una vez si estás trabajando con una fuente de datos estable. Por otro lado, si trabajas con datos que cambian con frecuencia, puedes ejecutar continuamente el flujo de transferencia (por ejemplo, en un activador de Cloud Firestore, cuando se actualiza un documento).

Incorporadores

Una incorporador es una función que toma contenido (texto, imágenes, audio, etc.) y crea un vector numérico que codifica el significado semántico del contenido original. Como se mencionó anteriormente, los incorporadores se aprovechan como parte del proceso de indexación; sin embargo, también se pueden usar de forma independiente para crear incorporaciones sin un índice.

Recuperadores

Un recuperador es un concepto que encapsula la lógica relacionada con cualquier tipo de recuperación de datos. Los casos de recuperación más populares suelen incluir recuperación desde almacenes de vectores, sin embargo, en Genkit, un recuperador puede ser cualquier función que devuelva datos.

Para crear un recuperador, puedes usar una de las implementaciones proporcionadas o crear una propia.

Indexadores, incorporadores y recuperadores compatibles

Genkit proporciona compatibilidad con indexadores y recuperadores a través de su sistema de complementos. Se admiten oficialmente los siguientes complementos:

  • Base de datos de vectores en la nube Pinecone

Además, Genkit admite los siguientes almacenes de vectores mediante plantillas de código predefinidas, que puedes personalizar para la configuración de tu base de datos y esquema:

La compatibilidad con el modelo de embedding se proporciona a través de los siguientes complementos:

Complemento Modelos
IA generativa de Google Embedding de texto de Gecko
Google Vertex AI Embedding de texto de Gecko

Define un flujo de RAG

Los siguientes ejemplos muestran cómo podrías transferir una colección de documentos PDF del menú de un restaurante a una base de datos de vectores y recuperarlos para usarlos en un flujo que determina qué alimentos hay disponibles.

Instala dependencias

En este ejemplo, usaremos la biblioteca textsplitter de langchaingo y la biblioteca de análisis de PDF ledongthuc/pdf:

go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf

Define un indexador

En el siguiente ejemplo, se muestra cómo crear un indexador para transferir una colección de documentos PDF y almacenarlos en una base de datos local de vectores.

Usa el recuperador de similitud vectorial local basado en archivos que Genkit listos para usar para realizar pruebas y crear prototipos (no uses en producción)

Crea el indexador

// Import Genkit's file-based vector retriever, (Don't use in production.)
import "github.com/firebase/genkit/go/plugins/localvec"

// Vertex AI provides the text-embedding-004 embedder model.
import "github.com/firebase/genkit/go/plugins/vertexai"
ctx := context.Background()

err := vertexai.Init(ctx, &vertexai.Config{})
if err != nil {
	log.Fatal(err)
}
err = localvec.Init()
if err != nil {
	log.Fatal(err)
}

menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
	"menuQA",
	localvec.Config{
		Embedder: vertexai.Embedder("text-embedding-004"),
	},
)
if err != nil {
	log.Fatal(err)
}

Crea la configuración de fragmentación

En este ejemplo, se usa la biblioteca textsplitter, que proporciona un separador de texto simple para dividir los documentos en segmentos que se pueden vectorizar.

La siguiente definición configura la función de fragmentación para devolver segmentos de documento de 200 caracteres, con una superposición entre los fragmentos de 20 caracteres.

splitter := textsplitter.NewRecursiveCharacter(
	textsplitter.WithChunkSize(200),
	textsplitter.WithChunkOverlap(20),
)

Puedes encontrar más opciones de fragmentación para esta biblioteca en la documentación de langchaingo

Define el flujo del indexador

genkit.DefineFlow(
	"indexMenu",
	func(ctx context.Context, path string) (any, error) {
		// Extract plain text from the PDF. Wrap the logic in Run so it
		// appears as a step in your traces.
		pdfText, err := genkit.Run(ctx, "extract", func() (string, error) {
			return readPDF(path)
		})
		if err != nil {
			return nil, err
		}

		// Split the text into chunks. Wrap the logic in Run so it
		// appears as a step in your traces.
		docs, err := genkit.Run(ctx, "chunk", func() ([]*ai.Document, error) {
			chunks, err := splitter.SplitText(pdfText)
			if err != nil {
				return nil, err
			}

			var docs []*ai.Document
			for _, chunk := range chunks {
				docs = append(docs, ai.DocumentFromText(chunk, nil))
			}
			return docs, nil
		})
		if err != nil {
			return nil, err
		}

		// Add chunks to the index.
		err = ai.Index(ctx, menuPDFIndexer, ai.WithIndexerDocs(docs...))
		return nil, err
	},
)
// Helper function to extract plain text from a PDF. Excerpted from
// https://github.com/ledongthuc/pdf
func readPDF(path string) (string, error) {
	f, r, err := pdf.Open(path)
	if f != nil {
		defer f.Close()
	}
	if err != nil {
		return "", err
	}

	reader, err := r.GetPlainText()
	if err != nil {
		return "", err
	}

	bytes, err := io.ReadAll(reader)
	if err != nil {
		return "", err
	}
	return string(bytes), nil
}

Ejecuta el flujo del indexador

genkit flow:run indexMenu "'menu.pdf'"

Después de ejecutar el flujo indexMenu, se iniciará la base de datos de vectores con documentos y lista para usar en flujos de Genkit, con pasos de recuperación.

Define un flujo con recuperación

En el siguiente ejemplo, se muestra cómo usar un recuperador en un flujo RAG. Como en el ejemplo del indexador, se usa el recuperador de vectores basado en archivos de Genkit, que no debes usar en producción.

	ctx := context.Background()

	err := vertexai.Init(ctx, &vertexai.Config{})
	if err != nil {
		log.Fatal(err)
	}
	err = localvec.Init()
	if err != nil {
		log.Fatal(err)
	}

	model := vertexai.Model("gemini-1.5-flash")

	_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
		"menuQA",
		localvec.Config{
			Embedder: vertexai.Embedder("text-embedding-004"),
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	genkit.DefineFlow(
		"menuQA",
		func(ctx context.Context, question string) (string, error) {
			// Retrieve text relevant to the user's question.
			docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
				Document: ai.DocumentFromText(question, nil),
			})
			if err != nil {
				return "", err
			}

			// Construct a system message containing the menu excerpts you just
			// retrieved.
			menuInfo := ai.NewSystemTextMessage("Here's the menu context:")
			for _, doc := range docs.Documents {
				menuInfo.Content = append(menuInfo.Content, doc.Content...)
			}

			// Call Generate, including the menu information in your prompt.
			return ai.GenerateText(ctx, model,
				ai.WithMessages(
					ai.NewSystemTextMessage(`
You are acting as a helpful AI assistant that can answer questions about the
food available on the menu at Genkit Grub Pub.
Use only the context provided to answer the question. If you don't know, do not
make up an answer. Do not add or change items on the menu.`),
					menuInfo,
					ai.NewUserTextMessage(question)))
		})

Escribe tus propios indexadores y recuperadores

También es posible crear tu propio recuperador. Esto es útil si tus documentos se administran en un almacén de documentos que no es compatible con Genkit (p. ej., MySQL, Google Drive, etcétera). El SDK de Genkit proporciona métodos flexibles que te permiten proporcionar un código personalizado para recuperar documentos.

También puedes definir recuperadores personalizados que se complementan con los recuperados existentes en Genkit y aplicar técnicas avanzadas de RAG (como una reclasificación o instrucción) por encima.

Por ejemplo, supongamos que quieres usar una función de reclasificación personalizada. El siguiente ejemplo define un recuperador personalizado que aplica tu función recuperador de menú definido anteriormente:

type CustomMenuRetrieverOptions struct {
	K          int
	PreRerankK int
}
advancedMenuRetriever := ai.DefineRetriever(
	"custom",
	"advancedMenuRetriever",
	func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
		// Handle options passed using our custom type.
		opts, _ := req.Options.(CustomMenuRetrieverOptions)
		// Set fields to default values when either the field was undefined
		// or when req.Options is not a CustomMenuRetrieverOptions.
		if opts.K == 0 {
			opts.K = 3
		}
		if opts.PreRerankK == 0 {
			opts.PreRerankK = 10
		}

		// Call the retriever as in the simple case.
		response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
			Document: req.Document,
			Options:  localvec.RetrieverOptions{K: opts.PreRerankK},
		})
		if err != nil {
			return nil, err
		}

		// Re-rank the returned documents using your custom function.
		rerankedDocs := rerank(response.Documents)
		response.Documents = rerankedDocs[:opts.K]

		return response, nil
	},
)