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

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

¿Qué es RAG?

La generación de aumento de recuperación es una técnica que se usa para incorporar fuentes de información externas en las respuestas de un LLM. Es importante poder hacerlo porque, si bien los LLM suelen entrenarse con una amplia variedad de materiales, su uso práctico suele requerir conocimientos específicos del dominio (por ejemplo, es posible que desees usar un LLM para responder las preguntas de los clientes sobre los productos de tu empresa).

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

En cambio, el informe RAV funciona mediante la incorporación de fuentes de datos externas en una instrucción en el momento en que se pasa al modelo. Por ejemplo, puedes imaginar que la instrucción, "¿Cuál es la relación de Bart con Lisa?" podría expandirse ("aumentada") agregando información relevante, lo que da como resultado la instrucción: "Los hijos de Homer y Marge se llaman Bart, Lisa y Maggie. ¿Cuál es la relación de Bart con 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 de forma continua y el LLM puede usar la información actualizada de inmediato.
  • Ahora puedes citar referencias en las respuestas de tu LLM.

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

El RAV es un área muy amplia y hay muchas técnicas diferentes que se usan para lograr un RAV de la mejor calidad. El framework principal de Genkit ofrece dos abstracciones principales para ayudarte a realizar el RAV:

  • Indexadores: agrega documentos a un “índice”.
  • Incorporadores: Transforma documentos en una representación vectorial
  • Recuperadores: recuperan documentos de un “índice” a partir de una consulta.

Estas definiciones son amplias a propósito porque Genkit no tiene opiniones sobre qué es un "índice" ni cómo se recuperan exactamente los documentos de él. Genkit solo proporciona un formato Document, y todo lo demás lo define el proveedor de implementación del rastreador o el indexador.

Indexadores

El índice es responsable de hacer un seguimiento de tus documentos para que puedas recuperar rápidamente los documentos relevantes en función de una consulta específica. Esto se logra con mayor frecuencia mediante una base de datos de vectores, que indexa tus documentos con vectores multidimensionales llamados incorporaciones. Una incorporación de texto (opaca) representa los conceptos expresados por un fragmento de texto; estos se generan con modelos de AA de propósito especial. Cuando se indexa texto con su incorporación, una base de datos vectorial puede agrupar texto relacionado de forma conceptual y recuperar documentos relacionados con una nueva cadena de texto (la consulta).

Antes de que puedas recuperar documentos con fines de generación, debes transferirlos a tu índice de documentos. Un flujo de transferencia típico hace lo siguiente:

  1. Divide los documentos grandes en otros más pequeños, de manera que solo se usen partes relevantes para aumentar tus instrucciones: la “fragmentación”. Esto es necesario porque muchos LLM tienen una ventana de contexto limitada, por lo que resulta poco práctico incluir documentos completos con una instrucción.

    Genkit no proporciona bibliotecas de fragmentación integradas. Sin embargo, hay bibliotecas de código abierto disponibles que son compatibles con Genkit.

  2. Genera incorporaciones para cada bloque. Según la base de datos que uses, puedes hacer esto de forma explícita con un modelo de generación de incorporaciones o usar el generador de incorporaciones que proporciona 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 trabajas 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, cada vez que se actualice un documento).

Incorporadores

Un insertador 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ó antes, 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 índice.

Retriever

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

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

Indexadores, incorporaciones y recuperados compatibles

Genkit brinda compatibilidad con indexadores y retriever a través de su sistema de complementos. Los siguientes complementos son compatibles oficialmente:

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

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

Plugin ajustables
IA generativa de Google Incorporación de texto de geco
Vertex AI de Google Incorporación de texto de geco

Define un flujo RAV

En los siguientes ejemplos, se muestra cómo puedes 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 determine qué alimentos están disponibles.

Instala dependencias para procesar PDFs

npm install llm-chunk pdf-parse
npm i -D --save @types/pdf-parse

Agregar un almacén de vectores local a tu configuración

import {
  devLocalIndexerRef,
  devLocalVectorstore,
} from '@genkit-ai/dev-local-vectorstore';
import { textEmbeddingGecko, vertexAI } from '@genkit-ai/vertexai';

configureGenkit({
  plugins: [
    // vertexAI provides the textEmbeddingGecko embedder
    vertexAI(),

    // the local vector store requires an embedder to translate from text to vector
    devLocalVectorstore([
      {
        indexName: 'menuQA',
        embedder: textEmbeddingGecko,
      },
    ]),
  ],
});

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 de vectores local.

Usa el recuperador de similitud vectorial basado en archivos local que Genkit proporciona de inmediato para realizar pruebas y prototipado simples (no se debe usar en la producción).

Crea el indexador

import { devLocalIndexerRef } from '@genkit-ai/dev-local-vectorstore';

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

Crear configuración de fragmentación

En este ejemplo, se usa la biblioteca llm-chunk, que proporciona un divisor 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 garantizar un segmento de documento de entre 1,000 y 2,000 caracteres, rota al final de una oración y con una superposición entre fragmentos de 100 caracteres.

const chunkingConfig = {
  minLength: 1000,
  maxLength: 2000,
  splitter: 'sentence',
  overlap: 100,
  delimiters: '',
} as any;

Puedes encontrar más opciones de fragmentación para esta biblioteca en la documentación de llm-chunk.

Define tu flujo indexador

import { index } from '@genkit-ai/ai';
import { Document } from '@genkit-ai/ai/retriever';
import { defineFlow, run } from '@genkit-ai/flow';
import { readFile } from 'fs/promises';
import { chunk } from 'llm-chunk';
import path from 'path';
import pdf from 'pdf-parse';
import * as z from 'zod';

export const indexMenu = defineFlow(
  {
    name: 'indexMenu',
    inputSchema: z.string().describe('PDF file path'),
    outputSchema: z.void(),
  },
  async (filePath: string) => {
    filePath = path.resolve(filePath);

    // Read the pdf.
    const pdfTxt = await run('extract-text', () =>
      extractTextFromPdf(filePath)
    );

    // Divide the pdf text into segments.
    const chunks = await run('chunk-it', async () =>
      chunk(pdfTxt, chunkingConfig)
    );

    // Convert chunks of text into documents to store in the index.
    const documents = chunks.map((text) => {
      return Document.fromText(text, { filePath });
    });

    // Add documents to the index.
    await index({
      indexer: menuPdfIndexer,
      documents,
    });
  }
);

async function extractTextFromPdf(filePath: string) {
  const pdfFile = path.resolve(filePath);
  const dataBuffer = await readFile(pdfFile);
  const data = await pdf(dataBuffer);
  return data.text;
}

Ejecuta el flujo del indexador

genkit flow:run indexMenu "'../pdfs'"

Después de ejecutar el flujo indexMenu, se propagarán documentos a la base de datos vectorial y estará lista para usarse en flujos de Genkit con pasos de recuperación.

Define un flujo con recuperación

En el siguiente ejemplo, se muestra cómo podrías usar un retriever en un flujo RAV. Al igual que el ejemplo del indexador, este ejemplo usa el recuperador de vectores basado en archivos de Genkit, que no debes usar en la producción.

import { generate } from '@genkit-ai/ai';
import { retrieve } from '@genkit-ai/ai/retriever';
import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore';
import { defineFlow } from '@genkit-ai/flow';
import { geminiPro } from '@genkit-ai/vertexai';
import * as z from 'zod';

// Define the retriever reference
export const menuRetriever = devLocalRetrieverRef('menuQA');

export const menuQAFlow = defineFlow(
  { name: 'menuQA', inputSchema: z.string(), outputSchema: z.string() },
  async (input: string) => {
    // retrieve relevant documents
    const docs = await retrieve({
      retriever: menuRetriever,
      query: input,
      options: { k: 3 },
    });

    // generate a response
    const llmResponse = await generate({
      model: geminiPro,
      prompt: `
    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.

    Question: ${input}
    `,
      context: docs,
    });

    const output = llmResponse.text();
    return output;
  }
);

Escribe tus propios indexadores y recuperados

También puedes crear tu propio retriever. 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 brinda métodos flexibles que te permiten proporcionar código personalizado para recuperar documentos. También puedes definir recuperadores personalizados que se basen en los recuperados existentes en Genkit y aplicar técnicas RAV avanzadas (como la reclasificación o las extensiones de mensaje) en la parte superior.

Retrieveres simples

Los recuperados simples te permiten convertir fácilmente el código existente en retrievers:

import {
  defineSimpleRetriever,
  retrieve
} from '@genkit-ai/ai/retriever';
import { searchEmails } from './db';
import { z } from 'zod';

defineSimpleRetriever({
  name: 'myDatabase',
  configSchema: z.object({
    limit: z.number().optional()
  }).optional(),
  // we'll extract "message" from the returned email item
  content: 'message',
  // and several keys to use as metadata
  metadata: ['from', 'to', 'subject'],
} async (query, config) => {
  const result = await searchEmails(query.text(), {limit: config.limit});
  return result.data.emails;
});

Recuperadores personalizados

import {
  CommonRetrieverOptionsSchema,
  defineRetriever,
  retrieve,
} from '@genkit-ai/ai/retriever';
import * as z from 'zod';

export const menuRetriever = devLocalRetrieverRef('menuQA');

const advancedMenuRetrieverOptionsSchema = CommonRetrieverOptionsSchema.extend({
  preRerankK: z.number().max(1000),
});

const advancedMenuRetriever = defineRetriever(
  {
    name: `custom/advancedMenuRetriever`,
    configSchema: advancedMenuRetrieverOptionsSchema,
  },
  async (input, options) => {
    const extendedPrompt = await extendPrompt(input);
    const docs = await retrieve({
      retriever: menuRetriever,
      query: extendedPrompt,
      options: { k: options.preRerankK || 10 },
    });
    const rerankedDocs = await rerank(docs);
    return rerankedDocs.slice(0, options.k || 3);
  }
);

(extendPrompt y rerank es algo que tendrías que implementar por tu cuenta, no lo proporciona el framework).

Y luego puedes intercambiar tu retriever:

const docs = await retrieve({
  retriever: advancedRetriever,
  query: input,
  options: { preRerankK: 7, k: 3 },
});