AI 搜索工具 Perplexity 在回答末尾会附上一排引用来源,用户通常默认这些蓝色链接能支撑上面的大段结论。Haus Research 的一次公开审计给出了一个值得注意的结果:大约三分之一的引用页面并不包含被引用的数字。换句话说,看似严谨的引用,并不等于信息来源真的支持这条断言。这篇文章不打算复述一份报告,而是用工程化方式拆解一个更通用的问题:当我们需要验证 AI 搜索的引用质量时,样本怎么选、规则怎么定、脚本怎么写、结果怎么解读。对做 AI 搜索测评、信息质量研究或内容审核的人来说,这是一条可复现的审计路径;对普通用户来说,也能建立对 AI 搜索答案更准确的判断尺度。这里说的审计是“引用来源完整性审计”,不是源代码审计,也不是日志审计。
1. 先理解 Perplexity 引用机制与审计目标
1.1 AI 搜索的引用不是被动的文字标注
传统搜索引擎返回的是链接列表,用户自己判断哪条结果可点、哪条可信。Perplexity 这类 AI 搜索工具不一样:它会先理解问题,再综合多个网页内容生成一段回答,并在关键句子上打上[1]、[2]这样的引用标记,最后在回答底部列出对应的 URL。
问题就出在“标记”这个动作上。引用标记的位置,是模型在生成回答时自己决定的。模型可以从检索到的网页里抽取信息,也可以根据训练记忆补全信息,还可以把多个来源的信息重新组合。组合的时候,来源 A 的统计数字可能被标成来源 B,来源 C 的年份可能被挪到来源 D 的结论里。用户看到[1],默认认为“这句话是第 1 个链接里的内容”,但实际链接页面里可能根本没有这句话,甚至没有这个数字。
Haus Research 的审计,针对的正是这种“引用位置与来源内容的对应关系”。它不直接判断 Perplexity 的回答是对是错,而是检查:当回答中引用了一个数字时,这个数字是否真的存在于对应的引用页面中。
1.2 审计关注数字,而不是所有观点
引用完整性审计的第一步,是选一个容易客观判定的对象。Haus Research 的公开结论特别提到“数字”,这是很合理的选择。
数字具有天然的可验证性。“约 72% 的用户”“成本下降 31%”“2023 年第二季度营收”这类表述,要么能在原文里找到,要么找不到。数字不像“效果更好”“风险较高”这类主观判断,每个读者可能会有不同解读。
数字审计的典型粒度为:
| 审计对象 | 例子 | 验证难度 |
|---|---|---|
| 数字 | “全球市场份额达到 23%” | 低,可做字符串匹配 |
| 实体 | “某公司在 2024 年发布了新版本” | 中,需要实体识别和上下文判断 |
| 完整论断 | “该政策导致中小商家收入下降” | 高,需要语义理解,依赖主观判断 |
因此,以数字为突破口,能让审计规则更清晰,也让结果更容易复现。
1.3 三分之一这个比例代表什么
“约三分之一的引用页面并不包含被引数字”是一个抽样结果,不是对 Perplexity 历史上所有回答的精确统计。它的意义在于提示一种系统性问题:大型语言模型生成的引用标记,并不天然等于来源页面的真实支撑。
解读这个比例时,至少要考虑三层口径:
- 判定“不包含”的标准是精确数字缺失,还是连等价表述都不存在。
- 审计样本覆盖了哪些类型的问题,是偏向数据型问题,还是包含新闻、观点、常识类问题。
- 审计时间是哪一天,页面是否发生了改版、下线或区域跳转。
所以,不要因为看到“三分之一”就说“Perplexity 有一半答案都在胡说”。“引用页面没有精确数字”和“回答内容完全错误”是两件事。前者是引用质量问题,后者是事实准确性问题。两者相关,但不等同。
注意:阅读任何 AI 搜索审计报告,先检查“样本量、问题构成、判定规则”三个要素,否则很容易把局部结论放大成全局定性。
2. 引用完整性审计的通用分析框架
2.1 先定义“包含”与“不包含”
如果没有统一的判定规则,审计结果无法横向对比。建议把引用来源按四级分类:
| 判定等级 | 含义 | 示例 |
|---|---|---|
| 命中 | 引用页面包含被引数字或等价表述 | 回答写“31%”,页面也写“31%” |
| 语义等价 | 页面用其他表达方式说明了同一数字 | 回答写“31%”,页面写“百分之三十一” |
| 部分相关 | 页面内容与主题相关,但无法找到被引数字 | 页面讨论同一产品,但没有该统计值 |
| 未命中 | 页面内容不包含该断言,或页面不可用 | 页面 404,或页面是另一个主题 |
| 无法判定 | 页面需要登录、被动态渲染、数字在图片中 | 技术手段无法读取正文 |
定义规则后,必须固定到审计文档里。例如:
- “包含”指精确数字匹配,允许千分位分隔符不同;
- “等价表述”指中英文数字、百分比符号、小数点精度差异;
- “页面不可用”计入“无法判定”,不计入“未命中”。
这样做的原因是:脚本判断一个页面“没有数字”很容易,但人眼复核时可能发现数字被写成了“三成”或“30 %”。规则不统一,最终比例就没有意义。
2.2 抽样要覆盖不同类型的查询
不要只测“2024 年全球智能手机出货量”这类单一类型的问题。AI 搜索在不同查询上的表现很可能不均衡。
建议按以下几个维度抽样:
- 数据类型:包含具体百分比、金额、年份、排名的查询。
- 时效类型:刚刚发生的新闻、一个月内的新闻、一年前的背景信息。
- 语言类型:中文、英文等不同语言的查询,引用页面的结构差异很大。
- 主题类型:科技、财经、医疗、体育、民生,不同主题的资料来源质量不同。
- 热度类型:热搜问题、长尾问题、专业性较强的问题。
样本量不需要一开始就很大,第一批可以先做 50 到 100 条。重点不是追求样本无限多,而是保证判定规则稳定、人工复核可执行。
2.3 记录访问时间和页面状态
网页内容处于持续变化中。今天能打开的页面,明天可能 404;今天的正文里有数字,改版后可能被移除。因此,审计结果必须带有时间维度和页面状态。
每条审计记录建议包含字段:
{ "query": "某公司2024年营收是多少", "answer_sentence": "某公司2024年营收为120亿美元[1]", "cited_url": "https://example.com/reports/2024", "http_status": 200, "audit_time": "2026-01-18T10:20:00Z", "has_number_in_page": false, "judge_level": "unmatched", "manual_review": true }有了这样的记录,后续别人可以重复访问页面,即使内容发生变化,也知道当时的判断依据。
2.4 机器初筛加人工复核
自动化脚本适合做批量初筛,但“页面是否包含被引数字”这个问题,不能完全交给一个简单的if number in text判断。同一个数字,页面可能是“31.0%”“31 percentage”“百分之三十一”,脚本若不处理,就会误判。
建议采用“机器初筛 + 人工抽检”的方式:
- 脚本批量抓取引用页面,抽取正文;
- 脚本输出每个样本的数字匹配结果;
- 将“未命中”的样本全部人工复核;
- 将“命中”的样本随机抽 10% 复核;
- 记录人工复核与脚本结果不一致的案例,反向修改脚本规则。
3. 用最小脚本复现一次引用完整性检查
3.1 技术选型与合规边界
下面用 Python 写一个最小可运行的审计脚本。它负责三件事:抓取引用页面文本、抽取回答中的数字和关键短语、判断页面中是否存在对应内容。
依赖:
pip install requests beautifulsoup4 lxml抓取外部页面时必须注意合规要求:
- 遵守目标网站的
robots.txt; - 控制请求频率,不并发爆破;
- 只读取公开可访问的页面,不绕过登录页;
- 用于学习和研究,不能用于商业数据采集。
注意:不要为了拿到动态页面里的数字而去破解验证码或伪造指纹。遇到反爬时,正确的做法是把页面标记为“无法判定”,而不是绕过访问限制。
3.2 准备测试数据
真实环境中,可以从 Perplexity 的回答里提取句子和对应的 URL。这里先用一组最小测试数据演示:
sample = { "query": "某公司2024年营收同比增长多少", "answer_text": "某公司2024年全年营收为120亿元,同比增长31%[1]", "citations": [ { "index": 1, "url": "https://example.com/company-2024-results" } ] }实际项目里,回答文本和引用列表可以从浏览器控制台、网页 DOM 或官方 API 中获取。获取方式不同,但核心判断逻辑一致。
3.3 抓取页面并提取正文
import re import time import requests from bs4 import BeautifulSoup USER_AGENT = "Mozilla/5.0 (ResearchBot/1.0; educational use)" def fetch_text(url): headers = {"User-Agent": USER_AGENT} resp = requests.get(url, timeout=10, headers=headers) if resp.status_code != 200: return None, resp.status_code content_type = resp.headers.get("Content-Type", "") if "text/html" not in content_type: return None, resp.status_code soup = BeautifulSoup(resp.text, "lxml") for tag in soup(["script", "style", "noscript"]): tag.decompose() text = soup.get_text(" ", strip=True) return text, resp.status_code这段代码的作用是拿到 HTML 页面后,先移除脚本和样式,再提取可见文本。要注意,requests拿到的只是服务端返回的 HTML。如果页面数字由 JavaScript 动态加载,这段代码会漏掉。
3.4 抽取数字并匹配
页面正文拿到后,第一步是从回答中提取数字,第二步是检查数字是否出现在页面正文里。
def extract_numbers(text): matches = set() patterns = [ r"\d+(?:\.\d+)?%?", r"\d+(?:,\d{3})+(?:\.\d+)?", r"\d+" ] for pattern in patterns: for match in re.findall(pattern, text): normalized = match.replace(",", "") matches.add(normalized) return matches def check_number_in_page(page_text, answer_numbers): if not page_text: return False for number in answer_numbers: if number in page_text: return True return False这是一个最原始的判断方式。它足够演示流程,但在生产中不够用。原因在于:
- “31%”在页面中可能写作“31 percent”;
- “120 亿元”在页面中可能写作“12 billion”;
- 货币单位、量级、精度都可能被改写。
所以在正式审计里,建议同时加入以下“短语兜底”:
def normalize_words(text): return " ".join(re.findall(r"[A-Za-z0-9]+", text.lower())) def check_phrase_in_page(page_text, sentence): page_norm = normalize_words(page_text) words = re.findall(r"[A-Za-z0-9]+", sentence) if len(words) < 5: return False for i in range(len(words) - 4): phrase = " ".join(words[i:i + 5]) if phrase in page_norm: return True return False这个函数抽出回答句子中连续的 5 个词,在页面正文里做精确匹配。它能捕捉部分“页面确实包含这句话但脚本只看数字没发现”的情况,但也会误报。所以它只能作为初筛补充,不能替代人工复核。
3.5 审计循环与结果输出
把上面的函数串起来,组成一个简单的审计流程:
def run_audit(samples, sleep_seconds=1): results = [] for idx, item in enumerate(samples): answer = item["answer_text"] number_matches = extract_numbers(answer) for citation in item["citations"]: page_text, status = fetch_text(citation["url"]) if page_text is None: results.append({ "query": item["query"], "url": citation["url"], "http_status": status, "number_hit": None, "phrase_hit": None, "conclusion": "unavailable" }) else: number_hit = check_number_in_page(page_text, number_matches) sentence = re.sub(r"\[\d+\]", "", answer) phrase_hit = check_phrase_in_page(page_text, sentence) if number_hit: conclusion = "hit" elif phrase_hit: conclusion = "semantic_suspect" else: conclusion = "unmatched" results.append({ "query": item["query"], "url": citation["url"], "http_status": status, "number_hit": number_hit, "phrase_hit": phrase_hit, "conclusion": conclusion }) time.sleep(sleep_seconds) return results运行后,统计每个conclusion的数量:
from collections import Counter results = run_audit(samples, sleep_seconds=1) print(Counter(r["conclusion"] for r in results))预期的输出可能类似:
Counter({'unmatched': 10, 'hit': 18, 'unavailable': 4, 'semantic_suspect': 2})这只是一个过程演示。真实审计时,你还需要把results写入 CSV 或 JSON,字段至少包括回答原文、引用 URL、HTTP 状态码、页面是否可读、数字是否命中、人工复核结果。
3.6 脚本结果只能作为初筛
这个脚本最大的价值,是把“引用页是否包含被引数字”从抽象问题变成一个可以批量观察的表格。但它也会犯两类错误:
- 漏报:页面确实有数字,但数字被渲染、转义、改写,脚本检测不到;
- 误报:页面恰好出现同一个数字,但该数字说的是另一件事。
因此,所有unmatched样本都应该人工查看。只有经过人工复核的数据,才适合放进最终报告。
4. 为什么三分之一引用会出现“找不到被引数字”
4.1 页面改版与内容迁移
最常见的原因之一是页面内容已经变化。Perplexity 在生成回答时,可能使用的是检索时的页面快照;用户几天后点击链接时,页面已经改版,数字被挪到子页面,或者被删除。
这类情况不是模型幻觉,而是“引用失效”。审计时如果只访问当前页面,很容易把这种时效性问题判定为“引用不存在”。
处理建议:在审计记录中同时保存访问时间,并在报告中区分“当前页面不存在该数字”和“历史页面曾经存在该数字”。如果预算允许,可以访问网络存档中的历史快照作为辅助证据。
4.2 动态渲染与反爬机制
很多新闻网站的数字不是直接写在 HTML 里,而是通过 JavaScript 从接口加载后再渲染。requests抓取的 HTML 中没有数字,但浏览器打开时能看到数字。
这种页面如果被脚本判为“不包含”,实际上可能是抓取能力不足。判断时可以观察:
- HTML 中是否有数字对应的 JSON 数据;
- 是否有独立的接口返回正文;
- 是否必须使用无头浏览器才能完成渲染。
在审计研究中,优先把这种页面标记为“无法判定”,而不是“未命中”。否则统计结果会系统性偏高。
4.3 模型生成引用时发生链接错位
这是最需要关注的原因。模型可能从多个网页中综合信息,最后生成回答时,把来源 A 的数字标注成来源 B。典型表现是:
- 回答里有三个引用链接;
- 第一个链接页面内容与回答主题相关;
- 但被引用的具体数字实际出现在第二个链接中;
- 第一个链接只是因为“整体内容相关”被选进了引用列表。
这种“相关但不对应”的引用,比完全不相关更隐蔽。用户点击后会觉得页面内容差不多,但仔细对不上数字。
4.4 数字以图表、图片或附件形式存在
不少财经和医疗内容的数字在图片里,PDF 里,或者交互式图表中。文本抽取脚本无法识别,但人眼可以通过查看图片或下载文档确认。
在审计标准中,这种应该判为“无法通过文本抽取确认”,不能直接归为“引用页面不包含被引数字”。特别是当页面主题完全吻合时,要优先考虑页面中是否存在非文本信息承载数字。
4.5 审计规则本身造成的假阴性
还有一种情况是页面确实包含数字,但表达方式和回答不同。例如:
| 回答中的表达 | 页面中的表达 |
|---|---|
| 31% | 百分之三十一 |
| 31 percent | 31% |
| 120亿元 | 120.0亿人民币 |
| 1.2万 | 12000 |
如果审计脚本只做子串匹配,这些都会被误判为“不包含”。这也是为什么前面反复强调:自动化初筛必须配人工复核,判定规则必须提前定义。
4.6 原因对照表
| 现象 | 常见原因 | 对审计结果的影响 |
|---|---|---|
| 页面有数字但脚本没抓到 | JS 渲染、图片数字、格式转换 | 假阴性,可用无头浏览器复核 |
| 页面 404 或权限受限 | 链接失效、区域跳转、登录墙 | 判定为无法判定,不等同于引用错误 |
| 页面主题相关但无精确数字 | 模型综合多来源后错标引用 | 可能是真实的引用质量问题 |
| 页面内容被改版 | 页面更新、内容迁移 | 需要保存访问时间和页面快照 |
| 页面有相同数字但含义不同 | 数字冲突、上下文无关 | 人工复核才能判断 |
在写审计结论时,一定要说明你排除了哪些干扰因素,否则“三分之一”这个数字很容易被简化为“AI 搜索的引用三分之一都是假的”。
5. 常见问题与排查链路
5.1 页面明明有数字,脚本却检测不到
现象:人工打开 URL,能看到“31%”,但脚本返回unmatched。
排查顺序:
- 确认页面是否依赖 JavaScript 渲染;
- 确认数字是否存在图片、PDF、附件中;
- 确认数字格式是否包含全角字符或转义符;
- 确认回答里的数字和页面数字是否属于同一量级、同一统计口径;
- 人工复制页面中的数字,与脚本抽取的文本做比较。
解决方案:先打印page_text[:500],看看页面文本里到底有什么。如果数字确实因为渲染缺失,换成 Playwright 等无头浏览器方案,但要增加请求成本和维护成本。
5.2 页面打不开或出现 403、429
现象:resp.status_code为 403 或 429,页面正文为空。
常见原因:
- 目标网站禁止非浏览器 User-Agent;
- 审计请求频率过高触发限流;
- 页面限制了数据中心 IP。
处理方式:
- 设置合理的 User-Agent,但不要伪造过拟合的浏览器环境;
- 增加请求间隔,降低并发;
- 将请求失败页面标记为
unavailable,不参与命中率计算; - 不绕过验证码,不伪造 Cookie 绕过反爬。
5.3 被引内容在二级页面
现象:一级页面是新闻列表或财报摘要,真正包含数字的链接在页面内。
排查步骤:
- 查看一级页面是否有“阅读原文”“查看完整报告”链接;
- 在回复列表中查找是否包含完整正文;
- 如果内容必须多跳一次才能看到,审计时应把“多跳可见”和“一级页面直接包含”分开统计。
5.4 引用链接是搜索页或聚合页
现象:Perplexity 引用的 URL 是搜索引擎结果页,或者某个标签聚合页,并不是具体文章。
这类引用对用户几乎没有验证价值。搜索页只会告诉你“网上有相关结果”,并不会直接支撑回答中的数字。审计时应归类为“无法定位精确断言”,并单独统计。
5.5 如何区分引用错误和审计误判
按下面的顺序排查:
| 步骤 | 操作 | 结论类型 |
|---|---|---|
| 1 | 页面能否正常访问 | 不能则无法判定 |
| 2 | 页面是否依赖 JS 渲染 | 依赖则补充渲染或无法判定 |
| 3 | 页面是否存在等价数字表达 | 存在则语义等价 |
| 4 | 数字是否在图片、附件中 | 存在则人工确认 |
| 5 | 数字是否存在二级页面 | 存在则记录多跳 |
| 6 | 以上均不存在 | 判定为未命中 |
这套链路可以让审计结论更稳定,也能在别人质疑结果时给出清楚证据。
6. 从审计结论到生产实践:如何评估 AI 搜索答案
6.1 对普通用户:不要因为带引用就默认可信
引用能提供线索,但不能自动证明回答正确。遇到以下场景时,优先打开原始来源核对:
- 数字涉及医疗、投资、法律等高风险决策;
- 多个来源对同一数字的表述不一致;
- 回答中的数字特别精确,但引用页面是短新闻或评论区;
- 引用 URL 是聚合页、搜索页或明显与主题无关的页面。
验证时,不要只看数字是否出现,还要看数字的统计口径、时间范围和适用对象是否一致。例如“增长 31%”和“利润占比 31%”是两个完全不同的概念。
6.2 对 AI 搜索产品:把引用校验做成质量模块
产品团队可以建立一条“引用可验证性”评估管线:
- 检索阶段:记录候选来源和对应的摘要片段;
- 生成阶段:让模型输出回答句与引用索引的对应关系;
- 验证阶段:对回答句中的数字断言,在对应引用页面上做匹配;
- 置信阶段:若页面中没有找到对应数字,降低该引用的展示置信度,或提示用户“该引用可能无法直接支撑”。
这一步不需要做到完美,哪怕只做“数字字符串匹配 + 语义等价判断”,已经能在上线前发现大量引用错位问题。
6.3 对内容网站:让数字更容易被识别和引用
内容站点如果希望自己成为 AI 搜索的可靠信息来源,可以做一些基础优化:
- 关键数字写进 HTML 正文字段,不要只放在图片里;
- 避免用难以解析的动效图表承载唯一数据;
- 提供包含全文的独立详情页,而不是只有摘要;
- 保持 URL 稳定,改版时做 301 跳转;
- 对外提供结构化数据,例如
DataFeed、Dataset或Article等 Schema。
这些做法不能保证 AI 一定正确引用,但能显著降低“页面存在数字,但抓取工具读不到”的概率。
6.4 对研究机构:建立可复现的审计平台
引用质量审计是长期工作,不应只在某次报告发布时做一次。建议建立:
- 查询样本集:每个月固定更新一批数据型问题;
- 审计脚本仓库:记录版本号,随时可以重新跑;
- 判定规则文档:明确什么是命中、未命中、无法判定;
- 结果数据库:保存每次审计的完整记录;
- 发布模板:固定披露样本量、时间、人工复核比例。
只有可复现,结果才有参考价值。否则下一次第三方做同样的审计,可能因为规则不同得到完全不同的数字。
6.5 可复用检查清单
发布前的引用质量检查清单:
- 引用 URL 是否可以直接访问,HTTP 状态是否正常;
- 被引数字是否出现在引用页面的 HTML 正文中;
- 数字格式是否与回答一致,单位、小数点、百分号是否对应;
- 页面是否需要 JS 渲染才能显示数字;
- 数字是否存在于图片、PDF、附件或子页面;
- 引用 URL 是否为聚合页、搜索页或登录页;
- 审计时间是否记录,页面快照是否保存;
- 未命中样本是否完成人工复核;
- 统计口径是否明确区分“未命中”和“无法判定”;
- 结论是否只针对本次抽样,而不是泛化到所有 AI 搜索场景。
7. 局限性、后续方向与一点建议
7.1 审计结论有边界
Haus Research 给出的“约三分之一”是一个值得重视的信号,但它来自特定时间、特定抽样问题和特定判定规则。它不能说明 Perplexity 在所有问题类型上都是这个水平,也不能说明其他 AI 搜索工具表现一致。引用完整性是评估 AI 搜索质量的重要维度,但不是唯一维度。事实准确性、回答流畅度、时效性、覆盖面都需要分别评估。
7.2 后续可以扩展的方向
如果你打算做更深的研究,可以考虑三个方向。
第一,语义匹配。不再只看数字字符串,而是用一个轻量语义模型判断引用页面文本是否蕴含回答句。这样可以识别“百分之三十一”和“31%”这类改写,也能一定程度处理表述差异。
第二,时序监测。对同一组查询每周跑一次引用审计,观察引用质量随搜索算法升级、页面改版、模型迭代的变化。长期曲线比单次报告更有说服力。
第三,跨工具对比。把同样的查询集分别用不同 AI 搜索工具跑一遍,对比引用命中率、来源质量和错误类型。这样可以帮助用户和产品团队理解各自系统的差异。
7.3 对工程团队和普通开发者的建议
最值得记住的一点不是“三分之一”本身,而是:AI 搜索的引用必须被当成一种可度量、可维护的产品指标,而不是一个漂亮的界面元素。
对普通开发者,可以先从今天的最小脚本开始,抓取 20 条数据型问题,跑一遍引用匹配,记录结果。你很快会发现两类问题:有些页面打不开,有些页面渲染不出数字,有些引用页面和回答内容完全不相关。这个过程比阅读十份报告更能建立对 AI 搜索机制的体感。
对团队负责人,建议把“引用可验证率”纳入 AI 搜索产品的质量看板。上线新版本前,除了看回答准确率,也看引用页面的命中率。引用链接在用户理解中代表“证据来源”,一旦证据经常缺失,整个回答的可信度都会受影响。把审计脚本和人工复核流程沉淀下来,比发布一篇道歉声明更有价值。