Mide la visibilidad de tu marca en respuestas de IA con Python
Pasamos 45 prompts por Google AI Mode tres veces cada uno. En 43 de 45 cambiaron las marcas nombradas entre ejecuciones idénticas. Método, cifras y un script en Python.

Para medir la visibilidad de una marca en las respuestas de IA, pasa un conjunto fijo de prompts por la superficie de IA varias veces, cuenta qué porcentaje de respuestas nombra la marca con una regex de límite de palabra y reporta un intervalo en lugar de un único número. Una sola ejecución no es una medición: el 19 de octubre de 2026 enviamos 45 prompts a Google AI Mode tres veces cada uno (135 respuestas), y en 43 de los 45 prompts el conjunto de marcas nombradas fue distinto entre ejecuciones idénticas. El script de abajo hace todo el trabajo en unas 70 líneas de Python, por 10 créditos por respuesta.
¿Por qué una sola ejecución de una respuesta de IA no es una medición?
Porque el mismo prompt devuelve una respuesta distinta cada vez. En las 135 respuestas, solo 1 de los 45 prompts produjo tres textos idénticos. Entre los pares (marca, prompt) en los que una marca apareció al menos una vez, 138 de 302 (el 46 %) faltaban en al menos una de las tres ejecuciones. Las respuestas son largas (mediana de 396 palabras) y nombran unas cinco de las marcas seguidas cada una, así que pequeños cambios de redacción hacen entrar y salir marcas.
Si revisas tu marca una vez por semana y la ves aparecer y luego desaparecer, eso es sobre todo este ruido, no un cambio en tu visibilidad. El post sobre AI Mode explica el formato de la respuesta; este trata de qué hacer con muchas respuestas.
¿Cómo se midió?
Tres categorías de producto que buscan los desarrolladores, 15 prompts cada una, escritos como los teclea la gente: “best vector database”, “best vector database for rag”, “best async python web framework”, “web scraping api with a free tier”. Ningún prompt nombra una marca. Cada prompt se envió a /v1/ai con country=us y language=en, tres veces. 135 llamadas, 1.350 créditos, 6,9 segundos de media (dos llamadas de confirmación a api.fastaf.co el mismo día tardaron 6,1 y 3,6 segundos, y cada una informó X-FastAF-Credits-Used: 10). Una llamada devolvió HTTP 200 con una respuesta vacía y se cobró 10 créditos en su momento; después de esta medición cambiamos la regla, y ahora una respuesta vacía de AI Mode se reembolsa. Tratada como dato, habría contado como “ninguna marca mencionada” para todas, así que el script sigue reintentando las respuestas vacías.
Elegí 14 marcas por categoría de antemano. Las menciones se cuentan solo en el texto de la respuesta (párrafos, elementos de lista y celdas de tabla), no en los títulos de las páginas citadas. “Posición” es el orden de la primera aparición entre las marcas seguidas. Nada de esto es sentimiento, un aval ni un ranking de los productos: es lo que dijo una superficie de Google un día concreto.
¿Qué muestran los datos?
La tasa de mención es el porcentaje de las 45 respuestas que nombra la marca. El intervalo es un intervalo bootstrap del 95 % que remuestrea prompts, así que incluye el efecto de cuáles 15 prompts elegiste. Las ejecuciones 1 a 3 son las tasas que habrías reportado con una sola pasada.
| Categoría | Marca | Tasa | Intervalo del 95 % | Ejec. 1 / 2 / 3 |
|---|---|---|---|---|
| Base de datos vectorial | Qdrant | 96 % | 87-100 % | 93 / 93 / 100 |
| Pinecone | 84 % | 71-96 % | 80 / 80 / 93 | |
| Weaviate | 73 % | 56-91 % | 67 / 73 / 80 | |
| Chroma | 53 % | 31-73 % | 60 / 53 / 47 | |
| Framework de Python | Flask | 91 % | 82-98 % | 80 / 93 / 100 |
| Starlette | 49 % | 31-67 % | 53 / 40 / 53 | |
| Litestar | 38 % | 20-58 % | 40 / 27 / 47 | |
| API de scraping | Firecrawl | 71 % | 51-91 % | 80 / 67 / 67 |
| ScraperAPI | 47 % | 29-64 % | 53 / 40 / 47 | |
| Apify | 42 % | 24-60 % | 33 / 40 / 53 |
Destacan tres cosas. Primero, las marcas de la parte media de una categoría oscilan entre 10 y 20 puntos entre pasadas individuales (Apify del 33 % al 53 %); la dispersión media de las marcas por encima del 20 % fue de 11 a 14 puntos. Segundo, los intervalos con 15 prompts son de más o menos 20 puntos en el centro del rango, así que un cambio de “+5 puntos” de una semana a otra es invisible. Tercero, los rankings por pasada individual difieren: en la categoría de scraping, ScraperAPI fue tercera en la pasada 1, y Apify reemplazó a Oxylabs en el top cinco solo en la pasada 3. Además, tasa y posición son cosas distintas: Pinecone se nombró antes (posición media 2,2) que Qdrant (2,9) pese a tener una tasa menor.
¿Las respuestas citan los sitios de las propias marcas?
En su mayoría no. De los hostnames distintos citados por respuesta, el porcentaje que pertenecía a una de las marcas seguidas fue del 44 % en las API de scraping, del 30 % en las bases de datos vectoriales y del 31 % en los frameworks de Python. El resto eran terceros: reddit.com apareció en 52 de las 135 respuestas (el 39 %), seguido de github.com, YouTube y blogs de comparativas. Quién escribe sobre ti importa tanto como lo que dice tu propio sitio.
Las fuentes también varían: dos ejecuciones del mismo prompt compartían de media entre el 28 y el 34 % de los sitios citados (solapamiento de Jaccard), así que saber “qué páginas cita la respuesta” también requiere repeticiones.
¿Cómo lo ejecuto yo mismo en Python?
Define FASTAF_API_KEY, rellena PROMPTS con 15 prompts o más y BRANDS con los nombres que te importan, y ejecútalo. Cada llamada se añade a runs.jsonl, así que un fallo o una nueva ejecución se salta lo que ya está hecho.
import json, os, random, re, time
from concurrent.futures import ThreadPoolExecutor
import requests
API = "https://api.fastaf.co/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FASTAF_API_KEY']}"}
REPEATS = 3
PROMPTS = ["best vector database", "best vector database for rag", "best self hosted vector database"] # use 15
BRANDS = { # case-sensitive on purpose: "Falcon" is not "falcon"
"Qdrant": r"\bQdrant\b", "pgvector": r"\bpgvector\b", "Milvus": r"\bMilvus\b",
"Pinecone": r"\bPinecone\b", "Weaviate": r"\bWeaviate\b", "Chroma": r"\bChroma(?:DB)?\b",
}
def text_of(node):
"""Every visible string of the answer: paragraphs, list items, table cells."""
if isinstance(node, str):
return [node]
if isinstance(node, list):
return [s for v in node for s in text_of(v)]
if isinstance(node, dict):
return [s for k, v in node.items() if k in ("snippet", "text", "items", "table") for s in text_of(v)]
return []
def ask(prompt):
for attempt in range(5):
r = requests.get(f"{API}/ai", headers=HEADERS, timeout=90,
params={"query": prompt, "country": "us", "language": "en"})
if r.status_code == 200:
text = "\n".join(text_of(r.json()["text_blocks"]))
if text: # an empty 200 is not "no brand mentioned": retry it
return text
time.sleep(3 * (attempt + 1))
raise RuntimeError(f"gave up on {prompt!r}")
def collect(path="runs.jsonl"):
done = {(r["prompt"], r["run"]) for r in map(json.loads, open(path))} if os.path.exists(path) else set()
jobs = [(p, n) for p in PROMPTS for n in range(1, REPEATS + 1) if (p, n) not in done]
def work(job):
prompt, n = job
row = {"prompt": prompt, "run": n, "text": ask(prompt)}
with open(path, "a") as f: # one line per finished call
f.write(json.dumps(row) + "\n")
with ThreadPoolExecutor(max_workers=5) as pool:
list(pool.map(work, jobs))
def interval(per_prompt, n_boot=4000):
"""95% bootstrap interval resampling PROMPTS, keeping each prompt's runs together."""
rng, k = random.Random(7), len(per_prompt)
rates = sorted(sum(rng.choices(per_prompt, k=k)) / k for _ in range(n_boot))
return rates[int(0.025 * n_boot)], rates[int(0.975 * n_boot)]
def report(path="runs.jsonl"):
rows = [json.loads(line) for line in open(path)]
print(f"{'brand':10}{'rate':>6}{'95% CI':>10}{'run1':>6}{'run2':>6}{'run3':>6}")
table = []
for brand, rx in BRANDS.items():
hit = {(r["prompt"], r["run"]): bool(re.search(rx, r["text"])) for r in rows}
per_prompt = [sum(hit[(p, n)] for n in range(1, REPEATS + 1)) / REPEATS for p in PROMPTS]
by_run = [sum(hit[(p, n)] for p in PROMPTS) / len(PROMPTS) for n in range(1, REPEATS + 1)]
table.append((sum(per_prompt) / len(PROMPTS), brand, interval(per_prompt), by_run))
for rate, brand, (lo, hi), by_run in sorted(table, reverse=True):
print(f"{brand:10}{rate:>6.0%}{f'{lo:.0%}-{hi:.0%}':>10}" + "".join(f"{x:>6.0%}" for x in by_run))
collect()
report()
Con la lista de tres prompts de arriba, el 19 de octubre de 2026 imprimió pgvector, Weaviate, Qdrant y Milvus al 100 % en todas las ejecuciones, Chroma al 78 % (67 / 67 / 100) y Pinecone al 67 %.
¿Qué errores de conteo cambian las cifras?
Las mayúsculas y los límites de palabra. Sin \b, “Jina” coincide dentro de palabras más largas; sin distinguir mayúsculas, “Bottle”, “Falcon” y “Reflex” coinciden con prosa corriente, y “scraper API” en minúsculas cuenta como el producto ScraperAPI. En mi conjunto, la coincidencia sin distinguir mayúsculas cambió ScraperAPI de 21 a 22 respuestas y Zyte de 4 a 5, poco aquí, pero es fácil toparse con un “falcon” o “bottle” en minúsculas si tu marca es una palabra común. Añade los alias que quieras (“ChromaDB”, “Bright Data” sin el espacio) y mantenlos en una lista revisada. Cuenta también solo la respuesta, no la consulta repetida, o un prompt que contenga el nombre de tu marca estará midiendo tu prompt.
Para los AI Overviews, la misma extracción funciona sobre el objeto ai_overview dentro de una respuesta de /v1/serp, que cuesta 5 créditos en lugar de 10; ese post trata la presencia y las fuentes por palabra clave.
¿Cambia el GEO lo que hay que medir?
El paper de GEO (Aggarwal et al., KDD 2024) reportó hasta un 40 % más de visibilidad en las respuestas de motores generativos en su benchmark GEO-bench, pero eso es un conjunto probado de ediciones de contenido, no una garantía para tu sitio. La guía de Google sobre las funciones de IA generativa, actualizada por última vez el 10 de julio de 2026 en el momento de esta revisión, dice “The best practices for SEO continue to be relevant” y que no hace falta ningún marcado especial ni un archivo llms.txt para aparecer en Google Search. Así que el trabajo sigue siendo SEO corriente; lo que añades es una forma de saber si algo se movió, y la respuesta de arriba es: solo con repeticiones y un intervalo.
¿Cuánto cuesta?
45 prompts x 3 ejecuciones son 1.350 créditos, unos 0,68 $ con el pack Starter de 10 $ (20.000 créditos). Una categoría de 15 prompts con 3 repeticiones son 450 créditos. Las solicitudes fallidas no se cobran. Mira los precios y la documentación.
En resumen
Cuenta las menciones con una regex de límite de palabra que distinga mayúsculas, ejecuta cada prompt al menos tres veces y reporta la tasa de mención con un intervalo que remuestree prompts. En nuestras 135 respuestas, el 46 % de las apariciones marca-prompt desapareció en al menos una repetición, y menos de la mitad de las fuentes citadas eran las propias marcas. Empieza gratis con 500 créditos y ejecuta el script con tu propia categoría.