
1. 为什么识别机器人这件事变得越来越不靠谱做网站分析或者接口服务的人最近应该都有一股共同的焦虑后台统计里那些异常流量越来越难辨认了。放在三年前识别一个机器人爬虫是很轻松的套路基本固定——看User-Agent是不是常见浏览器、看访问频率是不是高得离谱、看IP归属地是不是集中在某个IDC网段再用点简单的正则规则就能筛掉七八成。但现在这套东西明显失灵了。我最近处理的这个流量分析需求就是典型的看起来没毛病细看全是毛病。后台日志里大量会话的User-Agent是正常的Chrome和Safari访问间隔有3到8秒的随机波动会加载图片和CSS甚至会点击站内链接跳转好几个页面。从传统特征角度判断它们和真人几乎没有区别但PV总量又明显超出正常用户的体量。把这些会话语料丢给大语言模型做语义分析之后才真正看出问题这些用户的行为路径高度一致本质上是一批被精心训练过的AI机器人流量只是它们不再靠UA伪装高频抓取这种粗暴方式来工作而是学会了模仿人类浏览的节奏和路径。这其实是当前AI机器人流量井喷的一个缩影。过去对付爬虫本质上是在和低频、机械、单薄的行为特征对抗现在则在和一个懂得人类习惯、懂得规避检测、甚至懂得伪装意图的对手对抗。传统规则的判断维度太少、又太依赖固定模式所以必然出现大量漏判和误判。而LLM切入这个场景的价值恰好在于它能从行为语义层面做判断看到的是人怎么浏览和机器人怎么浏览之间的本质差异而非单纯查UA、查频率、查IP。但要说明一点LLM分析机器人流量并不是一种银弹方案它更适合作为判定链路中的语义分析层和传统规则、黑名单、验证码配合使用。这篇文章会把我自己踩坑后沉淀下来的整套方法展开讲清楚包括数据怎么准备、特征怎么提取、Prompt怎么设计、成本怎么控制以及哪些场景下LLM的判断会翻车。2. 数据准备阶段最容易被忽略的细节从日志到会话很多人在做LLM流量分析时第一步就错了——直接把原始日志塞进Prompt让模型判断是不是机器人。这基本等于让一个从没读过你网站代码的人通过看几行碎片化的日志来猜用户行为结果必然是瞎猜。LLM分析流量的前提是它需要看到一段完整的、有时间序列的、带上下文的行为路径才能形成合理判断。2.1 原始访问日志里到底藏着什么先看一个典型的日志行我用常见的NGINX结合JSON格式举例{ time_local: 2024-11-06T09:23:1708:00, remote_addr: 203.0.113.25, request_method: GET, request_uri: /product/electronics/2456, status: 200, body_bytes_sent: 15234, http_referer: https://www.google.com/search?qwirelessheadphones, user_agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36, response_time_ms: 132 }单看一行日志你根本判断不了任何事。但如果把同一IP、同一UA、同一时间段内的多行日志组合起来就能看到很多pattern这个用户是先访问首页再去列表页然后进入详情页还是每次都绕过首页直接访问深层页面停留时间分布是否合理页面的点击路径是否符合站内导航结构这些信息才是判断机器人流量的核心依据。2.2 会话切分别把多个人的行为混在一起我推荐的做法是用IP UA 时间窗口三个维度做会话切分。时间窗口通常设置成30分钟无操作即断开超过30分钟就算新的会话。切分逻辑如下同一IP和UA的连续请求间隔小于30分钟归为同一会话若同一IP来自不同UA则直接分到不同会话同一个UA但来自不同IP也作为不同会话处理。这是一个经典、稳妥的规则。但要注意代理池的情况大量机器人流量会轮流更换出口IP导致同UA但不同IP会被拆成很多小会话每个小会话里的行为数据较少不利于LLM判断。这种情况可以适当把窗口放宽比如采用同一个UA在1小时内多次短会话合并分析的策略。from collections import defaultdict from datetime import datetime, timedelta def split_sessions(logs, window_minutes30): sessions [] groups defaultdict(list) for line in logs: key (line[remote_addr], line[user_agent]) groups[key].append(line) for (ip, ua), items in groups.items(): items.sort(keylambda x: x[time_local]) current_session [items[0]] for item in items[1:]: gap item[time_local] - current_session[-1][time_local] if gap timedelta(minuteswindow_minutes): sessions.append({ session_id: f{ip}_{ua}_{len(sessions)}, ip: ip, user_agent: ua, entries: current_session }) current_session [item] else: current_session.append(item) sessions.append({ session_id: f{ip}_{ua}_{len(sessions)}, ip: ip, user_agent: ua, entries: current_session }) return sessions这个环节做得越细后面LLM的判断就越准。我自己测试下来会话长度少于3个请求的样本LLM判断的置信度会明显下降所以这类样本我一般不会交给LLM而是直接走规则引擎处理——比如低于3次请求且命中已知爬虫UA就直接标记为机器人。2.3 特征提取把原始请求压缩成行为特征序列原始日志直接给LLM肯定不行。一方面Token消耗太大一条完整会话可能有几百条请求直接全量丢进去成本极高另一方面日志里的很多字段对判断毫无帮助反而干扰模型。所以我做了特征工程把会话压缩成一段结构化的行为摘要。下面是我实际使用的特征结构{ session_id: 203.0.113.25_Mozilla/5.0_17, duration_seconds: 486, total_requests: 23, unique_paths: 17, unique_path_ratio: 0.74, avg_interval_seconds: 12.5, interval_stddev: 3.2, entry_page: /, exit_page: /product/electronics/2456, depth_metrics: { max_depth: 4, avg_depth: 2.3 }, has_images: true, has_scripts: true, referer_distribution: { direct: 2, search_engine: 18, internal: 3 }, click_pattern: [ /, /category/electronics, /product/2456, /checkout ], time_distribution: { night_ratio: 0.2, working_hours_ratio: 0.8 }, http_status_200_ratio: 1.0, http_status_4xx_5xx: 0, requested_burst_max: 3 }几个关键字段说明一下avg_interval_seconds相邻请求的平均间隔。真人浏览通常有较长的阅读停顿几秒到几十秒不等而机器人即便模拟了延迟间隔分布也不会像人那么自然。interval_stddev间隔标准差。真人用户访问间隔标准差较大因为人会停顿、阅读、思考而机器人的间隔往往非常稳定方差很小。这是LLM判断的一个核心特征。depth_metrics页面访问深度。真人从入口页到内部页通常遵循站内导航层次而很多模型只会爬固定路径深度分布异常集中。click_pattern把请求路径抽取出来形成行为序列这是交给LLM最核心的语义信息。referer_distribution来源分布。真人用户有很大比例直接从搜索引擎或社交平台跳转而机器人则大量出现来源不明、referer为空的情况。这个特征结构并不是固定的需要根据你的网站类型调整。电商网站可以加上是否浏览商品评论是否进行搜索过滤内容站可以加平均阅读时长估算滚动行为标记B端产品则可以加是否访问了登录接口是否有表单操作。2.4 为什么我不直接拿IP黑名单和UA正则做最终判断补充一个观点传统方法不是没用而是不够用。IP黑名单和UA正则适合做前置过滤能够快速处理那些明显的机器人流量为LLM节省大量成本。我把整个判定流程设计成三层漏斗第一层黑名单层命中已知爬虫UA或已被标记的恶意IP直接拦截不进入LLM。第二层规则层在无种子知识库的前提下用阈值规则处理明显异常例如一分钟内请求超过100次标记为疑似机器人。第三层LLM语义层对中间地带样本——行为有波动、特征不明显、规则无法确定的那批会话——进行深度语义分析。这种漏斗结构让LLM只处理真正有价值的样本既控制了成本又提高了判断精度。我把这个思路展开一下设定两个阈值低于明确阈值的走规则直接判定高于明确阈值的也走规则直接判定落在模糊区间的才进LLM。整体上可能有大几百到上千个会话样本需要LLM分析完全在可控范围。3. 核心实现如何设计LLM判定链路与Prompt到了最关键的部分怎么让LLM准确地判断一个会话是真人还是AI机器人。这一章我给出一个可以直接抄作业的实现方案同时说明每步设计的原因方便你在自己项目里做调整。3.1 选型思路通用LLM API还是专用模型先说结论这项任务不需要微调专用模型但要选对模型。机器人流量的行为语义识别本质上是一个常识推理 模式识别任务通用大模型完全能胜任这属于典型的少样本判断场景微调专属模型的成本远高于收益。服务商选择方面我自己主要对比过三个方向云端商用API、自部署的开源模型、以及部分提供流量分析能力的专业服务商。实测下来如果日会话量在1万以下调用云端API是性价比最高的不需要维护推理基础设施响应速度也能接受。如果流量规模很大、且数据隐私要求高则需要考虑在内部部署开源模型。具体选型时注意几点上下文窗口不要太小的模型因为你可能输入一个包含几十条路径的会话加上分析指令上下文至少要能容纳2000到4000个Token优先选择支持结构化输出即可以输出JSON格式的模型方便后续程序解析如果希望加速处理和降低成本选择可以调整采样参数的模型如温度调低到0.2以下让输出更稳定。3.2 Prompt结构上下文、任务指令、输出约束Prompt是整个LLM分析的核心。我踩过的最大坑是刚开始只丢了一句Please determine if this is a bot结果模型输出五花八门有的给文本描述有的给置信度有的直接给一段咨询建议完全没法工程化落库。后来我把Prompt做成五段固定结构问题解决了一大半。[系统角色定义] 你是一名资深网站流量分析师擅长通过用户行为序列判断流量是否由自动化程序产生。 [背景信息] 我运营一个电商网站下面是某次访问会话的行为特征和路径信息。请根据这些信息判断该访客是真人用户还是AI机器人/自动化脚本。 [行为特征数据] {session_feature_json} [判断要点] 请从以下维度进行推理 1. 请求时间间隔的分布是否自然是否存在过于规律的请求节奏 2. 浏览路径是否符合人类在电商网站上的自然导航习惯 3. 深度访问是否过于集中或过于随机 4. 来源referer与访问行为是否逻辑自洽 5. 是否存在人类典型的犹豫、回退、比较类行为暗示。 [输出要求] 请以JSON格式输出包含两个字段 1. verdict取值为 human 或 bot 2. reason一段200字以内的中文判断理由说明你最关键的3个决策依据。这个Prompt有几点设计考量给模型一个分析角色它能自动调用流量分析领域的常识来做推理比单纯给数据要准得多。列出判断要点相当于给模型划定了关注维度避免它被无关特征带偏。明确输出格式并且限定了reason长度保证后续能稳定解析。我提过很多次但这个再强调一遍输出格式约束必须写进Prompt不能只靠调用参数。否则你后面解析JSON的代码会崩溃无数次。3.3 批量分析的主流程代码实际生产中我通常把要判断的会话写成JSONL文件每行一个会话的特征数据然后逐个调用LLM把结果写回。下面是简化后的主流程import json import os def build_prompt(session_feature: dict) - str: # 从模板中读取替换特征数据部分 with open(prompt_template.txt, r, encodingutf-8) as f: template f.read() return template.replace({session_feature_json}, json.dumps(session_feature, ensure_asciiFalse)) def analyze_one(session_feature: dict) - dict: prompt build_prompt(session_feature) # 这里假设你已接入某LLM SDK底层会自动处理密钥、重试、限流等 response llm_completion(prompt, temperature0.1, response_formatjson) try: result json.loads(response) except json.JSONDecodeError: # 解析失败时做降级处理默认回退到规则判断 result {verdict: pending, reason: LLM输出解析失败需人工复检} return result def batch_analyze(input_path: str, output_path: str): results [] with open(input_path, r, encodingutf-8) as fin: for line in fin: if not line.strip(): continue feature json.loads(line) result analyze_one(feature) result[session_id] feature[session_id] results.append(result) print(fprocessed {feature[session_id]}: {result[verdict]}) with open(output_path, w, encodingutf-8) as fout: for r in results: fout.write(json.dumps(r, ensure_asciiFalse) \n)这个批处理脚本跑一轮下来输出文件里就是所有会话的判定结果和判断理由。3.4 判定结果的下游处理链路拿到LLM判定结果之后怎么用是另一门学问。我建议不要直接把判定为bot的会话IP加到防火墙黑名单里理由是LLM的判定并非100%准确误杀一个真人用户的影响往往比放走一个机器人更大。我通常的做法是给所有判定为bot的会话打标生成一份嫌疑会话列表并存入数据库把这些IP加入一个低优先级队列在后续请求中对其实施延迟响应或验证码挑战而不是全部拒绝统计这些嫌疑流量的目标URL分布看它们是否在爬取特定数据接口或商品价格页面一小时后自动复查嫌疑IP的活动轨迹——如果该IP还保持着bot行为特征再升级到封禁名单如果转向正常访问则移除嫌疑标记。这个分级处置的链路比一刀切封IP要稳得多。真实场景里误报率大概在5%到10%左右分级处置能有效避免一个判断失误赶走一个付费用户的尴尬。4. 实测效果、翻车场景与成本控制前面讲的都是方案和代码这一章聊点更实际的东西我用这套方案跑了几个星期之后到底收到了什么效果哪些场景下LLM的判断会翻车以及怎么让成本不至于失控。4.1 一段时间数据下的测试对比我拿某段时间的访问日志做了离线测试。日志总量大概是12万次请求切分后得到约1800个有效会话。人工标注了其中200个会话作为基准集并对比了几种方案的判定效果方法召回率精确率误报率人工复核成本UA正则匹配约40%约70%中等低IP黑名单 频率规则约55%约75%中等低传统规则组合约65%约80%中等偏高低LLM语义分析约88%约93%偏低中等偏高LLM 规则漏斗约93%约96%很低较低值得说明的是LLM单跑效果已经不错但加上规则漏斗后提升更明显。因为规则层帮忙处理了大量一眼假的会话LLM的注意力全部集中在模糊样本上判断质量自然更高。从召回率来看LLM方案比传统UA正则高了将近30个百分点这意味着你能抓到更多伪装良好的机器人流量。从误报率来看LLM方案在精确率方面的优势也很明显因为我前面提到的Prompt里要求模型打分时关注行为特征的自然性而不是机械地看某一项指标。4.2 最容易翻车的三类场景LLM方案的短板也很明显我整理了实际使用中频繁出现的三类翻车现场第一类超短会话。如果一个会话只有1到2个请求LLM几乎没有信息可判断。比如用户从搜索引擎点进一篇文章读到一半关掉了页面只留下1个请求记录。这种样本LLM会倾向于判断为bot因为行为路径看起来太简单了。但实际上可能是真人。我的处理方式是少于3个请求的会话不交给LLM直接走规则层或默认标记为样本不足。第二类单页应用或SPA站。如果你的网站是React/Vue单页应用大部分页面切换发生在前端路由很多请求根本不会打到后端日志上。这时候后端日志里的行为序列天然稀疏且无规律LLM很容易把真人判断成bot。这种情况需要在前端埋点把路由变化记录一并上报或者至少把访问时长和交互事件纳入特征。没有这些数据任何模型都很难准确判断。第三类被喂过的AI爬虫。现在已经有机器人框架会先抓取你网站的sitemap再用LLM自行生成模拟人话的访问路径比如先在首页停30秒、再点进商品、再加购物车、再离开。这种行为的语义已经非常接近真人。遇到这类样本单靠行为特征很难识别必须结合更底层的证据比如TLS指纹、无头浏览器特征WebDriver标记、Canvas指纹异常等。LLM分析在这个层面的作用是发现行为异常但无法定论需要转给更专业的风控系统做深入检查。4.3 成本核算一单8000会话要花多少钱这是很多人最关心的。我按云端商用API的价格做了个粗略测算成本项估算方式单会话成本Prompt输入特征指令约1200 Token / 会话接近0.001元级别模型输出reason JSON约200 Token / 会话忽略不计单个会话总成本约1400 Token约0.001-0.002元8000个会话-约10-20元所以如果你的网站日会话量在几万级别每天全量过一遍LLM分析的API成本并不会失控。但要注意这样两个隐性成本限流与重试批量调用时需要控制并发否则容易被限流导致部分会话拿不到判定结果。建议并发数控制在10左右并增加退避重试机制。缓存重复特征如果某些会话特征高度相似可以缓存已经分析过的结果避免重复付费。实测中同一批代理IP发出的会话特征往往高度相似缓存命中率能到40%以上。成本控制上还有两个小技巧一是把不重要的字段如referer完整URL截断减少Token消耗二是用一个便宜的快速模型做初筛只有置信度不高于0.6的样本才调用更强的模型做二次判断。这种级联策略能让整体费用再降30%到50%。4.4 一些流程上的建议给准备上手这个方案的人几个操作层面的建议数据先落地再分析不要把LLM调用直接接到实时请求处理链路里延迟扛不住。正确做法是把日志先写入Kafka或数据库用离线批处理任务做周期性分析。Prompt版本管理Prompt的改动对判定结果影响很大我建议给Prompt加版本号并存进数据库。跑完一轮分析后你要知道结果是用哪个版本的Prompt产出的。定期人工抽检我会每隔几天随机抽50条被判定为bot的会话人工看一遍reason。这样能及时发现Prompt在某个新爬虫模式上的误判并及时调整判断要点。这套方案最核心的收获是你把原来靠猜的流量分析变成了可解释、可追溯、可复盘的过程。每个判定都有reason每次误判都能定位到Prompt的哪个判断维度出了偏差。这种透明性是传统正则和黑名单完全无法提供的。