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?

