AI Engineering na prática: construindo o Gringo
RAG, busca semântica, classificação de intenção, e os padrões que fazem IA funcionar em produção
Tenho 80+ artigos publicados na newsletter. Processos seletivos, guias de negociação, comparações CLT vs PJ, dicas de entrevista. Conteúdo acumulado desde 2024.
O problema: esse conhecimento fica enterrado. Quando alguém pergunta “como é a entrevista da Brex?”, a resposta existe em algum artigo que ninguém lembra.
Então construí o Gringo, um assistente de IA no WhatsApp que responde perguntas usando minha base de conhecimento. Menciona @Gringo no grupo, faz uma pergunta, e ele responde em segundos.
Esse artigo é um deep-dive técnico em como construí isso. Não é um tutorial passo-a-passo. É uma explicação dos padrões de engenharia que usei e por que funcionam.
Gringo em ação
Antes de entrar nos detalhes técnicos, veja o sistema funcionando:
Alguém pergunta sobre entrevistas na Brex. O Gringo busca nos artigos relevantes, encontra informações de 2-3 fontes diferentes, e responde em ~3 segundos com links para os artigos originais.
Agora vamos ver como isso funciona por baixo.
O que esperar
RAG (Retrieval-Augmented Generation): como funciona busca semântica e por que é diferente de busca por palavras-chave
Embeddings: o que são, como gero, e por que escolhi Voyage AI
Classificação de intenção: como reduzo custos filtrando mensagens antes do pipeline pesado
Padrões de produção: cache, separação de responsabilidades, servidor não-bloqueante
RAG: o coração do sistema
RAG (Retrieval-Augmented Generation) é o padrão que permite um LLM responder perguntas usando uma base de conhecimento específica. Em vez de depender só do que o modelo aprendeu no treinamento, você busca informação relevante e passa como contexto.
A arquitetura tem duas partes:
Preparação: transformando artigos em vetores
Antes de qualquer pergunta, preciso preparar a base de conhecimento. O processo roda uma vez (e novamente quando publico artigos novos):
1. Buscar artigos do Substack
Uso a API do Substack para puxar todos os artigos publicados. Cada artigo vem com título, slug, URL, e conteúdo em HTML.
2. Converter para markdown
HTML é ruído para o modelo. Converto para markdown limpo, só o texto que importa.
3. Gerar resumos com IA
Cada artigo recebe um resumo de 2-3 frases gerado por LLM. Isso melhora a qualidade da busca: o resumo captura a essência do artigo em linguagem densa.
const { text: summary } = await generateText({
model: getModel("google:gemini-3-flash-preview"),
prompt: `Resumo de 2-3 frases que capture:
1. O problema/tema principal
2. Insights chave
3. Pra quem é útil
Escreva em português. Seja direto.
Artigo: ${title}
${content}`,
});
4. Dividir em chunks
Um artigo de 3000 palavras não cabe inteiro no contexto. Divido em pedaços de 200-1500 caracteres, respeitando quebras de parágrafo:
const chunks = defaultChunker(content, {
minCharsSoftLimit: 200, // mínimo por chunk
maxCharsSoftLimit: 1500, // alvo
maxCharsHardLimit: 3000, // máximo absoluto
delimiter: "\n\n", // quebra em parágrafos
});
Por que esses números? Chunks muito pequenos perdem contexto. Muito grandes diluem a relevância na busca. 200-1500 é o sweet spot para artigos técnicos em português.
5. Gerar embeddings
Cada chunk é convertido em um vetor de 1024 dimensões usando Voyage AI:
export const rag = new RAG(components.rag, {
textEmbeddingModel: voyage.textEmbeddingModel("voyage-3.5-lite"),
embeddingDimension: 1024,
});
O que são embeddings?
Embeddings são a tecnologia fundamental por trás de como LLMs processam texto. Cada token que entra em um modelo como GPT ou Gemini é convertido em um vetor de embedding antes de qualquer processamento. Sem embeddings, LLMs não existiriam.
A ideia: transformar texto em números de uma forma que capture significado. Imagine um espaço de 1024 dimensões (difícil de visualizar, mas a matemática funciona igual). Cada palavra, frase, ou parágrafo vira um ponto nesse espaço. O truque: textos com significados similares ficam próximos.
Por exemplo:
“negociação salarial” e “como pedir aumento” ficam próximos
“entrevista técnica” e “coding interview” ficam próximos
“receita de bolo” fica longe de todos esses
Antes de embeddings, métodos como TF-IDF comparavam texto palavra por palavra, sem entender significado. “Carro” e “automóvel” eram vetores completamente diferentes. Com embeddings, o modelo entende que são sinônimos porque aparecem em contextos similares durante o treinamento.
E aqui está o pulo do gato para RAG: se LLMs já usam embeddings internamente para “entender” texto, podemos usar a mesma técnica para conectar nosso próprio conhecimento ao modelo. Gero embeddings dos meus artigos, e quando alguém pergunta “como ganhar mais”, encontro artigos sobre “negociação salarial” mesmo sem as palavras exatas. É como dar memória extra ao LLM sobre um domínio específico.
Por que 1024 dimensões?
Voyage 3.5-lite usa vetores de 1024 dimensões. Mais dimensões significa melhor separação semântica, mas mais custo de storage e busca.
Para minha base (~80 artigos, ~400 chunks), 1024 é confortável. Poderia usar 256 e economizar 4x em storage, mas não é o gargalo. A latência vem do LLM, não da busca vetorial.
Por que Voyage AI?
A resposta honesta: velocidade para validar a ideia.
Existem alternativas open source excelentes. Modelos como intfloat/multilingual-e5-large ou BAAI/bge-m3 performam bem em português e rodam localmente. Mas exigem setup: infra pra hospedar, GPU, gerenciar deploys.
Voyage é uma API. Em 30 minutos eu tinha embeddings funcionando. Isso me permitiu colocar o bot no ar e começar a coletar feedback real.
Por que Voyage especificamente (e não OpenAI ou Cohere)?
Foco em multilingual: Voyage foi treinado com ênfase em idiomas além do inglês. Para uma base de conhecimento em português, isso importa
Custo competitivo: $0.02 por milhão de tokens, igual ao OpenAI text-embedding-3-small
Reputação em benchmarks: consistentemente bem avaliado para retrieval tasks, especialmente multilingual
Ainda não fiz testes comparativos rigorosos. O bot está no ar há poucos dias. Se os resultados de busca não estiverem bons, vou testar alternativas. Por enquanto, está funcionando.
A lição: para MVPs, otimize para ciclo de feedback rápido. A melhor arquitetura é a que te deixa aprender mais rápido.
Busca: encontrando informação relevante
Quando uma pergunta chega, o processo é:
1. Converter pergunta em embedding
A pergunta passa pelo mesmo modelo Voyage, gerando um vetor de 1024 dimensões.
2. Busca por similaridade
Comparo o vetor da pergunta com todos os chunks usando similaridade de cosseno. Chunks com score > 0.5 são considerados relevantes:
const { results, entries } = await rag.search(ctx, {
namespace: "articles",
query: question,
limit: 9, // busca mais, depois filtra
vectorScoreThreshold: 0.5,
});
3. Agrupar por artigo
Se 3 chunks do mesmo artigo são relevantes, não quero repetir. Agrupo por fonte e pego os 2 melhores chunks de cada artigo:
const articles = Array.from(articleMap.values())
.sort((a, b) => b.bestScore - a.bestScore)
.slice(0, 3); // top 3 artigos
4. Montar contexto para o LLM
Formato os chunks relevantes com título, resumo, e URL do artigo:
const context = articles.map(article => `
*${article.title}*
${article.summary}
Link: ${article.url}
Trechos relevantes:
${article.chunks.slice(0, 2).join("\n\n")}
`).join("\n\n---\n\n");
5. Gerar resposta
O contexto vai pro Gemini Flash junto com a pergunta. O modelo responde baseado nos artigos, citando as fontes.
O prompt de geração é onde a personalidade do Gringo vive. Uma versão simplificada:
const systemPrompt = `Você é o Gringo, o mascote da Na Gringa.
Um cachorro caramelo que ajuda devs brasileiros com carreiras.
PERSONALIDADE:
- Caloroso, brincalhão, genuinamente quer ajudar
- Usa humor brasileiro naturalmente
- Direto e honesto, mas sempre com carinho
- Às vezes faz referências a coisas de cachorro
COMO FALAR:
- Português brasileiro natural, como papo no WhatsApp
- Expressões: "bora", "manja", "show", "firmeza"
- Respostas curtas pro casual, completas pra técnico
- Use **negrito** pra destacar pontos importantes`;
const prompt = `Artigos relevantes encontrados:
${context}
Pergunta: ${question}
Use as informações dos artigos como base.
Ao final, liste os links dos artigos que você usou.`;
O prompt real tem ~200 linhas com repositório de piadas, referências do Remote Defender (nosso jogo), e instruções de segurança. Mas a essência é essa: personalidade definida + contexto dos artigos + pergunta.
Quando RAG não encontra nada
Nem toda pergunta tem resposta nos artigos. “Como configurar Kubernetes?” provavelmente não está na minha base de conhecimento sobre carreiras.
Quando nenhum chunk passa do threshold de similaridade (0.5), o sistema não inventa. O prompt muda: em vez de “responda baseado nesses artigos”, vira “não encontrei artigos específicos, mas posso ajudar com conhecimento geral de engenharia”.
Isso evita alucinações. O modelo sabe quando está respondendo com contexto da base vs. conhecimento geral.
Por que busca semântica > busca por palavras-chave?
Busca tradicional (Elasticsearch, Algolia) encontra documentos que contêm as palavras da query. Funciona bem quando o usuário sabe os termos exatos.
Busca semântica encontra documentos com significado similar. Funciona quando:
Usuário usa sinônimos (”aumento” vs “negociação salarial”)
Conceito está implícito (”como me destacar” → artigos sobre networking, currículo, visibilidade)
Pergunta é conversacional (”o que fazer quando meu chefe não reconhece meu trabalho”)
Para um assistente de IA, busca semântica é essencial. Usuários não perguntam com keywords. Perguntam com linguagem natural.
Arquitetura completa
Com RAG explicado, aqui está o fluxo completo:
A stack:
Baileys: biblioteca que conecta no WhatsApp Web (não oficial, mas funciona)
Express: camada HTTP que expõe o serviço de WhatsApp (health checks, API)
Convex: backend que orquestra tudo (banco, actions, cron jobs), o mesmo que uso no agregador de vagas
Gemini Flash: LLM rápido e barato para classificação e geração
Voyage AI: embeddings otimizados para português
Se você quiser ver outro exemplo de arquitetura similar (scraping + processamento + API), escrevi sobre como construí o agregador de vagas:
Classificação de intenção: otimizando custos
RAG é poderoso, mas caro. Cada pergunta envolve:
Gerar embedding da pergunta (Voyage API)
Buscar no banco vetorial
Gerar resposta (Gemini API)
Quando alguém manda “oi” pro bot, não faz sentido rodar esse pipeline. É só responder “Oi! No que posso ajudar?”
Então classifico a intenção antes de decidir o que fazer:
const IntentSchema = z.object({
intent: z.enum(["greeting", "attack", "casual", "question"]),
casualResponse: z.string().optional(),
});
greeting (”oi”, “eai”, “bom dia”)
→ Resposta estática aleatória. Custo: R$0.
attack (tentativas de prompt injection, pedir tokens secretos)
→ Resposta com rickroll. Custo: R$0.
casual (piadas, agradecimentos, papo informal)
→ LLM gera resposta curta. Custo: ~R$0,0003.
question (perguntas reais sobre carreira, entrevistas, vagas)
→ RAG completo. Custo: ~R$0,001.
A ordem importa
1. Verifica cache ← Hit? Retorna (R$0)
2. Classifica intenção ← Não é question? Resposta simples
3. Roda RAG ← Só quando necessário
Cache vem primeiro porque é O(1). Se a mesma pergunta foi feita nas últimas 24h, retorno a resposta cacheada sem gastar nada.
Classificação vem segundo porque é barato (~R$0,0001) e filtra a maioria das mensagens. Só perguntas reais chegam no RAG.
Personalidade através de variação
Bots que respondem igual toda vez parecem robôs. Tenho dezenas de variações para cada tipo:
const GREETING_RESPONSES = [
"Opa! No que posso ajudar?",
"Fala! Tô aqui, abanando o rabo virtual",
"Au au! Quer dizer... oi! Como posso ajudar?",
"Salve! Esse caramelo aqui tá pronto pra ajudar",
// ... dezenas mais
];
Para ataques (tentativas de prompt injection), deflecto com humor. Rickrolls, memes, respostas que não engajam com a tentativa. O importante é não executar o que o atacante pediu e não revelar informação sobre o sistema.
Separação de responsabilidades
Um padrão que facilita muito: o serviço de WhatsApp sabe como receber e enviar mensagens. O backend (Convex) sabe o que responder.
WhatsApp: "Alguém perguntou: 'como negociar salário?'"
Convex: "Responde isso: '...baseado nos artigos...'"
WhatsApp: "OK, enviei."
Isso permite:
Testar o backend sem WhatsApp real (simulo chamadas HTTP)
Trocar WhatsApp por Telegram/Discord sem mudar a lógica de IA
O “cérebro” (RAG, cache, LLM) fica isolado do “corpo” (transporte)
Custos e latência
Números reais do sistema em produção:
Custo médio por pergunta: ~R$0,001
Latência (cache hit): ~200ms
Latência (RAG completo): ~2-3s
Infraestrutura: VPS $5/mês
O bot está no ar há poucos dias. Quando tiver mais dados, compartilho métricas detalhadas.
Estimativa mensal: 1000 perguntas/mês = ~R$1 de APIs + R$30 de infra = ~R$31/mês para um assistente de IA em produção.
Próximos passos
O sistema atual só busca em artigos. A próxima versão é um agente que escolhe qual ferramenta usar:
“Quais vagas de React estão abertas?” → busca no portal de vagas
“Quantas empresas Tier S tem no site?” → consulta estatísticas
O código existe. Falta testar em produção.
Conclusões
Algumas coisas que aprendi construindo isso:
Prompts complexos não são melhores. A primeira versão do classificador de intenção tinha um prompt elaborado com muitos exemplos. Funcionava mal. Simplifiquei para algo direto e a acurácia melhorou. Às vezes menos contexto é mais.
Personalidade importa mais do que você imagina. Quando tirei as variações de resposta pra simplificar, o bot ficou sem graça. Voltei atrás. A variação (”abanando o rabo virtual”) faz diferença na experiência.
WhatsApp é cheio de pegadinhas. O formato de identificação de usuários mudou (JID vs LID). Tive que corrigir a detecção de menções múltiplas vezes. Se você for integrar com WhatsApp, prepare-se para debugging de formatos obscuros.
Cache desde o dia 1 valeu a pena. Implementei cache de 24h junto com o MVP. Não custou muito a mais e já economiza chamadas de API quando as mesmas perguntas se repetem.
O gargalo nunca é onde você espera. Minha latência é 2-3s. O LLM é 95% disso. Otimizar o servidor (Express → Fastify) economizaria 10ms. Não vale o esforço ainda.
O padrão mais importante: filtre antes de processar. Nem toda mensagem precisa do pipeline completo.
Se você está construindo algo com IA, comece pelo filtro de intenção. É o padrão com melhor ROI que descobri.
Se você curtiu esse deep-dive técnico, talvez goste de como estou usando IA no meu workflow de desenvolvimento:
Quer ver o Gringo em ação? Ele está ativo na comunidade do Na Gringa. Assinantes podem entrar no grupo e testar.
Manda um “@Gringo” e faz uma pergunta.











Que artigo maravilhoso! Incrível como você consegue transmitir o passo a passo técnico de forma objetiva mas ainda assim, leve. Parabéns!
Tava esperando ansiosamente por esse artigo!