Dev.to AI 🤖 Ai 👁 0 📖 3 min read

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?

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.