Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

RAG do zero: busque contexto antes de responder com IA

Entenda Retrieval-Augmented Generation criando uma busca local, montando o contexto com fontes, tratando ausência e avaliando se o trecho certo apareceu.

Rodolfo Mori5 min de leitura

RAG faz o programa buscar informação relevante antes de pedir uma resposta ao modelo. Nesta lição, você vai criar uma recuperação local para as políticas da Club Store, colocar os trechos encontrados no contexto, citar a origem e medir se o documento correto chegou em primeiro lugar.

A sigla vem de Retrieval-Augmented Generation, ou geração aumentada por recuperação. “Retrieval” é a busca; “augmented” indica que a entrada ganhou contexto; “generation” é a resposta produzida depois. O modelo não aprende para sempre esses documentos e nenhum treinamento acontece nessa etapa.

O laboratório usa uma busca lexical pequena e determinística no Node. Ela foi executada localmente e todas as saídas são reais. Não usamos embeddings nem uma API externa nesta demonstração; por isso não vamos chamar o resultado de busca semântica.

A bibliotecária encontra páginas; ela não reescreve todos os livros

Imagine perguntar na biblioteca qual é a política de troca. A bibliotecária consulta o catálogo, escolhe uma ficha, pega o livro e marca a página. Você não precisa levar todas as estantes para a mesa. Também consegue conferir de onde a informação veio.

No RAG, o catálogo representa o índice; a pergunta vira consulta; os livros são documentos; as páginas são chunks, ou trechos; e a mesa é a janela de contexto enviada ao modelo. O limite da analogia é que um mecanismo de busca pode pontuar um trecho irrelevante e o modelo pode ignorar a evidência. A seleção precisa de avaliação, não de confiança automática.

O fluxo mínimo cabe em quatro verbos:

text
indexar -> buscar -> montar contexto -> gerar resposta
Pipeline definido: a geração só começa depois da recuperação.

Essa sequência complementa a visão do guia de inteligência artificial. Vamos isolar primeiro a etapa que podemos provar sem modelo.

Três documentos pequenos formam uma base verificável

Crie o tipo e as políticas locais:

ts
type Trecho = {
  id: string;
  titulo: string;
  texto: string;
};

const baseConhecimento: Trecho[] = [
  {
    id: "politica-trocas",
    titulo: "Política de trocas",
    texto: "Livros sem sinais de uso podem ser trocados em até 30 dias após a entrega.",
  },
  {
    id: "prazo-entrega",
    titulo: "Prazo de entrega",
    texto: "Após o despacho, a entrega padrão ocorre em até 5 dias úteis.",
  },
  {
    id: "pagamento-pix",
    titulo: "Pagamento por Pix",
    texto: "O pedido por Pix é confirmado após o pagamento ser identificado.",
  },
];
Base local carregada: 3 trechos, cada um com id, título e texto.

Em produto real, guarde também versão, URL, data de validade, permissões e estrutura do documento. Um id estável permite mostrar a fonte e descobrir qual conteúdo sustentou uma resposta antiga.

O tamanho do chunk muda o resultado. Um trecho gigante traz ruído; um trecho curto demais perde o contexto. Comece com unidades que façam sentido no domínio — uma política, seção ou procedimento — e ajuste usando perguntas reais.

A primeira busca conta palavras compartilhadas

Nossa busca de brinquedo normaliza acentos, remove palavras muito comuns e separa termos:

ts
const stopwords = new Set([
  "a", "as", "de", "do", "em", "e", "o", "os", "por", "um", "uma",
]);

function termos(texto: string): string[] {
  return texto
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .split(/[^a-z0-9]+/)
    .filter((termo) => termo.length > 1 && !stopwords.has(termo));
}

console.log(termos("Posso trocar um livro sem uso?"));
[ 'posso', 'trocar', 'livro', 'sem', 'uso' ]

Depois conte quantos termos da pergunta aparecem em cada documento, descarte score zero e ordene do maior para o menor:

ts
type TrechoComScore = Trecho & { score: number };

function buscarTrechos(pergunta: string, limite = 2): TrechoComScore[] {
  const consulta = new Set(termos(pergunta));

  return baseConhecimento
    .map((documento) => {
      const palavras = new Set(termos(`${documento.titulo} ${documento.texto}`));
      const score = [...consulta].filter((termo) => palavras.has(termo)).length;
      return { ...documento, score };
    })
    .filter((documento) => documento.score > 0)
    .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
    .slice(0, limite);
}
Função carregada: resultados com score zero são removidos e o limite padrão é 2.

Execute a consulta:

ts
const encontrados = buscarTrechos("Posso trocar um livro sem uso?");
console.log(encontrados.map(({ id, titulo, score }) => ({ id, titulo, score })));
[ { id: 'politica-trocas', titulo: 'Política de trocas', score: 2 } ]

O algoritmo encontrou “livro” e “uso”. Ele não percebe que “devolver um exemplar” pode significar a mesma coisa que “trocar um livro”. Essa é a limitação objetiva da busca lexical. Embeddings transformam conteúdos em vetores para comparar proximidade semântica; ainda assim, relevância deve ser medida no seu conjunto de perguntas.

O contexto carrega texto e identidade da fonte

Entregar apenas o parágrafo ao modelo dificulta a citação. Monte um envelope delimitado com id e título:

ts
function montarContexto(trechos: TrechoComScore[]): string {
  if (trechos.length === 0) return "Nenhum trecho relevante encontrado.";

  return trechos
    .map((item) => `[${item.id}] ${item.titulo}: ${item.texto}`)
    .join("\n");
}

console.log(montarContexto(encontrados));
[politica-trocas] Política de trocas: Livros sem sinais de uso podem ser trocados em até 30 dias após a entrega.

No prompt, declare que o conteúdo recuperado é dado, não instrução, e peça que a resposta use somente as fontes fornecidas. Delimitadores melhoram a organização, mas não neutralizam prompt injection. Documentos também podem conter texto malicioso; ferramentas e permissões continuam sob controle do código.

A engenharia de prompt organiza essa fronteira. Structured Outputs pode exigir uma lista de ids em fontes, mas a aplicação ainda deve conferir se cada id realmente estava nos trechos recuperados.

Ausência de resultado é informação, não convite para inventar

Faça uma pergunta fora da base:

ts
const nada = buscarTrechos("Vocês vendem bicicletas?");
console.log(nada);
console.log(montarContexto(nada));
[] Nenhum trecho relevante encontrado.

Nesse estado, o comportamento seguro é dizer que falta informação ou encaminhar a uma pessoa. Não preencha o contexto com o documento “menos ruim” apenas para evitar uma lista vazia. Um limiar de score ou similaridade ajuda a preservar essa opção de abstinência.

Transforme a ausência num erro controlado quando a operação exigir fonte:

ts
function exigirFonte(trechos: TrechoComScore[]): TrechoComScore[] {
  if (trechos.length === 0) throw new Error("CONTEXTO_RELEVANTE_NAO_ENCONTRADO");
  return trechos;
}

try {
  exigirFonte(buscarTrechos("Vocês vendem bicicletas?"));
} catch (erro) {
  console.log((erro as Error).message);
}
CONTEXTO_RELEVANTE_NAO_ENCONTRADO

Uma interface conversacional pode converter esse erro numa resposta cordial. O log, porém, deve conservar o motivo técnico para que a equipe descubra lacunas na base ou na busca.

Avalie recuperação antes de avaliar a frase final

Se o documento certo nunca chega ao modelo, mexer no prompt de resposta ataca o lugar errado. Crie perguntas com o id esperado e meça hit@1: em quantas o primeiro resultado é o correto.

ts
const casos = [
  { pergunta: "Qual o prazo após despacho?", esperado: "prazo-entrega" },
  { pergunta: "Como trocar livro sem uso?", esperado: "politica-trocas" },
  { pergunta: "Quando o Pix confirma?", esperado: "pagamento-pix" },
];

const acertos = casos.filter(
  (caso) => buscarTrechos(caso.pergunta, 1)[0]?.id === caso.esperado,
).length;

console.log({ hitAt1: acertos / casos.length, casos: casos.length });
{ hitAt1: 1, casos: 3 }

Três casos são suficientes para entender a métrica, não para declarar uma busca pronta. Acrescente sinônimos, erros de digitação, perguntas sem resposta e permissões diferentes. A lição de avaliação de aplicações de IA mostra como separar conjuntos e regressões.

Quando trocar o índice local por recuperação de produção

Para muitos documentos, busca linear em memória deixa de ser adequada. A plataforma OpenAI oferece vector stores e uma operação de busca; file_search também pode ser disponibilizada como ferramenta da Responses API. Outra opção é gerar embeddings, armazená-los num banco vetorial e controlar a recuperação na sua aplicação.

O formato básico da busca hospedada atual é:

ts
const resultados = await client.vectorStores.search(vectorStoreId, {
  query: "Qual é a política de trocas?",
});

console.log(resultados.data.length);
Não executado: requer vector store e credenciais reais; consulte a resposta da sua própria conta.

Não publique uma contagem inventada. No seu projeto, registre versão dos documentos, modelo de embedding, filtros, limiar, ids recuperados, latência e custos. Evite incluir conteúdo sensível em logs e aplique as mesmas permissões da fonte original antes da recuperação.

Missão: faça a busca revelar a própria limitação

Adicione o caso "Posso devolver um exemplar novo?" esperando politica-trocas. Rode a avaliação sem alterar o algoritmo. A busca lexical provavelmente falhará porque os sinônimos não estão no documento.

Em seguida, acrescente um mapa mínimo de sinônimos na consulta:

ts
const sinonimos: Record<string, string[]> = {
  devolver: ["trocar", "troca"],
  exemplar: ["livro"],
};

const expandir = (lista: string[]) => [
  ...lista,
  ...lista.flatMap((termo) => sinonimos[termo] ?? []),
];

console.log(expandir(termos("devolver um exemplar")));
[ 'devolver', 'exemplar', 'trocar', 'troca', 'livro' ]

A missão termina quando o novo caso encontra a política, os três casos antigos continuam passando e “bicicletas” continua sem resultado. Você não construiu um buscador semântico; construiu uma melhoria verificável e documentou seu limite. Esse hábito vale igual quando o índice passar a usar embeddings.

  • rag
  • retrieval augmented generation
  • busca semantica
  • embeddings
  • openai

Perguntas frequentes

O que significa RAG?
Retrieval-Augmented Generation significa geração aumentada por recuperação. A aplicação busca trechos relevantes e os entrega ao modelo junto da pergunta para apoiar a resposta.
RAG treina o modelo com meus documentos?
Não. Os documentos são recuperados no momento da consulta e entram no contexto. Os pesos do modelo não são alterados por esse processo.
Toda busca usada em RAG precisa de embeddings?
Não. Busca por palavras, SQL e filtros também podem recuperar contexto. Embeddings ajudam na similaridade semântica, mas devem ser escolhidos e avaliados conforme os dados e a intenção da busca.
RAG impede alucinação?
Não garante. Ele oferece evidência relevante, mas a recuperação pode falhar e o modelo pode interpretar mal. Exija fontes, trate ausência e avalie recuperação e resposta separadamente.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 26.3.0 e TypeScript 7.0.2; recuperação lexical e avaliações executadas localmente, sem chamada à API, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. OpenAI Docs — Retrieval — developers.openai.com
  2. OpenAI Docs — File search — developers.openai.com
  3. OpenAI Docs — Embeddings — developers.openai.com

Continue por aqui