Messung des AI-Zitierungseffekts: Analytik, Protokolle und Panel-Scrapes
Einführung
KI-gesteuerte Entdeckungen verteilen sich mittlerweile auf mehrere Messschichten. Eine Person sieht möglicherweise ein in einer KI-Antwort zitiertes Unternehmen, klickt auf einen Link ohne nützlichen Referrer, besucht die Seite über einen Browser, der Kampagnendaten entfernt, oder liest eine Antwort, ohne überhaupt zu klicken. Gleichzeitig kann ein KI-Crawler die Seite des Unternehmens anfordern, ohne einen menschlichen Besucher darzustellen.
Ein nützliches Messsystem muss daher vier Fragen trennen:
- Wurde die Seite gecrawlt oder abgerufen?
- Wurde die Seite in einer KI-Antwort zitiert?
- Hat eine Person die Seite über diese Antwort besucht?
- Hat dieser Besuch ein sinnvolles Engagement oder Umsatz generiert?
Stand 22. September 2026 verbessert sich die Erstanbieter-Berichterstattung. Die Google Search Console bietet einen Performance-Bericht für generative KI für AI Overviews und AI-Modus, einschließlich Impressionen, Seiten, Ländern, Geräten und Daten. Der Bericht wurde am 31. August 2026 weltweit verfügbar. Googles Performance-Bericht für generative KI (support.google.com)
Microsoft bietet ebenfalls einen AI-Leistungsbericht in den Bing Webmaster Tools an. Die öffentliche Vorschau berichtet über Gesamtzitate, zitierte Seiten, unterstützende Abfragen und Zitationstrends über unterstützte Microsoft KI-Erlebnisse hinweg. Bing Webmaster Tools AI-Leistung (blogs.bing.com)
Diese Tools sollten für ihre eigenen Oberflächen als maßgeblich angesehen werden, sie bilden jedoch kein vollständiges, assistentenübergreifendes Messsystem. Der empfohlene Ansatz ist die Kombination von Erstanbieter-Berichten, Server-Logs, kontrollierten Tracking-Links, Analyseexporten und geplanten Beobachtungen von Antwortpanels.
1. Warum die Messung von KI-Verweisen schwierig ist
Traditionelle Webanalyse hängt stark vom Referer-Header und Kampagnenparametern ab. Beides kann fehlen oder unvollständig sein.
Der HTTP-Referer-Header kann die vorherige Seite, nur den Ursprung der vorherigen Website oder überhaupt keinen Wert enthalten. Browser-Referrer-Richtlinien können diese Informationen absichtlich einschränken oder entfernen. MDN Referrer-Policy Dokumentation (developer.mozilla.org)
KI-Systeme schaffen zusätzliche Komplikationen:
- Ein Assistent kann auf eine Seite verlinken, ohne einen erkennbaren Referrer zu senden.
- Ein Assistent kann eine umgeleitete oder umgeschriebene URL verwenden.
- Ein Assistent kann Inhalte serverseitig abrufen, bevor er eine Antwort anzeigt.
- Ein KI-Crawler kann eine Seite besuchen, ohne dass ein Mensch sie sieht.
- Die Antwort kann eine kanonische URL zitieren, anstatt der ursprünglich abgerufenen URL.
- Die Antwort kann je nach Standort, Gerät, Sprache, Zeit oder Personalisierung variieren.
Beispielsweise unterscheidet OpenAI zwischen OAI-SearchBot für die Sucherkennung, GPTBot für mögliche Trainingszwecke und ChatGPT-User für vom Benutzer initiierte Aktionen. Perplexity identifiziert separat PerplexityBot und Perplexity-User. Anthropic identifiziert ClaudeBot, Claude-SearchBot und Claude-User. Diese Anfragen sollten nicht zu einer generischen „AI-Traffic“-Zahl zusammengefasst werden. OpenAI Crawler-Dokumentation (openai.com) Perplexity Crawler-Dokumentation (docs.perplexity.ai) Anthropic Crawler-Dokumentation (support.anthropic.com)
Die zentrale Regel lautet:
Crawler-Aktivität ist kein Verweis-Traffic, die Sichtbarkeit von Zitaten ist kein Traffic, und Traffic ist kein Geschäftserfolg.
2. Das Mess-Framework
Verwenden Sie fünf Messschichten.
| Ebene | Hauptfrage | Empfohlene Daten |
|---|---|---|
| Zugriff | Hat ein KI-System die Seite angefordert? | Webserver-Logs, Firewall-Logs |
| Sichtbarkeit | Wurde die Seite angezeigt oder zitiert? | Google Search Console, Bing Webmaster Tools, Beobachtungen von Antwortpanels |
| Akquisition | Ist eine Person von einer KI-Oberfläche gekommen? | Referrer, Kampagnenparameter, Kurzlink-Klicks, Analysen |
| Engagement | Hat der Besucher sinnvoll interagiert? | Analyseereignisse, Seitenaufrufe, Engagement-Zeit |
| Wert | Hat der Besuch ein gewünschtes Ergebnis erzielt? | Anmeldungen, Leads, Käufe, Abonnements |
Empfohlene Hierarchie der Wahrheitsquellen
Verwenden Sie für jede Frage das stärkste verfügbare Signal:
- Erstanbieter-Plattformberichte für Impressionen und Zitationen.
- Antwortpanel-Beobachtungen für Sichtbarkeit auf Prompt-Ebene und Wettbewerbsvergleiche.
- Weiterleitungs-Logs für kontrollierte Link-Klicks.
- Webanalyse für Sitzungen, Engagement und Konversionen.
- Server-Logs für Crawler- und Abrufaktivität.
- Modellierte Schätzungen nur, wenn keine direkten Beweise vorliegen.
Fassen Sie nicht alle Signale zu einer einzigen Zahl zusammen, ohne die Quelle und das Konfidenzniveau beizubehalten.
3. Kern-Datenmodell
Erstellen Sie ein Data Warehouse mit separaten Tabellen für Rohbeobachtungen und abgeleitete Metriken.
panel_runs
Eine Zeile für jeden Prompt- und Oberflächen-Check.
text run_id observed_at surface topic prompt_id query_text locale country device panel_present panel_type answer_hash raw_html_path screenshot_path collector_version status
panel_citations
Eine Zeile für jeden in einem Antwortpanel beobachteten Link.
text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited
ai_crawler_requests
Aggregierte Server-Log-Daten.
text date bot_name bot_role path requests successful_requests error_requests unique_ip_count
shortener_clicks
Eine Zeile für jede Anfrage über einen kontrollierten Link.
text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path
ai_sessions
Eine normalisierte Analysetabelle.
text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue
Das Feld confidence ist wichtig. Empfohlene Werte sind:
- A — Direkt: Kontrollierter Kurzlink-Klick oder vertrauenswürdiger Kampagnenparameter.
- B — Stark: Bekannter Assistenten-Referrer oder verifizierte Benutzer-Abrufanfrage.
- C — Wahrscheinlich: Kampagnen- oder Landingpage-Nachweise ohne vollständigen Referrer.
- D — Modelliert: Abgeleitet aus Timing, Panel-Beobachtungen oder aggregierten Änderungen.
Berichten Sie A und B getrennt von C und D.
4. Metrikdefinitionen
Zitationsanteil nach Thema
Der Zitationsanteil nach Thema misst, wie oft eine Zieldomäne unter den für ein Thema beobachteten Quellen erscheint.
[ \text{Zitationsanteil}_{Thema}
\frac{\text{Vorkommen von Zitationen der Zieldomäne}} {\text{Alle Zitationsvorkommen}} ]
Beispiel:
- 100 gesamte Zitationen für „Projektmanagement-Software“ beobachtet
- 12 Zitationen verweisen auf Ihre Domain
- Zitationsanteil nach Thema = 12%
Diese Metrik misst den Quellenanteil, nicht, ob eine Antwort Ihre Website mindestens einmal enthielt.
Zitationsabdeckung
Verwenden Sie eine zweite Metrik für die Präsenz auf Antwortebene:
[ \text{Zitationsabdeckung}_{Thema}
\frac{\text{Antwort-Durchläufe mit einer Zielzitation}} {\text{Antwort-Durchläufe, bei denen ein Panel vorhanden war}} ]
Die Zitationsabdeckung ist für nicht-technisches Publikum oft leichter verständlich:
„Unsere Website erschien in 34% der beobachteten KI-Antworten zu diesem Thema.“
Zitationsposition
Erfassen Sie die Position einer Zitation innerhalb der Antwort:
1= erste beobachtete Quelle2= zweite beobachtete Quelle3= dritte beobachtete Quelle
Da KI-Oberflächen Quellen oft gruppieren oder neu anordnen, behandeln Sie die Position als deskriptive Beobachtung, nicht als universelles Ranking.
Assistenten-Verweisrate
Die Assistenten-Verweisrate misst den Anteil der menschlichen Website-Sitzungen, die einer KI-Oberfläche zugeordnet werden können.
[ \text{Assistenten-Verweisrate}
\frac{\text{KI-zugeordnete menschliche Sitzungen}} {\text{Alle menschlichen Sitzungen}} ]
Berichten Sie mindestens zwei Versionen:
text Bestätigte Assistenten-Verweisrate = KI-Sitzungen mit A-Konfidenz / alle menschlichen Sitzungen
Erweiterte Assistenten-Verweisrate = KI-Sitzungen mit A + B + C-Konfidenz / alle menschlichen Sitzungen
Die bestätigte Version ist konservativer. Die erweiterte Version ist nützlich für Trendanalysen, sollte aber nicht als vollständig beobachteter Traffic dargestellt werden.
Panel-Durchläufe nicht als Impressionen verwenden
Wenn Sie 500 Prompts scrapen, bedeutet das nicht, dass 500 Personen diese Antworten gesehen haben. Ihr Scraper misst eine kontrollierte Stichprobe von Antwortzuständen. Verwenden Sie Erstanbieter-Plattform-Impressionsberichte, wenn diese verfügbar sind.
Engagement nach Zitation
Das Engagement nach Zitation misst, ob Personen, die über eine KI-Zitation kamen, eine sinnvolle Aktion durchgeführt haben.
[ \text{Engagement nach Zitation}
\frac{\text{Engagierte KI-Sitzungen}} {\text{KI-zugeordnete Sitzungen}} ]
Für Google Analytics 4 dauert eine engagierte Sitzung länger als 10 Sekunden, beinhaltet ein Schlüsselereignis oder mindestens zwei Seiten- oder Bildschirmansichten. Google Analytics Dokumentation zur Engagement-Rate (support.google.com)
Berichten Sie auch:
text Engagement-Zeit nach Zitation = mittlere engagierte Sekunden pro KI-Sitzung
Konversionsrate nach Zitation = KI-Sitzungen mit einem Schlüsselereignis / KI-zugeordnete Sitzungen
Umsatz pro Sitzung nach Zitation = Umsatz aus KI-Sitzungen / KI-zugeordnete Sitzungen
Die mittlere Engagement-Zeit ist in der Regel stabiler als die durchschnittliche Engagement-Zeit, da eine kleine Anzahl sehr langer Sitzungen den Durchschnitt verzerren kann.
Zusätzliche nützliche Metriken
Erfolgsrate des KI-Crawlers
[ \text{Crawler-Erfolgsrate}
\frac{\text{KI-Crawler-Anfragen, die 2xx oder 3xx zurückgeben}} {\text{KI-Crawler-Anfragen}} ]
Schlüsseln Sie dies nach Bot und Pfad auf. Ein starker Rückgang der Zitationen in Kombination mit einer steigenden Crawler-Fehlerrate kann auf ein Hosting-, Firewall- oder Robots-Konfigurationsproblem hinweisen.
Zitations-Klickrate
Für kontrollierte Beobachtungen von Antwortpanels:
[ \text{Beobachtete Zitations-Klickrate}
\frac{\text{Bestätigte Kurzlink-Klicks}} {\text{Beobachtete Zielzitationen}} ]
Dies ist keine echte Benutzer-Klickrate, es sei denn, der Nenner repräsentiert tatsächliche Benutzerimpressionen. Kennzeichnen Sie es deutlich als beobachtete operative Metrik.
Zitationsaktualität
Messen Sie, wie oft die zitierte URL die aktuelle Seitenversion widerspiegelt.
text Zitationsaktualität = zitierte Seiten mit aktuellem Content-Hash / zitierte Seiten
Dies ist nützlich für Dokumentations-, Preis-, Produkt- und Richtlinienseiten, die sich häufig ändern.
5. Server-Log-Analyse
Strukturierte Zugriffsprotokolle konfigurieren
Strukturierte Protokolle sind einfacher zu analysieren als herkömmliche, durch Leerzeichen getrennte Protokolle.
Beispiel einer Nginx-Konfiguration:
nginx log_format ai_json escape=json '{' '"ts":"$time_iso8601",' '"remote_addr":"$remote_addr",' '"request":"$request",' '"status":$status,' '"bytes":$body_bytes_sent,' '"referer":"$http_referer",' '"user_agent":"$http_user_agent",' '"host":"$host"' '}';
access_log /var/log/nginx/ai-access.jsonl ai_json;
Speichern Sie die Protokolldateien an einem eingeschränkten Ort. Senden Sie keine rohen IP-Adressen an eine allgemeine Analysetabelle, es sei denn, Sie haben einen klaren operativen Grund.
AI-Anfragetypen identifizieren
Trennen Sie mindestens:
text OpenAI:
- OAI-SearchBot
- ChatGPT-User
- GPTBot
Anthropic:
- Claude-SearchBot
- Claude-User
- ClaudeBot
Perplexity:
- PerplexityBot
- Perplexity-User
Suchmaschinen:
- Googlebot
- Bingbot
Googles KI-Suchfunktionen verwenden im Allgemeinen bestehende Google Search Crawling-Steuerungen anstelle eines separaten universellen „AI Overview Bot“. Google gibt an, dass Googlebot den Zugriff für die Suche, einschließlich KI-Funktionen, steuert. Google AI-Funktionen und Websites (developers.google.com)
User-Agent-Strings können gefälscht werden. Für operative Entscheidungen kombinieren Sie die User-Agent-Übereinstimmung mit vom Anbieter veröffentlichten IP-Bereichen oder verifizierten Reverse-DNS, wo verfügbar. OpenAI und Perplexity veröffentlichen IP-Informationen für ihre relevanten Crawler und Fetcher. (openai.com)
Reproduzierbarer Log-Summarizer
Speichern Sie Folgendes als ai_log_summary.py.
python #!/usr/bin/env python3
import csv import hashlib import hmac import json import os import re import sys from collections import Counter, defaultdict from datetime import datetime from urllib.parse import urlsplit
BOT_PATTERNS = { "OAI-SearchBot": ("openai", re.compile(r"\bOAI-SearchBot\b", re.I)), "ChatGPT-User": ("openai_user", re.compile(r"\bChatGPT-User\b", re.I)), "GPTBot": ("openai_training", re.compile(r"\bGPTBot\b", re.I)), "Claude-SearchBot": ("anthropic", re.compile(r"\bClaude-SearchBot\b", re.I)), "Claude-User": ("anthropic_user", re.compile(r"\bClaude-User\b", re.I)), "ClaudeBot": ("anthropic_training", re.compile(r"\bClaudeBot\b", re.I)), "PerplexityBot": ("perplexity", re.compile(r"\bPerplexityBot\b", re.I)), "Perplexity-User": ("perplexity_user", re.compile(r"\bPerplexity-User\b", re.I)), "Googlebot": ("google_search", re.compile(r"\bGooglebot\b", re.I)), "Bingbot": ("bing_search", re.compile(r"\bbingbot\b", re.I)), }
SECRET = os.environ.get("HMAC_SECRET", "replace-this-in-production").encode()
def hmac_ip(ip): return hmac.new(SECRET, ip.encode(), hashlib.sha256).hexdigest()[:16]
def classify_bot(user_agent): for bot_name, (role, pattern) in BOT_PATTERNS.items(): if pattern.search(user_agent or ""): return bot_name, role return None, None
def parse_path(request_line): try: parts = request_line.split() return urlsplit(parts[1]).path except Exception: return "/"
def main(input_path): totals = defaultdict(lambda: { "requests": 0, "success": 0, "errors": 0, "ips": set(), "paths": Counter(), })
with open(input_path, "r", encoding="utf-8") as source:
for line in source:
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
bot_name, bot_role = classify_bot(record.get("user_agent", ""))
if not bot_name:
continue
timestamp = record.get("ts", "")
date = datetime.fromisoformat(
timestamp.replace("Z", "+00:00")
).date().isoformat()
status = int(record.get("status", 0))
path = parse_path(record.get("request", ""))
key = (date, bot_name, bot_role)
totals[key]["requests"] += 1
totals[key]["ips"].add(hmac_ip(record.get("remote_addr", "")))
totals[key]["paths"][path] += 1
if 200 <= status < 400:
totals[key]["success"] += 1
else:
totals[key]["errors"] += 1
writer = csv.writer(sys.stdout)
writer.writerow([
"date",
"bot_name",
"bot_role",
"requests",
"successful_requests",
"error_requests",
"unique_ip_count",
"top_paths",
])
for (date, bot_name, bot_role), values in sorted(totals.items()):
top_paths = values["paths"].most_common(10)
writer.writerow([
date,
bot_name,
bot_role,
values["requests"],
values["success"],
values["errors"],
len(values["ips"]),
json.dumps(top_paths),
])
if name == "main": if len(sys.argv) != 2: print("Usage: python ai_log_summary.py /path/to/access.jsonl") sys.exit(1)
main(sys.argv[1])
Führen Sie es aus mit:
bash
HMAC_SECRET='a-long-random-secret'
python ai_log_summary.py /var/log/nginx/ai-access.jsonl \
data/ai_crawler_daily.csv
Dies liefert operative Nachweise über die Abrufaktivität, nicht über menschlichen Traffic.
6. Benutzerdefinierte Kurzlinks für AI-bezogene Zusammenfassungen
Ein Erstanbieter-Link-Shortener bietet einen kontrollierten Messpunkt, wenn Sie Zusammenfassungen oder Quellmaterialien veröffentlichen, die von KI-Systemen referenziert werden könnten.
Verwenden Sie eine Domain, die Sie kontrollieren, wie z.B.:
text https://go.example.com/ai-docs
Das Ziel sollte Kampagnenparameter enthalten, wie z.B.:
text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3
Google empfiehlt Kampagnenparameter zur Identifizierung von Traffic-Quellen und Kampagnen. Google Analytics Dokumentation zu Kampagnen-URLs (support.google.com)
Empfohlene Link-Felder
text surface topic prompt_id asset_id content_version destination_path
Fügen Sie keine persönlichen Informationen, E-Mail-Adressen oder Benutzersuchanfragen in den Link ein. Kampagnen-Identifikatoren sollten das veröffentlichte Asset beschreiben, nicht eine Person identifizieren.
Wichtige Einschränkungen
Ein Kurzlink ist ein Kalibrierungsgerät, keine Universallösung:
- Der Assistent kann die Ziel-URL umschreiben.
- Der Assistent kann stattdessen die kanonische URL zitieren.
- Der Link kann angezeigt, aber nie angeklickt werden.
- Ein Browser oder Datenschutztool kann den Referrer entfernen.
- Einige Systeme können das Ziel serverseitig abrufen.
- Ein Kurzlink misst keine Benutzer, die sich an die Marke erinnern und später zurückkehren.
Verwenden Sie keinen Shortener, um das Ziel zu verbergen, Sicherheitssysteme zu umgehen oder Zitationen zu manipulieren. Verwenden Sie eine klare Weiterleitung zu einer relevanten, kanonischen Seite.
Minimaler Erstanbieter-Shortener
Das folgende Beispiel verwendet FastAPI und SQLite. Schützen Sie den Link-Erstellungsendpunkt mit Authentifizierung, bevor Sie ihn in der Produktion verwenden.
python
shortener.py
import hashlib import hmac import os import secrets import sqlite3 from datetime import datetime, timezone from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from fastapi import FastAPI, HTTPException, Request from fastapi.responses import RedirectResponse from pydantic import BaseModel
DB_PATH = os.environ.get("SHORTENER_DB", "shortener.db") HMAC_SECRET = os.environ.get( "HMAC_SECRET", "replace-this-in-production" ).encode()
ALLOWED_HOST = "www.example.com"
app = FastAPI()
def db(): connection = sqlite3.connect(DB_PATH) connection.row_factory = sqlite3.Row return connection
def init_db(): connection = db() connection.execute(""" CREATE TABLE IF NOT EXISTS links ( slug TEXT PRIMARY KEY, destination TEXT NOT NULL, surface TEXT NOT NULL, topic TEXT, prompt_id TEXT, asset_id TEXT, created_at TEXT NOT NULL ) """) connection.execute(""" CREATE TABLE IF NOT EXISTS clicks ( id INTEGER PRIMARY KEY AUTOINCREMENT, occurred_at TEXT NOT NULL, slug TEXT NOT NULL, surface TEXT, topic TEXT, prompt_id TEXT, referrer_host TEXT, user_agent TEXT, ip_hmac TEXT ) """) connection.commit() connection.close()
def append_query(url, params): parsed = urlsplit(url) query = dict(parse_qsl(parsed.query, keep_blank_values=True)) query.update(params) return urlunsplit(( parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment, ))
def hash_ip(ip): return hmac.new( HMAC_SECRET, (ip or "").encode(), hashlib.sha256 ).hexdigest()[:16]
class LinkRequest(BaseModel): destination_path: str surface: str topic: str | None = None prompt_id: str | None = None asset_id: str | None = None
@app.on_event("startup") def startup(): init_db()
@app.post("/links") def create_link(payload: LinkRequest): destination = f"https://{ALLOWED_HOST}{payload.destination_path}"
if not payload.destination_path.startswith("/"):
raise HTTPException(400, "destination_path must be a site-relative path")
slug = secrets.token_urlsafe(7).replace("_", "-").replace("/", "-")
target = append_query(destination, {
"utm_source": payload.surface,
"utm_medium": "ai_answer",
"utm_campaign": "citation_measurement",
"utm_content": payload.asset_id or "unspecified",
})
connection = db()
connection.execute("""
INSERT INTO links
(slug, destination, surface, topic, prompt_id, asset_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
slug,
target,
payload.surface,
payload.topic,
payload.prompt_id,
payload.asset_id,
datetime.now(timezone.utc).isoformat(),
))
connection.commit()
connection.close()
return {"slug": slug, "url": f"https://go.example.com/{slug}"}
@app.get("/{slug}") def redirect(slug: str, request: Request): connection = db() row = connection.execute( "SELECT * FROM links WHERE slug = ?", (slug,) ).fetchone()
if not row:
connection.close()
raise HTTPException(404, "Unknown link")
referrer = request.headers.get("referer", "")
referrer_host = urlsplit(referrer).hostname or ""
connection.execute("""
INSERT INTO clicks
(occurred_at, slug, surface, topic, prompt_id,
referrer_host, user_agent, ip_hmac)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
datetime.now(timezone.utc).isoformat(),
slug,
row["surface"],
row["topic"],
row["prompt_id"],
referrer_host,
request.headers.get("user-agent", "")[:300],
hash_ip(request.client.host if request.client else ""),
))
connection.commit()
connection.close()
return RedirectResponse(row["destination"], status_code=302)
Führen Sie es aus mit:
bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080
Verwenden Sie eine 302-Weiterleitung für Messlinks, damit Browser und Zwischensysteme die Weiterleitung weniger wahrscheinlich dauerhaft cachen.
7. Geplantes Scraping von AI-Antwortpanels
Das Scraping von Panels ist nützlich, um Fragen zu beantworten, die Erstanbieter-Analysen möglicherweise nicht offenlegen:
- Welche konkurrierenden Quellen werden zitiert?
- Welche Seite wird für einen bestimmten Prompt zitiert?
- Ändert sich die Antwort nach Land oder Gerät?
- Erscheint die Zitation in der Antwort, in der Quellenliste oder in beiden?
- Wie stabil ist die Antwort über die Zeit?
- Zitiert der Assistent einen Kurzlink oder die kanonische URL?
Das Scraping muss jedoch sorgfältig erfolgen.
Regeln für verantwortungsvolle Datenerfassung
- Bevorzugen Sie eine offizielle API, einen lizenzierten Suchdatenanbieter oder einen Erstanbieter-Bericht.
- Überprüfen Sie die Bedingungen des Anbieters, bevor Sie Anfragen automatisieren.
- Beachten Sie gegebenenfalls Robots-Anweisungen.
- Umgehen Sie keine CAPTCHAs, Anmeldesperren, Ratenbegrenzungen oder Bot-Schutzmaßnahmen.
- Verwenden Sie einen kleinen, festen Prompt-Satz.
- Führen Sie mit geringer Frequenz aus.
- Speichern Sie die Antwort und den Zeitstempel, damit die Ergebnisse überprüft werden können.
- Stoppen Sie, wenn ein Anbieter eine Herausforderung oder eine Zugriffsverweigerung zurückgibt.
Das Robots Exclusion Protocol ist ein Präferenzmechanismus für Crawler, kein Sicherheitssystem. Es autorisiert keinen Zugriff auf geschützte Inhalte. RFC 9309: Robots Exclusion Protocol (rfc-editor.org)
Prompt-Manifest erstellen
csv prompt_id,topic,query,locale,surface,target_domain pm_001,project-management,"best project management software for remote teams",en-US,google,example.com pm_001,project-management,"best project management software for remote teams",en-US,bing,example.com pm_002,analytics,"how to measure AI referral traffic",en-US,google,example.com
Halten Sie Prompts stabil. Wenn sich Prompts ändern, werden Trendvergleiche weniger zuverlässig.
Speichern Sie auch:
- Land
- Sprache
- Gerätetyp
- Browserversion
- Prompt-Version
- Inhaltsversion
- Erfassungszeit in UTC
Google weist darauf hin, dass die Antworten von AI Overview und AI-Modus variieren können, da sie unterschiedliche Systeme verwenden und verwandte Suchanfragen oder „Query Fan-out“ nutzen können. Google AI-Funktionen und Website-Dokumentation (developers.google.com)
Playwright Sammlungsskript
Das folgende Skript ist ein allgemeines Forschungstool. Es zeichnet die gerenderte Seite, einen Screenshot, Seitenlinks und den Antworttext auf. Es umgeht absichtlich keine Herausforderungen oder versucht, Zugriffskontrollen zu überwinden.
python
scrape_panels.py
import asyncio import csv import hashlib import json import os import re from datetime import datetime, timezone from pathlib import Path from urllib.parse import quote_plus, urljoin, urlsplit
from playwright.async_api import async_playwright
DATA_DIR = Path("data/panels") DATA_DIR.mkdir(parents=True, exist_ok=True)
SURFACE_URLS = { "google": "https://www.google.com/search?q={query}&hl=en&gl=us", "bing": "https://www.bing.com/search?q={query}&setlang=en-us", }
PANEL_MARKERS = { "google": ["AI Overview", "AI Mode"], "bing": ["Copilot", "Generative search"], }
BLOCK_MARKERS = [ "captcha", "unusual traffic", "verify you are human", "access denied", ]
def clean_text(value): return re.sub(r"\s+", " ", value or "").strip()
def host_matches(url, target_domain): host = (urlsplit(url).hostname or "").lower() target = target_domain.lower().lstrip("www.") return host == target or host.endswith("." + target)
def page_hash(text): normalized = clean_text(text).encode("utf-8") return hashlib.sha256(normalized).hexdigest()
async def collect_one(page, row): surface = row["surface"] query = row["query"] target_domain = row["target_domain"]
search_url = SURFACE_URLS[surface].format(
query=quote_plus(query)
)
await page.goto(
search_url,
wait_until="domcontentloaded",
timeout=45_000,
)
await page.wait_for_timeout(2500)
text = await page.locator("body").inner_text()
lowered = text.lower()
if any(marker in lowered for marker in BLOCK_MARKERS):
return {
**row,
"observed_at": datetime.now(timezone.utc).isoformat(),
"status": "blocked_or_challenged",
"panel_present": False,
"links": [],
}
links = await page.locator("a").evaluate_all("""
anchors => anchors.map(a => ({
href: a.href || "",
text: (a.innerText || "").trim()
})).filter(x => x.href)
""")
panel_present = any(
marker.lower() in lowered
for marker in PANEL_MARKERS.get(surface, [])
)
observed_at = datetime.now(timezone.utc)
run_id = hashlib.sha256(
f"{surface}|{row['prompt_id']}|{observed_at.isoformat()}".encode()
).hexdigest()[:20]
html_path = DATA_DIR / f"{run_id}.html"
screenshot_path = DATA_DIR / f"{run_id}.png"
await page.screenshot(path=str(screenshot_path), full_page=True)
html_path.write_text(await page.content(), encoding="utf-8")
output_links = []
for position, link in enumerate(links, start=1):
output_links.append({
"href": urljoin(search_url, link["href"]),
"text": clean_text(link["text"])[:300],
"position": position,
"target_cited": host_matches(
link["href"],
target_domain
),
})
return {
**row,
"run_id": run_id,
"observed_at": observed_at.isoformat(),
"status": "ok",
"panel_present": panel_present,
"answer_hash": page_hash(text),
"answer_text": clean_text(text)[:50_000],
"html_path": str(html_path),
"screenshot_path": str(screenshot_path),
"link_scope": "page_fallback",
"links": output_links,
}
async def main(manifest_path): rows = [] with open(manifest_path, newline="", encoding="utf-8") as source: rows = list(csv.DictReader(source))
output_path = DATA_DIR / (
datetime.now(timezone.utc).strftime("%Y-%m-%d") + ".jsonl"
)
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context(
locale="en-US",
timezone_id="UTC",
)
page = await context.new_page()
with open(output_path, "w", encoding="utf-8") as output:
for row in rows:
try:
result = await collect_one(page, row)
except Exception as exc:
result = {
**row,
"observed_at": datetime.now(timezone.utc).isoformat(),
"status": f"error:{type(exc).__name__}",
"panel_present": False,
"links": [],
}
output.write(json.dumps(result) + "\n")
output.flush()
# Keep request volume low.
await asyncio.sleep(8)
await browser.close()
if name == "main": asyncio.run(main("prompts.csv"))
Installieren und ausführen:
bash pip install playwright playwright install chromium python scrape_panels.py
Planen Sie es mit einer Sperre, damit sich zwei Ausführungen nicht überschneiden können:
cron
15 3 * * * flock -n /tmp/ai-panel.lock
/usr/bin/python3 /opt/ai-measure/scrape_panels.py
Genauigkeit mit Oberflächenadaptern verbessern
Das Fallback-Skript zeichnet Links auf Seitenebene auf. Das ist nützlich für die Entdeckung, kann aber auch gewöhnliche Suchergebnis-Links enthalten. Für den Produktionseinsatz:
- Verwenden Sie eine lizenzierte Antwortpanel-API, wenn verfügbar.
- Erstellen Sie einen separaten Selektor-Adapter für jede Oberfläche.
- Speichern Sie die Selektor-Version.
- Speichern Sie rohes HTML und einen Screenshot.
- Markieren Sie jeden Datensatz mit
link_scope = "panel_selector"oderlink_scope = "page_fallback".
Berechnen Sie den Zitationsanteil nur aus panel_selector-Daten, wenn Sie das Panel zuverlässig identifizieren können. Halten Sie Fallback-Beobachtungen in einem separaten explorativen Bericht.
8. Google Analytics und BigQuery Integration
Google Analytics 4 kann Rohereignisdaten nach BigQuery exportieren. Tägliche Exporte bieten stabile Tabellen wie events_JJJJMMTT; Streaming-Exporte sind nützlich für die nahezu Echtzeit-Überwachung, können aber unvollständig sein, bis die tägliche Tabelle finalisiert ist. Google Analytics BigQuery-Export (support.google.com)
Eine vereinfachte Sitzungsabfrage könnte so aussehen:
sql WITH page_events AS ( SELECT event_date, user_pseudo_id,
(
SELECT value.int_value
FROM UNNEST(event_params)
WHERE key = 'ga_session_id'
) AS session_id,
(
SELECT value.string_value
FROM UNNEST(event_params)
WHERE key = 'page_location'
) AS page_location,
(
SELECT value.string_value
FROM UNNEST(event_params)
WHERE key = 'session_engaged'
) AS session_engaged,
event_name
FROM PROJECT.analytics_PROPERTY.events_*
WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260921'
),
session_rollup AS ( SELECT event_date, user_pseudo_id, session_id,
MAX(
CASE
WHEN REGEXP_CONTAINS(
LOWER(page_location),
r'[?&]utm_medium=ai_answer'
)
THEN 1 ELSE 0
END
) AS ai_session,
MAX(
CASE
WHEN session_engaged = '1'
THEN 1 ELSE 0
END
) AS engaged_session,
MAX(
CASE
WHEN event_name IN ('purchase', 'generate_lead', 'sign_up')
THEN 1 ELSE 0
END
) AS key_event
FROM page_events GROUP BY event_date, user_pseudo_id, session_id )
SELECT event_date, COUNTIF(ai_session = 1) AS ai_sessions, COUNTIF(ai_session = 1 AND engaged_session = 1) AS engaged_ai_sessions, COUNTIF(ai_session = 1 AND key_event = 1) AS ai_key_events, SAFE_DIVIDE( COUNTIF(ai_session = 1 AND engaged_session = 1), COUNTIF(ai_session = 1) ) AS post_citation_engagement_rate FROM session_rollup GROUP BY event_date ORDER BY event_date;
Für eine ausgereifte Implementierung fügen Sie surface, topic und prompt_id zur Kampagnenstruktur hinzu oder verknüpfen Sie sie aus der Kurzlink-Klick-Tabelle.
Erstellen Sie keine hochkardinalen Analysedimensionen für jeden einzelnen Benutzer oder jede Sitzung. Google warnt, dass übermäßige hochkardinale benutzerdefinierte Dimensionen die Berichtsqualität beeinträchtigen können. Google Analytics benutzerdefinierte Dimensionen und Metriken (support.google.com)
9. Dashboard-Design
Ein praktisches Dashboard sollte fünf Ansichten enthalten.
Management Summary
Anzeige:
- Zitationsanteil nach Thema
- Zitationsabdeckung nach Thema
- Bestätigte Assistenten-Verweisrate
- Erweiterte Assistenten-Verweisrate
- Engagement nach Zitation
- Erfolgsrate des KI-Crawlers
- KI-zugeordnete Schlüsselereignisse oder Einnahmen
Trend des Zitationsanteils
Liniendiagramm:
text Datum × Thema × Oberfläche
Zeigen Sie beides:
- Vorkommen von Zitationen der Zieldomäne
- Alle beobachteten Zitationsvorkommen
Traffic-Trichter der Assistenten
text Beobachtete Zitationen ↓ Kontrollierte Link-Klicks ↓ KI-zugeordnete Sitzungen ↓ Engagierte KI-Sitzungen ↓ Schlüsselereignisse
Jede Stufe sollte ihr Attributionskonfidenz anzeigen.
Crawler-Zustand
Tabelle nach Bot:
text bot_name requests successful_requests error_rate unique_ip_count top_paths
Alarmieren Sie, wenn:
- die Fehlerrate 2 Prozent überschreitet
- ein Bot plötzlich verschwindet
- ein zuvor zugänglicher Inhaltsbereich 403 oder 429 zurückgibt
- Crawler-Anfragen sich auf einen unbeabsichtigten privaten Pfad konzentrieren
Content-Chancen-Tabelle
Seiten und Themen nach Rang ordnen durch:
text hoher Zitationsanteil + niedrige Verweisrate hohe Impressionen + geringe Zitationsabdeckung hohes Engagement + geringe Zitationssichtbarkeit hohe Crawler-Fehler + hohe geschäftliche Bedeutung
Diese Kombinationen führen in der Regel zu besseren Entscheidungen, als nur das Traffic-Volumen zu betrachten.
Minimales Streamlit-Dashboard
Speichern Sie als dashboard.py.
python import glob import json from pathlib import Path
import pandas as pd import streamlit as st
st.set_page_config( page_title="AI Citation Impact", layout="wide", )
@st.cache_data(ttl=300) def load_panel_rows(): rows = []
for filename in glob.glob("data/panels/*.jsonl"):
with open(filename, encoding="utf-8") as source:
for line in source:
record = json.loads(line)
run_id = record.get("run_id")
for link in record.get("links", []):
hostname = link.get("href", "").split("/")[2]
rows.append({
"run_id": run_id,
"observed_at": record.get("observed_at"),
"surface": record.get("surface"),
"topic": record.get("topic"),
"prompt_id": record.get("prompt_id"),
"panel_present": record.get("panel_present"),
"target_domain": record.get("target_domain"),
"cited_domain": hostname,
"target_cited": link.get("target_cited", False),
"position": link.get("position"),
"link_scope": record.get("link_scope"),
})
return pd.DataFrame(rows)
panels = load_panel_rows()
st.title("Dashboard für KI-Zitationsauswirkungen")
if panels.empty: st.warning("Keine Panel-Beobachtungen gefunden.") st.stop()
panels["date"] = pd.to_datetime( panels["observed_at"], errors="coerce" ).dt.date
citation_share = ( panels.groupby("topic") .apply( lambda group: group["target_cited"].sum() / max(len(group), 1) ) .reset_index(name="citation_share") )
coverage = ( panels.groupby(["topic", "run_id"], dropna=False) .agg( panel_present=("panel_present", "max"), target_cited=("target_cited", "max"), ) .reset_index() )
coverage = ( coverage[coverage["panel_present"] == True] .groupby("topic") .apply(lambda group: group["target_cited"].mean()) .reset_index(name="citation_coverage") )
col1, col2 = st.columns(2)
with col1: st.subheader("Zitationsanteil nach Thema") st.dataframe(citation_share, use_container_width=True) st.bar_chart( citation_share.set_index("topic")["citation_share"] )
with col2: st.subheader("Zitationsabdeckung nach Thema") st.dataframe(coverage, use_container_width=True) st.bar_chart( coverage.set_index("topic")["citation_coverage"] )
st.subheader("Zitationsposition") st.dataframe( panels.groupby(["topic", "surface"], dropna=False) .agg( citations=("target_cited", "sum"), median_position=("position", "median"), observations=("run_id", "nunique"), ) .reset_index(), use_container_width=True, )
st.subheader("Tägliche Beobachtungen") daily = ( panels.groupby(["date", "surface"], dropna=False) .agg( citations=("target_cited", "sum"), observed_links=("cited_domain", "count"), ) .reset_index() )
st.line_chart( daily.pivot( index="date", columns="surface", values="citations" ).fillna(0) )
Führen Sie es aus mit:
bash pip install streamlit pandas streamlit run dashboard.py
Für die Produktion laden Sie normalisierte Tabellen in DuckDB, BigQuery, Snowflake oder PostgreSQL, anstatt Rohdateien direkt zu lesen.
10. Qualitätskontrollen
Ein Messsystem kann präzise aussehende, aber irreführende Zahlen produzieren. Fügen Sie diese Kontrollen hinzu.
Bots von Menschen trennen
Zählen Sie niemals OAI-SearchBot, GPTBot, ClaudeBot oder PerplexityBot als menschliche Sitzungen. Dies sind Crawler- oder Abrufsignale.
Rohdaten aufbewahren
Bewahren Sie für jeden Panel-Durchlauf auf:
- rohes HTML
- Screenshot
- Antworttext
- Zeitstempel
- Browser- und Collector-Version
- Prompt und Locale
- Antwort-Hash
Dies ermöglicht es, zu erklären, warum sich eine Metrik geändert hat.
Panel-Abwesenheit verfolgen
Ein fehlendes KI-Panel ist nicht dasselbe wie eine Antwort ohne Ihre Zitation.
Verwenden Sie:
text panel_nicht_vorhanden panel_vorhanden_ohne_ziel panel_vorhanden_ziel_zitiert blockiert_oder_herausgefordert sammlungsfehler
Stichprobenverzerrung kontrollieren
Ein geplanter Scraping-Vorgang wird beeinflusst durch:
- Standort
- Gerät
- Sprache
- Zeit
- Personalisierung
- Browser-Zustand
- Prompt-Formulierung
Verwenden Sie einen festen Kernsatz von Prompts und einen rotierenden Satz von explorativen Prompts. Halten Sie die beiden Gruppen in Berichten getrennt.
Falsche Präzision vermeiden
Berichten Sie Konfidenzintervalle oder minimale Stichprobengrößen für kleine Themengruppen. Ein Thema mit drei Beobachtungen sollte nicht direkt mit einem Thema mit 300 Beobachtungen verglichen werden.
Gleiches mit Gleichem vergleichen
Beim Vergleich von Oberflächen halten Sie konstant:
- Prompt
- Locale
- Land
- Gerät
- Sammelstunde
- Zieldomänen-Regeln
- Zitations-Parser-Version
11. Empfohlener Rollout-Plan
Woche 1: Instrumentierung
- Strukturierte Server-Logs konfigurieren.
- Das Bot-Register erstellen.
- Die Shortener-Domain erstellen.
- Kampagnen-Benennungsregeln hinzufügen.
- Die Themataxonomie definieren.
- Google Analytics 4 BigQuery-Export aktivieren.
Woche 2: Kontrollierte Links
- Fünf bis zehn getrackte Zusammenfassungen veröffentlichen.
- Links für verschiedene Themen und Inhaltsversionen erstellen.
- Weiterleitungs-Logs mit Analytics-Sitzungen validieren.
- Bestätigen, dass keine persönlichen Informationen in URLs oder Analyseparameter gelangen.
Woche 3: Panel-Beobachtungen
- Ein Prompt-Manifest erstellen.
- Beginnen Sie mit zwei Oberflächen und 20 bis 50 stabilen Prompts.
- Einmal täglich ausführen.
- Screenshots und HTML speichern.
- Sammelfehler getrennt von fehlenden Zitationen messen.
Woche 4: Dashboard und Governance
- Themen- und Oberflächen-Dashboards erstellen.
- Crawler-Zustandsalarme hinzufügen.
- Konfidenzregeln definieren.
- Erstanbieter-Berichte mit Panel-Beobachtungen vergleichen.
- Richtlinien für Datenaufbewahrung, -zugriff und -löschung dokumentieren.
Fazit
Die Messung von KI-Zitationen sollte als Multi-Signal-Attributionssystem aufgebaut werden, nicht als einzelner Verweisbericht.
Das zuverlässigste Framework kombiniert:
- Google- und Bing-Erstanbieter-Sichtbarkeitsberichte
- Server-Logs für Crawler- und Abrufaktivität
- Erstanbieter-Kurzlinks für die kontrollierte Klickmessung
- geplante, konforme Beobachtungen von Antwortpanels
- Analyse- und Konversionsdaten für den nachgelagerten Wert
Die drei wichtigsten Metriken sind:
- Zitationsanteil nach Thema — wie viel des beobachteten Quellenraums zu Ihrer Domain gehört.
- Assistenten-Verweisrate — wie viel menschlicher Traffic KI-Oberflächen zugeschrieben werden kann.
- Engagement nach Zitation — ob von KI verwiesene Besucher sinnvolle Aktionen durchführen.
Das System wird nützlich, wenn jede Zahl ihre Oberfläche, den Zeitraum, den Nenner, die Attributionsmethode und das Konfidenzniveau enthält. Diese Disziplin verhindert, dass Crawler-Zugriffe mit Nutzern, gescrapte Antworten mit Impressionen und Zitationen mit Geschäftsergebnissen verwechselt werden.
Auto