AI可见性样本大小:一个数字实际上需要多少次运行
Senior Web Scraping Engineer
TL;DR:
- 一次捕获的 AI 回答仅显示了 49.6% 的引用域,而相同提示的五次捕获显示了更多,测量是在五个答案引擎、四个提示和在每个输入保持不变的情况下进行五次重复的 100 捕获矩阵中。
- 引用域集在重复之间的重叠的均值成对 Jaccard 为 0.384,但单个引擎与提示单元的范围为 0.114 到 0.956 — 没有单一的稳定性数字可以涵盖“AI 可见性”。
- 答案文本中的品牌提及是最不可靠的层次:在 137 个品牌与单元的观察中,44.5% 出现在所有五次重复中,24.8% 则仅出现在一次。
- 集合成员资格问题收敛很快 — 三次重复恢复了 83.1% 的引用域,而五次重复找到了这些引用域 — 因此,引用审核需要几次运行,而不是每天的四分之一捕获。
- 比率问题收敛缓慢:在观察的 40% 出现率下,五次运行给出了一个 95% 区间 65 个点宽,而达到 ±10 个点则大约需要每个引擎每个提示 100 次运行。
- 整个矩阵运行在 4 分钟 9 秒 内,所以这些数字仅测量模型级别的非确定性;一个跨天采样的程序应该预计至少有这么多的波动,而不是更少。
- 在 Scrapeless 免费计划上对您自己的提示集运行相同的矩阵,获取您自己的数字,而不是借用这个数字。
每个 AI 可见性仪表板都会报告一个数字。引用份额、声音份额、推荐排名、情感 — 每个都有一个干净的数字和一个趋势箭头在旁边。没有一个附带误差条。
这一省略在这里比在经典搜索中更重要,因为被测量的表面在每次请求时会重新生成其答案。研究人员将五个模型固定在名义上确定的设置下执行八个任务,仍然记录到 运行间高达 15% 的准确度波动,最佳与最差可能运行之间的差距达到了 70%。一个不会在被告知时重复自己的模型,也不会为您提供从一次捕获中获得的可重复可见性数字。
本文的其余部分反复向相同的引擎发送相同的提示,保持每次输入不变,并测量答案实际移动的距离。测量到的移动随后转化为监控程序可以采取的唯一输出 — 运行次数。
快速查看管道
该管道有四个阶段,最后一阶段是可交付成果:
捕获重复矩阵 → 将每个答案简化为可比较的层次 → 评分每个单元的稳定性 → 将稳定性转换为所需的样本量
第一阶段每个引擎多次触发相同请求并存储每个原始响应。第二阶段将每个响应简化为可在重复之间比较的三件事:引用的域集、答案文本,以及文本中提及的监控名单名称及其顺序。第三阶段测量这三层之间的移动程度。第四阶段则将测量到的移动结果转化为报告团队实际关心的问题,即一项声明需要多少次捕获作为支持。
这位于其依赖的捕获管道之上。如果您还没有建立捕获方面,六个 AI 答案引擎的引用分享管道是基础;本文测量该管道生成的系列在您运行的样本量下是否可读。
此测量所回答的问题
四个决定依赖于答案而变化,而这四个决定今天都在没有答案的情况下做出。
周比周的变化是否真实。 一个从 40% 降到 25%的引用份额数字看起来像是一个回归。在每个单元进行五次运行时,这两个数字都在同一置信区间内,因此下降与引擎重新生成答案是 indistinguishable 的。
一个程序需要多少捕获预算。 样本量的设置是有成本的。一个需要比率估计的团队和一个需要引用库存的团队在要求上差异超过一个数量级,而为两者购买较大的预算会浪费大部分预算。
围绕哪个指标建立目标。 一些 AI 答案的层面足够稳定,可以设定目标。另一些则不然,并且在不稳定的层面上建立 OKR 会产生四分之一的噪音被解释为进展。
权重赋予哪个引擎。 每个引擎的稳定性在该矩阵中变化大约有两倍的因素。一个引用几乎不变的引擎可以从少量捕获中得到可用的读数;一个频繁重排的引擎则需要几倍的采样才能获得相同的置信度。
前面的样本设计
该矩阵包含五个引擎、四个提示、五次重复:100 次捕获,其中 20 个引擎与提示单元各持有五次重复。
| 维度 | 值 |
|---|---|
| 引擎 | Perplexity, Grok, Gemini, Google AI Mode, ChatGPT |
| 提示 | 一个产品类别中的4个类别级问题 |
| 每个单元的重复次数 | 5 |
| 总捕获量 | 100 |
| 国家 | 每次调用固定为 US |
| Grok推理模式 | 每次调用固定为 MODEL_MODE_FAST |
| 捕获窗口 | 4分钟9秒,单次会话 |
| 独特输入指纹 | 20 — 每个单元一个,由其5个重复共享 |
两个设计选择决定了结果。第一个是每个输入都是固定的:相同的提示字符串、相同的国家、相同的推理模式、相同的网络搜索标志。改变其中任何一个都将混合输入的差异,使测量毫无意义。第一阶段的指纹数量存在是为了证明固定保持有效。
第二个是压缩窗口。所有100个捕获都在四分钟内完成。这是经过深思熟虑的,也是整个过程的最严格限制——在引用任何数据之前,请查看限制部分。
前提条件
- Python 3.10或更新版本,仅限标准库。
- 导出为
SCRAPELESS_API_KEY的Scrapeless API密钥。通用抓取API下的LLM聊天抓取器将所有五个引擎放在一个端点和一个响应信封后面,这使得跨引擎矩阵成为可能。 - 一个
watchlist.txt文件,每行一个品牌名称——您自己的品牌和您期望在该类别中竞争的名称。如果列表包含敏感信息,请将其保密。 - 大约100次演员调用的预算进行初步尝试。基于使用量的定价在Scrapeless定价页面上。
bash
export SCRAPELESS_API_KEY="your_scrapeless_api_key"
mkdir -p ai-visibility-variance && cd ai-visibility-variance
printf 'YourBrand\nRivalOne\nRivalTwo\n' > watchlist.txt
阶段1 — 捕获重复矩阵
阶段1触发每个单元的五个重复,并将每个原始响应未加修改地写入磁盘。存储完整的信封而不是解析的摘要非常重要,因为您想要比较的层在您查看散布之前并不是显而易见的。
每个引擎接受相同的prompt和country;仅所需的额外参数不同,并且每个额外参数都是固定的,以便在重复之间不会漂移。所有单元同时运行,因为对100个捕获的串行传递将延长窗口,并使现实世界的漂移渗透到原本应排除实际漂移的测量中。
python
# capture.py — capture a fixed engine x prompt x repeat matrix
import hashlib
import json
import os
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor
API = "https://api.scrapeless.com/api/v2/scraper/execute"
KEY = os.environ["SCRAPELESS_API_KEY"]
OUT = "runs"
REPEATS = 5
PROMPTS = {
"P1": "What are the best web scraping APIs for developers in 2026?",
"P2": "Which services provide cloud browsers for automated data collection?",
"P3": "What tools do developers use to collect Google search results programmatically?",
"P4": "Recommend a proxy provider for large-scale public web data collection.",
}
# Same prompt and country everywhere. Only actor-required extras differ, and
# every one of them is pinned: an input that moves between repeats would be
# measured as engine variance.
ENGINES = {
"perplexity": ("scraper.perplexity", {}),
"grok": ("scraper.grok", {"mode": "MODEL_MODE_FAST"}),
"gemini": ("scraper.gemini", {}),
"aimode": ("scraper.aimode", {}),
"chatgpt": ("scraper.chatgpt", {"web_search": True, "shopping": False}),
}
def execute(actor, payload, timeout=300):
request = urllib.request.Request(
API,
data=json.dumps({"actor": actor, "input": payload}).encode(),
headers={"Content-Type": "application/json", "x-api-token": KEY},
method="POST",
)
started = time.time()
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode()), round(time.time() - started, 1)
def capture(job):
engine, prompt_id, repeat = job
actor, extra = ENGINES[engine]
payload = {"prompt": PROMPTS[prompt_id], "country": "US", **extra}
# Hash the actor with the payload. Three of these engines take an identical
# payload, so a payload-only hash would collapse them into one fingerprint
# and stop proving that each cell repeated the same request.
fingerprint = hashlib.sha256(
json.dumps({"actor": actor, **payload}, sort_keys=True).encode()).hexdigest()[:16]
try:
body, elapsed = execute(actor, payload)
except Exception as exc:
# A cell that returns nothing is logged as missing, never dropped.
# A silent hole would bias every statistic computed downstream.
return {"ok": False, "note": f"{engine} {prompt_id} r{repeat}: {type(exc).__name__}"}
with open(f"{OUT}/{engine}__{prompt_id}__r{repeat}.json", "w") as handle:
json.dump({
"engine": engine, "prompt_id": prompt_id, "repeat": repeat,
"elapsed_s": elapsed, "input_fingerprint": fingerprint,
"captured_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"response": body,
}, handle)
return {"ok": True, "fingerprint": fingerprint}
os.makedirs(OUT, exist_ok=True)
jobs = [(e, p, r) for e in ENGINES for p in PROMPTS for r in range(1, REPEATS + 1)]
print(f"{len(jobs)} captures = {len(ENGINES)} engines"
f" x {len(PROMPTS)} prompts x {REPEATS} repeats")
with ThreadPoolExecutor(max_workers=12) as pool:
results = list(pool.map(capture, jobs))
stored = [r for r in results if r["ok"]]
print(f"stored {len(stored)} captures in {OUT}/")
print(f"missing cells: {[r['note'] for r in results if not r['ok']] or 'none'}")
print(f"{len({r['fingerprint'] for r in stored})} distinct input fingerprints "
f"across {len(stored)} captures")
指纹行是诚信检查。在100个捕获中有20个不同的指纹意味着每个单元的五个重复请求是字节相同的,因此任何在响应中不同的内容都来自于引擎。
阶段2 — 将每个捕获简化为三个可比层
阶段2将五个不同的响应信封压平为一个具有三个测量层的行形状,因为“AI可见性”不是一个量,将其视为一个量是错误开始的地方。
这三个层回答不同的问题,正如结果所示,需要不同的样本量:
| 层 | 它包含的内容 | 它提供的度量家族 |
|---|---|---|
| L1 引用 | 引擎引用的可注册域的集合 | 引用份额,引用域差距 |
| L2 文章 | 答复文本,作为字符和作为标记集 | 情感,摘要,文本差异 |
| L3 品牌 | 文本提到的监视名单名称,按首次提及顺序 | 声音份额,推荐排名 |
每个引擎根据其自己的密钥报告其来源——Perplexity使用web_results,Grok根据引用ID使用footnotes,Gemini和AI模式均使用citations,ChatGPT的搜索面板以search_result到达。归一化到可注册主机消除了深度文章URL和主页链接之间的差异,即引文份额度量实际上计算的内容。
python
# layers.py — reduce every capture to three comparable measurement layers
import json
import os
import re
from urllib.parse import urlparse
WATCHLIST = [line.strip() for line in open("watchlist.txt") if line.strip()]
CITATIONS = {
"perplexity": lambda r: [c.get("url") for c in r.get("web_results") or []],
"grok": lambda r: [c.get("url") for c in (r.get("footnotes") or {}).values()],
"gemini": lambda r: [c.get("url") for c in r.get("citations") or []],
"aimode": lambda r: [c.get("url") for c in r.get("citations") or []],
"chatgpt": lambda r: [c.get("url") for c in r.get("search_result") or []],
}
ANSWER_KEY = {
"perplexity": "result_text", "grok": "full_response", "gemini": "result_text",
"aimode": "result_text", "chatgpt": "result_text",
}
def registrable(url):
netloc = urlparse(url).netloc.lower()
return netloc[4:] if netloc.startswith("www.") else netloc
def mentions(text):
"""Watchlist names present in the answer, keyed by first-mention offset."""
lowered = text.lower()
found = {}
for name in WATCHLIST:
hit = re.search(rf"(?<![a-z0-9]){re.escape(name.lower())}(?![a-z0-9])", lowered)
if hit:
found[name] = hit.start()
return found
rows = []
for filename in sorted(os.listdir("runs")):
record = json.load(open(f"runs/{filename}"))
result = record["response"].get("task_result") or {}
engine = record["engine"]
answer = result.get(ANSWER_KEY[engine]) or ""
found = mentions(answer)
rows.append({
"engine": engine,
"prompt_id": record["prompt_id"],
"repeat": record["repeat"],
"cited_hosts": sorted({
host for url in CITATIONS[engine](result)
if url and (host := registrable(url))
}),
"answer_chars": len(answer),
"answer_tokens": sorted(set(re.findall(r"[a-z0-9]+", answer.lower()))),
# Ordered by first mention, so list position survives into stage 3.
"brands_present": sorted(found, key=found.get),
})
with open("layers.jsonl", "w") as handle:
for row in rows:
handle.write(json.dumps(row) + "\n")
cited = sum(len(r["cited_hosts"]) for r in rows)
print(f"reduced {len(rows)} captures -> layers.jsonl")
print(f"L1 citations: {cited} cited hosts, {cited / len(rows):.1f} per capture")
print(f"L2 prose: {sum(r['answer_chars'] for r in rows) / len(rows):.0f} chars per answer")
print(f"L3 brands: {sum(len(r['brands_present']) for r in rows)} watchlist hits total")
阶段3 — 按引擎和提示评分稳定性
阶段3测量每个单元内的运动,它按引擎报告而不是汇总,因为汇总的数字隐藏了您最需要了解的内容。
三个统计数据完成了这项工作。对于引用和文章层,在五个重复之间的平均成对Jaccard指数——交集的大小与并集的大小之比,平均计算单元中的所有十对。对于答案长度,变异系数,表示测量散布的标准差作为均值的百分比,以便不同长度的答案保持可比性。对于品牌,所有五个重复中出现的名称的份额,而不是其中的一些。
python
# stability.py — how far each layer moves inside a cell
import json
import statistics as stats
from collections import defaultdict
from itertools import combinations
cells = defaultdict(dict)
for line in open("layers.jsonl"):
row = json.loads(line)
cells[(row["engine"], row["prompt_id"])][row["repeat"]] = row
def jaccard(left, right):
union = left | right
return len(left & right) / len(union) if union else None
def mean_pairwise(sets):
scores = [j for a, b in combinations(sets, 2) if (j := jaccard(a, b)) is not None]
return stats.mean(scores) if scores else None
per_engine = defaultdict(lambda: defaultdict(list))
brands = defaultdict(lambda: {"stable": 0, "flipping": 0})
for (engine, _prompt), reps in sorted(cells.items()):
keys = sorted(reps)
hosts = [set(reps[k]["cited_hosts"]) for k in keys]
tokens = [set(reps[k]["answer_tokens"]) for k in keys]
lengths = [reps[k]["answer_chars"] for k in keys]
per_engine[engine]["cite"].append(mean_pairwise(hosts))
per_engine[engine]["prose"].append(mean_pairwise(tokens))
per_engine[engine]["cv"].append(stats.pstdev(lengths) / stats.mean(lengths) * 100)
per_engine[engine]["union"].append(len(set().union(*hosts)))
per_engine[engine]["core"].append(len(set.intersection(*hosts)))
for brand in set().union(*[set(reps[k]["brands_present"]) for k in keys]):
seen = sum(1 for k in keys if brand in reps[k]["brands_present"])
brands[engine]["stable" if seen == len(keys) else "flipping"] += 1
header = f"{'engine':<12}{'citeJaccard':>12}{'proseJaccard':>13}{'lengthCV%':>11}"
print(header + f"{'hostsEveryRun':>15}{'brandsStable%':>15}")
for engine, series in per_engine.items():
core, union = sum(series["core"]), sum(series["union"])
counts = brands[engine]
stable_pct = counts["stable"] / (counts["stable"] + counts["flipping"]) * 100
print(f"{engine:<12}{stats.mean(series['cite']):>12.3f}"
f"{stats.mean(series['prose']):>13.3f}{stats.mean(series['cv']):>11.1f}"
f"{f'{core}/{union}':>15}{stable_pct:>14.1f}%")
阶段4 — 将稳定性转换为样本大小
阶段4将传播转化为运行计数,并且这样做两次,因为这两种问题类型的行为完全不同。
对于一个集合成员问题——这个引擎为我的类别引用了哪些领域——有用的输出是一条覆盖曲线:五次运行出现的领域中,单次、两次或三次运行出现的领域占比是多少。这直接通过对每种重复组合进行子采样从捕获中计算得出。
对于一个比例问题——有多少回答提到我的品牌——输出是一个比例的置信区间。Wilson得分区间在这些样本大小下是合适的工具:NIST的参考实现使其成为比例置信限的默认方法,对样本大小从1到1,000的系统比较发现Wilson区间的表现超过了教科书的Wald区间,特别是在小n以及比例接近0或1的情况下。这两个条件正好描述了品牌提及数据。
python
# samplesize.py — measured stability -> a required number of runs
import json
import math
import statistics as stats
from collections import Counter, defaultdict
from itertools import combinations
cells = defaultdict(dict)
for line in open("layers.jsonl"):
row = json.loads(line)
cells[(row["engine"], row["prompt_id"])][row["repeat"]] = row
def wilson(hits, runs, z=1.96):
proportion = hits / runs
denominator = 1 + z * z / runs
centre = (proportion + z * z / (2 * runs)) / denominator
half = z * math.sqrt(
proportion * (1 - proportion) / runs + z * z / (4 * runs * runs)) / denominator
return max(0.0, centre - half) * 100, min(1.0, centre + half) * 100
coverage = defaultdict(list)
for reps in cells.values():
keys = sorted(reps)
universe = set().union(*[set(reps[k]["cited_hosts"]) for k in keys])
if not universe:
continue
for n in range(1, len(keys) + 1):
coverage[n].append(stats.mean([
len(set().union(*[set(reps[k]["cited_hosts"]) for k in combo])) / len(universe)
for combo in combinations(keys, n)
]))
print("SET QUESTION — share of the 5-run cited-domain union that n runs surface")
for n in sorted(coverage):
print(f" n={n}: {stats.mean(coverage[n]) * 100:5.1f}%")
observed = Counter()
for reps in cells.values():
keys = sorted(reps)
for brand in set().union(*[set(reps[k]["brands_present"]) for k in keys]):
observed[sum(1 for k in keys if brand in reps[k]["brands_present"])] += 1
total = sum(observed.values())
print(f"\nRATE QUESTION — {total} brand-by-cell observations at n=5")
for seen in sorted(observed):
low, high = wilson(seen, 5)
print(f" seen {seen}/5 in {observed[seen]:>3} cases ({observed[seen] / total * 100:4.1f}%)"
f" 95% interval {low:5.1f}%-{high:5.1f}% width {high - low:4.1f}pt")
print("\nRATE QUESTION — interval width at a 40% appearance rate, by run count")
for n in (1, 5, 10, 20, 30, 50, 100, 200):
low, high = wilson(round(0.4 * n), n)
print(f" n={n:<4} {low:5.1f}%-{high:5.1f}% width {high - low:4.1f}pt")
矩阵实际显示的内容
一切都在变化,且引擎移动的幅度并不相同。
| 引擎 | 引用领域Jaccard | 文章标记Jaccard | 回答长度CV | 每次运行引用的主机 | 品牌提及稳定性 |
|---|---|---|---|---|---|
| Perplexity | 0.534 | 0.446 | 28.7% | 34中有8 | 47.6% |
| Gemini | 0.511 | 0.361 | 16.3% | 29中有6 | 40.6% |
| Grok | 0.366 | 0.392 | 10.2% | 61中有6 | 45.5% |
| Google AI Mode | 0.282 | 0.333 | 15.9% | 182中有11 | 50.0% |
| ChatGPT | 0.228 | 0.342 | 9.4% | 80中有1 | 40.0% |
在所有20个单元中,引用领域的平均成对Jaccard为0.384。单元级的传播是更有用的数字:低端为0.114,高端为0.956。取自一个引擎在一个提示下的稳定性声明可以落在这个范围的任何地方,这正是为什么对单个引擎进行三次运行探测是一个引导而不是发现。
覆盖曲线是最值得记录的数字:
| 运行次数 | 五次运行引用领域并集的占比 |
|---|---|
| 1 | 49.6% |
| 2 | 70.3% |
| 3 | 83.1% |
| 4 | 92.3% |
| 5 | 100% |
一次捕获向你展示大约一半的领域,而五次捕获向你展示的领域。每个引擎在n=1时,范围从Perplexity的73.6%到ChatGPT的35.1%,因此在最不稳定的引擎上进行单次捕获引用审计大约缺少了其应当清点的三分之二的内容。曲线也迅速平坦化:第三次运行增加了12.8点,第四次增加了9.2。对于引用清单而言,三到四次重复购买到大部分可用的内容。
Google AI Mode产生了迄今为止最广泛的领域并集——在四个提示中有182个不同的主机,只有11个出现在他们单元的五次重复中。谷歌自己的指导解释了这一机制:AI模式和AI概述都可以使用查询分发技术,发出多个相关的子主题搜索,因此可以显示出比经典搜索更广泛和多样化的链接集。当一个引擎被构建为在子主题中传播其引用时,一次捕获仅采样了该传播的一部分。
比例层讲述了相反的故事。在137个品牌按单元观察中,只有**44.5%**出现在所有五次重复中。**24.8%**只出现在五次中的一次——一个名字,一个单次捕获要么报告为明确存在,要么报告为明确缺失,完全取决于你选择了哪个捕获。
| 出现次数 | 案例 | 占比 | 真正比例的95%区间 | 宽度 |
|---|---|---|---|---|
| 5次中1次 | 34 | 24.8% | 3.6% – 62.4% | 58.8点 |
| 5次中2次 | 16 | 11.7% | 11.8% – 76.9% | 65.2点 |
| 5次中3次 | 14 | 10.2% | 23.1% – 88.2% | 65.2点 |
| 5次中4次 | 12 | 8.8% | 37.6% – 96.4% | 58.8点 |
| 5次中5次 | 61 | 44.5% | 56.6% – 100% | 43.4点 |
一个在五次运行中出现两次的品牌,其真实出现率介于12%和77%之间,这个范围太宽,无法支持利益相关者将会采取行动的任何陈述。扩大样本会慢慢缩小这个范围:
| 每个单元的运行次数 | 在观察到40%的比例下的95%区间 | 宽度 |
|---|---|---|
| 1 | 0.0% – 79.3% | 79.3点 |
| 5 | 11.8% – 76.9% | 65.2点 |
| 10 | 16.8% – 68.7% | 51.9点 |
| 20 | 21.9% – 61.3% | 39.5点 |
| 30 | 24.6% – 57.7% | 33.1点 |
| 50 | 27.6% – 53.8% | 26.2点 |
| 100 | 30.9% – 49.8% | 18.9点 |
| 200 | 33.5% – 46.9% | 13.5点 |
| 因此,“多少次运行”的答案是:这取决于您报告的层次,两个答案相差超过一个数量级。 引文清单在三到四次重复中可读。您打算每周进行比较的速率需要约100次捕获每个引擎每个提示,然后才能将间隔收紧到±10点。两到三周的每日捕获大约达到14到21次——对于设定问题来说足够舒适,而对于速率问题约短了五倍。 |
有两个较小的结果值得记录。排名的变化不如提及的变化大:在多个重复中出现的名称中,首次提及的位置在ChatGPT上平均移动了0.89个位置,在Grok上移动了1.95个位置,因此排名跟踪指标继承了提及的不稳定性,但仅添加了一点自己的不稳定。还有一个单元——云浏览器提示上的单个引擎——在其任意五次重复中没有提到任何监视列表品牌,而是根据能力类别作出回答。持续的缺失也是一个稳定的读数,而将空结果视为失败捕获的监视器将丢失真实信号。
测量的分布与已发布的研究一致,而不是相互矛盾。一项方差成分研究对12,933个大型语言模型(LLM)品牌响应进行了分解,涵盖20个品牌、8种语言和3个模型,发现仅重采样就占总方差的34.8%,单个响应的可靠性接近0.01。该研究使用了不同的语料库和不同的统计方法,仍然得出了相同的结论:一个答案几乎没有品牌区分信号。
如果您已经在运行每个引擎的品牌情感管道或AI推荐排名跟踪器,这两者都读取了散文层,因此都继承了缓慢收敛的样本大小,而不是快速的。
获取您自己的此表版本大约需要一百个调用。从Scrapeless免费计划开始,在您设定这些指标的目标之前,针对您自己的提示运行矩阵。
该测量无法告诉您的事项
压缩的捕获窗口是限制上述每个数字的因素。所有100次捕获均在单日内的4分钟9秒内完成。这清晰地隔离了一个运动来源——模型重新生成其答案——并完全排除了其他几个来源:索引更新、新闻周期、您发布的内容、模型版本更改,以及引擎检索中日常的真实漂移。
结果仅在一个方向上运行。**这些数字是下限,而不是总方差的估计。**一个在数周内采样相同提示的程序暴露于模型级的非确定性加上四分钟窗口保持不变的所有因素。因此,实际监控程序所需的样本大小至少应达到这个水平,而四分钟数字与四周数字之间的差距本身也是这里没有人进行的测量。
有四个更窄的限制适用:
- 二十个单元是一个小设计。 每个引擎的数据是四个提示的平均值,因此引擎的数字并不精确地代表该引擎。单元级的范围从0.114到0.956是一个诚实的总结,说明单个单元的差异程度。
- 一个类别,一个语言,一个国家。 所有提示都属于一个产品类别,使用英语,固定为
US。上述引用的方差成分研究发现查询语言占总方差的26.5%,因此不应假定不同语言会像这一语言一样表现。 - 威尔逊区间假定独立抽样。 五个请求在一个会话中同时发出,可能共享上游状态,这会使得有效样本比名义样本更小,真实区间可能比表格显示的更宽。
- 品牌层依赖于监视列表。 只有在列表上的名称才被计算,因此您未列出的竞争对手对L3是不可见的,同时如果引擎引用其域名,则对L1完全可见。
这些都没有打破头条结果,即在相同条件下测量的层之间的比较。它们意味着特定的百分比属于该矩阵,而不是一般的AI回答。
将数字融入监控计划中
分别安排这两种问题类型的日程,因为对两者使用相同的节奏会在设定问题上过度支付,而在速率问题上则提供不足的支持。
关于引用库存, 每个引擎每个提示三到四次重复,每周。这可以在每轮采集中回收83%到92%的被引用领域宇宙,而该集合的周间差异是可以读取的。每轮存储联合数据而不是单次运行的列表;比较两个单次运行会产生大部分为重新取样的变化。
关于比率指标— 提及份额、声音份额、情感份额、排名 — 累积而不是快照。每个单元每周进行20次采集,在每周数字上产生±20点的区间,在合并的月度数字上产生±10点。每次报告时在数字旁边标注区间。一个显示42%而不显示31%–50%的仪表板声称了一种采集设计所不支持的精确度。
无论您选择什么节奏,都要固定一切。 相同的提示字符串、相同的国家、相同的推理模式。第一阶段的输入指纹应存储在您的行中,以便未来的分析师能够区分真正的变化与被某人编辑过的提示。
当引擎变化时重新测量。 稳定性是当前模型和检索堆栈的属性,而不是常量。当引擎发布可见变化时,上个季度足够的运行次数再次成为假设。在这里应用同样的纪律,就像在任何模型评估中一样,策划一个代表性的数据集和选择一个有意义的方法论应该排在指标之前,而不是之后。
结论:报告区间,而不仅仅是数字
有用的发现不是AI答案会变动。每个指南已经说过这一点。有用的发现是它们在不同层次上的变动幅度不同,因此所需的样本大小也相应拆分:对于引用库存,几个运行;对于您打算在会议中辩护的比率,大约一百次。
该拆分背后的设计是四个文件和一百个调用。在输入固定的情况下进行捕获,减少到可比较的层次,评估变动,转换为运行次数。使用您自己的提示集运行一次,您就会停止在节奏上猜测,因为您将拥有每个AI可见性仪表板目前缺少的一个数字——误差条。
准备好测量您自己的AI可见性方差吗?
五个答案引擎通过一个终端和一个响应信封坐落在Scrapeless Universal Scraping API后面,这使得一百次采集矩阵在一个下午内完成,而不是五次单独集成。每个参与者所用的参数和响应形状均在Scrapeless API文档中。
创建一个免费的Scrapeless帐户并为您的可见性数字添加误差条。
常见问题
问:一个AI可见性数字实际上需要多少次运行?
这取决于层次,两个答案相差超过一个数量级。在这个矩阵中,每个引擎每个提示三到四次的重复恢复了83%到92%的被引用领域,而五次重复则显示,因此在该样本大小下,引用库存是可读取的。比率指标如提及份额大约需要每个引擎每个提示100次采集,才能使95%的区间缩小到约±10点,而五次采集则使其保持在大约65点宽。
问:为什么当提示和设置相同时,AI答案会变化?
模型从概率分布中进行抽样,而馈送它们的检索层也不是固定的,因此相同的输入并不保证相同的输出。将五个模型固定在名义上确定的设置下,跨越八个任务仍然记录了高达15%的准确性波动,并将该效应归因于推理批处理的处理方式,而不是配置错误。
问:被引用的来源是否比答案文本更稳定?
不可靠,并且不够以跳过任何层的重复采样。在这个矩阵中,被引用领域集的平均成对Jaccard重叠为0.384,而单次采集仅显示了五次采集所显示领域的49.6%,因此引用层在绝对意义上有着实质性的变动。它的收敛速度快于比率指标,这与稳定性是不同的一个说法。
问:我追踪的引擎是否会改变我需要多少次运行?
是的,大约在引用层面上有两倍的因子。一次捕获在这个矩阵中最稳定的引擎上呈现了 73.6% 的五次运行域的联合,而在最不稳定的引擎上呈现了 35.1%,因此为一个引擎选择的运行计数对于另一个引擎来说是错误的。每个引擎计算覆盖曲线,而不是将一个数字应用于所有引擎。
问:这个测量是否也考虑了日常漂移?
不考虑。所有 100 次捕获在 4 分钟 9 秒内完成,这隔离了模型级别的非确定性,并排除了索引更新、新闻周期、内容变化和模型版本。将这里的每个数字视为一个下限:跨周采样的程序携带了这种方差加上短时间窗口内保持不变的所有因素。
问:以这种方式捕获 AI 回答合法吗?
捕获您所撰写的提示的公开可用的 AI 回答是普通的公共网络数据收集,而这里的提示和响应不包含个人数据。保持数量有限且与测量成比例,仅存储分析所需的内容,并在大规模运行程序之前检查适用于您所在司法管辖区和使用案例的条款。
问:我可以在没有 AI 代理或 SDK 的情况下运行这个吗?
可以。这里的每个脚本都是针对一个 HTTPS 端点的 Python 标准库,因此整个管道可以从 cron 作业或 CI 日程中运行,不需要安装超出 Python 的任何东西。唯一的外部依赖是 API 密钥。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



