Mesurer sa visibilité dans les réponses IA (AI Mode) avec Python
Nous avons passé 45 prompts dans Google AI Mode, trois fois chacun : 43 sur 45 ont changé de marques citées entre deux runs identiques. Méthode, chiffres et script Python.

Pour mesurer la visibilité d’une marque dans les réponses IA, passez un jeu fixe de prompts dans la surface IA plusieurs fois, comptez la part des réponses qui citent la marque avec une regex à frontières de mot, et publiez un intervalle plutôt qu’un chiffre unique. Un seul run n’est pas une mesure : le 19 octobre 2026, nous avons envoyé 45 prompts dans Google AI Mode, trois fois chacun (135 réponses), et pour 43 des 45 prompts l’ensemble des marques citées différait entre des runs identiques. Le script ci-dessous fait tout le travail en environ 70 lignes de Python, pour 10 crédits par réponse.
Pourquoi un seul run d’une réponse IA n’est-il pas une mesure ?
Parce que le même prompt renvoie une réponse différente à chaque fois. Sur les 135 réponses, un seul prompt sur 45 a produit trois textes identiques. Parmi les paires (marque, prompt) où une marque est apparue au moins une fois, 138 sur 302 (46 %) manquaient dans au moins un des trois runs. Les réponses sont longues (médiane de 396 mots) et citent chacune environ cinq des marques suivies : de petites variations de formulation font donc entrer et sortir des marques.
Si vous vérifiez votre marque une fois par semaine et la voyez apparaître, puis disparaître, c’est surtout ce bruit, pas un changement de votre visibilité. L’article sur AI Mode traite du format de réponse ; celui-ci explique quoi faire de nombreuses réponses.
Comment cela a-t-il été mesuré ?
Trois catégories de produits que les développeurs recherchent, 15 prompts chacune, formulés comme les gens les tapent : « best vector database », « best vector database for rag », « best async python web framework », « web scraping api with a free tier ». Aucun prompt ne nomme de marque. Chaque prompt est parti vers /v1/ai avec country=us et language=en, trois fois. 135 appels, 1 350 crédits, 6,9 secondes en moyenne (deux appels de confirmation vers api.fastaf.co le même jour ont pris 6,1 et 3,6 secondes, chacun indiquant X-FastAF-Credits-Used: 10). Un appel a renvoyé HTTP 200 avec une réponse vide et a été facturé 10 crédits à l’époque ; après cette mesure, nous avons changé la règle, et une réponse AI Mode vide est désormais remboursée. Traitée comme une donnée, elle aurait compté comme « aucune marque citée » pour tout le monde, donc le script relance quand même les réponses vides.
J’ai choisi 14 marques par catégorie à l’avance. Les mentions sont comptées dans le texte de la réponse uniquement (paragraphes, éléments de liste et cellules de tableau), pas dans les titres des pages citées. La « position » est l’ordre de première apparition parmi les marques suivies. Rien ici n’est un sentiment, une recommandation ou un classement des produits : c’est ce qu’une surface Google a dit un jour donné.
Que montrent les données ?
Le taux de mention est la part des 45 réponses qui citent la marque. L’intervalle est un intervalle bootstrap à 95 % qui rééchantillonne les prompts, il inclut donc l’effet du choix des 15 prompts. Les runs 1 à 3 sont les taux que vous auriez publiés avec un seul passage.
| Catégorie | Marque | Taux | Intervalle à 95 % | Run 1 / 2 / 3 |
|---|---|---|---|---|
| Base de données vectorielle | 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 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 |
Trois choses ressortent. D’abord, les marques au milieu d’une catégorie oscillent de 10 à 20 points entre deux passages (Apify de 33 % à 53 %) ; l’écart moyen pour les marques au-dessus de 20 % était de 11 à 14 points. Ensuite, des intervalles calculés sur 15 prompts font plus ou moins 20 points au milieu de la plage, donc un changement de « +5 points » d’une semaine à l’autre est invisible. Enfin, les classements diffèrent d’un passage à l’autre : dans la catégorie scraping, ScraperAPI était troisième au passage 1, et Apify n’a remplacé Oxylabs dans le top cinq qu’au passage 3. Le taux et la position sont aussi deux choses distinctes : Pinecone était nommé plus tôt (position moyenne 2,2) que Qdrant (2,9) malgré un taux plus bas.
Les réponses citent-elles les sites des marques elles-mêmes ?
Pour la plupart, non. Parmi les noms d’hôte distincts cités par réponse, la part appartenant à l’une des marques suivies était de 44 % pour les API de scraping, 30 % pour les bases de données vectorielles et 31 % pour les frameworks Python. Le reste venait de tiers : reddit.com apparaissait dans 52 des 135 réponses (39 %), puis github.com, YouTube et des blogs comparatifs. Qui écrit sur vous compte autant que ce que dit votre propre site.
Les sources varient aussi : deux runs du même prompt partageaient en moyenne 28 à 34 % de leurs sites cités (recouvrement de Jaccard), donc « quelles pages la réponse cite-t-elle » demande aussi des répétitions.
Comment l’exécuter moi-même en Python ?
Définissez FASTAF_API_KEY, remplissez PROMPTS avec 15 prompts ou plus et BRANDS avec les noms qui vous intéressent, puis lancez le script. Chaque appel est ajouté à runs.jsonl, donc un plantage ou une relance saute ce qui est déjà fait.
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()
Avec la liste de trois prompts ci-dessus, il a affiché, le 19 octobre 2026, pgvector, Weaviate, Qdrant et Milvus à 100 % dans chaque run, Chroma à 78 % (67 / 67 / 100) et Pinecone à 67 %.
Quelles erreurs de comptage changent les chiffres ?
La casse et les frontières de mot. Sans \b, « Jina » correspond à l’intérieur de mots plus longs ; sans sensibilité à la casse, « Bottle », « Falcon » et « Reflex » correspondent à de la prose ordinaire, et « scraper API » en minuscules compte comme le produit ScraperAPI. Dans mon jeu, une recherche insensible à la casse a fait passer ScraperAPI de 21 à 22 réponses et Zyte de 4 à 5, peu ici, mais un « falcon » ou un « bottle » en minuscules est facile à attraper avec une marque qui est un mot courant. Ajoutez les alias voulus (« ChromaDB », « Bright Data » sans l’espace) et gardez-les dans une liste relue. Ne comptez aussi que la réponse, pas la requête répétée en écho, sinon un prompt qui contient le nom de votre marque mesure votre prompt.
Pour AI Overviews, la même extraction fonctionne sur l’objet ai_overview dans une réponse /v1/serp, qui coûte 5 crédits au lieu de 10 ; cet article traite de la présence et des sources par mot-clé.
Le GEO change-t-il ce qu’il faut mesurer ?
L’article GEO (Aggarwal et al., KDD 2024) a rapporté jusqu’à 40 % de visibilité en plus dans les réponses de moteurs génératifs sur son benchmark GEO-bench, mais il s’agit d’un ensemble testé de modifications de contenu, pas d’une garantie pour votre site. Le guide de Google sur les fonctionnalités d’IA générative, mis à jour pour la dernière fois le 10 juillet 2026 à la date de cette vérification, dit que « les bonnes pratiques SEO restent pertinentes » et qu’aucun balisage spécial ni fichier llms.txt n’est nécessaire pour apparaître dans la recherche Google. Le travail reste donc du SEO ordinaire ; ce que vous ajoutez, c’est un moyen de savoir si quelque chose a bougé, et la réponse ci-dessus est : seulement avec des répétitions et un intervalle.
Combien ça coûte ?
45 prompts x 3 runs font 1 350 crédits, soit environ 0,68 $ avec le pack Starter à 10 $ (20 000 crédits). Une catégorie de 15 prompts avec 3 répétitions fait 450 crédits. Les requêtes échouées ne sont pas facturées. Voir les tarifs et la documentation.
En bref
Comptez les mentions avec une regex sensible à la casse et à frontières de mot, exécutez chaque prompt au moins trois fois, et publiez le taux de mention avec un intervalle qui rééchantillonne les prompts. Dans nos 135 réponses, 46 % des apparitions marque-prompt ont disparu dans au moins une répétition, et moins de la moitié des sources citées appartenaient aux marques elles-mêmes. Démarrez gratuitement avec 500 crédits et lancez le script sur votre propre catégorie.