Testando um agente de IA antes de deixar ele escrever no CRM (com pytest)
Agente de IA conectado ao CRM costuma ser testado assim: manda umas mensagens no WhatsApp, vê se a resposta ficou boa e libera. Só que a resposta boa não diz nada sobre o que foi gravado no banco. A ideia aqui é tirar o
Agente de IA conectado ao CRM costuma ser testado assim: manda umas mensagens no WhatsApp, vê se a resposta ficou boa e libera. Só que a resposta boa não diz nada sobre o que foi gravado no banco.
A ideia aqui é tirar o LLM da equação e testar a camada que decide gravar. O modelo devolve uma proposta de ação, e o backend valida antes de executar. Essa camada é código determinístico, então dá para cobrir com testes comuns.
A camada de decisão
# decisao.py
import unicodedata
import phonenumbers
def chave_nome(nome: str) -> str:
"""Chave só para BUSCAR possíveis duplicados. Nunca para sobrescrever o nome."""
return unicodedata.normalize("NFKC", nome).casefold().strip()
def invisiveis(texto: str) -> list[str]:
return [
f"U+{ord(c):04X}"
for c in texto
if unicodedata.category(c) == "Cf"
or (unicodedata.category(c) == "Zs" and c != " ")
]
def telefone_e164(raw: str, regiao: str = "BR") -> str:
numero = phonenumbers.parse(raw, regiao)
return phonenumbers.format_number(numero, phonenumbers.PhoneNumberFormat.E164)
def decidir(proposta: dict, candidatos: list, eventos_processados: set) -> str:
if proposta["event_id"] in eventos_processados:
return "ignorar"
if len(candidatos) != 1:
return "revisao_humana"
if not proposta.get("confirmado_pelo_cliente"):
return "pedir_confirmacao"
return "gravar"
def processar(proposta, candidatos, eventos_processados, fila, crm):
decisao = decidir(proposta, candidatos, eventos_processados)
if decisao == "ignorar":
return decisao
if decisao == "revisao_humana":
fila.append({"event_id": proposta["event_id"], "motivo": "identificacao_ambigua"})
elif decisao == "gravar":
crm.append({"contato": candidatos[0], **proposta["campos"]})
eventos_processados.add(proposta["event_id"])
return decisao
Os testes
# test_decisao.py
from decisao import chave_nome, invisiveis, telefone_e164, processar
def proposta(event_id="evt-1", confirmado=True):
return {"event_id": event_id, "campos": {"endereco": "Rua X, 120"},
"confirmado_pelo_cliente": confirmado}
# 1. Nomes visualmente iguais geram a mesma chave de busca
def test_nome_com_acento_combinante():
assert chave_nome("Joa\u0303o") == chave_nome("João")
def test_nome_com_letra_estilizada():
assert chave_nome("𝐉𝐨𝐚𝐨") == "joao"
# 2. Mesmo telefone em formatos diferentes
def test_telefone_normalizado():
assert telefone_e164("(27) 99999-0000") == "+5527999990000"
assert telefone_e164("+55 27 99999 0000") == "+5527999990000"
# 3. Caracteres invisíveis são detectados, não apagados
def test_espaco_largura_zero():
assert invisiveis("Ana\u200bSilva") == ["U+200B"]
def test_espaco_nao_separavel():
assert invisiveis("Ana\u00a0Silva") == ["U+00A0"]
# 4. Sem confirmação explícita, nada é gravado
def test_sem_confirmacao_nao_grava():
fila, crm = [], []
r = processar(proposta(confirmado=False), ["id_1"], set(), fila, crm)
assert r == "pedir_confirmacao" and crm == []
# 5. Ambiguidade vai para humano, e o humano recebe de fato
def test_duas_marinas_vao_para_fila():
fila, crm = [], []
r = processar(proposta(), ["marina_1", "marina_2"], set(), fila, crm)
assert r == "revisao_humana"
assert crm == []
assert fila[0]["motivo"] == "identificacao_ambigua"
# Bônus: webhook reentregue não duplica
def test_idempotencia():
fila, crm, eventos = [], [], set()
processar(proposta(), ["id_1"], eventos, fila, crm)
processar(proposta(), ["id_1"], eventos, fila, crm)
assert len(crm) == 1
pip install pytest phonenumbers
pytest -v
Repare que nenhum teste chama o LLM. Esse é o ponto: se a segurança do seu cadastro depende de o modelo "se comportar", ela não é testável. Se depende de uma camada de decisão, é.
Em produção, a idempotência deve ficar no banco (constraint UNIQUE no event_id), não num set em memória. Aqui é só para o exemplo rodar.
Os 5 cenários completos
Os testes acima vêm de um roteiro que publicamos no Algoritmo Diário, com o raciocínio de cada cenário (Unicode NFC × NFKC, E.164, confirmação × consentimento na LGPD, handoff) e uma matriz de QA para rodar antes de liberar a integração:
👉 A IA respondeu certo, mas cadastrou o cliente errado: 5 testes antes de conectar um agente ao CRM
Que outro cenário vocês colocariam nessa suíte?
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.