Generación de aumento de recuperación (RAG)

Firebase Genkit proporciona abstracciones que te ayudan a compilar una generación de aumento de recuperación (RAG), así como los complementos que ofrecen integraciones con herramientas relacionadas.

¿Qué es la RAG?

La generación de recuperación aumentada es una técnica que se usa para incorporar fuentes de información en las respuestas de un LLM. Es importante poder hacer ya que, si bien los LLM suelen entrenarse con un conjunto amplio el uso práctico de los LLM suele requerir conocimientos específicos del dominio (para Por ejemplo, podrías usar un LLM para responder las preguntas preguntas sobre tu productos de la empresa).

Una solución es ajustar el modelo con datos más específicos. Sin embargo, puede ser costoso en términos de costos de procesamiento y del esfuerzo necesario para preparar los datos de entrenamiento adecuados.

Por el contrario, el modelo RAG incorpora fuentes de datos externas en una consigna el tiempo que se pasa al modelo. Por ejemplo, puedes imaginar la consigna, "¿Cuál es la relación de Barto con Lisa?" podría expandirse ("aumentado") anteponiendo información relevante, lo que resulta en el mensaje, "Homer and Los hijos de Marge se llaman Bart, Lisa y Maggie. ¿Cuál es la relación de Bart? 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 tomar uso de 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 un poco de API de LLM de servicios cobra por cada token de entrada que envías. En última instancia, debes evaluar el y compensar los costos de tus aplicaciones.

El RAV 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 para ayudarte con la RAG:

  • Indexadores: Agrega documentos a un “índice”.
  • Incorporadores: Transforma 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 tiene opiniones sobre qué es un "índice" o la forma exacta en que se recuperan los documentos. Solo Genkit proporciona un formato Document, y el retriever o proveedor de implementación del indexador.

Indexadores

El índice es responsable de realizar un seguimiento de tus documentos de tal manera que puedes recuperar rápidamente documentos relevantes en función de una consulta específica. Esto es lo más se logra mediante una base de datos de vectores, que indexa tus documentos usando vectores multidimensionales llamados incorporaciones. Una incorporación de texto (opaquelía) representa los conceptos expresados en un fragmento de texto; estos se generan modelos de AA con propósitos especiales. Mediante la indexación de texto con su incorporación, un vector La base de datos 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 la lo siguiente:

  1. Divide documentos grandes en documentos más pequeños para que solo las apps relevantes se usan para aumentar las indicaciones: “fragmentar”. Esto es necesario ya que muchos LLM 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 las bibliotecas fuente que son compatibles con Genkit.

  2. Generar incorporaciones 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 incorporaciones puede usar el generador de incorporaciones 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 cambia con frecuencia, puedes ejecutar continuamente el flujo de transferencia (para ejemplo, en un activador de Cloud Firestore, cuando se actualiza un documento).

Incorporadores

Una incorporación 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, las incorporaciones 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 retriever es un concepto que encapsula la lógica relacionada con cualquier tipo de documento y la recuperación de datos. Los casos de recuperación más populares suelen incluir la recuperación Sin embargo, en Genkit, un retriever puede ser cualquier función que devuelva datos.

Para crear un retriever, puedes usar una de las implementaciones proporcionadas o tú mismo.

Indexadores, incorporaciones y recuperadores compatibles

Genkit proporciona compatibilidad con indexadores y retriever a través de su sistema de complementos. El 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 conjuntos de datos plantillas de código, que puedes personalizar para la configuración de tu base de datos y esquema:

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

Complemento Modelos
IA generativa de Google Incorporación de texto de Gecko
Vertex AI de Google Incorporación de texto de Gecko

Cómo definir un flujo de RAG

Los siguientes ejemplos muestran cómo podrías transferir una colección de documentos PDF del menú de un restaurante. en una base de datos de vectores y los recupera 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 ofrece pruebas y prototipos listos para usar (no usar 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)
}

Crear configuración de fragmentación

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

La siguiente definición configura la función de fragmentación para devolver el documento segmentos 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 = menuPDFIndexer.Index(ctx, &ai.IndexerRequest{Documents: 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 listo 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 retriever en un flujo RAG. Me gusta 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-pro")

    _, 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.
            resp, err := model.Generate(ctx, &ai.GenerateRequest{
                Messages: []*ai.Message{
                    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),
                },
            }, nil)
            if err != nil {
                return "", err
            }

            return resp.Text()
        })

Escribe tus propios indexadores y recuperadores

También es posible crear tu propio retriever. Esto es útil si tus los 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 permiten Proporcionas un código personalizado para recuperar documentos.

También puedes definir retrievers personalizados que se complementan con los recuperados existentes. en Genkit y aplicar técnicas avanzadas de RAG (como una reclasificación o instrucción en la parte superior.

Por ejemplo, supongamos que quieres usar una función de reclasificación personalizada. El El siguiente ejemplo define un recuperador personalizado que aplica tu función al Menú recuperador 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
    },
)