A maioria dos chatbots corporativos ainda falha na mesma pergunta simples: “o que diz o nosso documento interno sobre X?” Um modelo generalista como o GPT-6 Astra não sabe o que está nos teus PDFs, nas tuas notas do Confluence ou no teu manual de apoio ao cliente. A solução chama-se RAG (Retrieval-Augmented Generation) e, com a API da OpenAI, dá para construir um sistema funcional num único fim de tarde. Este tutorial mostra o processo completo: gerar embeddings, guardar vetores numa base de dados, pesquisar por semelhança e gerar respostas fundamentadas com o GPT-6 Astra, tudo em Node.js.

Vais sair daqui com um projeto real, testável, que qualquer programador consegue adaptar a um caso de uso específico: apoio ao cliente, pesquisa em documentação técnica ou um assistente interno de conhecimento. O código está dividido em 12 passos, cada um com um bloco de código testável.

O momento também ajuda. Com o lançamento do GPT-6 Astra a 3 de setembro e a chegada do Sol e do Luna a 22 de setembro, a OpenAI reforçou a família de modelos de chat ao mesmo tempo que manteve estáveis os modelos de embeddings usados neste tutorial. Isso dá alguma previsibilidade a quem está a decidir a arquitetura de um projeto novo: podes escolher o modelo de geração consoante o orçamento e trocar mais tarde sem tocar na camada de pesquisa. Reserva cerca de 90 minutos para seguir todos os passos com calma, incluindo a instalação das dependências e os primeiros testes com dados reais.

O Que é RAG e Porque Usar a API da OpenAI em 2026

RAG combina dois mecanismos: uma pesquisa que encontra os trechos de texto mais relevantes para uma pergunta e um modelo de linguagem que gera a resposta usando esses trechos como contexto. Em vez de depender só do conhecimento que o modelo aprendeu no treino, o sistema vai buscar informação atualizada e específica à tua própria base de dados antes de responder. Isto reduz alucinações e permite citar a fonte exata usada em cada resposta.

A OpenAI lançou três novos modelos GPT-6 em setembro de 2026: o GPT-6 Astra (3 de setembro), e o GPT-6 Sol e o GPT-6 Luna (22 de setembro), estes últimos posicionados como alternativas mais baratas ao Astra. Para geração de texto num sistema RAG, o Astra é o modelo mais documentado e testado em produção até à data, por isso é o que usamos neste tutorial. Do lado dos embeddings, a OpenAI mantém dois modelos ativos: text-embedding-3-small e text-embedding-3-large, sem substituto anunciado até setembro de 2026 segundo a documentação oficial da API de embeddings.

Porque não usar só um prompt gigante com todos os documentos colados? Porque os modelos têm limite de contexto e custo por token. Enviar 500 páginas em cada pergunta é lento, caro e, na prática, piora a qualidade da resposta porque o modelo tem de “procurar” a informação certa no meio de ruído. O RAG resolve isto com pesquisa prévia: só os 3 a 10 trechos mais relevantes chegam ao modelo.

Na prática, este padrão aparece em quase todos os produtos de IA que lidam com conhecimento privado. Um assistente de apoio ao cliente que responde com base no histórico de tickets, uma ferramenta interna que pesquisa contratos e políticas da empresa, ou um chatbot de e-commerce que explica políticas de devolução específicas de cada loja: todos usam a mesma arquitetura de base. A diferença entre uma implementação amadora e uma que aguenta produção está nos detalhes que este tutorial cobre a seguir: como dividir o texto, que base de dados usar e como estruturar o prompt para reduzir alucinações.

Pré-requisitos: Contas, Versões e Custos Estimados

Antes de começar, confirma que tens isto instalado e configurado. As versões abaixo foram verificadas em setembro de 2026 e são as recomendadas para este projeto:

  • Conta na OpenAI Platform com faturação ativa (cartão associado, mesmo que uses o saldo gratuito inicial)
  • Node.js 24 LTS (“Krypton”, versão 24.19.0) ou superior
  • PostgreSQL 16 ou superior, com privilégios para instalar extensões
  • Extensão pgvector na versão 0.8.6 (a mais recente disponível)
  • Pacote npm openai na versão 7.21.0 (SDK oficial em JavaScript/TypeScript)
  • Editor de código e terminal com acesso a psql ou a um cliente PostgreSQL gráfico

Sobre custos: gerar embeddings com text-embedding-3-small custa $0,02 por milhão de tokens, o que torna a indexação de milhares de documentos praticamente gratuita. O custo real está na geração de respostas com o GPT-6 Astra. A tabela seguinte resume os preços oficiais por modelo, confirmados na documentação da OpenAI:

Vale a pena decidir já no início se o projeto vai correr localmente durante o desenvolvimento e migrar depois para um servidor cloud, ou se vais desenvolver logo contra uma base de dados remota. Para este tutorial, uma instância local do PostgreSQL com o pgvector instalado é suficiente para seguir todos os passos sem custos de infraestrutura, e a migração para um servidor de produção resume-se a trocar o valor de DATABASE_URL no ficheiro .env.

ModeloFunçãoInput (por 1M tokens)Output (por 1M tokens)Dimensões
text-embedding-3-smallEmbeddings$0,02–1.536
text-embedding-3-largeEmbeddings$0,13–3.072
text-embedding-ada-002 (legado)Embeddings$0,10–1.536
gpt-6-astraGeração de resposta$10,00 ($1,00 em cache)$50,00–

Repara na diferença entre text-embedding-3-small e o modelo legado ada-002: o pequeno é cinco vezes mais barato e mais recente, por isso não faz sentido escolher o antigo num projeto novo. Guarda estes números, porque vamos voltar a eles no passo sobre custos em produção.

Este tutorial assume conhecimento básico de JavaScript e de SQL, mas não assume experiência prévia com bases de dados vetoriais nem com a API da OpenAI. Se nunca chamaste uma API de IA generativa antes, cada passo inclui o código completo e testável, por isso consegues seguir sem saltar secções. Se já usaste a API da OpenAI para chat simples, podes avançar mais depressa pelos primeiros dois passos e concentrar-te a partir do passo 3, onde começa a lógica específica do RAG.

Passo 1: Criar a Conta OpenAI e Gerar a Chave de API

Entra em platform.openai.com, cria uma organização (se ainda não tiveres) e vai a “API Keys” para gerar uma chave nova. Dá-lhe um nome descritivo, como rag-tutorial-dev, e copia o valor imediatamente: a OpenAI só o mostra uma vez. Guarda essa chave num ficheiro .env na raiz do projeto, nunca no código-fonte.

mkdir rag-openai-tutorial && cd rag-openai-tutorial
npm init -y
npm install [email protected] pg dotenv express
echo "OPENAI_API_KEY=sk-a-tua-chave-aqui" > .env
echo "DATABASE_URL=postgresql://user:pass@localhost:5432/rag_db" >> .env
echo ".env" > .gitignore

Se o teu projeto vai correr em produção, cria uma segunda chave só para esse ambiente e define limites de gasto mensal no painel de faturação da OpenAI. Isto evita surpresas na fatura se algo correr mal num ciclo de indexação automático.

Passo 2: Preparar o Ambiente Node.js e a Ligação à Base de Dados

Com as dependências instaladas, cria um ficheiro de configuração central que outros módulos vão importar. Isto evita repetir a inicialização do cliente OpenAI em cada ficheiro e centraliza a ligação à base de dados.

// config.js
import 'dotenv/config';
import OpenAI from 'openai';
import pg from 'pg';

export const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});

Testa a ligação com um script rápido antes de avançar: node -e "import('./config.js').then(m => m.pool.query('SELECT NOW()').then(r => console.log(r.rows)))". Se devolver a data e hora atuais, a base de dados está acessível e podes seguir para a preparação dos documentos.

Passo 3: Preparar os Documentos e Dividir em Chunks

Um erro comum é enviar documentos inteiros para gerar embeddings. Um PDF de 40 páginas transformado num único vetor perde toda a precisão: a pesquisa vai devolver “o documento inteiro” em vez do parágrafo exato que responde à pergunta. A solução é dividir o texto em chunks (blocos) de tamanho controlado, com sobreposição entre eles para não cortar frases a meio de uma ideia importante.

// chunk.js
export function chunkText(texto, tamanho = 800, sobreposicao = 100) {
  const chunks = [];
  let inicio = 0;

  while (inicio < texto.length) {
    const fim = Math.min(inicio + tamanho, texto.length);
    chunks.push(texto.slice(inicio, fim).trim());
    inicio += tamanho - sobreposicao;
  }

  return chunks.filter(c => c.length > 20);
}

O valor de 800 carateres por chunk com 100 de sobreposição é um bom ponto de partida para documentação técnica em português. Para transcrições de áudio ou conversas de apoio ao cliente, chunks mais pequenos (400 a 600 carateres) costumam funcionar melhor porque cada troca de mensagem já é uma unidade de sentido completa.

Uma alternativa mais avançada é o chunking semântico, que divide o texto em pontos onde o significado muda (por exemplo, entre secções de um manual), em vez de usar um número fixo de carateres. Isto costuma dar melhores resultados em documentos longos e bem estruturados, como contratos ou manuais técnicos com títulos claros, mas exige mais lógica de pré-processamento. Para começar, o chunking por tamanho fixo com sobreposição é suficiente na grande maioria dos casos e evita complexidade desnecessária logo na primeira versão.

Passo 4: Gerar Embeddings com a API da OpenAI

Um embedding é uma representação numérica do significado de um texto: um vetor de 1.536 números, no caso do text-embedding-3-small. Textos com significados parecidos produzem vetores matematicamente próximos, o que permite comparar “semelhança de sentido” em vez de comparar palavras exatas.

// embed.js
import { openai } from './config.js';

export async function gerarEmbedding(texto) {
  const resposta = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: texto,
  });
  return resposta.data[0].embedding;
}

export async function gerarEmbeddingsEmLote(textos) {
  const resposta = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: textos,
  });
  return resposta.data.map(d => d.embedding);
}

Usa sempre a função em lote (gerarEmbeddingsEmLote) quando estiveres a indexar muitos chunks de uma vez. Enviar 200 chunks numa única chamada é mais rápido e mais barato do que 200 chamadas separadas, porque reduz a sobrecarga de rede por pedido.

Para bases de documentos muito grandes, divide o processo de indexação em lotes de 100 a 500 chunks por chamada e adiciona uma pequena pausa entre lotes para não esbarrar no limite de tokens por minuto do teu Tier de conta. Um script de indexação simples, corrido uma vez por documento novo ou atualizado, é suficiente para a maioria das equipas: não precisas de um pipeline complexo de streaming para começar a ter valor real do sistema.

Passo 5: Escolher a Base de Dados Vetorial

Precisas de um sítio para guardar os vetores e pesquisar por semelhança de forma rápida. As três opções mais usadas com a API da OpenAI são o pgvector (extensão do PostgreSQL), o Qdrant e o Chroma. Cada uma tem um perfil de uso diferente:

Base de dadosTipoOnde correrQuando escolher
pgvectorExtensão open source do PostgreSQLServidor próprio ou gestor cloud (RDS, Supabase, Neon)Já usas PostgreSQL e queres um só sistema para tudo
QdrantBase de dados vetorial dedicada, open sourceSelf-hosted (Docker) ou Qdrant Cloud geridoPrecisas de filtragem avançada e alta escala
ChromaBase de dados vetorial embutida, open sourceLocal, dentro do próprio processo da aplicaçãoProtótipos rápidos e projetos pequenos sem servidor dedicado

Este tutorial usa o pgvector porque a maioria das equipas já tem PostgreSQL em produção e evita gerir um sistema extra só para vetores. Se o teu volume de documentos ultrapassar os milhões de chunks ou precisares de filtros compostos muito complexos, o Qdrant tende a escalar melhor. Para uma prova de conceito local sem infraestrutura, o Chroma resolve em minutos.

Há também serviços vetoriais totalmente geridos, como o Pinecone, que retiram da tua equipa a responsabilidade de operar e escalar a infraestrutura, ao custo de mais uma conta e mais uma fatura para gerir. Para o objetivo deste tutorial, ficar com o PostgreSQL evita essa complexidade adicional sem sacrificar desempenho para volumes até algumas centenas de milhares de documentos, que cobre a esmagadora maioria dos projetos internos de uma empresa.

Passo 6: Criar o Esquema no PostgreSQL com pgvector

Liga-te à base de dados com psql e ativa a extensão. Depois cria a tabela que vai guardar o texto original, o vetor e metadados úteis como o nome do ficheiro de origem.

-- schema.sql
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE documentos (
  id SERIAL PRIMARY KEY,
  conteudo TEXT NOT NULL,
  embedding VECTOR(1536),
  metadata JSONB,
  criado_em TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX documentos_embedding_idx
  ON documentos USING hnsw (embedding vector_cosine_ops);

O índice HNSW (Hierarchical Navigable Small World) acelera a pesquisa por semelhança em tabelas grandes. Sem ele, cada pesquisa compara o vetor da pergunta com todos os vetores da tabela, um a um, o que fica lento a partir de algumas dezenas de milhares de linhas. Agora cria a função que guarda cada chunk indexado:

// store.js
import { pool } from './config.js';

export async function guardarChunk(conteudo, embedding, metadata = {}) {
  await pool.query(
    `INSERT INTO documentos (conteudo, embedding, metadata)
     VALUES ($1, $2, $3)`,
    [conteudo, JSON.stringify(embedding), metadata]
  );
}

Passo 7: Implementar a Pesquisa Semântica

Com os dados indexados, o próximo passo é pesquisar. O operador <=> do pgvector calcula a distância de cosseno entre dois vetores. Quanto menor a distância, mais parecidos os textos. Convertemos a distância em “similaridade” (1 menos a distância) só para facilitar a leitura dos resultados.

// search.js
import { pool } from './config.js';

export async function pesquisarSemelhantes(embeddingConsulta, limite = 5) {
  const { rows } = await pool.query(
    `SELECT conteudo, metadata,
            1 - (embedding <=> $1) AS similaridade
     FROM documentos
     ORDER BY embedding <=> $1
     LIMIT $2`,
    [JSON.stringify(embeddingConsulta), limite]
  );
  return rows;
}

O valor de limite (também chamado top-k) controla quantos trechos chegam ao modelo. Cinco é um bom valor por omissão: suficiente contexto sem inflacionar o custo por pedido. Para perguntas mais complexas, que exigem cruzar informação de várias secções, sobe para 8 ou 10.

O pgvector também suporta distância euclidiana (<->) e produto interno (<#>) além da distância de cosseno usada aqui. Para embeddings gerados pela API da OpenAI, a distância de cosseno é a recomendação prática mais comum, porque estes vetores já vêm normalizados e a métrica de cosseno lida bem com essa normalização sem cálculos extra do teu lado.

Passo 8: Construir o Prompt Aumentado

Este é o passo que separa um RAG que funciona de um que alucina. O prompt tem de deixar claro ao modelo que só pode usar o contexto fornecido, e tem de lhe dar uma saída explícita para quando a resposta não está lá.

// prompt.js
export function construirPrompt(pergunta, contexto) {
  const blocos = contexto
    .map((c, i) => `[Fonte ${i + 1}]\n${c.conteudo}`)
    .join('\n\n');

  return `Responde à pergunta usando apenas a informação no contexto abaixo.
Se a resposta não estiver no contexto, diz claramente que não tens essa informação.
Cita o número da fonte usada entre parênteses, por exemplo (Fonte 2).

Contexto:
${blocos}

Pergunta: ${pergunta}`;
}

Pedir explicitamente a citação da fonte tem um efeito prático duplo: obriga o modelo a ancorar-se no texto fornecido e dá-te uma forma automática de verificar, do lado do utilizador, de onde veio cada afirmação.

Podes ir mais longe e separar as instruções fixas da pergunta variável usando uma mensagem de sistema (role: "system") em vez de colocar tudo numa única mensagem de utilizador. Isto ajuda o cache de prompt da OpenAI, mencionado no passo 12, a reconhecer a parte repetida do pedido e cobrar um preço mais baixo por esses tokens em chamadas seguintes.

Passo 9: Gerar a Resposta com o GPT-6 Astra

Com o prompt montado, falta só chamar o modelo de chat. Usa uma temperatura baixa (0,1 a 0,3) para reduzir variação criativa desnecessária: num sistema de resposta a perguntas factuais, queres consistência, não originalidade.

// generate.js
import { openai } from './config.js';
import { construirPrompt } from './prompt.js';

export async function gerarResposta(pergunta, contexto) {
  const prompt = construirPrompt(pergunta, contexto);

  const resposta = await openai.chat.completions.create({
    model: 'gpt-6-astra',
    messages: [{ role: 'user', content: prompt }],
    temperature: 0.2,
    max_tokens: 500,
  });

  return resposta.choices[0].message.content;
}

Se o custo do gpt-6-astra ($10 de input e $50 de output por milhão de tokens) for elevado para o teu volume de pedidos, o GPT-6 Sol ou o GPT-6 Luna, lançados a 22 de setembro de 2026 como opções mais económicas, são candidatos a testar para casos de uso menos exigentes. A OpenAI ainda não publicou tabelas de preço detalhadas para esses dois modelos, por isso confirma sempre o valor atual na tua conta antes de trocar de modelo em produção.

Para interfaces com utilizador em tempo real, considera ativar streaming na resposta, trocando chat.completions.create por uma chamada com stream: true e lendo os pedaços de texto à medida que chegam. Isto não reduz o custo nem o tempo total de geração, mas melhora muito a perceção de velocidade porque o utilizador começa a ver texto em vez de olhar para um indicador de carregamento parado.

Passo 10: Expor Tudo Numa API REST com Express

Agora junta as peças numa rota HTTP simples que qualquer frontend, bot de Slack ou aplicação móvel pode chamar.

// server.js
import express from 'express';
import { gerarEmbedding } from './embed.js';
import { pesquisarSemelhantes } from './search.js';
import { gerarResposta } from './generate.js';

const app = express();
app.use(express.json());

app.post('/perguntar', async (req, res) => {
  try {
    const { pergunta } = req.body;
    if (!pergunta) {
      return res.status(400).json({ erro: 'Campo "pergunta" em falta' });
    }

    const embeddingConsulta = await gerarEmbedding(pergunta);
    const contexto = await pesquisarSemelhantes(embeddingConsulta, 5);
    const resposta = await gerarResposta(pergunta, contexto);

    res.json({ resposta, fontes: contexto.length });
  } catch (erro) {
    console.error(erro);
    res.status(500).json({ erro: 'Falha ao processar o pedido' });
  }
});

app.listen(3000, () => console.log('RAG API a correr na porta 3000'));

Testa com um pedido real. Cria uma tabela de exemplo, indexa dois ou três documentos curtos com os passos 4 e 6, e faz a chamada:

curl -X POST http://localhost:3000/perguntar \
  -H "Content-Type: application/json" \
  -d '{"pergunta": "Qual é o prazo de entrega padrão?"}'

# Resposta esperada:
{
  "resposta": "O prazo de entrega padrão é de 3 a 5 dias úteis (Fonte 1).",
  "fontes": 5
}

Antes de expor esta rota fora da tua rede local, adiciona um middleware de autenticação, mesmo que simples, como uma chave partilhada no cabeçalho Authorization. Sem isso, qualquer pessoa com o URL consegue gastar o teu saldo da OpenAI a fazer perguntas, o que é um dos vetores de abuso mais comuns em APIs de IA deixadas abertas por engano.

Passo 11: Testar e Avaliar a Qualidade das Respostas

Um sistema RAG que “parece funcionar” em três testes manuais pode falhar silenciosamente em produção. Cria um pequeno conjunto de perguntas com resposta conhecida (10 a 20 é suficiente para começar) e corre-as automaticamente sempre que alterares o prompt, o modelo ou o tamanho dos chunks.

  • Verifica se a resposta cita a fonte correta, não só se o texto “soa bem”
  • Testa perguntas cuja resposta não existe na base de dados, para confirmar que o modelo admite não saber em vez de inventar
  • Mede o tempo médio de resposta ponta a ponta (embedding + pesquisa + geração), não só o tempo do modelo
  • Regista o número de tokens gasto por pedido para prever custos à escala real

Se tiveres tempo para ir mais além, junta uma etapa de re-ranking (explicada nas dicas avançadas abaixo) e volta a correr o mesmo conjunto de testes para medir se a precisão das respostas melhorou de facto, e não só na perceção.

Ferramentas de avaliação automática, como bibliotecas de LLM-as-judge, ajudam a escalar este processo quando o número de perguntas de teste cresce para centenas. A ideia é usar um segundo modelo para pontuar se a resposta gerada corresponde à resposta esperada, o que evita reveres manualmente cada resultado sempre que fazes um ajuste no prompt ou no tamanho dos chunks. Para um projeto pequeno, a verificação manual de 15 a 20 perguntas continua a ser suficiente e mais rápida de configurar.

Passo 12: Colocar em Produção – Cache, Custos e Limites de Taxa

Antes de expor a API a utilizadores reais, confirma os limites de taxa (rate limits) do teu nível de conta para o GPT-6 Astra. Estes limites sobem automaticamente à medida que a conta gasta mais e acumula histórico de pagamento sem incidentes:

Nível (Tier)Pedidos/min (RPM)Tokens/min (TPM)Limite de fila em batch
Tier 1500500.0001.500.000
Tier 25.0001.000.0003.000.000
Tier 35.0002.000.000100.000.000
Tier 410.0004.000.000200.000.000
Tier 515.00040.000.00015.000.000.000

Para reduzir custos, ativa o cache de input: pedidos que reutilizam o mesmo prefixo de prompt (por exemplo, instruções de sistema fixas) pagam $1 por milhão de tokens em cache, contra $10 no input normal do GPT-6 Astra, uma redução de 90%. Estrutura o teu prompt para colocar as partes fixas (instruções, formato de resposta) no início e a pergunta variável no fim, para maximizar o aproveitamento do cache.

Configura também alertas de gasto no painel da OpenAI antes de anunciares a API a qualquer utilizador real. Um alerta a 50% e outro a 90% do orçamento mensal dá-te tempo de reagir a um pico de tráfego inesperado ou a um bug que gera pedidos em loop, sem esperar pela fatura no fim do mês para descobrir o problema.

Erros Comuns ao Construir um Sistema RAG

Estes são os erros que mais aparecem em implementações de RAG feitas à pressa, e que custam horas de debugging quando passam despercebidos. A maioria não gera um erro visível no ecrã: o sistema continua a responder, só que com qualidade pior, o que os torna mais difíceis de apanhar do que uma falha explícita da API.

  • Chunks demasiado grandes: blocos de mais de 1.500 carateres diluem o significado específico e baixam a precisão da pesquisa.
  • Sem sobreposição entre chunks: corta frases a meio, perdendo contexto que ficava dividido entre dois blocos.
  • Misturar dimensões de embeddings: indexar com text-embedding-3-small (1.536 dimensões) e pesquisar com text-embedding-3-large (3.072) não funciona, os vetores têm tamanhos incompatíveis.
  • Ignorar metadados: sem filtrar por data, autor ou categoria, a pesquisa pode devolver informação desatualizada em vez da versão mais recente de um documento.
  • Prompt sem instrução de “não sei”: sem essa saída explícita, o modelo tende a preencher lacunas com informação plausível mas inventada.
  • Top-k fixo demasiado baixo: limitar sempre a 2 ou 3 resultados corta contexto útil em perguntas que exigem cruzar várias fontes.
  • Não monitorizar os limites de taxa: um pico de tráfego sem tratamento de erro 429 derruba a aplicação em vez de aplicar retry com espera progressiva.

Resolução de Problemas: os Erros Mais Frequentes

Lista de problemas reais que vais provavelmente encontrar, com a causa mais comum e a correção direta. Antes de investigares um erro em profundidade, confirma sempre estes três pontos básicos: se a chave de API está a ser carregada corretamente, se a extensão pgvector está ativa na base de dados que a aplicação está mesmo a usar, e se as dimensões do vetor na tabela correspondem ao modelo de embedding escolhido. Estes três detalhes explicam a maioria dos problemas reportados por quem está a construir o primeiro sistema RAG.

Erro ou sintomaCausa provávelCorreção
401 Incorrect API key providedChave errada, revogada ou variável de ambiente não carregadaConfirma o .env e que dotenv/config é importado antes do cliente OpenAI
429 Rate limit reachedDemasiados pedidos por minuto para o teu Tier atualImplementa retry com espera exponencial e agrupa embeddings em lote
context_length_exceededContexto recuperado + pergunta ultrapassa o limite de tokens do modeloReduz o top-k ou o tamanho dos chunks antes de montar o prompt
operator does not exist: vector <-> vectorExtensão pgvector não ativada ou coluna com tipo erradoCorre CREATE EXTENSION IF NOT EXISTS vector; antes de criar a tabela
Respostas genéricas, sem usar o contextoPrompt mal estruturado ou pesquisa a devolver resultados irrelevantesRevê a instrução do prompt e valida manualmente os resultados da pesquisa
Pesquisa vetorial lentaTabela sem índice HNSW ou índice desatualizadoCria o índice do passo 6 e corre VACUUM ANALYZE documentos;
Mismatch de dimensão no INSERTColuna criada com dimensão diferente do modelo de embedding usadoGarante que VECTOR(1536) corresponde ao modelo escolhido
Fatura da OpenAI acima do esperadoContexto demasiado grande ou re-indexação repetida dos mesmos documentosAtiva cache de prompt e guarda hash dos documentos já indexados
CORS bloqueado no frontendMiddleware CORS em falta na API ExpressAdiciona app.use(cors()) com as origens permitidas
Respostas inconsistentes na mesma perguntaTemperatura alta ou ausência de seed fixoBaixa a temperatura para 0,1–0,3 no passo 9

Dicas Avançadas: Re-ranking, Pesquisa Híbrida e Metadados

Depois de teres a versão base a funcionar, há quatro melhorias que costumam ter maior impacto na qualidade percebida pelo utilizador final. Nenhuma delas é obrigatória para um primeiro lançamento, mas todas valem a pena assim que o sistema sair da fase de protótipo e começar a receber tráfego real.

Re-ranking: a pesquisa vetorial devolve os resultados mais próximos matematicamente, mas nem sempre os mais úteis para responder à pergunta exata. Uma técnica comum é pesquisar mais resultados do que precisas (por exemplo, 20) e depois usar o próprio modelo de chat, ou um modelo dedicado de re-ranking, para reordenar esses 20 e escolher os 5 melhores antes de montar o prompt final.

Pesquisa híbrida: combina a pesquisa por semelhança semântica com pesquisa tradicional por palavras-chave (full-text search do próprio PostgreSQL). Isto ajuda em casos onde o utilizador procura um termo técnico exato, como um código de erro ou uma referência de produto, que a pesquisa semântica sozinha pode não priorizar corretamente.

Filtragem por metadados: usa a coluna JSONB para guardar data de publicação, departamento ou nível de acesso, e filtra a query SQL antes de calcular a distância vetorial. Isto é essencial em ambientes multi-tenant, onde um utilizador nunca deve receber contexto de documentos de outra organização, e reduz o espaço de pesquisa, o que também acelera a query.

Reindexação incremental: em vez de apagar e recriar toda a tabela sempre que um documento muda, guarda um hash do conteúdo original em cada linha. Quando um documento é atualizado, recalcula só os chunks desse ficheiro e substitui as linhas correspondentes. Isto poupa chamadas à API de embeddings e evita indexares repetidamente conteúdo que não mudou, um erro comum listado mais acima nesta lista de armadilhas.

Perguntas Frequentes

O que é RAG e em que difere de um chatbot normal?
RAG (Retrieval-Augmented Generation) é uma arquitetura que pesquisa informação relevante numa base de dados antes de gerar a resposta. Um chatbot normal responde só com o conhecimento que o modelo aprendeu no treino, sem acesso aos teus documentos privados ou a informação recente.

Preciso de uma base de dados vetorial dedicada ou posso usar só o PostgreSQL?
Não precisas de um sistema dedicado. O pgvector transforma o PostgreSQL numa base de dados vetorial funcional, adequada para a maioria dos projetos até alguns milhões de vetores.

Qual é o custo aproximado de indexar 10.000 páginas de documentos?
Com o text-embedding-3-small a $0,02 por milhão de tokens, indexar 10.000 páginas (cerca de 5 milhões de tokens, dependendo da densidade do texto) custa tipicamente menos de $0,50. O custo real do sistema está nas chamadas de geração de resposta, não na indexação.

Devo escolher text-embedding-3-small ou text-embedding-3-large?
Começa sempre pelo small. É mais barato, mais rápido e suficiente para a maioria dos casos de uso em português. Só migra para o large se testares e confirmares uma diferença real na precisão da pesquisa para o teu domínio específico.

Posso substituir o GPT-6 Astra por um modelo mais barato como o GPT-6 Sol ou Luna?
Sim, tecnicamente basta trocar o nome do modelo na chamada da API. A OpenAI lançou o Sol e o Luna a 22 de setembro de 2026 como alternativas de custo mais baixo ao Astra, mas confirma sempre os preços atualizados na tua conta antes de mudar um sistema em produção.

Como evito que o modelo invente respostas fora do contexto fornecido?
A instrução explícita no prompt (“se a resposta não estiver no contexto, diz que não sabes”) reduz muito o problema. Combinar isso com temperatura baixa e um top-k de pesquisa adequado (nem muito curto, nem excessivo) melhora ainda mais a fidelidade das respostas.

O RAG funciona bem com documentos em português europeu?
Sim. Os modelos de embedding e de chat da OpenAI suportam português de forma nativa. A qualidade da pesquisa depende mais da estratégia de chunking e da limpeza do texto do que do idioma em si.

Preciso de fazer fine-tuning do modelo para usar RAG?
Não. RAG e fine-tuning resolvem problemas diferentes: o RAG dá acesso a informação nova e específica sem alterar o modelo, enquanto o fine-tuning ajusta o comportamento e o estilo do modelo com exemplos de treino. Para a maioria dos casos de apoio ao cliente ou pesquisa documental, o RAG sozinho já resolve o problema. As duas técnicas também não são mutuamente exclusivas: uma equipa pode usar fine-tuning para ensinar o modelo a responder sempre num tom e formato específicos, e usar RAG na mesma aplicação para lhe dar acesso a factos atualizados que o fine-tuning, por si só, nunca resolveria.

Este sistema funciona com outros formatos além de texto, como PDFs e planilhas?
Sim, desde que extraias o texto desses ficheiros antes do passo 3. Para PDFs, uma biblioteca de extração de texto trata da conversão para texto simples. Para planilhas, converte cada linha relevante numa frase descritiva antes de gerar o chunk, porque o modelo de embedding trabalha com texto corrido, não com tabelas estruturadas.