
AI 爬虫越来越“勤快”不光会抓你当前页面还会顺着你页面里的外链继续往下一层抓。这种链接跟随行为轻则让无关页面被收录重则会让受版权保护的内容、后台管理页、临时授权链接被 AI 训练数据池吸收。最近看到 RequestGuard 这个项目思路它的核心目标就是解决这个问题在 AI 请求“跟随链接”之前用请求级的判断把它拦下来。本文会从 AI 链接跟随的风险讲起拆解 RequestGuard 的判断逻辑并给出一个基于 Flask 的最小可运行 Demo最后补充常见的误杀问题和灰度上线建议。1. 为什么 AI 会跟随你的链接1.1 链接跟随和普通爬虫有什么区别传统搜索引擎爬虫比如 Googlebot会按照 robots.txt 的规则抓取整个站点并且不会在页面渲染阶段去点击页面里的每一个出站链接。大多数情况下搜索引擎会把链接当成“待抓取队列”的一部分由搜索引擎自己的调度系统统一安排而不是让一次抓取会话里自动访问页面里出现的所有外链。但 AI 平台的爬虫行为并不完全一样。很多面向大模型训练、AI 搜索、AI Agent 的工具会模拟真实用户访问网页会读取页面里的文本、链接、元数据甚至会在一次会话里跟随多个链接去收集上下文。比如部分 AI 爬虫会以“浏览者”的身份访问一个链接后继续访问该页面中的相关链接以此构建知识图谱或训练语料。这种“链接跟随”如果发生在你并不希望被访问的页面上风险就很直接临时授权链接被盗用AI 爬虫带着你的鉴权上下文访问了本不该公开的资源。内部文档、开发环境地址、测试页面被跟随抓取进入第三方索引或训练数据。页面里的出站链接被污染AI 会把你页面里单纯的外链引用当成“推荐站点”来抓取。自建站点带宽被大量 AI 爬虫占用日志里出现一堆陌生的 User-Agent。也就是说问题不只是“AI 搜索来抓我的首页”而是 AI 会顺着你的链接进入你原本没有打算对 AI 开放的二级页面、出站页面、临时页面。1.2 RequestGuard 解决什么问题RequestGuard 的定位可以理解为“在请求边界上加一道 AI 访问守卫”。它不是一个普通的 robots.txt 生成器而是直接面对 HTTP 请求做决策当 AI 爬虫访问你的页面时你可以放行普通用户但拦截 AI 爬虫进入敏感目录。当 AI 爬虫尝试跟随你页面中的出站链接时你可以把出站链接统一改写成受控跳转地址由服务端二次判断是否放行。当 AI 爬虫携带非标准 User-Agent或者明显带有自动化抓取特征时你可以根据规则返回 403、429 或者降级响应。当某个 IP 在短时间内请求频率过高时你可以自动限流防止批量抓取把站点拖垮。从实现形态来看它可以是反向代理层的一个插件也可以是 Web 框架里的中间件甚至可以是你自建站点入口处的一个轻量网关。具体形态不重要重要的是它把“是否允许 AI 访问我的链接”这个决策集中到了一起而不是让每个页面自己处理。1.3 哪些场景特别需要这类保护我梳理了三个典型场景你可以对照自己的项目判断是否需要接入内容型网站博客、文档站、知识库。如果你的文章被 AI 大量抓取并用于模型训练但又没有合理的授权协议你会希望至少拦截一部分 AI 爬虫。带鉴权的 Web 应用后台管理、数据看板、内部工具。这类应用最怕 AI Agent 跟随 URL 时带着 Cookie 或 Token 去访问接口可能导致敏感数据被自动化读取。营销落地页和带参数链接如果页面里有很多带 UTM 参数的外链AI 爬虫的跟随会污染渠道数据也会消耗第三方服务配额。2. 核心原理请求边界上的判断逻辑2.1 一次链接跟随请求的完整链路要理解 RequestGuard先看一个普通用户点击链接和 AI 跟随链接的区别。普通用户的请求链路是浏览器 - DNS - CDN/Nginx - Web 服务 - 页面资源 - 用户点击出站链接 - 新请求AI 爬虫的请求链路是AI 爬虫 - DNS - CDN/Nginx - Web 服务 - 解析页面 - 提取链接 - 继续请求下一个链接两者的第一个差异是普通用户访问出站链接时浏览器会带上 Referer、User-Agent、Cookie 等标准请求头AI 爬虫跟随链接时同样会带上这些头但很多 AI 爬虫的 User-Agent 是可识别的。第二个差异是普通用户不会在一秒内连续请求十多个链接而 AI 爬虫在解析页面后可能会在短时间内发起大量跟进请求。RequestGuard 要做的就是在这条链路里插入一个决策节点。常见插入位置有两个站点入口在 Nginx 或 Web 框架的 before_request 阶段统一判断。链接出口页面里的出站链接统一改写成受控跳转地址用户点击时再判断是否放行。2.2 判断一个请求是否来自 AI判断是否来自 AI没有一个 100% 准确的方法但可以组合多个特征打分。最基本的特征包括User-Agent 是否命中已知 AI 爬虫列表。User-Agent 是否为空或者包含明显的自动化标识。请求是否支持 JavaScriptAI 爬虫通常不执行复杂前端逻辑所以请求头里缺少 JS 相关标记。请求频率是否超过人类正常访问阈值。请求来源 IP 是否属于已知云厂商或数据中心 IP 段。请求路径是否呈现出“顺序链接遍历”特征比如刚访问完首页马上访问首页里所有的文章详情链接。注意这里最容易被绕过的是 User-Agent 检测。AI 爬虫完全可以通过自定义 UA 伪装成普通浏览器所以生产环境不能只依赖 UA。2.3 为什么不能只依赖 robots.txtrobots.txt 是网站主与爬虫之间的一种“君子协议”它告诉爬虫哪些路径不允许抓取。可它有几个明显的局限它只能约束遵守协议的爬虫不能约束恶意爬虫和 AI Agent。它对“AI Agent 模拟用户点击”的场景基本无效因为 Agent 可能不读取 robots.txt。它只能控制“抓取”不能控制“跟随出站链接”。不同 AI 平台对 robots.txt 的承认程度不一致。所以RequestGuard 的思路是把控制粒度从“站点级”下沉到“请求级”。即使 robots.txt 没有覆盖某条路径只要请求层的规则判定它是 AI 跟随请求就可以直接拦截。3. 环境准备与项目结构3.1 运行环境下面给出的示例以 Python 和 Flask 为例演示一个最小可运行的 RequestGuard 逻辑。环境要求如下Python 3.8 或更高版本。Flask 2.x 或 3.x本文示例使用常见的 Flask 安装方式。操作系统不限Windows、Linux、macOS 均可。生产环境建议在 Nginx 后面运行示例中不依赖 Nginx。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。3.2 项目结构我们先创建一个演示项目目录结构如下requestguard-demo/ ├── app.py # Flask 应用入口 ├── guard.py # RequestGuard 核心逻辑 ├── rules.json # 防护规则配置 ├── requirements.txt # 依赖声明 └── templates/ ├── index.html # 首页模板 └── article.html # 文章页模板包含出站链接改写示例这个结构是一个最小演示生产环境可以按模块继续拆分比如把规则文件存入数据库、把限流数据放到 Redis、把日志采集到 Elasticsearch。4. 实现一个 RequestGuard 示例4.1 创建虚拟环境并安装依赖先创建项目目录并安装 Flaskmkdir requestguard-demo cd requestguard-demo python -m venv venv # Windows venv\Scripts\activate # Linux/macOS source venv/bin/activate pip install flask如果你希望把依赖记录下来可以在安装后生成 requirements.txtpip freeze requirements.txt本文示例核心依赖只有 Flask规则解析使用 Python 标准库 json不需要额外依赖。4.2 编写规则文件 rules.json在项目根目录创建rules.json内容如下{ mode: deny, ai_uas: [ gptbot, anthropic-ai, claude, claudebot, google-extended, ccbot, perplexitybot, bytespider, amazonbot, meta-externalagent, diffbot ], rate_limit: { max_requests_per_minute: 30, window_seconds: 60 }, allowed_domains: [ https://example.com, https://docs.example.com ] }规则字段说明mode默认策略。deny表示默认拦截适合敏感站点如果站点内容希望尽量开放可以改为allow只拦截规则里明确列出的 AI UA。ai_uasAI 爬虫 User-Agent 的关键词列表。这里用的是常见 AI 爬虫标识实际使用时需要根据你日志中观察到的 UA 持续补充。rate_limit单 IP 在窗口时间内允许的最大请求数。示例是 60 秒内最多 30 次请求。allowed_domains出站链接跳转接口允许的域名白名单用于避免开放重定向漏洞。4.3 编写核心守卫逻辑 guard.py创建guard.py写入以下代码import json import time import threading from collections import defaultdict class RequestGuard: RequestGuard 核心守卫类。 负责三件事 1. 根据 User-Agent 判断请求是否来自已知 AI 爬虫。 2. 基于内存滑动窗口做单 IP 频率限制。 3. 判断出站链接是否在允许域名白名单内。 def __init__(self, rules_path): with open(rules_path, r, encodingutf-8) as f: self.rules json.load(f) # 记录每个 IP 的访问时间戳用于简单限流 self.hits defaultdict(list) self.lock threading.Lock() def _client_ip(self, request): 获取客户端真实 IP。 如果应用部署在 Nginx 后面需要读取 X-Forwarded-For 的第一个值。 生产环境还要考虑 X-Real-IP 等头部并确保你的 Nginx 配置正确。 xff request.headers.get(X-Forwarded-For, ) if xff: return xff.split(,)[0].strip() return request.remote_addr or def _is_ai_ua(self, user_agent): 判断 User-Agent 是否命中 AI 爬虫关键词。 这里使用子串匹配简单但有效。 缺点是 AI 爬虫可以伪装 UA所以它只能作为第一层防线。 ua_lower (user_agent or ).lower() for ai_ua in self.rules.get(ai_uas, []): if ai_ua.lower() in ua_lower: return True return False def decide(self, request): 对当前请求做出决策。 返回结果 - block: 拦截请求 - rate_limit: 触发限流 - allow: 允许访问 user_agent request.headers.get(User-Agent, ) # 第一层AI UA 拦截 if self._is_ai_ua(user_agent): return block # 第二层单 IP 限流 ip self._client_ip(request) max_requests self.rules.get(rate_limit, {}).get(max_requests_per_minute, 30) window self.rules.get(rate_limit, {}).get(window_seconds, 60) now time.time() with self.lock: # 清理窗口之外的记录 self.hits[ip] [t for t in self.hits[ip] if now - t window] self.hits[ip].append(now) if len(self.hits[ip]) max_requests: return rate_limit return allow def allowed_target(self, url): 校验出站链接是否允许跳转。 安全要求 - 只能允许 http/https 协议。 - 目标地址必须在 allowed_domains 白名单内。 - 禁止用户通过拼接 URL 实现开放重定向。 if not url: return False if not (url.startswith(http://) or url.startswith(https://)): return False for domain in self.rules.get(allowed_domains, []): if url.startswith(domain): return True return False def record(self, request, decision): 记录审计日志。 生产环境可以改造为输出到文件、日志采集系统或数据库。 注意需要对 IP 等敏感信息做脱敏处理。 ua request.headers.get(User-Agent, ) ip self._client_ip(request) path request.path # 脱敏示例只保留 IP 前 3 段 masked_ip ..join(ip.split(.)[:3]) .0 if ip else unknown print(ftime{time.strftime(%Y-%m-%d %H:%M:%S)} fip{masked_ip} path{path} decision{decision} ua{ua[:80]})这个类把规则判断、限流、跳转白名单、日志记录集中到了一起。需要注意示例里的限流使用的是内存字典只适合单进程演示环境生产环境必须换成 Redis 这类共享存储否则多进程下限流会失效。4.4 编写 Flask 应用 app.py创建app.py写入以下代码from flask import Flask, request, render_template, redirect, abort, jsonify from guard import RequestGuard app Flask(__name__) guard RequestGuard(rules.json) app.before_request def global_guard(): 全局请求守卫。 对静态资源和健康检查接口放行其余请求进入 RequestGuard 决策逻辑。 # 静态资源通常直接放行避免影响页面加载 if request.path.startswith(/static): return None decision guard.decide(request) if decision block: guard.record(request, block) return jsonify({error: 拒绝访问该请求被 AI 防护策略拦截}), 403 if decision rate_limit: guard.record(request, rate_limit) return jsonify({error: 请求过于频繁请稍后再试}), 429 return None app.get(/) def index(): 演示首页。 return render_template(index.html) app.get(/article/1) def article(): 演示文章详情页。 return render_template(article.html) app.get(/go) def go(): 受控出站链接跳转接口。 页面里的出站链接会改写成 /go?urlxxx由这里统一校验。 这样 AI 爬虫即使拿到链接也只会访问到 /go 接口 无法直接跳转到未授权的域名。 target request.args.get(url, ) if guard.allowed_target(target): return redirect(target, code302) guard.record(request, redirect_blocked) return jsonify({error: 目标链接不在白名单内}), 400 if __name__ __main__: # 生产环境不要使用 debug 模式 app.run(host0.0.0.0, port5000)这里有几个关键设计before_request在每次请求进入路由之前执行适合做统一拦截。静态资源直接放行避免 JS、CSS、图片被限流误伤。/go接口用于出站链接保护目标地址必须在allowed_domains白名单内。被拦截的请求统一返回 JSON便于前端处理也便于调试。4.5 编写页面模板创建templates/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleRequestGuard Demo/title /head body h1这是一个普通页面/h1 p普通浏览器可以正常访问。/p pAI 爬虫访问时会根据规则被拦截。/p p 外部链接示例 a href/go?urlhttps://example.com/article/1访问 example.com/a /p /body /html创建templates/article.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title文章详情/title /head body h1内部文章不希望被 AI 跟随的内容/h1 p这段内容是业务敏感内容不希望被 AI 爬虫抓取。/p p 相关文档链接 a href/go?urlhttps://docs.example.com/guide查看文档/a /p /body /html注意模板里的出站链接都改成了/go?url...的形式这是 RequestGuard 保护出站链接的关键一步。4.6 运行与验证在项目根目录执行python app.py看到如下输出说明启动成功* Running on http://0.0.0.0:5000接下来我们用curl模拟不同场景。场景一普通浏览器 UA 访问首页。curl -i -H User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 http://localhost:5000/预期结果返回 200页面正常渲染。场景二模拟 GPTBot 访问首页。curl -i -H User-Agent: GPTBot/1.0 http://localhost:5000/预期结果返回 403提示“拒绝访问该请求被 AI 防护策略拦截”。场景三快速连续请求触发限流。for i in $(seq 1 35); do curl -s -o /dev/null -w %{http_code}\n -H User-Agent: Mozilla/5.0 http://localhost:5000/; done预期结果前 30 次返回 200后续请求返回 429。场景四访问白名单外的出站链接。curl -i http://localhost:5000/go?urlhttps://evil.example.com/phishing预期结果返回 400目标链接不在白名单内。5. 进阶方案把保护能力延伸到链接本身5.1 出站链接的统一改写策略上面的示例里我们已经把出站链接改写成了/go?url...。在真实项目中这一步通常由模板引擎自动完成而不是手动在每一个a标签里改。比如在 Flask 中你可以注册一个模板过滤器from flask import url_for app.template_filter(safe_link) def safe_link_filter(target): return url_for(go, urltarget)然后在模板里使用a href{{ https://example.com/page | safe_link }}访问链接/a这样做的好处是所有出站链接都经过同一个决策点。后续如果规则升级只需要改/go接口的逻辑不需要改页面。日志可以统一记录 AI 爬虫对哪些出站链接发起了跟随请求。5.2 分级返回策略除了直接拦截你还可以设计分级响应策略放行AI 爬虫访问公开内容页返回完整页面。降级AI 爬虫访问受限内容时返回一个简化版页面只保留标题和摘要。挑战返回一个需要 JS 执行的验证页拦住不执行 JS 的简单爬虫。拦截直接返回 403。分级响应的好处是你不会因为一刀切拦截而失去 AI 搜索带来的流量线索同时又能保护最核心的内容。5.3 日志审计与告警大多数 AI 跟随攻击并不猛烈但它会持续发生。建议在 RequestGuard 里增加结构化日志输出至少包含以下字段字段示例说明time2025-01-01 10:00:00请求时间ip203.0.113.5客户端 IP需要脱敏path/article/1请求路径uaGPTBot/1.0User-Agent 原文decisionblock决策结果request_id8f3a2c1e链路追踪 ID生产环境可以把这些日志发送到 Elasticsearch、Loki 或云日志服务并设置告警规则比如“每小时 block 次数超过 1000”就通知管理员。6. 常见问题与排查思路6.1 问题对照表问题现象常见原因解决思路正常浏览器用户被 403User-Agent 包含 AI 关键词比如用户安装了带 AI 功能的浏览器插件检查命中日志细化 ai_uas 匹配规则AI 爬虫伪装成普通 UA 访问对方不使用标准 AI UA增加频率限制和行为特征判断不把 UA 作为唯一依据限流误伤同一个办公室 IPNAT 出口共用同一个公网 IP扩大 CDN 层真实 IP 判断或提高阈值/go跳转接口被滥用没有校验域名白名单导致开放重定向严格校验协议和域名业务地址改走内部映射表规则更新后不生效应用多进程部署内存规则未刷新或 CDN 缓存了响应规则改为动态配置或重启窗口内自动刷新验证 CDN 缓存策略拦截了但日志没有记录决策分支里忘了调用 record统一在中间件层记录不放在业务路由里6.2 误杀排查流程如果你发现正常用户被误杀建议按下面顺序排查从日志里找到被拦截请求的完整 User-Agent 和 Referer。检查命中了哪条规则。可以把ai_uas里的关键词列表打印出来和 UA 做逐一匹配。判断是否存在“子串误命中”。比如你的 AI 关键词里有ai那会误杀含email、retail的 UA。如果是浏览器插件导致 UA 包含 AI 关键词建议把匹配方式从“子串包含”改为“精确匹配”或“前缀匹配”。更新规则后用 curl 复测。6.3 限流误判排查内存版限流在开发环境就够了生产环境必须换 Redis否则会出现以下问题多进程下每个进程都有独立的hits字典限流阈值实际被放大 N 倍。应用重启后限流记录丢失恶意请求可以趁重启窗口刷新频率。单机内存存储无法在多实例部署时共享状态。如果你只有单台服务器且不想引入 Redis可以改用 Nginx 层面的limit_req模块它比应用层限流更靠近请求入口性能更高。6.4 跳转接口安全/go接口是出站链接保护的关键也是最容易被攻击的地方。攻击者可能会构造https://your-site.com/go?urlhttps://evil.com/phishing如果allowed_target只做domain in url判断攻击者可以轻松绕过例如https://your-site.com/go?urlhttps://example.com.evil.com/phishing所以生产环境建议使用urlparse解析目标地址取scheme和netloc精确比较而不是做子串包含。维护一个“业务域名表”在业务侧注册允许跳转的完整域名不允许任意域名。如果业务场景必须允许用户自定义跳转地址需要增加人工审核或关键词过滤。下面给出一个更严谨的域名校验示例from urllib.parse import urlparse def is_allowed_target(url, allowed_domains): parsed urlparse(url) if parsed.scheme not in (http, https): return False netloc parsed.netloc.lower() for domain in allowed_domains: parsed_domain urlparse(domain) if netloc parsed_domain.netloc: return True return False7. 最佳实践与工程建议7.1 默认拒绝还是默认放行规则配置的初始策略很关键。如果你的站点内容是公开的、希望被搜索引擎收录建议mode先设为allow只拦截日志里明确出现的 AI UA观察两周后再逐步收紧。反之如果是内部系统、后台管理、数据接口可以一开始就把mode设为deny只允许白名单 UA 访问。7.2 规则管理不要把规则写死在代码里。至少要做到规则文件独立于代码仓库方便运维修改。修改规则后支持优雅热加载不用重启应用。简单做法是记录文件修改时间定时重新读取。规则变更要有审计记录尤其是从“放行”改“拦截”这种高风险操作。7.3 安全边界RequestGuard 本质是一种“请求级访问控制”它不能解决所有安全问题。需要注意不要把它当成唯一的安全防线敏感数据仍然需要认证和授权。日志里的 IP 属于个人敏感信息生产环境要脱敏并在日志系统中设置访问权限。管理接口、规则调整页面需要额外的管理员认证不能让任何人随意修改拦截策略。7.4 性能优化RequestGuard 的核心决策逻辑很简单单次判断的耗时应该控制在微秒级。需要注意的点是不要在请求路径里做同步磁盘 IO比如把每条日志都立刻写数据库这会拖慢请求响应。频率统计建议用 Redis 的INCR和EXPIRE而不是在应用内存里维护一个不断增长的列表。如果站点流量非常大建议把 RequestGuard 下沉到 Nginx 层用map指令处理 AI UA 匹配用limit_req做限流避免 Python 应用层消耗过多 CPU。7.5 灰度上线建议最后是上线方式。不建议直接在所有流量上开启拦截最好分三步走先在测试环境跑通用 curl 模拟 AI UA确认拦截和放行符合预期。选择一条低风险路径比如/article/列表页灰度开启拦截观察误杀率。确认无异常后再扩展到全站同时配置告警监控 403、429 比例变化。这样即使规则有误影响范围也是可控的。从 RequestGuard 这个项目思路来看它解决的是一个非常具体的工程问题AI 请求正在从传统的“搜索抓取”演化为“会话式跟随抓取”而网站主需要一种请求级的控制手段。Demo 里的规则、限流、跳转白名单只是最小实现生产落地还有很多细节可以扩展比如接入 Redis、增加行为指纹、对接日志分析平台。建议你从自己的站点日志出发先统计有哪些 AI UA 在访问你的链接再决定要不要引入这样一道守卫。如果你也在做类似的 AI 访问控制可以从这个示例开始改造成自己的版本。