RAG no Delphi: fundamente um LLM nos seus próprios documentos

· Componentes

Resposta rápida: Retrieval-Augmented Generation (RAG) são duas ideias unidas. Primeiro, você gera embeddings dos seus documentos uma única vez e armazena os vetores resultantes. Depois, a cada pergunta, você gera o embedding da pergunta, recupera os trechos mais próximos por significado e os entrega ao LLM como contexto, de modo que a resposta venha dos seus dados em vez da memória de treinamento do modelo. No sgcWebSockets, todo o pipeline são três componentes: TsgcAIOpenAIEmbeddings para transformar texto em vetores, TsgcAIDatabaseVectorFile ou TsgcAIDatabaseVectorPinecone para armazená-los e pesquisá-los, e TsgcHTTP_API_OpenAI para escrever a resposta final fundamentada.

Um modelo de chat de propósito geral sabe muito sobre o mundo e nada sobre o manual do seu produto, seus chamados de suporte ou o relatório interno do último trimestre. Pergunte sobre isso e ele vai recusar ou, pior, inventar algo plausível. O RAG resolve isso sem retreinar nada: você mantém o modelo como está e fornece a ele as passagens certas do seu próprio acervo no momento da pergunta. Abaixo está o ciclo completo no Delphi, do começo ao fim, com nomes reais de componentes.

O que o RAG realmente faz

Um embedding é uma lista de números que captura o significado de um trecho de texto. Duas passagens sobre o mesmo tema ficam próximas nesse espaço numérico, mesmo quando não compartilham palavras-chave. Um banco de dados vetorial armazena esses números e, dado um vetor de consulta, retorna as entradas mais próximas ordenadas por similaridade. O RAG encadeia tudo isso:

EtapaO que acontecePeça do sgcWebSockets
1. Ingestão (uma vez)Divide documentos em trechos, gera o embedding de cada trecho, armazena os vetoresTsgcAIOpenAIEmbeddings.CreateEmbeddingsFromFile
2. ArmazenamentoMantém os vetores em um arquivo local ou em um índice na nuvemTsgcAIDatabaseVectorFile · TsgcAIDatabaseVectorPinecone
3. Recuperação (por pergunta)Gera o embedding da pergunta, encontra os trechos mais próximosGetEmbeddingQueryData
4. RespostaColoca os trechos no prompt, pergunta ao modeloTsgcHTTP_API_OpenAI._CreateChatCompletion

As etapas 1 e 2 são executadas quando os seus dados mudam. As etapas 3 e 4 são executadas a cada pergunta do usuário. Vamos construir cada uma delas.

Etapa 1 — gere os embeddings dos seus documentos

Crie um TsgcAIOpenAIEmbeddings, forneça a ele uma chave da OpenAI, aponte sua propriedade Database para um armazenamento vetorial e chame CreateEmbeddingsFromFile. Essa única chamada lê o arquivo, divide-o em trechos (controlado por EmbeddingsOptions.ChunkSize), gera o embedding de cada trecho e grava os vetores no armazenamento por meio da sequência BeginAddData / AddData / EndAddData para você.

uses
  sgcAI, sgcAI_OpenAI_Embeddings,
  sgcAI_DB_Vector, sgcAI_DB_Vector_File, sgcAI_DB_Vector_Pinecone;

var
  Embeddings: TsgcAIOpenAIEmbeddings;
  DBFile: TsgcAIDatabaseVectorFile;
begin
  Embeddings := TsgcAIOpenAIEmbeddings.Create(nil);
  Embeddings.OpenAIOptions.ApiKey := 'sk-...';

  // local, file-based vector store
  DBFile := TsgcAIDatabaseVectorFile.Create(nil);
  DBFile.VectorFileOptions.InputFilename  := 'corpus.sgcif';
  DBFile.VectorFileOptions.VectorFilename := 'corpus.sgcvf';

  Embeddings.Database := DBFile;
  Embeddings.CreateEmbeddingsFromFile('docs.txt');
end;

Essa é toda a etapa de ingestão. Execute-a uma vez, ou sempre que os seus documentos mudarem. O modelo de embedding padrão é text-embedding-3-small; altere-o por meio de EmbeddingsOptions.Model se precisar de outro. Há mais detalhes na página do componente Embeddings.

Etapa 2 — escolha onde os vetores ficam

Ambos os backends descendem do mesmo componente base, TsgcAIDatabaseVector, então são intercambiáveis: troque um pelo outro e o seu código de ingestão e consulta não muda. A única diferença é onde os dados ficam.

Para um aplicativo desktop, uma ferramenta offline ou um acervo menor, TsgcAIDatabaseVectorFile mantém tudo em um arquivo local, sem nenhum serviço externo. Quando o índice é grande, precisa ser compartilhado entre processos ou usuários, ou tem que escalar além de uma máquina, mude para TsgcAIDatabaseVectorPinecone, que faz o upsert de cada trecho por meio da API REST gerenciada do Pinecone:

var
  DBPinecone: TsgcAIDatabaseVectorPinecone;
begin
  DBPinecone := TsgcAIDatabaseVectorPinecone.Create(nil);
  DBPinecone.PineconeOptions.ApiKey         := 'pc-...';
  DBPinecone.PineconeIndexOptions.IndexName := 'sgc-embeddings';

  Embeddings.Database := DBPinecone;
  Embeddings.CreateEmbeddingsFromFile('docs.txt');
end;

Repare que a linha de ingestão é idêntica à da Etapa 1. Esse é todo o propósito da classe base compartilhada. Veja a página de bancos de dados vetoriais para o backend em arquivo e a página do Pinecone para o de nuvem.

Etapa 3 — recupere e responda

Agora o caminho por pergunta. Gere o embedding da pergunta do usuário e encontre os trechos armazenados mais próximos em uma única chamada: GetEmbedding gera o embedding do texto e o executa por meio do QueryData do banco de dados, retornando as passagens mais relevantes do seu acervo. Essas passagens são o seu contexto. Concatene-as com a pergunta e envie tudo ao modelo de chat:

var
  Question, Context, Prompt, Answer: string;
  OpenAI: TsgcHTTP_API_OpenAI;
begin
  Question := 'How do I enable the WatchDog reconnect?';

  // retrieve the closest chunks from your own data
  Context := Embeddings.GetEmbedding(Question, '');

  // build a grounded prompt
  Prompt :=
    'Answer the question using only the context below.' + sLineBreak +
    'If the context does not contain the answer, say you do not know.' +
    sLineBreak + sLineBreak +
    'Context:' + sLineBreak + Context + sLineBreak + sLineBreak +
    'Question: ' + Question;

  // ask the model
  OpenAI := TsgcHTTP_API_OpenAI.Create(nil);
  OpenAI.OpenAIOptions.ApiKey := 'sk-...';
  Answer := OpenAI._CreateChatCompletion('gpt-4o-mini', Prompt);

  Memo1.Lines.Text := Answer;
end;

Isso é RAG. O modelo nunca viu os seus documentos durante o treinamento, mas responde a partir deles, porque você colocou as passagens relevantes diante dele no momento da requisição. Mude o acervo e as respostas mudam junto, sem nenhum fine-tuning envolvido. A instrução de recusar quando o contexto está vazio é o que mantém o modelo honesto em vez de adivinhar.

Local ou nuvem, o mesmo código

Um detalhe que vale repetir: a escolha entre o armazenamento em arquivo e o Pinecone é reversível. Como TsgcAIDatabaseVectorFile e TsgcAIDatabaseVectorPinecone compartilham a base TsgcAIDatabaseVector, você pode prototipar com o arquivo local (zero infraestrutura, roda offline) e migrar para o Pinecone depois, trocando o componente que você atribui a Embeddings.Database. Nada no seu código de ingestão ou consulta muda. O mesmo vale para o LLM no final: _CreateChatCompletion no TsgcHTTP_API_OpenAI pode ser trocado pelo componente da Anthropic ou da Gemini se você preferir outro modelo para escrever a resposta final.

Uma nota sobre divisão em trechos e qualidade

A qualidade da recuperação depende de como os seus documentos são divididos. Trechos menores tornam as correspondências mais precisas, mas podem perder contexto; trechos maiores mantêm o contexto, mas diluem a correspondência. EmbeddingsOptions.ChunkSize controla isso para CreateEmbeddingsFromFile, então vale a pena ajustá-lo ao seu material. Para um controle mais fino, você também pode gerar embeddings de strings individuais com CreateEmbeddings e moldar os trechos você mesmo antes de fazer a ingestão.

Primeiros passos

Todos os três componentes vêm no sgcWebSockets. Baixe a versão de avaliação gratuita, coloque os componentes de embeddings e de armazenamento vetorial em um formulário, aponte-os para um arquivo de texto e você terá um ciclo RAG funcionando em bem menos de cem linhas. Explore o conjunto completo de blocos de construção de IA no hub de componentes de IA & LLM.

Dúvidas sobre como aplicar isso ao seu próprio acervo? Entre em contato — você receberá uma resposta das pessoas que escreveram o código.

Relacionados