O nome apareceu. O link também?
Em quais respostas a marca apareceu? E em quais houve um link para o site dela? Se você já guardou respostas de IA, dá para conferir essas duas coisas com Python. A diferença importa: escrever o nome de uma empresa e citar uma página dela são ocorrências que queremos contar separadamente.
Vamos começar com quatro observações fictícias. Uma falhou, outra ficou com as fontes por conferir e uma URL parece pertencer à marca, mas leva a outro domínio. Esse pequeno lote permite entender a conta antes de usar respostas reais.
Você vai criar dois arquivos e rodar um comando no seu computador. O programa usa apenas a biblioteca padrão do Python: lê o material salvo, sem consultar assistentes, abrir links ou consumir créditos de API. Ao final, teremos contagens para revisar. Saber se a resposta recomendou a empresa ou disse algo verdadeiro sobre ela continua dependendo da leitura do conteúdo.
Guarde a resposta junto com as condições da pergunta
Só o texto da resposta não conta como ela foi obtida. Para comparar duas coletas, você também precisa saber qual pergunta foi feita, em qual produto e em que condições. Guarde as perguntas completas numa lista com versão e registre os campos abaixo junto de cada resposta.
| Campos | O que guardar |
|---|---|
| id, prompt_id, repeticao | Um ID para a observação, o ID da pergunta na lista e o número da tentativa |
| lote, banco | O nome desta coleta e a versão da lista de perguntas |
| produto, modelo, modo, idioma | Onde e como a resposta foi obtida; use nao_informado quando a interface não revelar o modelo |
| data, status, texto | Data com fuso, estado ok ou erro e o texto integral da resposta |
| fontes_coletadas, fontes | Se você conferiu a área de citações e quais URLs estavam nela |
Não conseguiu conferir as fontes? Marque fontes_coletadas como false e deixe a lista vazia. Conferiu e não havia nenhum link? Marque true, também com a lista vazia. No primeiro caso falta informação; no segundo, você observou a ausência. Essa diferença muda a conta.
Usaremos JSONL: um arquivo de texto com um objeto JSON completo por linha. Salve em UTF-8. Se a resposta contiver quebras de linha, elas precisam ficar escapadas dentro do JSON, sem partir o registro em várias linhas físicas. A documentação de JSON do Python explica como ler e escrever o objeto JSON de cada registro. Aqui, o script percorre o arquivo JSONL e aplica json.loads a cada linha, em vez de tentar ler o arquivo inteiro como um único objeto.
O código reúne registros com os mesmos valores de lote, banco, produto, modelo, modo e idioma. Mantenha a mesma grafia para a mesma condição. Uma nova tentativa de responder à pergunta, mesmo depois de uma falha, recebe outra repetição e outro ID.
Veja primeiro o lote que vamos conferir
Exemplo hipotético: as quatro observações abaixo foram criadas para testar o programa. O produto, as respostas e os resultados são fictícios. Copie as quatro linhas para um arquivo chamado observacoes.jsonl, mantendo cada registro inteiro na sua linha.
{"lote":"2026-10-06-a","banco":"v1","produto":"assistente-teste","modelo":"nao_informado","modo":"busca","idioma":"pt-BR","data":"2026-10-06T12:00:00+00:00","repeticao":1,"id":"o1","prompt_id":"p1","status":"ok","texto":"A Brasil GEO aparece nesta comparação.","fontes_coletadas":true,"fontes":["https://brasilgeo.ai/guia"]}
{"lote":"2026-10-06-a","banco":"v1","produto":"assistente-teste","modelo":"nao_informado","modo":"busca","idioma":"pt-BR","data":"2026-10-06T12:00:00+00:00","repeticao":1,"id":"o2","prompt_id":"p2","status":"ok","texto":"Outra empresa é mencionada.","fontes_coletadas":true,"fontes":["https://brasilgeo.ai.exemplo.org/guia"]}
{"lote":"2026-10-06-a","banco":"v1","produto":"assistente-teste","modelo":"nao_informado","modo":"busca","idioma":"pt-BR","data":"2026-10-06T12:00:00+00:00","repeticao":1,"id":"o3","prompt_id":"p3","status":"erro","texto":"","fontes_coletadas":false,"fontes":[]}
{"lote":"2026-10-06-a","banco":"v1","produto":"assistente-teste","modelo":"nao_informado","modo":"busca","idioma":"pt-BR","data":"2026-10-06T12:00:00+00:00","repeticao":1,"id":"o4","prompt_id":"p4","status":"ok","texto":"A BRASIL GEO também é mencionada.","fontes_coletadas":false,"fontes":[]}Antes de rodar qualquer coisa, confira o lote com os olhos. A marca aparece em o1 e o4. Em o2, a URL começa com o nome do domínio, mas pertence a exemplo.org. A linha o3 registra uma falha, não uma resposta sem menção.
Portanto, esperamos quatro tentativas, uma falha, três respostas válidas e duas menções. As fontes foram conferidas em duas respostas; só uma delas cita o domínio alvo. Guardar esse resultado esperado ajuda a perceber um erro de cópia ou de interpretação quando o programa devolver o relatório.
Agora rode o script na mesma pasta
Salve o código abaixo como auditoria_geo.py, na mesma pasta de observacoes.jsonl. Use Python 3.11 ou posterior. Você não precisa instalar bibliotecas extras para este exemplo.
O script compara nomes sem distinguir maiúsculas de minúsculas, ajusta espaços repetidos e normaliza formas Unicode equivalentes. Isso permite contar a menção em BRASIL GEO sem modificar a resposta guardada. Para os links, ele aceita o domínio exato e seus subdomínios; o endereço que imita o começo do nome fica de fora.
import hashlib
import json
import re
import sys
import unicodedata
from collections import Counter, defaultdict
from datetime import datetime
from urllib.parse import urlsplit
GRUPO = ("lote", "banco", "produto", "modelo", "modo", "idioma")
def normalizar(texto):
texto = unicodedata.normalize("NFKC", texto).casefold()
return re.sub(r"\s+", " ", texto).strip()
def pertence(url, dominio):
partes = urlsplit(url)
if partes.scheme not in ("http", "https") or not partes.hostname:
raise ValueError("Fonte deve ser uma URL HTTP ou HTTPS completa.")
host = partes.hostname.lower().rstrip(".")
return host == dominio or host.endswith("." + dominio)
def taxa(numerador, denominador):
return round(100 * numerador / denominador, 2) if denominador else None
def analisar(linhas, marca, dominio):
marca = normalizar(marca)
dominio = dominio.lower().strip().rstrip(".")
if not marca or not dominio or any(c in dominio for c in "/:@ "):
raise ValueError("Informe marca e domínio simples, sem protocolo.")
padrao = re.compile(r"(?<!\w)" + re.escape(marca) + r"(?!\w)")
grupos, ids, chaves = defaultdict(Counter), {}, set()
duplicadas = 0
for numero, linha in enumerate(linhas, 1):
if not linha.strip():
continue
r = json.loads(linha)
campos = ("id", *GRUPO, "prompt_id", "data", "status")
if not isinstance(r, dict) or any(
not isinstance(r.get(c), str) or not r[c].strip() for c in campos
):
raise ValueError(f"Linha {numero}: identificação incompleta.")
data = datetime.fromisoformat(r["data"])
if data.tzinfo is None:
raise ValueError(f"Linha {numero}: data precisa de fuso horário.")
if r["status"] not in ("ok", "erro"):
raise ValueError(f"Linha {numero}: status inválido.")
if type(r.get("repeticao")) is not int or r["repeticao"] < 1:
raise ValueError(f"Linha {numero}: repetição inválida.")
if not isinstance(r.get("texto"), str):
raise ValueError(f"Linha {numero}: texto inválido.")
if r["status"] == "ok" and not r["texto"].strip():
raise ValueError(f"Linha {numero}: resposta válida está vazia.")
if type(r.get("fontes_coletadas")) is not bool:
raise ValueError(f"Linha {numero}: declare a coleta das fontes.")
if not isinstance(r.get("fontes"), list) or any(
not isinstance(u, str) for u in r["fontes"]
):
raise ValueError(f"Linha {numero}: fontes precisam ser uma lista.")
if not r["fontes_coletadas"] and r["fontes"]:
raise ValueError(f"Linha {numero}: fontes contradizem o estado.")
assinatura = hashlib.sha256(json.dumps(
r, sort_keys=True, ensure_ascii=False
).encode("utf-8")).hexdigest()
if r["id"] in ids:
if ids[r["id"]] != assinatura:
raise ValueError(f"Linha {numero}: ID com conteúdo conflitante.")
duplicadas += 1
continue
chave = tuple(r[c] for c in GRUPO) + (r["prompt_id"], r["repeticao"])
if chave in chaves:
raise ValueError(f"Linha {numero}: execução repetida com outro ID.")
ids[r["id"]] = assinatura
chaves.add(chave)
c = grupos[tuple(r[c] for c in GRUPO)]
c["tentativas"] += 1
if r["status"] == "erro":
c["falhas"] += 1
continue
c["validas"] += 1
c["mencoes"] += int(bool(padrao.search(normalizar(r["texto"]))))
if r["fontes_coletadas"]:
c["fontes_observadas"] += 1
resultados = [pertence(u, dominio) for u in r["fontes"]]
c["citacoes_dominio"] += int(any(resultados))
saida = []
for chave, c in sorted(grupos.items()):
item = dict(zip(GRUPO, chave))
for campo in ("tentativas", "falhas", "validas", "mencoes",
"fontes_observadas", "citacoes_dominio"):
item[campo] = c[campo]
item["mencao_pct"] = taxa(c["mencoes"], c["validas"])
item["cobertura_fontes_pct"] = taxa(c["fontes_observadas"], c["validas"])
item["citacao_fontes_observadas_pct"] = taxa(
c["citacoes_dominio"], c["fontes_observadas"]
)
saida.append(item)
return {"duplicadas_ignoradas": duplicadas, "grupos": saida}
if __name__ == "__main__":
if len(sys.argv) != 4:
raise SystemExit(
'Uso: python auditoria_geo.py observacoes.jsonl "Marca" dominio'
)
with open(sys.argv[1], encoding="utf-8-sig") as arquivo:
relatorio = analisar(arquivo, sys.argv[2], sys.argv[3])
print(json.dumps(relatorio, ensure_ascii=False, indent=2))
Para se localizar no código, acompanhe três funções:
analisarrecebe as linhas do arquivo, confere campos e duplicatas e reúne as contagens de cada grupo.pertenceconfere se o endereço pertence ao domínio escolhido ou a um subdomínio dele. Ela examina a URL, sem abrir o link.taxatransforma a contagem em porcentagem. Sem denominador, retornaNone, que aparece comonullno relatório JSON.
Abra o terminal nessa pasta e execute:
python auditoria_geo.py observacoes.jsonl "Brasil GEO" brasilgeo.aiNo Windows, se sua instalação usa o lançador Python, substitua python por py -3. O relatório aparece no terminal em JSON. Confira os totais previstos na seção anterior e guarde a saída junto do lote.
Se o programa interromper a análise, leia a mensagem antes de tentar novamente. Ele verifica identificação, data com fuso, estados da coleta e conflitos entre registros. Uma cópia idêntica com o mesmo ID é ignorada e contada; o mesmo ID com outro conteúdo, ou a mesma execução com outro ID, causa erro. Corrija uma cópia de trabalho e preserve o arquivo original para conseguir explicar o ajuste.
Metade de quais respostas?
No nosso lote fictício, a citação de domínio ficou em 50%. Aqui isso quer dizer uma de duas respostas com as fontes conferidas. A terceira resposta válida ainda tem fontes desconhecidas; por isso não entrou nessa conta. Já a taxa de menção usa as três respostas válidas, porque temos o texto delas.
| Valor na saída do script (de 0 a 100) | Conta do exemplo | O que podemos dizer |
|---|---|---|
| Menção: 66.67 | 2 menções / 3 respostas válidas | O nome completo apareceu em duas das três respostas |
| Cobertura de fontes: 66.67 | 2 áreas de citação conferidas / 3 respostas válidas | Falta conferir as fontes de uma resposta |
| Citação entre fontes observadas: 50 | 1 resposta com domínio alvo / 2 com fontes conferidas | Uma das duas respostas conferidas citou o domínio |
Se aparecer null, não leia como zero: faltou denominador para calcular a taxa. Um lote só com falhas, por exemplo, não tem respostas válidas para medir menções. A ideia de declarar o que foi observado acompanha a orientação do guia de governança de métricas do Portal Leadlovers.
Agora volte a algumas respostas, tanto às que entraram na contagem quanto às que ficaram de fora. O programa procura o nome completo com limites de palavra; não resolve apelidos, grafias alternativas ou homônimos. A frase ilustrativa "não recomendo a Brasil GEO" contém o nome e seria contada como menção. Para saber se houve recomendação, crítica ou erro factual, você precisa ler a resposta.
Faça o mesmo com links encurtados ou intermediários. O script não segue redirecionamentos; confira o destino antes de decidir como registrar a fonte. Uma citação também não garante que a página sustente a afirmação feita pelo assistente.
Ao comparar outra coleta, mantenha o banco de perguntas, os produtos e as condições relevantes. Repita para observar a variação, mas confira também se as perguntas representam o seu público. E, se precisar reunir grupos, some os numeradores e denominadores adequados antes de calcular: tirar a média dos percentuais pode dar outra resposta. Essas taxas descrevem o lote coletado, não a presença da marca em todas as respostas de IA.
Quando chegar a hora de usar respostas reais
Depois do teste, escolha um pequeno lote obtido por registro manual, exportação permitida ou integração autorizada. Guarde a resposta integral e as condições da coleta. Uma resposta de API e outra da interface de um assistente podem usar recursos diferentes; compare cada uma pelo que você de fato observou.
Este script não pede chave de API. Se mais adiante você construir um coletor, aí será preciso confirmar conta, produto, autorização e cobrança. Estime as chamadas pelas perguntas, produtos, repetições e ciclos, incluindo novas tentativas. O preço dependerá também dos modelos, das entradas, das saídas e das ferramentas usadas. Credenciais ficam em variáveis de ambiente ou num gerenciador de segredos, fora do código e dos relatórios.
Começar pequeno também facilita conferir o resultado. Embora leia uma linha por vez, o programa guarda IDs, chaves de execução e resumos criptográficos em memória para reconhecer duplicatas. Antes de ampliar o lote, avalie memória, espaço para guardar os arquivos e tempo de revisão. Restrinja o acesso quando as respostas trouxerem informação sensível.
Mantenha o arquivo de entrada, o código e os critérios num formato que você possa levar para outra ferramenta. Assim, uma mudança na interface não leva junto o histórico da sua avaliação.
Faça uma mudança no teste e confira o que acontece
Antes de trocar o exemplo por um lote real, faça duas pequenas alterações numa cópia de observacoes.jsonl. Você já sabe o resultado original; agora consegue verificar se os controles funcionam.
- Copie a primeira linha sem mudar nada. Rode novamente. As contagens do grupo devem continuar iguais, e
duplicadas_ignoradasdeve passar a 1. - Na cópia acrescentada, altere o texto e mantenha o ID. Rode outra vez. O programa deve interromper a análise com a mensagem de ID com conteúdo conflitante. Desfaça essa alteração antes de seguir.
- Use um pequeno lote real autorizado. Confira algumas menções, os domínios e as fontes desconhecidas à mão. Se encontrar uma descrição errada da empresa, guarde a resposta e investigue a afirmação e suas fontes; a contagem sozinha não explica a causa.
Para outra pessoa repetir a análise, entregue o arquivo de entrada, a versão do script, a marca e o domínio usados. Registre os ajustes que fez sem apagar a evidência original. A mesma entrada deve produzir as mesmas contagens.
Se a frequência de citação mudar na próxima coleta, você terá uma diferença observada para investigar, ainda sem provar o que a causou. Visitas, contatos e contratos pedem seus próprios registros. O ganho deste primeiro exercício é conseguir mostrar de onde saiu cada número e o que ainda falta conferir.
Fontes
- Portal Leadlovers 2026, observação de presença em IA, consulta em 6 de outubro de 2026.
- Portal Leadlovers 2026, definição e governança de métricas, consulta em 6 de outubro de 2026.
- Portal Leadlovers 2026, fontes e limites de medição, consulta em 6 de outubro de 2026.
- Python, leitura e escrita de JSON, consulta em 6 de outubro de 2026.
- Python, normalização Unicode, consulta em 6 de outubro de 2026.
- Python, decomposição de URLs, consulta em 6 de outubro de 2026.