Python para auditar menções de marca em respostas de IA
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 exemplo | Status | Menção literal | Interpretação |
|---|---|---|---|
| p01-r01 | ok | true | Nome encontrado; a fonte ainda precisa de conferência. |
| p02-r01 | ok | false | Nome não encontrado nessa resposta válida. |
| p03-r01 | erro | null | Coleta sem resposta utilizável; não entra como ausência. |
| Linha JSON quebrada, se acrescentada | invalido | null | Corrigir 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.