O Firebase Genkit fornece abstrações que ajudam a criar geração aumentada de recuperação (RAG), bem como plug-ins que fornecem integrações com ferramentas relacionadas.
O que é RAG?
A geração aumentada de recuperação é uma técnica usada para incorporar fontes de informação nas respostas de LLMs. É importante ser capaz de fazer porque, embora os LLMs sejam normalmente treinados com um amplo material, o uso prático de LLMs requer conhecimento de domínio específico (por exemplo, talvez você queira usar um LLM para responder perguntas sobre os produtos da sua empresa).
Uma solução é ajustar o modelo usando dados mais específicos. No entanto, pode ser caro tanto em termos de custo computacional quanto de esforço para preparar dados de treinamento adequados.
Em contraste, o RAG funciona incorporando fontes de dados externas em um comando na hora em que é passado para o modelo. Por exemplo, é possível imaginar o comando, "Qual é a relação de Bart com Lisa?" pode ser expandida ("aumentado") adicionando algumas informações relevantes, resultando no comando, "Os filhos de Homer e Marge se chamam Bart, Lisa e Maggie. Qual é a relação de Bart com Lisa?"
Essa abordagem tem várias vantagens:
- Essa opção pode ser mais econômica porque não é necessário treinar novamente o modelo.
- Você pode atualizar continuamente sua fonte de dados e o LLM pode fazer uso imediato das informações atualizadas.
- Agora você pode citar referências nas respostas do seu LLM.
Por outro lado, usar RAG naturalmente significa prompts mais longos, e alguns serviços de API LLM cobram por cada token de entrada enviado. Em última análise, você precisa avaliar as compensações de custo para suas aplicações.
RAG é uma área muito ampla e existem muitas técnicas diferentes usadas para obter RAG da melhor qualidade. O framework Genkit principal oferece duas abstrações principais para ajudar você a fazer a RAG
- Indexadores: adicionam documentos a um "índice".
- Incorporadores: transformam documentos em uma representação vetorial.
- Recuperadores: recuperam documentos de um "índice", de acordo com uma consulta.
Essas definições são propositalmente amplas porque o Genkit não tem opinião sobre o que é um "índice" ou como exatamente os documentos são recuperados dele. O Genkit fornece apenas um formato de Document
e todo o resto é definido pelo provedor de implementação do recuperador ou indexador.
Indexadores
O índice é responsável por monitorar seus documentos de maneira que você possa recuperar rapidamente documentos relevantes com base em uma consulta específica. Na maioria das vezes, isso é feito usando um banco de dados vetorial, que indexa seus documentos usando vetores multidimensionais chamados embeddings. Um embedding de texto (opacamente) representa os conceitos expressos por uma passagem de texto. Eles são gerados usando modelos de ML para fins especiais. Ao indexar texto usando sua incorporação, um banco de dados vetorial é capaz de agrupar texto conceitualmente relacionado e recuperar documentos relacionados a uma nova sequência de texto (a consulta).
Antes de poder recuperar documentos para fins de geração, é necessário ingeri-los em seu índice de documentos. Um fluxo de ingestão típico faz o seguinte:
Divida documentos grandes em documentos menores para que apenas as partes relevantes sejam usadas para aumentar seus comandos: "fragmentação". Isto é necessário porque muitos LLMs têm uma janela de contexto limitada, tornando impraticável incluir documentos inteiros com um comando.
O Genkit não fornece bibliotecas de chunking integradas. No entanto, existem bibliotecas de código aberto disponíveis que são compatíveis com o Genkit.
Gerar embeddings para cada bloco. Dependendo do banco de dados que você está usando, você pode fazer isso explicitamente com um modelo de geração de incorporação ou pode usar o gerador de incorporação fornecido pelo banco de dados.
Adicione o bloco de texto e o índice dele ao banco de dados.
Você pode executar seu fluxo de ingestão com pouca frequência ou apenas uma vez se estiver trabalhando com uma fonte de dados estável. Por outro lado, se você estiver trabalhando com dados que mudam frequentemente, poderá executar continuamente o fluxo de ingestão (por exemplo, em um gatilho do Cloud Firestore, sempre que um documento for atualizado).
Incorporadores
Um incorporador é uma função que usa conteúdo (texto, imagens, áudio etc.) e cria um vetor numérico que codifica o significado semântico do conteúdo original. Como mencionado acima, os embeddings são aproveitados como parte do processo de indexação, mas também podem ser usados de forma independente para criar embeddings sem um índice.
Recuperadores
Um recuperador é um conceito que encapsula a lógica relacionada a qualquer tipo de recuperação de documento. Os casos de recuperação mais populares normalmente incluem a recuperação de armazenamentos de vetores. No entanto, no Genkit, um recuperador pode ser qualquer função que retorne dados.
Para criar um recuperador, você pode usar uma das implementações fornecidas ou criar seu próprio.
Indexadores, recuperadores e incorporadores compatíveis
O Genkit oferece suporte ao indexador e ao recuperador por meio do sistema de plug-in. Os plug-ins a seguir são oficialmente compatíveis:
- Banco de dados de vetores de nuvem Pinecone.
Além disso, o Genkit oferece suporte aos seguintes armazenamentos de vetores por meio de modelos de código predefinidos, que você pode personalizar para a configuração e o esquema do seu banco de dados:
- PostgreSQL com
pgvector
O suporte a modelos de embedding é fornecido pelos seguintes plug-ins:
Plug-in | Modelos |
---|---|
IA generativa do Google | Embedding de texto da Gecko |
Vertex AI do Google | Embedding de texto da Gecko |
Como definir um fluxo RAG
Os exemplos a seguir mostram como você pode ingerir uma coleção de documentos PDF de cardápio de restaurante em um banco de dados vetorial e recuperá-los para uso em um fluxo que determina quais itens alimentares estão disponíveis.
Instalar dependências
Neste exemplo, usaremos a biblioteca textsplitter
da langchaingo
e
da biblioteca de análise de PDF ledongthuc/pdf
:
go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf
Definir um indexador
O exemplo a seguir mostra como criar um indexador para ingerir uma coleção de documentos PDF e armazená-los em um banco de dados vetorial local.
Ele usa o recuperador de similaridade vetorial baseado em arquivo local que o Genkit fornece pronto para uso para testes e prototipagem simples (não use na produção)
Criar o 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)
}
Criar configuração de divisão
Este exemplo usa a biblioteca textsplitter
, que fornece um divisor de texto simples para dividir documentos em segmentos que podem ser vetorizados.
A definição a seguir configura a função chunking para retornar segmentos de documentos de 200 caracteres, com uma sobreposição entre pedaços de 20 caracteres.
splitter := textsplitter.NewRecursiveCharacter(
textsplitter.WithChunkSize(200),
textsplitter.WithChunkOverlap(20),
)
Mais opções de divisão para esta biblioteca podem ser encontradas na
documentação do langchaingo
.
Definir o fluxo do 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
}
Executar o fluxo do indexador
genkit flow:run indexMenu "'menu.pdf'"
Depois de executar o fluxo indexMenu
, o banco de dados vetorial será propagado com documentos e pronto para ser usado em fluxos Genkit com etapas de recuperação.
Definir um fluxo com recuperação
O exemplo a seguir mostra como você pode usar um recuperador em um fluxo RAG. Assim como o exemplo do indexador, este exemplo usa o recuperador de vetores baseado em arquivo do Genkit, que você não deve usar na produção.
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)))
})
Escrever seus próprios indexadores e recuperadores
Também é possível criar seu próprio recuperador. Isso é útil se seus documentos são gerenciados em um repositório de documentos que não é compatível com o Genkit (por exemplo, MySQL, Google Drive etc.). O Genkit SDK fornece métodos flexíveis que permitem fornecer código personalizado para buscar documentos.
Você também pode definir recuperadores personalizados que se baseiam em recuperadores existentes no Genkit e aplicar técnicas avançadas de RAG (como reclassificação ou extensão de comando) na parte de cima.
Por exemplo, suponha que você queira usar uma função de reclassificação personalizada. O exemplo a seguir define um recuperador personalizado que aplica sua função ao recuperador de menu 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
},
)