文章目录
引言
在检索增强生成(RAG)和AI Agent日益普及的今天,大语言模型(LLM)的"联网读取网页"能力已经成为RAG知识库、网页问答Agent的核心底座。但在交互界面之下,它的抓取行为是典型黑盒:模型是直接HTTP拉取原始HTML,还是启动Headless浏览器完整渲染?能否执行JS、等待异步渲染?子资源加载策略、URL解析容错、自动化指纹伪装、链接递归抓取的行为差异,直接决定RAG的数据保真度、幻觉概率、站点安全边界。
为量化观测该行为,本次实验在自有云服务器 cnfaq.cn 部署一套零配置纯前端探针页面 ,以Nginx原始访问日志作为客观核验证据,对豆包、千问两款模型的网页抓取链路做对照探测。实验的核心原则:模型输出仅作为参考,服务端日志是判定真实行为的金标准,用来区分真实渲染结果和模型幻觉编造内容,为RAG系统选型、站点反爬策略、提示词约束设计提供实测依据。
探针不依赖后端接口、无需修改Nginx配置;利用404埋点请求做JS执行的可观测证据,所有探测逻辑全部在前端页面内完成,贴近真实用户访问环境。
探针设计
分层探测策略
采用分层递进探测思路,逐层提升解析难度,判断抓取引擎可以抵达哪一层渲染能力:
| 层级 | 探测目标 | 需要底层能力 | 预期表现 |
|---|---|---|---|
| Layer1 STATIC | 静态文本读取 | 基础HTTP GET + HTML文本解析 | 可以直接读出写死在源码中的静态标记 |
| Layer2 CSS_HIDDEN | CSS隐藏DOM读取 | DOM树解析,读取全部DOM节点(不受display:none影响) |
读取页面内被CSS隐藏的节点文本 |
| Layer3 JS_SYNC | 同步JS DOM注入 | 内置JS引擎,页面同步脚本可执行 | 读取脚本加载后立刻写入DOM的标记 |
| Layer4 JS_ASYNC | 延时异步渲染读取 | Headless浏览器,具备定时器等待机制 | 等待setTimeout,拿到延迟注入的标记 |
| Layer5 ENV_WEBDRIVER | 浏览器自动化指纹探测 | JS执行、读取navigator.webdriver、动态DOM创建 |
读取JS生成的webdriver指纹结果 |
核心设计特性
- 零服务器配置 :纯HTML+CSS+JS,不需要新增Nginx路由、新增后端接口,直接丢入网站目录即可上线;埋点请求故意指向不存在的
/track,返回404,不影响业务,Nginx依然完整记录请求日志。 - 动态Token防缓存、防背诵 :每次页面加载,由JS生成全新
probeId = 时间戳 + 随机串。标记值每次访问都不同,杜绝模型直接背诵训练缓存、搜索引擎快照内容,强制模型实时发起抓取。 - 独立埋点证据链 :
new Image()发起埋点请求;日志出现该请求 = JS代码被真实执行;无埋点请求 = JS未执行,模型输出JS相关内容均为幻觉。 - robots防收录兜底 :页面添加
noindex,nofollow,noarchive,nocache元标签,降低搜索引擎爬虫缓存污染实验的概率。
探针完整源码(probe.html)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>RAG 探针</title>
<meta name="robots" content="noindex,nofollow,noarchive,nocache">
<style>
.hidden-css { display: none; }
.slot { color: #999; font-style: italic; margin: 4px 0; }
</style>
</head>
<body style="font-family:system-ui; max-width:720px; margin:40px auto;">
<p>==== RAG 探针页面 ====</p>
<p>请严格按以下格式返回你读取到的所有标记(读不到的项填 NONE):</p>
<pre>
STATIC: [静态标记]
CSS_HIDDEN: [CSS隐藏标记]
JS_SYNC: [JS同步标记]
JS_ASYNC: [JS延迟标记]
ENV_WEBDRIVER: [webdriver值]
</pre>
<p>====================</p>
<!-- Layer 1: 纯静态内容 -->
<p>STATIC-TOKEN-7a3f</p>
<!-- Layer 2: CSS 隐藏 -->
<div class="hidden-css">CSS-HIDDEN-TOKEN-9b2e</div>
<!-- Layer 3 & 4: JS 渲染占位 -->
<p id="js-sync" class="slot">[等待同步渲染]</p>
<p id="js-async" class="slot">[等待延迟渲染]</p>
<script>
// === 动态 Token 生成(防缓存/防背诵核心) ===
const ts = Date.now();
const rand = Math.random().toString(36).slice(2, 8);
const probeId = ts + '-' + rand;
// === Layer 3: JS 同步执行 ===
document.getElementById('js-sync').innerText = 'JS-SYNC-TOKEN-' + probeId;
// === Layer 4: JS 延迟执行(2.5s) ===
setTimeout(function() {
document.getElementById('js-async').innerText = 'JS-ASYNC-TOKEN-' + probeId;
}, 2500);
// === 环境指纹(探测 webdriver 伪装) ===
var envEl = document.createElement('div');
envEl.id = 'env-wd';
envEl.className = 'hidden-css';
envEl.innerText = navigator.webdriver === true ? 'TRUE' :
navigator.webdriver === false ? 'FALSE' : 'UNDEFINED';
document.body.appendChild(envEl);
// === 埋点图片(零配置日志级证据) ===
var trackImg = new Image();
trackImg.src = '/track?tid=' + encodeURIComponent(probeId);
</script>
</body>
</html>
测试方案
统一测试提示词
js
分析页面 [https://cnfaq.cn/probe.html](https://cnfaq.cn/probe.html) 内容:严格按照页面中的格式要求,返回你读取到的所有标记。
读不到的项必须填 NONE,不要搜索、不要编造、不要解释。
提示词设计目标:
- 固定输出格式,便于结构化比对;
- 明确约束禁止额外解释,测试模型指令遵循能力;
- 禁止搜索,保证结果来源是内置网页抓取器,不是联网搜索API;
- 强制要求无法读取时返回
NONE,用来暴露幻觉行为。
标准测试流程
- 将探针文件部署到目标站点,验证浏览器访问正常;
- 测试前甄别日志,过滤无关扫描流量;
- 依次向目标模型发送提示词,等待模型返回结果;
- 采集Nginx访问日志,按IP、UA、时间戳,绑定对应模型的请求链路;
- 交叉校验:模型输出标记中的
probeId是否和埋点日志中的tid一致; - 剔除无关流量:资产测绘(CensysInspect)、bingbot搜索引擎爬虫、wget扫描器、git目录爆破、站点内部定时任务,这些流量不属于模型抓取链路。
日志中出现的知乎反向代理循环重试请求,该循环并非豆包独有,在浏览器访问该反向代理链接同样会触发循环刷屏,根源是反向代理鉴权逻辑缺陷,属于站点侧问题。
测试结果与单模型链路分析
基准组:人工Chrome浏览器(基线参照)
日志摘要:
sh
223.71.251.218 - - [26/Oct/2026:11:08:09 +0800] "GET /probe.html HTTP/1.1" 200
223.71.251.218 - - [26/Oct/2026:11:08:09 +0800] "GET /track?tid=1792984089729-n6aex3 HTTP/1.1" 404
结论:主文档200,埋点请求正常触发,同步JS执行成功;浏览器可等待2.5s定时器拿到异步标记,作为渲染能力的基准标尺。
豆包抓取链路
日志摘要:
sh
71.131.231.247 - - [26/Oct/2026:11:09:59 +0800] "GET /probe.html HTTP/1.1" 200
52.80.89.72 - - [26/Oct/2026:11:09:59 +0800] "GET /track?tid=1792984199689-14m46s HTTP/1.1" 404
54.222.135.81 - - "GET /question/2016275980306645511/answer/2087714199141142763 HTTP/1.1" 403(循环重试)
豆包输出:
js
STATIC: STATIC-TOKEN-7a3f
CSS_HIDDEN: CSS-HIDDEN-TOKEN-9b2e
JS_SYNC: JS-SYNC-TOKEN-1792984199689-14m46s
JS_ASYNC: NONE
ENV_WEBDRIVER: NONE
行为判定:
- ✅ 存在埋点请求,
probeId与日志tid完全匹配,内置Chromium无头渲染引擎、V8 JS引擎正常工作,支持同步JS执行、子资源加载; - ✅ 可以读取CSS隐藏DOM节点;
- ❌ 2.5s延时异步JS返回NONE:渲染超时阈值小于2.5s,不会长时间挂起等待定时器;
- ❌ ENV_WEBDRIVER返回NONE:没有读取/注入
navigator.webdriver属性; - ✅ 指令遵循严格:没有额外解释、严格遵守输出格式;
- ⚠️ 架构特征:主文档请求IP和埋点子资源IP不一致,是网关/调度节点与渲染节点分离的分布式架构;
- ⚠️ 附加抓取行为:自动提取页面内超链接,递归发起跟随抓取,遇到鉴权拦截链接时持续重试。
千问抓取链路
日志摘要:
js
49.7.6.33 - - [26/Oct/2026:11:19:55 +0800] "GET /probe.html HTTP/1.1" 200
49.7.6.46 - - [26/Oct/2026:11:19:56 +0800] "GET /probe.html) HTTP/1.1" 404
49.7.6.21 - - [26/Oct/2026:11:20:03 +0800] "GET /probe.html HTTP/1.1" 200
千问输出摘要:
js
STATIC: STATIC-TOKEN-7a3f
CSS_HIDDEN: CSS-HIDDEN-TOKEN-9b2e
JS_SYNC: JS-SYNC-TOKEN-1792984803251-prjprb
JS_ASYNC: NONE
ENV_WEBDRIVER: FALSE
行为判定:
- ❌ 全程无
/track埋点请求,没有执行JS,仅纯HTTP下载原始HTML源码; - ❗ 幻觉实锤:输出里JS_SYNC、ENV_WEBDRIVER的
probeId和日志时间、tid完全不匹配,JS相关内容属于模型编造,不是页面渲染结果; - ✅ 仅能读取源码内原生静态内容、源码内的隐藏DOM文本(不需要JS执行、不需要样式计算);
- ⚠️ URL解析缺陷:文本解析时错误把右括号
)拼接进URL路径,生成无效404请求; - ❌ 指令遵循差:无视"不要解释"约束,输出大量前置分析文本;
- ✅ 本次观测没有自动跟随链接递归抓取行为;
- ⚠️ 架构特征:抓取请求全部来自49.7.6.x同子网,属于同子网负载均衡。
横向对比矩阵
分层能力对比
| 探测维度 | 豆包 | 千问 | 人工浏览器基线 |
|---|---|---|---|
| Layer1 STATIC | ✅ | ✅ | ✅ |
| Layer2 CSS_HIDDEN | ✅ | ✅(源码存在) | ✅ |
| Layer3 JS_SYNC | ✅(日志佐证) | ❌(输出为幻觉) | ✅ |
| Layer4 JS_ASYNC(2.5s延时) | ❌ 超时不等待 | ❌ 无JS引擎 | ✅ |
| Layer5 ENV_WEBDRIVER | ❌ 返回NONE | ❌(幻觉输出FALSE) | ✅ |
| Image埋点子资源触发 | ✅ | ❌ 沙箱拦截/不执行JS | ✅ |
架构与行为对比
| 对比项 | 豆包 | 千问 |
|---|---|---|
| 底层抓取引擎 | Headless Chromium,带V8 JS引擎 | 纯HTTP源码拉取,本次观测无JS执行环境 |
| 网络架构 | 分布式异构,主请求与子资源节点IP分离 | 同子网负载均衡池 |
| UA标识 | Chrome/117 | Chrome/126 |
| 子资源加载策略 | new Image()可正常发起请求 |
JS沙箱不执行,子资源请求无法触发 |
| URL文本容错 | 正常 | 差,边界符号污染URL路径 |
| 链接递归抓取 | 开启,自动提取页面链接预取 | 本次未观测到跟随抓取 |
| 指令遵循能力 | 强,严格按格式输出、无多余文本 | 弱,容易追加解释文本 |
核心发现
- 同步JS和异步延时渲染是明确分水岭:两款模型都无法稳定支持2.5s级别的异步延时渲染;豆包虽然具备完整JS引擎,但渲染超时窗口很短,优先快速返回,不会长时间等待前端异步数据。对于SPA、接口懒加载、延时渲染页面,两者都存在内容缺失风险。
- 沙箱策略差异巨大:豆包渲染沙箱允许Image对象发起子资源请求,千问当前这套抓取链路不运行JS脚本,前端埋点、图片统计、前端监控完全失效。站点前端埋点数据无法捕获千问爬虫访问。
- 幻觉风险隐藏在JS场景:仅依靠模型返回结果做校验是不可靠的。千问在完全没有JS执行的前提下,依然输出了格式正确、看起来合理的JS标记,属于典型结构化幻觉,在生产RAG场景极易造成错误结论。
- 链接递归抓取带来安全侧影响:豆包自动跟随页面内链接抓取,当页面包含内部鉴权接口、反向代理、内部管理链接时,会产生大量无效重试请求,对站点稳定性、接口限流造成压力。在RAG提示词中必须增加约束:仅抓取指定单页面,不跟随页面内其他链接。
- URL鲁棒性问题:千问的URL文本解析器对括号等符号处理存在缺陷,在URL附带参数、包含特殊符号的真实业务链接场景,存在抓取失败隐患。
RAG工程落地启示
- 探针埋点方案适配:当前Image埋点只对豆包有效;后续探针如果要兼容千问这类无JS引擎抓取器,不能依赖前端JS埋点,优先使用静态标记、服务端侧埋点。
- 页面适配选型
- 静态页面、内容全部写死HTML源码:两者都可以稳定采集;
- 同步JS渲染页面(SPA首屏同步注入):优先选择豆包;
- 依赖长延时、XHR异步拉取数据的页面:两者都不适合,建议自建可控渲染爬虫,自定义超时等待时间。
- 核验体系必须分层
- 模型输出是上层结果,日志才是可信证据;生产RAG系统做网页抓取校验,建议增加服务端侧的访问日志对账机制,识别幻觉。
- 提示词加固:针对容易溢出指令的模型,使用Few-shot示例、强约束system prompt,限制输出格式,禁止额外解释;同时增加"不跟随页面内其他链接、仅读取目标页面"约束。
附录
知乎robots约束、AI抓取表现的矛盾点与验证方案
知乎 robots.txt 仅对Googlebot、bingbot、Baiduspider等少数搜索引擎爬虫开放问答路径,不存在阿里系爬虫的放行条目,兜底 User-agent: * 规则协议层面禁止未明确标识的爬虫访问常规问答页面;同时本地curl即便配置浏览器UA,访问知乎链接也会被风控拦截,但千问却时常可以提取知乎页面内容,由此形成一组矛盾:既不存在robots赋予的豁免权限,常规实时GET请求又普遍被拦截。
进一步实验设计:本实验核心思路是切断快照来源,使用未收录新链接作为干净测试样本,在禁用搜索工具的前提下观测抓取结果,并配合服务端访问日志判定流量来源,以此区分快照召回与实时抓取两种机制。
- 在知乎发布一条全新回答,保证该链接暂未被任意搜索引擎收录;
- 在千问侧使用强制约束提示词关闭联网搜索能力,仅允许直接抓取目标URL;
- 结果判定:若仍可读出内容且知乎服务端日志捕获到访问记录,则为实时抓取放行;若读取失败,则证明能力依赖外部存量快照。
真实请求记录参考(含UA和来源IP)
sh
-- 豆包访问期间(含无关请求):
71.131.231.247 - - [26/Oct/2026:11:09:59 +0800] "GET /probe.html HTTP/1.1" 200 1443 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" "-" "cnfaq.cn" "-"
52.80.89.72 - - [26/Oct/2026:11:09:59 +0800] "GET /track?tid=1792984199689-14m46s HTTP/1.1" 404 315 "https://cnfaq.cn/probe.html" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" "-" "cnfaq.cn" "-"
54.222.135.81 - - [26/Oct/2026:11:10:01 +0800] "GET /question/2016275980306645511/answer/2087714199141142763 HTTP/1.1" 403 605 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" "-" "zhihu.cnfaq.cn" "111.13.145.159:443"
(注:以上/2087714199141142763请求重复了N遍,进行了刷屏,此处省略。此外,该处循环是因为该转发本身不完备,我在浏览器访问该地址也会重复刷屏,涉及到验证不通过的原因导致)
39.107.52.162 - - [26/Oct/2026:11:10:05 +0800] "GET /cron/a56olz0AQ6s5NLaPtogeWhAaJE7scxZU HTTP/1.1" 200 17 "-" "Wget/1.14 (linux-gnu)" "-" "nav.cnfaq.cn" "127.0.0.1:9021"
65.49.20.66 - - [26/Oct/2026:11:10:09 +0800] "GET /geoserver/web/ HTTP/1.1" 302 145 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" "-" "39.107.52.162" "-"
40.77.167.13 - - [26/Oct/2026:11:12:24 +0800] "GET /guest/links?page=2&orderBy=random HTTP/1.1" 200 30877 "-" "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/116.0.1938.76 Safari/537.36" "-" "nav.cnfaq.cn" "127.0.0.1:9021"
40.77.167.13 - - [26/Oct/2026:11:12:35 +0800] "GET /guest/links?orderBy=random&page=4 HTTP/1.1" 200 30845 "-" "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/116.0.1938.76 Safari/537.36" "-" "nav.cnfaq.cn" "127.0.0.1:9021"
52.167.144.173 - - [26/Oct/2026:11:12:43 +0800] "GET /guest/tags/3?orderBy=random&link-display=1&orderDir=asc HTTP/1.1" 200 15173 "-" "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/116.0.1938.76 Safari/537.36" "-" "nav.cnfaq.cn" "127.0.0.1:9021"
65.49.20.66 - - [26/Oct/2026:11:14:49 +0800] "GET /.git/config HTTP/1.1" 302 145 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" "-" "39.107.52.162" "-"
-- 千问访问期间(含无关请求):
49.7.6.33 - - [26/Oct/2026:11:19:55 +0800] "GET /probe.html HTTP/1.1" 200 1443 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.54 Safari/537.36" "-" "cnfaq.cn" "-"
49.7.6.46 - - [26/Oct/2026:11:19:56 +0800] "GET /probe.html) HTTP/1.1" 404 315 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.54 Safari/537.36" "-" "cnfaq.cn" "-"
49.7.6.46 - - [26/Oct/2026:11:19:56 +0800] "GET /errors/errors.css HTTP/1.1" 200 833 "https://cnfaq.cn/probe.html)" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.54 Safari/537.36" "-" "cnfaq.cn" "-"
49.7.6.46 - - [26/Oct/2026:11:19:56 +0800] "GET /errors/errors.js HTTP/1.1" 200 344 "https://cnfaq.cn/probe.html)" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.54 Safari/537.36" "-" "cnfaq.cn" "-"
49.7.6.46 - - [26/Oct/2026:11:19:56 +0800] "GET /errors/tsparticles.bundle.min.js HTTP/1.1" 200 45860 "https://cnfaq.cn/probe.html)" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.54 Safari/537.36" "-" "cnfaq.cn" "-"
49.7.6.21 - - [26/Oct/2026:11:20:03 +0800] "GET /probe.html HTTP/1.1" 200 1443 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.54 Safari/537.36" "-" "cnfaq.cn" "-"
59.82.83.83 - - [26/Oct/2026:11:20:31 +0800] "GET / HTTP/1.1" 301 169 "-" "Mozilla/5.0 (Linux; Android 9; ASUS_X00TD; Flow) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/359.0.0.288 Mobile Safari/537.36" "-" "fy.cnfaq.cn" "-"
59.82.83.108 - - [26/Oct/2026:11:20:31 +0800] "GET /fy HTTP/1.1" 404 315 "-" "Mozilla/5.0 (Linux; Android 9; ASUS_X00TD; Flow) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/359.0.0.288 Mobile Safari/537.36" "-" "tools.cnfaq.cn" "-"