自动化评分(真人 vs. 脚本)
自动化评分用 0.0–1.0 之间的分数,衡量每个用户的 API 流量更像脚本驱动还是真人驱动:
HIGH(→ 1.0)——流量主要由自动脚本 / 批处理任务 / cron 驱动。
LOW(→ 0.0)——有人在交互式地使用服务(聊天界面,或 Claude Code 这类由人驱动的编码 agent)。
它是用来初筛的启发式指标,不是定论。一定要结合返回的 confidence 和各信号的明细一起看。
备注
这个功能会分析单个用户的流量模式。每算一次评分,都要读取这个用户在时间窗口内的 api_logs 行:什么时候发的请求、prompt 有多长、用的哪个客户端,以及为最新一条用户消息预先算好的形态统计(长度、字符熵和一个哈希,见信号 7)。只有下文的管理端点和运维 CLI 能调用它,用户本人不能,任何公开路由也不能。如果部署在真实流量上使用这个功能,应当在自己的隐私政策里写明,并且只把评分当作人工决策的参考,不要据此自动处置。
输入
评分只看最近一段时间窗口内的数据(默认 30 天;管理端点的 days 参数可取 1..90)。只统计 user_id 非空的 api_logs 行,匿名流量不计入。每个用户会聚合出以下指标:
请求数
N,以及n_chat=num_user_turns IS NOT NULL的行数(聊天型请求;embedding / 原始 completion 为NULL);单轮请求数(
num_user_turns = 1)以及num_user_turns的 p90;工具调用计数(
num_tool_calls IS NOT NULL,以及> 0);大于 0 的
prompt_tokens的 25/50/75 分位数,以及n_sz=prompt_tokens > 0的行数(prompt 长度类信号靠它判断是否可用);带编码 agent 开场白的请求占比(存在
metadata->>'agent'——见下文agent_opener_override信号);按请求加权的
metadata->>'user_agent'分布;UTC 小时直方图(24 个桶);
相邻请求之间到达间隔的 25/50/75 分位数。
各个信号
评分由七个信号融合而成。每个信号把自己的原始指标映射成 [0, 1] 之间的自动化子分(HIGH = 像自动化),并带有一个默认权重。这个功能最初是围绕四个信号设计的:用户轮次、轮次长度、user-agent 和日活动形态;另有两个小的辅助信号,专门用来区分最容易混淆的一类用户(流量很大、但由真人驱动的编码 agent);user_message_shape 则专门看用户自己写的消息。
信号 |
维度 |
默认权重 |
可用条件 |
|---|---|---|---|
|
用户轮次 |
0.24 |
|
|
prompt 总长度 |
0.17 |
|
|
用户消息长度与熵 |
0.15 |
下述分项至少有 1 个可用 |
|
user-agent |
0.16 |
始终(≥ 1 个请求) |
|
日活动形态 |
0.27 |
下述时间分项至少有 1 个可用 |
|
(辅助) |
0.08 |
|
|
(辅助) |
0.08 |
|
某个用户的数据不足以算出某个信号时,这个信号会被丢弃,剩下的权重在其余信号之间重新归一化。缺失的信号绝不会按 0 补上,否则会把评分错误地拉向「真人」。这些权重加起来不必等于 1.0(实际总和是 1.15),运行时总是除以实际可用的权重之和。如果用户的请求全都发生在用户消息那几列加上之前,就只是缺少 user_message_shape:其余六个信号照常融合,confidence 会略低一些。
1. turn_pattern——用户轮次
交互式会话每次都会把越来越长的历史重发一遍,所以 num_user_turns 会按 1, 2, 3, … 往上涨;而脚本一个接一个地发互相独立的单轮 completion,几乎每个请求都是 num_user_turns = 1。
f1 = one_shot_chat_requests / n_chat # fraction stuck at 1 user turn
depth_factor = 0.5 if p90(num_user_turns) >= 3 else 1.0
sub = clamp01(f1 * depth_factor)
如果用户确实有过较深的多轮对话(p90 ≥ 3),depth_factor 会把子分减半。这样,一个真人即使同时也发了大量单轮请求,也不会被当成脚本。
2. prompt_size_dispersion——用户轮次长度
这里用的是 prompt_tokens。按 OpenAI 的语义,它是整个请求的输入总量:system prompt + 本轮重发的整段对话历史 + 工具定义 + 最新一条用户消息,而不单是用户消息本身(api_logs 里没有按角色拆分的 token 明细)。要衡量「用户轮次长度」,这是唯一能用的替代指标。模板化的自动化流量,每个请求都由固定模板拼出来,所以 prompt 总长度非常集中;而真人交互时,请求长短差别很大(先是一句话的追问,接着又粘贴一大段)。因此判别依据是总长度的稳健相对离散度(IQR / 中位数):它与量纲无关,也不会被偶尔一次粘贴的超长 prompt 带偏:
rcv = (p75 - p25) / median # over positive prompt_tokens
sub = clamp01(1 - rcv / 0.5) # rcv >= 0.5 -> 0 (human-varied); rcv = 0 -> 1 (templated)
3. client_tool_prior——user-agent
User-Agent 是最容易伪造的信号,所以它只是一个权重很低的软性先验,从不起决定作用。每个请求的 UA 都会归入一个客户端类别(逻辑移植自前端的 parseClientTool),每个类别对应这个请求的自动化取值:
客户端类别 |
示例 |
取值 |
|---|---|---|
交互式 / 编码 agent |
|
|
无法区分的 SDK |
|
|
原始 HTTP 库 / API 工具 |
|
|
未知(能识别出前导 token) |
|
|
缺失 / 无法识别的 UA |
— |
|
ua_base = request-weighted mean of the per-request class values
sub = clamp01(ua_base * (1 - 0.85 * min(agent_share, 1)))
SDK 取中性的 0.5,因为真人用的聊天界面底下也完全可能是 openai-python。有了 agent_share 这一项,即使 UA 看起来很像脚本,编码 agent 的开场白(见下文)也能把先验拉向真人。
4. daily_activity_shape——日活动形态
这是最难大规模伪造的行为指纹。最多由四个分项融合而成,只在数据量达到下限的分项之间重新归一化。每一项都是 HIGH = 自动化:
分项 |
权重 |
公式 |
数据下限 |
|---|---|---|---|
小时覆盖率 |
0.20 |
|
|
小时熵 |
0.20 |
|
|
夜间休息间隙 |
0.30 |
|
|
到达间隔规律性 |
0.30 |
|
|
max_quiet_gap_hours 是一天 24 个钟点里最长的连续无活动时段,按环形计算(跨零点也算连续)。真人夜里睡觉留下的空档会把休息间隙这一分项压向 0,而 7×24 小时运行则完全没有空档(→ 1)。与时区无关的几个分项(规律性、休息间隙、熵)占了大部分权重,所以即使不知道用户所在的时区,也只会让直方图平移,不会扭曲结论(小时按 UTC 分桶)。
5. tool_call_human_tell——辅助信号
Agent 式的工具调用(num_tool_calls > 0,来自 OpenAI 的 tool_calls / Anthropic 的 tool_use 块)是真人驱动编码循环的典型特征。它只能单向证明是真人:完全没有工具调用时,这个信号被整个丢弃(不用工具并不说明是自动化);有工具调用时,它也只能把评分拉向真人,绝不会抬高评分:
sub = clamp01(0.5 - toolcall_share) # share>=0.5 -> 0 (strongly human); share~0 -> ~0.5 (neutral)
6. agent_opener_override——辅助信号
metadata->>'agent' 是从 system prompt 的开场白("You are Claude Code, …")里解析出来的编码 agent 身份。它来自内容而不是请求头,很难无意中伪造出来,因此可以当作真人的有力证据。只有这个开场白出现在 ≥ 5% 的请求里时,这个信号才可用,因此它只能把评分拉向真人;没有它也说明不了什么:
sub = clamp01(0.15 - agent_share) # opener pervasive -> ~0 (human)
它还会触发组合步骤里的真人硬封顶(hard human clamp)。
7. user_message_shape——用户消息长度与熵
prompt_size_dispersion 看的是整个输入,这个信号看的则是用户自己写的消息。写日志时,会为每个请求里最新一条 user 角色消息预先算好三个属性,存成几个开销很小的列(在 user_message_stats 里,与 conversation_shape 并列):字符长度、香农字符熵(bit/字符),以及去掉首尾空白后文本的稳定 64 位哈希。这样算评分时就不必对 prompt 做 de-TOAST。三个分项融合时,只在达到数据下限的分项之间重新归一化,每一项都是 HIGH = 自动化:
分项 |
权重 |
公式 |
数据下限 |
|---|---|---|---|
长度离散度 |
0.40 |
|
|
单条消息熵 |
0.25 |
|
|
跨消息重复度 |
0.35 |
|
|
模板化的自动化流量,用户消息长度几乎不变(长度离散度低),内容是低熵的结构化载荷,而且同一条消息会一遍遍重发(去重比例低 → 重复度高);真人交互时这三项都会变化。这些列只对新流量写入,所以如果一个用户的请求全都早于这次迁移,这个信号就直接不可用(被丢弃)。
由于它只衡量用户自己写的消息,脚本要想在这一项上显得像人,唯一的办法就是真的让发送的内容有变化。
信号的组合
A = signals available for this user
weight_sum = sum(weight_i for i in A)
raw = sum(sub_i * weight_i for i in A) / weight_sum # re-normalized blend
# Hard human clamp: a high-volume coding-agent user with a real nightly rest gap
# can never be branded above "mixed" on volume alone.
if agent_share >= 0.3 and rest_gap_part_available and rest_gap_score < 0.5:
raw = min(raw, 0.5)
# Confidence shrinkage toward the neutral 0.5 prior for low-volume users.
alpha = N / (N + 30)
score = clamp01(alpha * raw + (1 - alpha) * 0.5)
# coverage = available weight / total signal weight, so confidence stays in [0,1].
confidence = alpha * (weight_sum / TOTAL_WEIGHT)
重新归一化(
raw)让融合结果只取决于确实有数据的信号。所以一个只调 embedding 的批量用户,即使num_*_turns列全是NULL,仍然可以根据 user-agent 和日活动形态打分。收缩(
alpha = N / (N + 30))会把流量很少的用户往中性先验0.5拉。N = 30时(alpha = 0.5),数据和先验各占一半;请求再多,数据就占上风。N = 5时alpha ≈ 0.14(评分约有 86% 被拉向0.5)。confidence综合了请求量(alpha)和实际可用的信号权重占总权重的比例(weight_sum / TOTAL_WEIGHT),所以数据稀疏的结论,以及缺少较新信号的用户,置信度都会更低。请求数
N < 5的用户会被标记为insufficient_data。
档位
最终评分会映射到一个档位标签(仅供参考,务必结合 confidence 一起看):
每档包含下界、不包含上界(SCORE_BANDS 从上往下扫描,条件是 score >= lower),所以 0.35、0.60 和 0.80 都归入分数更高的那一档:
档位 |
范围 |
|---|---|
|
|
|
|
|
|
|
|
注意事项
这只是启发式方法。没有哪个信号能单独下结论;每个信号都可能被单独伪造,所以 user-agent 的权重很低,以行为类信号为主。把评分当作初筛线索,而不是证据。
时区。小时按 UTC 分桶;设计上主要依靠与时区无关的日活动分项,所以不在 UTC 时区的真人不会被误判为夜间活跃;但真正跨多个时区使用的账号,确实会抬高小时覆盖率。
共享账号 / 角色账号。共享账号或团队账号会把真人流量和脚本流量混在一起,得到居中的「mixed」分数(这是符合预期的)。
internal/admin账号跑自动化本来就可能是正当的,而评分不区分角色,所以不要对它们做任何自动处置,看评分时也要考虑它们的角色。
用 CLI 复现
# rank the most script-like users in the last 30 days
python ops/db/analysis/user_automation_score.py --min-requests 20
# full per-signal breakdown for one user
python ops/db/analysis/user_automation_score.py --email [email protected]
代码位置
层 |
位置 |
|---|---|
核心评分 + 聚合 SQL |
|
Store 方法 |
|
管理端点 |
|
管理仪表盘 |
Users 标签页里的单用户按钮 + 批量「Automation」列 |
命令行工具 |
|
评分方法和按用户聚合指标的 SQL 都放在同一个模块里(serving.analytics.automation_score),管理端点和 CLI 共用它,所以两边的算法永远保持一致。纯评分函数不依赖数据库,单元测试在 tests/unit/test_user_automation_score.py。