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.
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:
indexar -> buscar -> montar contexto -> gerar respostaEssa 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:
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.",
},
];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:
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?"));Depois conte quantos termos da pergunta aparecem em cada documento, descarte score zero e ordene do maior para o menor:
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);
}Execute a consulta:
const encontrados = buscarTrechos("Posso trocar um livro sem uso?");
console.log(encontrados.map(({ id, titulo, score }) => ({ id, titulo, score })));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:
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));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:
const nada = buscarTrechos("Vocês vendem bicicletas?");
console.log(nada);
console.log(montarContexto(nada));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:
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);
}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.
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 });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 é:
const resultados = await client.vectorStores.search(vectorStoreId, {
query: "Qual é a política de trocas?",
});
console.log(resultados.data.length);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:
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")));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.
Perguntas frequentes
O que significa RAG?
RAG treina o modelo com meus documentos?
Toda busca usada em RAG precisa de embeddings?
RAG impede alucinação?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- OpenAI Docs — Retrieval — developers.openai.com
- OpenAI Docs — File search — developers.openai.com
- OpenAI Docs — Embeddings — developers.openai.com


