ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于LLM的AI机器人流量识别:从行为语义到工程实践

基于LLM的AI机器人流量识别:从行为语义到工程实践

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:17+08: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?q=wireless+headphones", "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_minutes=30): 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(key=lambda 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(minutes=window_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节省大量成本。我把整个判定流程设计成三层漏斗:

  1. 第一层:黑名单层,命中已知爬虫UA或已被标记的恶意IP,直接拦截,不进入LLM。
  2. 第二层:规则层,在无种子知识库的前提下,用阈值规则处理明显异常(例如一分钟内请求超过100次),标记为"疑似机器人"。
  3. 第三层: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", encoding="utf-8") as f: template = f.read() return template.replace("{session_feature_json}", json.dumps(session_feature, ensure_ascii=False)) def analyze_one(session_feature: dict) -> dict: prompt = build_prompt(session_feature) # 这里假设你已接入某LLM SDK,底层会自动处理密钥、重试、限流等 response = llm_completion(prompt, temperature=0.1, response_format="json") 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", encoding="utf-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(f"processed {feature['session_id']}: {result['verdict']}") with open(output_path, "w", encoding="utf-8") as fout: for r in results: fout.write(json.dumps(r, ensure_ascii=False) + "\n")

这个批处理脚本跑一轮下来,输出文件里就是所有会话的判定结果和判断理由。

3.4 判定结果的下游处理链路

拿到LLM判定结果之后,怎么用是另一门学问。我建议不要直接把判定为bot的会话IP加到防火墙黑名单里,理由是LLM的判定并非100%准确,误杀一个真人用户的影响往往比放走一个机器人更大。

我通常的做法是:

  1. 给所有判定为bot的会话打标,生成一份"嫌疑会话列表",并存入数据库;
  2. 把这些IP加入一个"低优先级队列",在后续请求中对其实施延迟响应或验证码挑战,而不是全部拒绝;
  3. 统计这些嫌疑流量的目标URL分布,看它们是否在爬取特定数据接口或商品价格页面;
  4. 一小时后自动复查嫌疑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的哪个判断维度出了偏差。这种透明性,是传统正则和黑名单完全无法提供的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进