Todos os projetos
Case corporativoPlataforma interna

Geração de leads com IA

Plataforma de Prospecção B2B

Leads B2B gerados com IA, cadastro conferido, sem duplicata no CRM e um modelo que aprende por que o comercial descarta.

Papel
Autor · produto, backend, ML e operação
Período
mai a out 2026
Stack
  • Python
  • FastAPI
  • React
  • PostgreSQL
  • SQLite
  • Gemini
  • LightGBM
  • SSE
  • OIDC
  • Docker
negócios extraídos para treinar o modelo
27,4 mil
mais rápida, a consulta de enriquecimento
167×
commits em 5 meses
233
arquivos de teste no backend
92

Números medidos no repositório.

Geração · fabricantes de embalagens no Sul412 / 600 créditos no mês

CNAEs sugeridos pela IA

  • 2221-8/00
  • 2222-6/00
  • 1731-1/00
  • 1732-0/00

job em andamento · 18 de 25

EmpresaCNPJUFDesdeGate
Embalagens Exemplo Ltda00.000.000/0001-00PR2009PERMIT
Plásticos Fictícios S.A.00.000.000/0001-00SC1998PERMIT
Caixas Modelo Indústria00.000.000/0001-00RS2015REVIEW
Papelão Demonstração Ltda00.000.000/0001-00PR2021BLOCK
Filmes Teste Industrial00.000.000/0001-00SC2004PERMIT
Recriação ilustrativa com empresas fictícias e CNPJs mascarados. A interface real fica atrás do login corporativo.

Contexto e problema

Montar uma lista de prospecção B2B exige três coisas que costumam viver separadas: achar empresas que valem o contato, conferir o cadastro de cada uma e colocar o resultado no CRM sem duplicar o que já existe lá. Cada etapa erra de um jeito. Entra empresa baixada, entra filial no lugar da matriz, o mesmo CNPJ aparece duas vezes em funis diferentes.

A solução foi juntar as três etapas numa plataforma interna. O usuário descreve um mercado ou sobe uma planilha, a plataforma qualifica cada empresa com uma régua única e importa no CRM em lotes, com progresso ao vivo e rollback. Com o tempo vieram mais módulos: um que resgata negócios descartados, um modelo que tenta aprender por que o comercial descarta e uma área de inteligência comercial com funil e mapa.

Os blocos “Detalhes técnicos” abrem com um clique, para quem quiser ver como cada parte funciona por dentro.

Restrições do ambiente

  • Uma régua só. Uma empresa reprovada na consulta cadastral não pode passar porque chegou por outro caminho, seja IA, planilha ou resgate.
  • O CRM tem vida própria. Ele roda automações nativas que reagem a cada escrita, e a plataforma precisa conviver com elas.
  • Custo controlado por pessoa. Cada usuário tem créditos por mês, e nenhuma falha ou repetição pode cobrar duas vezes.
  • Login da empresa. SSO corporativo com OIDC e PKCE. A autorização por papel fica na própria plataforma.
  • Buscas que podem ser enormes. A base cadastral cobre o país inteiro, e uma busca mal recortada derruba o servidor.

Decisões que definiram o produto

Arquitetura

OrigemIA, planilha ou descarteConsulta cadastralCache, fonte, provedoresGate por regrasBloqueio, regras, CNAEScore do modeloLightGBM, só aconselhaFase AMarca o negócioFase BMove e limpa o donoCRMCrédito cobrado 1 vezRetomadaReenvia só a fase B

A empresa chega por geração com IA, por planilha ou como negócio descartado. A consulta cadastral passa por cache, fonte oficial e provedores públicos, sempre com a mesma régua. Depois vêm a lista de bloqueio, as regras de negócio e o scorer de CNAE. O score do modelo aparece, mas não barra. Quando o negócio já existe no CRM, a movimentação acontece em duas fases: primeiro a marcação, depois a troca de etapa. O crédito é cobrado uma vez por negócio.

O backend é um FastAPI em Python 3.12 que também serve o frontend. A fonte da verdade é um PostgreSQL remoto, que concentra o que já foi indicado e importado. O estado local da aplicação fica num SQLite: sessões, jobs, cache de CNPJ e créditos. Trabalho longo (geração, importação, resgate) roda em segundo plano, e o progresso chega ao navegador por SSE.

Detalhes técnicos: stack e escolhas de infraestruturaFastAPI · React sem build · OIDC

O frontend é uma SPA em React 18 sem etapa de build: o JSX é transpilado no navegador e a própria API entrega a página. Escolhi isso para que o deploy fosse um único contêiner, sem pipeline de frontend. O custo aparece no primeiro carregamento, mais lento, e na falta de tipagem e de testes de componente.

A divisão entre os dois bancos segue o dono do dado. O PostgreSQL remoto é compartilhado com outros sistemas e diz o que já existe. O SQLite guarda o que só interessa a esta aplicação, e por isso fica num volume do Docker, fora do contêiner, para sobreviver a um rebuild.

O login é OIDC com PKCE contra o SSO da empresa, com cookie seguro por padrão. Os papéis (usuário e administrador) e os créditos mensais ficam na própria plataforma, porque o SSO diz quem é a pessoa, não o que ela pode gastar. A suíte de testes roda num workflow de CI.

O que a plataforma entrega

Consulta cadastral

Cada CNPJ passa pela mesma régua, venha de onde vier. O resultado fica em cache por 120 dias.

Detalhes técnicos: cache, fonte oficial e três provedorescache · TTL 120 d

A ordem é fixa. O cache em SQLite responde se tiver um registro com menos de 120 dias. Sem ele, a consulta vai para a API cadastral oficial. Se a oficial falhar, e o administrador permitir, entram três provedores públicos em sequência. A régua (ativa, matriz, porte, fundação há 3 anos ou mais, natureza jurídica que não seja de pessoa física) é aplicada depois da resposta, igual em todas as camadas, para que a escolha do provedor não altere o veredito.

O preço do cache é a defasagem: uma empresa que muda de situação dentro da janela só é vista na próxima ida à fonte. Aceitei isso porque a mesma empresa é consultada muitas vezes por usuários diferentes, e cada consulta à fonte paga latência e limite de requisições.

Geração de leads com IA

O usuário descreve o mercado e recebe uma lista de empresas já qualificadas e sem duplicatas contra a base.

Detalhes técnicos: CNAEs sugeridos pelo Gemini, dedup e trava por usuáriojob · trava por usuário

O Gemini pesquisa o mercado descrito e sugere os códigos de atividade econômica (CNAE). A partir deles a plataforma extrai empresas, passa cada uma pela régua cadastral e remove o que já existe na fonte da verdade. CNAEs secundários entram só se o usuário marcar a opção. A geração roda em segundo plano, e as listas ficam salvas: um reload não perde o trabalho.

Cada usuário tem uma trava contra geração concorrente, então duas gerações dele nunca disputam os mesmos créditos e as mesmas empresas. Quando a geração esgota empresas novas, ela recorre ao resgate e move negócios que já existem no CRM, em vez de criar duplicatas. Esse caminho é best effort: se falhar, a geração entrega o que tem e não cai.

Importação para o CRM

Planilha XLSX ou CSV entra, negócios saem no CRM, com progresso ao vivo e a opção de desfazer o job inteiro.

Detalhes técnicos: lotes, throttle, retry exponencial e rollback por jobSSE · rollback por job

A importação é assíncrona e ganha um id de job assim que começa. As linhas são deduplicadas e enviadas em lotes com throttle, para não estourar o limite de requisições do CRM. Falhas transitórias têm retry com backoff exponencial. O progresso vai para o navegador por SSE.

Tudo que um job criou fica associado ao id dele, e o rollback desfaz só aquele job. O que falhou, e o que foi barrado por já existir na fonte da verdade, pode ser exportado num arquivo para correção e reenvio, em vez de se perder no log.

Resgate de negócios descartados

Negócios descartados voltam a ser avaliados com as mesmas regras de um lead novo, e os que passam são devolvidos ao funil.

Detalhes técnicos: gate em camadas e duas fases idempotentesgate L1 a L4 · 2 fases
Camada O que faz Bloqueia?
L1 Lista de bloqueio heurística (condomínios, fundos, pessoa física pela natureza jurídica) Sim
L2 5 regras de negócio da qualificação Sim
L3 Scorer de CNAE Sim
L4 Score do LightGBM Não, só aconselha

Quem passa vai para a movimentação em duas fases. A fase A grava a marcação, a plataforma espera um intervalo configurável, e a fase B move o negócio e limpa o responsável. Separei as fases depois de ver o CRM mandar para deduplicação, e apagar, negócios que recebiam marcação e movimentação na mesma escrita.

A chave de idempotência combina execução e negócio. Um negócio preso entre as fases é reconhecido, e a retomada reenvia só a fase B. O crédito é cobrado uma vez por negócio movido, garantido por índice único no livro de créditos. Um limite segue aberto: um dos funis desfaz em silêncio mudanças de categoria feitas pela API, mesmo respondendo sucesso. Daí ficou a regra de tirar um retrato de todos os campos antes de qualquer escrita, para poder reverter com valores lidos e não deduzidos.

Modelo de descarte

Um modelo que aprende com o histórico por que o comercial descarta leads e classifica cada lead novo como PERMIT, REVIEW ou BLOCK antes de ele virar negócio.

Detalhes técnicos: LightGBM calibrado, split temporal e gate de promoçãoLightGBM · calibrado

Foram extraídos 27,4 mil negócios, dos quais 14,8 mil com rótulo conhecido. O split é temporal, 80/10/10 ordenado por data de criação, porque um split aleatório vazaria o futuro para o treino. Variáveis categóricas usam target encoding com suavização. O desbalanceamento é tratado com scale_pos_weight em vez de SMOTE, a saída passa por calibração isotônica, e cada previsão vem com a explicação do SHAP.

No teste: ROC-AUC de 0,797, PR-AUC de 0,095 e precision@10 de 0,239. O PR-AUC parece baixo, mas a classe positiva é rara, e o número tem que ser lido contra essa taxa base. Um modelo novo só é promovido no registro se ganhar em PR-AUC e mantiver o erro de calibração em até 0,05. Para o concept drift, a janela de treino caiu de 365 para 180 dias, com meia-vida de 60 dias nos pesos.

O enriquecimento cruza os negócios com uma tabela de 71 milhões de linhas. A primeira versão filtrava com ANY() sobre um array de texto e caía em varredura completa. Trocar por um JOIN com unnest deixou a consulta 167× mais rápida.

Inteligência comercial

Um funil de prospecção em 7 etapas, um mapa com detalhamento por cidade e importação direto da cidade escolhida.

Detalhes técnicos: busca de similares por escopo e checagem de tamanhoHTTP 422 · ≥N

A primeira versão da busca de empresas similares usava todos os CNAEs da empresa-alvo como semente. Num caso real, um código secundário de transporte bastou para 87,5% dos candidatos serem transportadoras. Hoje a semente é o CNAE principal, expandido pela hierarquia oficial em escopo exato, por grupo ou por setor, e os secundários entram só por opção do usuário.

Buscas amplas batiam no timeout de 120 s por dois motivos: contagens sem filtro de UF e um índice de CNAEs secundários usado mesmo quando não era preciso. Agora o índice só entra quando a busca pede, uma checagem prévia recusa com HTTP 422 o escopo grande demais, e as contagens têm teto curto: acima dele a tela mostra “≥N” em vez de esperar o número exato.

No mapa, a importação por cidade usa a cidade e a UF exatas, sem normalizar grafia, para que o número de créditos bata com o que o mapa mostrou. As abas de análise comercial foram portadas de aplicativos Streamlit de um colega, com conferência numérica contra os originais.

Meu papel

Concebi e construí a plataforma: produto, backend, frontend, modelo de ML, integrações e operação. Foram 233 commits entre maio e outubro de 2026, 92 arquivos de teste no backend e CI rodando a suíte. As abas de análise comercial vieram de aplicativos de um colega; portei essas abas e conferi que os números batiam com os originais.

O que eu faria diferente

  • Retrato dos campos antes de escrever, desde o primeiro dia. Um dos funis ainda desfaz em silêncio mudanças feitas pela API. Eu teria adotado a regra antes de descobrir o problema.
  • Rotular a população certa para o resgate. O modelo de descarte foi treinado em outra população e por isso só aconselha. Com rótulos dos próprios negócios resgatados, ele poderia decidir.
  • Testar o frontend. A SPA sem build é simples de servir, mas tem 3 testes, contra 92 arquivos no backend.

Quer algo parecido no seu time?

Falar comigo