Agente de Vendas para WhatsApp com LangGraph
Agente conversacional de vendas integrado ao WhatsApp que modela o funil comercial como grafo de estados explícito com LangGraph. Cada etapa do funil é um nó com responsabilidade única, qualificação, entendimento, apresentação, objeção e fechamento. Com transições determinísticas em vez de comportamento derivado de prompt único.

Agente de Vendas para WhatsApp com LangGraph
Este projeto nasceu como uma refatoração de uma versão que eu tinha construído em 2023. A versão anterior funcionava. O problema era que "funcionar" e "ser mantível" são coisas diferentes em sistemas com LLM. O código antigo misturava lógica de orquestração, chamadas ao modelo, integração com canal de saída e estado da conversa em partes que dependiam umas das outras de formas difíceis de rastrear.
A refatoração teve um objetivo claro: separar o que o agente decide do como cada decisão é executada. O resultado é um sistema mais modular, com fluxo explícito e espaço real para evoluir sem reescrever o núcleo.
O problema
O sistema recebe mensagens de leads via WhatsApp e precisa fazer três coisas: interpretar em que etapa comercial a conversa está, responder com contexto suficiente para avançar a venda e manter continuidade entre mensagens.
O problema de modelar isso como um chatbot genérico é que o comportamento fica totalmente dependente do modelo. O agente faz o que a inferência mandar, sem estrutura que limite o espaço de decisão. Na prática, isso significa inconsistência: o agente às vezes pula etapas, às vezes responde perguntas que deveriam ter sido respondidas antes, às vezes trata um lead qualificado como se fosse uma conversa inicial.
A solução foi modelar o funil comercial de forma explícita. O agente opera em etapas bem definidas:
qualify: verificar se o lead tem o perfil esperadounderstand: mapear o problema e a dor principalpresent: apresentar a solução com contexto recuperadoobjection: lidar com resistências antes de avançarclose: conduzir ao fechamento
Isso não elimina o LLM do processo. Ele ainda decide o que dizer em cada etapa. Mas ele decide dentro de um espaço de execução previsível, não num espaço aberto.
A decisão central: grafo de estados em vez de prompt único
A escolha mais importante do projeto foi usar LangGraph para representar o agente como uma máquina de estados com transições explícitas, não como um único prompt que tenta fazer tudo.
A alternativa mais comum é um prompt longo com todas as instruções do funil, e deixar o modelo decidir como se comportar em cada situação. Isso funciona para demos. Em produção, o comportamento derivado de um prompt único é difícil de depurar, difícil de testar e difícil de modificar sem impacto em outras partes.
Com LangGraph, cada etapa do funil é um nó com responsabilidade única. As transições entre etapas são regras explícitas, não inferência do modelo. O grafo atual funciona assim:
qualify -> understand -> present -> objection (se detectar resistência) -> close
Esse design transforma o comportamento do agente em algo inspecionável. O
estado guarda um node_trace com o histórico de nós percorridos. Quando
algo sai errado numa conversa, é possível olhar exatamente onde o fluxo
desviou, sem precisar tentar reproduzir o problema via prompt.
Estado como dado estruturado, não como lista de mensagens
Outra decisão relevante foi modelar a conversa como estado tipado, não apenas como um histórico de mensagens que o modelo precisa reinterpretar a cada turno.
O ConversationState mantém campos como lead_profile, current_stage,
objections_handled, ready_to_close, retrieved_chunks, missing_fields
e node_trace.
O lead_profile, por exemplo, é enriquecido progressivamente durante a
conversa. Nome, objetivo e dor principal do lead são capturados e mantidos
como dados estruturados, não como informação espalhada no histórico que o
modelo precisa relembrar a cada resposta.
Isso resolve um problema real de agentes conversacionais: memória baseada puramente na janela de contexto do modelo é frágil. O modelo pode "esquecer" detalhes de turnos anteriores, especialmente em conversas longas. Parte da memória sendo computacional, tipada e controlável reduz essa fragilidade sem precisar de complexidade adicional.
RAG pragmático para grounding de produto
O agente usa RAG local com ChromaDB para injetar contexto factual nas respostas
do nó present. A base de conhecimento é carregada de um arquivo texto
simples, fragmentada e indexada com embeddings.
O objetivo aqui não foi demonstrar um pipeline sofisticado de knowledge engineering. Foi garantir que o agente responda com informação real sobre o produto, sem alucinação, e que essa informação seja fácil de atualizar sem mexer no código.
O RAG é encapsulado num RetrieverService separado. O nó do grafo sabe que
precisa de contexto para apresentar a solução, mas não sabe como esse contexto
é recuperado. Isso é uma decisão de design importante: quando o vector store
mudar ou o método de chunking for ajustado, o núcleo do agente não precisa
ser alterado.
Isolamento de dependências externas
O projeto evita acoplamento direto entre a lógica de orquestração e os provedores externos. Isso aparece em três camadas:
Modelo de linguagem. O ChatProvider encapsula a integração com o modelo.
Hoje aponta para OpenRouter com ChatOpenAI, mas o restante do sistema não
depende desse detalhe. Se o provedor mudar, a mudança fica contida nessa
camada.
Embeddings. O EmbeddingsProvider alterna entre embeddings reais e
FakeEmbeddings quando não há chave configurada. Isso desacopla o ciclo de
desenvolvimento e testes de dependências pagas, o que acelera bastante o
trabalho local.
Canal de saída. O EvolutionClient encapsula o envio de mensagens para
a Evolution API com retry por backoff exponencial via tenacity. Falhas
transitórias de rede não chegam ao núcleo do agente.
Esse padrão de isolamento responde a uma crença que carrego: o código que toma decisões não deve estar misturado com o código que fala com APIs externas. Quando as duas coisas estão juntas, qualquer mudança de provedor pode introduzir bugs em lógica de negócio que não tinha motivo para mudar.
Fluxo de uma mensagem entrante
- A Evolution API envia um
POST /webhook - FastAPI valida o payload com Pydantic e filtra mensagens enviadas pelo próprio bot
- O sistema identifica a sessão e recupera o estado conversacional atual
- O
AgentServiceinjeta a nova mensagem no estado e executa o grafo - O nó correto do grafo processa a mensagem, enriquece o estado e, se necessário, consulta o retriever
- A resposta final é enviada ao usuário pelo
EvolutionClient - O estado atualizado é persistido no store
Testabilidade
Os testes cobrem três aspectos centrais: persistência e isolamento de cópia no store conversacional, processamento do webhook com payload válido e execução do fluxo do agente com stubs para LLM e retriever.
O teste do AgentService usa doubles explícitos para simular o modelo e o
retriever. Isso permite validar a lógica de orquestração do grafo sem depender
de chamadas reais a provedores externos. Em sistemas com IA, isolar a lógica
determinística do comportamento probabilístico do modelo é uma das habilidades
de engenharia mais importantes, e também uma das mais ignoradas.
Trade-offs assumidos conscientemente
O sistema atual não tenta resolver tudo. Algumas limitações foram mantidas de propósito para privilegiar clareza arquitetural:
- o estado da conversa está em memória e se perde ao reiniciar o processo
- o projeto opera em modo single-tenant
- a ingestão da base de conhecimento é simples e local
- o classificador de objeção usa heurísticas por palavras-chave
Esses não são acidentes. São decisões de fase. Introduzir Redis, Supabase, multi-tenant routing e observabilidade pesada antes de estabilizar o fluxo do agente é uma forma segura de acumular complexidade sem entender ainda onde ela vai importar.
Próximas evoluções naturais
A sequência que faria sentido a partir daqui:
- Trocar o store em memória por Redis para persistência entre reinicializações
- Substituir o repositório de configuração por Supabase para suporte multi-tenant
- Enriquecer o classificador de estágio e objeção com abordagem híbrida ou LLM-as-judge
- Adicionar tracing por nó do grafo para observabilidade real do funil
- Melhorar chunking e metadados da base vetorial para recuperação mais precisa
- Criar avaliação offline de qualidade de resposta por etapa
Essa ordem importa: primeiro estabilizar a espinha dorsal do fluxo, depois escalar persistência, depois sofisticar inteligência e observabilidade. Fazer ao contrário significa construir infraestrutura para um fluxo que ainda vai mudar.