Pular para o conteúdo principal

Python para auditar menções de marca em respostas de IA

Por Alexandre Caramaschi, Chief Strategy Officer da Nuvini (Nasdaq: NVNI), Founder da Brasil GEO, cofundador da NAIA e cofundador da AI Brasil, ex-CMO da Semantix (Nasdaq) · Março 2026

Revisão editorial: .

Uma automação confiável começa preservando as respostas e as condições de coleta. O exemplo abaixo usa apenas a biblioteca padrão do Python para ler JSONL, identificar menção literal, manter URLs fornecidas e separar respostas válidas de erros. Ele funciona com dados já salvos, sem chave de API, SDK de modelo ou cobrança de chamadas.

O que este exercício mede

Em Generative Engine Optimization (GEO), “a marca apareceu” pode significar coisas diferentes. Este programa verifica somente a presença literal de um nome no texto. Ele não decide se houve recomendação, se a afirmação é verdadeira ou se uma fonte sustenta a menção. Essas avaliações ficam explicitamente pendentes.

Uma resposta da API também não representa automaticamente o que alguém vê na interface do produto. Busca, histórico, instruções e configuração podem ser diferentes. Por isso, cada registro informa superfície e modo; séries distintas precisam ser analisadas separadamente. Se o modelo não é exposto, registre essa limitação em vez de deduzir sua versão.

1. Salve uma rodada em JSONL

JSONL contém um objeto JSON por linha. Preserve o arquivo original e crie identificadores que distingam pergunta e rodada. Os dados abaixo são fictícios: não representam respostas de um fornecedor real, e example.com é um domínio usado apenas para o exercício. Salve o bloco como respostas.jsonl, em UTF-8.

{"id": "p01-r01", "prompt": "Cite opções de gestão.", "produto": "assistente_exemplo", "superficie": "interface", "modo": "com_busca", "idioma": "pt-BR", "regiao": "Brasil", "coletado_em": "2026-10-06T10:00:00-03:00", "status": "ok", "resposta": "A Marca Exemplo aparece entre as opções.", "fontes": ["https://example.com/catalogo"]}
{"id": "p02-r01", "prompt": "Compare soluções.", "produto": "assistente_exemplo", "superficie": "api", "modo": "sem_busca", "idioma": "pt-BR", "regiao": "nao_configurada", "coletado_em": "2026-10-06T10:01:00-03:00", "status": "ok", "resposta": "Considere prazo, suporte e integração.", "fontes": []}
{"id": "p03-r01", "prompt": "Qual opção atende pequenas equipes?", "produto": "assistente_exemplo", "superficie": "api", "modo": "sem_busca", "idioma": "pt-BR", "regiao": "nao_configurada", "coletado_em": "2026-10-06T10:02:00-03:00", "status": "erro", "erro": "tempo_limite"}

status="ok" exige uma resposta utilizável; status="erro" representa falha de coleta. Uma recusa ou resposta “não sei” recebida do produto pode ser um texto válido para analisar, sem demonstrar conhecimento sobre a marca. Anote separadamente esse comportamento se for relevante para sua pesquisa.

Em uma coleta real, acrescente modelo informado, parâmetros, identificador da pergunta, tentativa e referências para as evidências. Não inclua senhas, tokens ou dados pessoais desnecessários. A lista fontes deve conter os links realmente fornecidos na resposta ou no campo de citações da integração, sem completar lacunas por suposição.

2. Execute o normalizador local

Salve como normalizar.py. O programa usa recursos documentados nos módulos json e argparse do Python. Ele processa uma linha por vez, recusa sobrescrever a saída e registra entradas inválidas sem transformá-las em ausência da marca.

import argparse
import json
import re
import unicodedata
from datetime import datetime
from pathlib import Path
from urllib.parse import urlsplit


def normalizar(texto):
    return unicodedata.normalize("NFC", texto).casefold()


def classificar(registro, padrao):
    if not isinstance(registro, dict):
        raise ValueError("A linha precisa conter um objeto JSON.")
    campos = ("id", "prompt", "produto", "superficie", "modo",
              "idioma", "regiao", "coletado_em")
    for campo in campos:
        if not isinstance(registro.get(campo), str) or not registro[campo].strip():
            raise ValueError("Campo obrigatório ausente ou vazio: " + campo)
    if registro["superficie"] not in ("api", "interface"):
        raise ValueError("Superfície deve ser api ou interface.")
    instante = datetime.fromisoformat(registro["coletado_em"].replace("Z", "+00:00"))
    if instante.tzinfo is None:
        raise ValueError("A data precisa informar o fuso horário.")
    resultado = dict(registro)
    resultado.update(mencao_literal=None, urls_fontes=[],
                     exatidao="nao_avaliada", recomendacao="nao_avaliada")
    if registro.get("status") == "erro":
        return resultado
    if registro.get("status") != "ok":
        raise ValueError("Status deve ser ok ou erro.")
    resposta = registro.get("resposta")
    if not isinstance(resposta, str) or not resposta.strip():
        raise ValueError("Status ok exige resposta não vazia.")
    fontes = registro.get("fontes", [])
    if not isinstance(fontes, list):
        raise ValueError("Fontes precisa ser uma lista de URLs.")
    for url in fontes:
        if not isinstance(url, str):
            raise ValueError("Cada fonte precisa ser uma URL em texto.")
        partes = urlsplit(url)
        if partes.scheme not in ("http", "https") or not partes.netloc:
            raise ValueError("Fonte sem URL HTTP ou HTTPS válida.")
    resultado["mencao_literal"] = bool(padrao.search(normalizar(resposta)))
    resultado["urls_fontes"] = list(dict.fromkeys(fontes))
    return resultado


def main():
    parser = argparse.ArgumentParser(description="Auditar respostas JSONL já coletadas.")
    parser.add_argument("entrada", type=Path)
    parser.add_argument("saida", type=Path)
    parser.add_argument("marca")
    args = parser.parse_args()
    marca = args.marca.strip()
    if not marca:
        parser.error("A marca não pode ser vazia.")
    if args.entrada.resolve() == args.saida.resolve():
        parser.error("Entrada e saída precisam ser arquivos distintos.")
    padrao = re.compile(r"(?<!\w)" + re.escape(normalizar(marca)) + r"(?!\w)")
    with args.entrada.open(encoding="utf-8-sig") as entrada:
        # Modo x recusa substituir uma saída já existente.
        with args.saida.open("x", encoding="utf-8") as saida:
            for numero, linha in enumerate(entrada, start=1):
                try:
                    resultado = classificar(json.loads(linha), padrao)
                except (ValueError, TypeError) as erro:
                    resultado = {"status": "invalido", "mencao_literal": None,
                                 "urls_fontes": [], "erro": str(erro)[:180]}
                resultado["linha_origem"] = numero
                saida.write(json.dumps(resultado, ensure_ascii=False) + "\n")


if __name__ == "__main__":
    main()
python normalizar.py respostas.jsonl resultado.jsonl "Marca Exemplo"

No Windows, se o comando disponível for o lançador do Python, use py -3 no lugar de python. Execute em uma pasta de trabalho local com Python 3 instalado. O programa não inicia servidor, não instala dependências e não faz acesso à rede. Para repetir, escolha um novo nome de saída ou remova conscientemente o arquivo anterior.

3. Confira a saída antes de construir um indicador

Registro do exemploStatusMenção literalInterpretação
p01-r01oktrueNome encontrado; a fonte ainda precisa de conferência.
p02-r01okfalseNome não encontrado nessa resposta válida.
p03-r01erronullColeta sem resposta utilizável; não entra como ausência.
Linha JSON quebrada, se acrescentadainvalidonullCorrigir o dado de entrada e preservar o diagnóstico.

Critério de aceite: os três registros geram três linhas, com resultados true, false e null. Acrescente uma quarta linha com JSON incompleto: ela deve produzir invalido, sem eliminar as anteriores. Substituir o nome por “Marca Exemplos” não deve contar como correspondência de “Marca Exemplo”.

A comparação ignora diferenças de caixa e normaliza Unicode, mas não faz reconhecimento semântico. Abreviações e grafias alternativas exigem regras explícitas; homônimos exigem revisão humana. A validação de URL verifica estrutura, não disponibilidade, conteúdo ou relação com a marca. Deduplicar URLs iguais tampouco demonstra que todas são citações válidas.

4. Agregue sem esconder falhas

Calcule a taxa de menção literal usando respostas válidas do mesmo produto, superfície, modo e conjunto de perguntas. Mostre também tentativas, respostas válidas, erros e registros inválidos. Não publique uma média que mistura interface com API ou busca habilitada com busca desabilitada.

Taxa de menção literal = respostas válidas com menção / respostas válidas
Taxa de falha de coleta = coletas com status erro / tentativas registradas
Registros inválidos = entradas que precisam de correção antes da análise

O exemplo contém superfícies diferentes para mostrar o contrato de dados, portanto não constitui uma amostra comparativa de visibilidade. Uma porcentagem só descreve a amostra e a configuração observadas; não estima automaticamente a probabilidade de qualquer usuário receber a marca.

5. Planeje a coleta paga em uma etapa separada

Se decidir integrar APIs, consulte a documentação vigente do fornecedor escolhido, confira autenticação, permissões, modelos e cobrança na conta correta. Assinatura de um chat não implica créditos de API. Conte chamadas de busca, tokens, tentativas adicionais, armazenamento e revisão humana no orçamento. Este normalizador continua gratuito em consumo de API porque não executa essa etapa.

Imponha teto de tentativas e de gasto; registre falhas de limite e indisponibilidade sem repetições ilimitadas. Uma tentativa adicional precisa de identificador próprio para não duplicar silenciosamente a mesma observação. Planeje retenção dos arquivos: texto costuma ser suficiente para o processamento, enquanto capturas de tela podem ser reservadas aos incidentes que precisam dessa evidência.

Use o curso LLM FinOps, do portal de Alexandre Caramaschi, para desenhar limites e custo total. O GEO Intent Mapping ajuda a escolher perguntas antes de aumentar o volume de coleta. O próximo ganho de qualidade vem de uma amostra útil e de validação humana, não apenas de mais chamadas.

Perguntas frequentes

Este script consulta ChatGPT, Claude ou Gemini?

Não. Ele lê respostas previamente salvas em JSONL e produz outro arquivo local. Não usa rede, chave de API ou SDK de modelo.

Uma falha de coleta conta como ausência da marca?

Não. Falhas recebem menção literal nula e ficam separadas das respostas válidas sem o nome. Entradas inválidas também precisam ser corrigidas antes da análise.

O resultado comprova citação correta ou recomendação?

Não. A correspondência é literal e as URLs são preservadas como dados fornecidos. Exatidão, identidade da marca, suporte da fonte e recomendação exigem avaliações adicionais.