Durante o desenvolvimento do Engenhoso AI, encontrei um daqueles problemas que parecem simples quando descritos em uma frase:
uma mensagem era enviada, o sistema aparentemente funcionava, mas o destinatário nem sempre recebia a resposta como esperado.
À primeira vista, a pergunta parecia óbvia:
onde estava o erro?
Mas sistemas distribuídos raramente respondem bem a perguntas excessivamente simples.
Uma mensagem que atravessa WhatsApp, webhook, aplicação, modelo de inteligência artificial e novamente a infraestrutura de mensageria não percorre um único caminho.
Ela atravessa uma cadeia de acontecimentos.
E foi justamente essa cadeia que precisei aprender a observar.
O primeiro indício: POST /webhook 200 OK
Nos logs do serviço que hospeda o webhook do Engenhoso AI, as requisições apareciam normalmente:
POST /webhook HTTP/1.1 200 OK
Em um primeiro momento, isso parecia significar que tudo estava funcionando.
Mas havia um problema conceitual nessa interpretação.
Um 200 OK apenas informava que determinada requisição havia sido recebida e tratada pelo endpoint.
Ele não respondia às perguntas seguintes:
- era uma mensagem enviada pelo usuário?
- era apenas uma atualização de status da Meta?
- a aplicação chamou a OpenAI?
- a resposta foi gerada?
- a Meta aceitou a resposta para envio?
- a mensagem chegou ao telefone do destinatário?
- ela foi lida?
Foi então que uma distinção aparentemente simples se tornou central para o diagnóstico:
Webhook recebeu ≠ IA respondeu ≠ Meta aceitou ≠ mensagem foi entregue ≠ mensagem foi lida
Em software, duas etapas consecutivas podem parecer uma coisa só quando não temos instrumentos para observá-las.
O teste com comandos locais
O Engenhoso AI possui alguns comandos locais que não dependem de geração por inteligência artificial.
Um deles é AJUDA.
Durante os testes, esse comando funcionou.
A mensagem chegou ao webhook, foi reconhecida e o menu foi devolvido ao usuário.
Isso permitiu retirar alguns suspeitos da investigação.
Se AJUDA funcionava, então pelo menos parte deste caminho estava operacional:
WhatsApp → Meta → Webhook → Aplicação → Meta → WhatsApp
Mas uma pergunta aberta dependia de uma etapa adicional:
Aplicação → OpenAI → Aplicação
Era necessário investigar essa fronteira separadamente.
O contador da API também pode enganar
Durante parte da investigação, o painel da API permaneceu mostrando o mesmo número de solicitações.
Isso parecia indicar que as perguntas não estavam chegando à OpenAI.
Mais tarde, porém, o contador mudou.
As solicitações estavam sendo contabilizadas.
O que havia ocorrido era uma diferença temporal entre o acontecimento dentro da aplicação e a atualização que eu observava no painel.
Foi um aprendizado importante:
um painel de uso é uma ferramenta de observação, mas não deve ser confundido automaticamente com um log em tempo real da aplicação.
Quando investigamos um sistema distribuído, precisamos saber exatamente o que cada instrumento mede.
Descobrindo a chamada real da OpenAI
Em vez de continuar formulando hipóteses a partir dos painéis, fui ao código.
A aplicação utiliza a Responses API e uma chamada semelhante a:
response = await self._client.responses.create(
model=self._model,
instructions=...,
input=text,
reasoning={"effort": OPENAI_REASONING_EFFORT},
max_output_tokens=self._max_output_tokens,
store=False,
)
Algumas informações começaram então a ficar objetivas.
O projeto estava configurado para utilizar um modelo definido por variável de ambiente.
O esforço adicional de raciocínio estava desativado:
OPENAI_REASONING_EFFORT = "none"
Também encontrei:
max_retries=0
Ou seja, o cliente não estava realizando sucessivas tentativas automáticas capazes de explicar sozinho uma espera de vários minutos.
Mais importante ainda: a aplicação possuía limites explícitos de tempo.
Timeout não era apenas uma hipótese
O projeto já possuía uma arquitetura de deadlines.
Na configuração de referência apareciam, entre outros, estes valores:
OPENAI_TIMEOUT_SECONDS=12OPENAI_WEBHOOK_TOTAL_TIMEOUT_SECONDS=15
E o código realmente propagava um prazo para a geração.
De maneira simplificada:
generation_task = asyncio.create_task(
_run_openai_generation(...)
)
generation_timed_out = not await _wait_for_openai_task(
generation_task,
deadline=openai_deadline,
)
Quando o limite era ultrapassado, o Engenhoso AI selecionava uma resposta de fallback.
Isso explicava uma mensagem que apareceu em um dos testes:
Desculpe, não consegui gerar uma resposta agora. Tente novamente em alguns instantes.
Poucos minutos depois, a mesma pergunta recebeu uma resposta normal.
O problema era, portanto, intermitente.
E já não fazia sentido atribuir automaticamente vários minutos de espera a uma única chamada indefinida para o modelo.
O envio à Meta também tinha prazo
Continuei seguindo o código.
A própria etapa de envio da resposta possuía um deadline:
remaining_delivery_seconds = delivery_deadline - loop.time()
if remaining_delivery_seconds <= 0:
return
E a chamada era executada dentro de uma janela controlada:
async with asyncio.timeout_at(meta_deadline):
await _send_started_reply(...)
Isso trouxe outra conclusão importante.
O projeto já havia sido construído para tentar impedir que uma operação permanecesse indefinidamente presa em uma etapa externa.
Mas ainda faltava uma informação fundamental:
o que acontecia depois que a Meta aceitava uma mensagem?
O ponto cego: statuses
A API do WhatsApp envia callbacks relacionados ao estado das mensagens.
Entre eles podem aparecer estados como:
sentdeliveredreadfailed
O Engenhoso AI já possuía um teste que verificava que uma atualização de status deveria ser ignorada como mensagem de usuário:
def test_atualizacao_de_status_e_ignorada() -> None:
...
assert response.status_code == 200
assert fake.sent == []
Isso estava correto.
Um callback de status não deve provocar uma nova resposta automática.
O problema era outro:
ele também desaparecia do ponto de vista da observabilidade.
O código extraía:
messages = _extract_messages(payload, settings)
Mas um callback poderia conter, por exemplo:
{
"statuses": [
{
"status": "delivered"
}
]
}
em vez de uma nova mensagem.
Nesse caso, o webhook respondia 200 OK, mas eu não conseguia saber pelo log se aquele evento representava sent, delivered, read ou até failed.
Foi aí que percebi que o problema não era apenas corrigir alguma coisa.
Era tornar o sistema capaz de explicar o que estava acontecendo.
Criando observabilidade sem registrar a conversa
Foi criada uma função independente para extrair apenas os estados técnicos:
def _extract_status_updates(payload):
...
Conceitualmente, ela percorre:
entry → changes → field = messages → value → statuses
e identifica estados como sent, delivered, read e failed.
Mas havia outra preocupação.
Observabilidade não deveria significar registrar indiscriminadamente informações dos usuários.
Por isso, o message_id original não precisava aparecer nos logs.
Foi utilizada uma referência derivada:
message_ref = (
hashlib.sha256(message_id.encode("utf-8")).hexdigest()[:12]
if message_id
else "unknown"
)
Os registros podem então assumir formas semelhantes a:
whatsapp_status status=sent ref=8a92e740fb21 timestamp=...
whatsapp_status status=delivered ref=8a92e740fb21 timestamp=...
whatsapp_status status=read ref=8a92e740fb21 timestamp=...
A mesma referência permite correlacionar os eventos sem registrar:
- o texto da conversa;
- o telefone;
- o nome do usuário;
- o identificador original da mensagem.
Para mim, esse ponto é particularmente importante.
Observabilidade não precisa significar abandono da privacidade.
Testar antes de integrar
Antes de conectar essa informação ao fluxo real, acrescentei um teste específico para os estados sent, delivered, read e failed.
A sequência de desenvolvimento foi deliberadamente simples:
criar extrator → criar teste → ligar ao log → Pull Request → Merge → Deploy
A alteração foi desenvolvida em uma branch separada:
fix/whatsapp-status-observability
Depois passou por Pull Request antes de chegar à main.
Esse processo pode parecer mais trabalhoso do que editar diretamente a produção.
Mas existe uma diferença entre fazer uma mudança e construir um histórico compreensível de mudanças.
Quanto mais o projeto cresce, mais essa diferença importa.
A infraestrutura também entrou no experimento
Durante a investigação, o webhook ainda estava executando em uma instância gratuita do serviço de hospedagem.
Decidi migrá-lo para uma instância paga de entrada.
No momento da mudança, a configuração selecionada foi:
- Starter
- 0,5 CPU
- 512 MB de RAM
A decisão não foi tomada porque eu tivesse demonstrado que a hospedagem era a causa de todas as falhas.
Essa distinção é importante.
A migração foi uma forma de retirar uma variável da investigação e dar ao webhook uma infraestrutura mais adequada ao seu papel atual.
Depois do deploy, o serviço voltou ao estado Live, enquanto o health check continuou respondendo 200 OK.
A partir desse momento, o comportamento posterior poderia ser comparado com os registros anteriores à migração.
Não é correto afirmar:
“o plano pago resolveu o problema”
apenas porque uma mensagem posterior funcionou.
É necessário observar uma série de interações.
Essa diferença entre coincidência temporal e evidência causal é um aprendizado que considero importante também fora da programação.
O que mudou na maneira de diagnosticar
Antes dessa investigação, uma pergunta poderia ser formulada assim:
Por que a pessoa não recebeu a mensagem?
Agora ela pode ser dividida em perguntas melhores:
- O webhook recebeu o evento?
- Era uma mensagem ou um callback de status?
- A aplicação decidiu chamar a IA?
- A geração terminou ou ocorreu timeout?
- Foi utilizado fallback?
- A resposta foi enviada para a Meta?
- A Meta marcou como
sent? - Foi marcada como
delivered? - Foi marcada como
read? - Houve
failed?
Quanto melhor a pergunta técnica, menor a necessidade de adivinhar.
Uma arquitetura que começou a se tornar observável
Hoje consigo representar o fluxo de forma mais clara:
Usuário → WhatsApp → Meta → Webhook Engenhoso AI
A partir do webhook, a aplicação pode seguir por um comando local ou por geração de IA.
Quando existe geração:
Webhook → OpenAI → resposta gerada → Meta
Depois disso, os callbacks ajudam a observar o que ocorreu com a mensagem:
Meta → sent → delivered → read
ou, quando algo não ocorre como esperado:
Meta → failed
O sistema não ficou apenas mais fácil de depurar.
Ele ficou mais capaz de contar a própria história operacional.
O aprendizado maior
Quando comecei essa investigação, eu procurava um erro.
No decorrer do processo, percebi que havia uma questão anterior ao erro:
eu possuía instrumentos suficientes para saber onde ele estava?
Nem sempre a próxima melhoria de um sistema é uma nova funcionalidade.
Às vezes, o que falta é capacidade de observação.
Um 200 OK pode ser verdadeiro e, ao mesmo tempo, insuficiente.
A chamada da IA pode ter acontecido e o painel ainda não ter atualizado.
A Meta pode ter aceitado uma mensagem sem que isso signifique que ela já chegou ao destinatário.
São estados diferentes.
E aprender a respeitar essas diferenças talvez seja uma das mudanças mais importantes quando um projeto deixa de ser apenas um experimento e começa a se comportar como um sistema real.
O Engenhoso AI continua sendo, para mim, um ambiente de aprendizagem.
Cada dificuldade técnica revela não apenas alguma coisa sobre Python, APIs, webhooks ou inteligência artificial.
Ela revela também uma forma de pensar:
antes de corrigir aquilo que não compreendemos, precisamos construir meios para enxergá-lo.
Transparência sobre o processo
Este texto nasceu de uma experiência real de desenvolvimento do projeto Engenhoso AI.
Ferramentas de inteligência artificial foram utilizadas como apoio na investigação técnica, organização das hipóteses, leitura do código e revisão do texto.
As decisões sobre arquitetura, testes, alterações no projeto, interpretação dos resultados e publicação permanecem sob responsabilidade do autor.
Engenhoso AI
Aprender, construir, observar e compreender.
Top comments (0)