自动化评分(人类 vs. 脚本)
自动化评分在 0.0–1.0 区间上刻画每个用户的 API 流量有多少是脚本驱动的、有多少是人类驱动的:
HIGH(→ 1.0)——流量主要由自动脚本 / 批处理任务 / cron 驱动。
LOW(→ 0.0)——有人在交互式地使用服务(聊天界面,或 Claude Code 这类由人驱动的编码 agent)。
它是用于分诊的启发式判断,不是定论。务必结合返回的 confidence 和逐信号明细一起看。
备注
该功能会刻画单个用户的流量特征。计算一次评分要读取该用户在窗口内的 api_logs 行——他们何时发出请求、prompt 有多长、用的是哪个客户端,以及对其最新一条用户消息预先算好的形态统计(长度、字符熵和一个哈希——见信号 7)。它只能从下文的管理端点和运维 CLI 触达,用户自己无法访问,任何公开路由也无法访问。在真实流量上启用它的 deployment 应当在自己的隐私政策里说明这一点,并且应把评分当作人工决策的输入,而不是自动处置的依据。
代码位置
层 |
位置 |
|---|---|
核心评分 + 聚合 SQL |
|
Store 方法 |
|
管理端点 |
|
管理仪表盘 |
Users 标签页里的单用户按钮 + 批量「Automation」列 |
CLI |
|
评分方法和聚合每用户指标的 SQL 都在同一个模块里(serving.analytics.automation_score),由管理端点和 CLI 共用,因此两者绝不会漂移。纯评分函数不依赖数据库,单元测试在 tests/unit/test_user_automation_score.py。
输入
评分在一个回溯窗口内计算(默认 30 天;管理端点接受 1..90 之间的 days)。只统计 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);正
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 是后来以 0.15 的权重加入的,并没有改动原有的六个,所以缺少(较新的)用户消息列的用户——例如迁移之前记录下来的流量——会丢弃它,得到与之前相同的融合评分,只是 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)
depth_factor 会把子分减半,条件是该用户确实有过深度(p90 ≥ 3)多轮会话,这样一个同时也发大量一次性请求的人不会被判成脚本。
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 的 Python 移植),每个类别对应一个每请求的自动化取值:
客户端类别 |
示例 |
取值 |
|---|---|---|
交互式 / 编码 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 这一项让编码 agent 开场白(见下文)即便藏在一个很像脚本的 UA 背后,也能把先验拉向人类。
4. daily_activity_shape——日活动形态
这是最难大规模伪造的行为指纹。最多融合四个分项,并在其中数据量达标的那些上重新归一化。每一项都是 HIGH = 自动化:
分项 |
权重 |
公式 |
数据下限 |
|---|---|---|---|
小时覆盖率 |
0.20 |
|
|
小时熵 |
0.20 |
|
|
夜间休息间隙 |
0.30 |
|
|
到达间隔规律性 |
0.30 |
|
|
max_quiet_gap_hours 是不活跃钟点的最长环形连续段——人类夜间睡眠形成的间隙会把休息间隙这一分项压向 0,而 7×24 运行则完全没有间隙(→ 1)。与时区无关的几个分项(规律性、休息间隙、熵)占了大部分权重,所以用户时区未知只会平移直方图,不会扭曲结论(小时按 UTC 分桶)。
5. tool_call_human_tell——辅助信号
Agentic 的工具调用(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)
它同时驱动组合步骤里的人类硬钳制。
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 |
|
|
模板化的自动化发出的用户消息长度几乎恒定(长度离散度低)、载荷是低熵的结构化内容,并且一遍遍重发同一条消息(去重比例低 → 重复度高);交互式的人类三者都在变。由于这些列只对新流量填充,对于请求全部早于该次迁移的用户,这个信号直接不可用(被丢弃)。
这个信号把用户输入单独隔离出来,再配合 metadata->>'agent' 里的元数据和逐消息哈希,不真正改变内容就很难伪造。
信号的组合
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(评分被拉向0.5约 86%)。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 user@example.com