你们的团队为某个大语言模型SaaS平台开发了一项智能请求路由功能。该功能能够实时解析每条传入的请求,并决定是将这些请求发送到快速的标准模型,还是更强大的高级模型。在离线测试中,这一功能使任务完成率提高了6个百分点。

你们准备在生产环境中对其进行测试,但此时平台工程师指出了一个结构性问题:在用户层面进行随机分配是不可能的。

这个问题的根源在于因果推断机制,其本质远超单纯的技术限制。所有用户都是从同一个高级模型资源池中获取使用权限的。在这种环境下,传统的A/B测试会造成不公平的竞争环境——当路由算法仅适用于实验组时,这些用户会优先消耗高级资源,从而导致对照组的可用资源减少。

路由算法所起的作用不仅仅是改变实验组的体验,它实际上还会影响所有其他用户的资源使用情况。你们并没有真正隔离算法的影响,而是测量了路由算法与实验设计对对照组造成的“人为稀缺”效应共同作用的结果。这种测量方式存在混淆因素,并不能构成一个严谨的实验。

对于基于大语言模型的平台以及任何共享资源的产品来说,当在用户层面进行随机分配会破坏比较结果的准确性时,“切换测试”就是标准的解决方法。在这种情况下,不应再对用户进行随机分配,而是改为对时间槽进行随机安排。

整个系统会先开启人工智能路由功能运行30分钟,然后关闭该功能再运行30分钟。重复这一周期,收集足够的数据后,就可以通过对比开启与关闭算法时的实验结果来估算平均效果。

本教程将详细讲解如何使用Python来实现这种切换测试流程:包括如何从会话日志中构建时间序列数据、如何检测数据中的“延续效应”污染、如何在考虑或忽略这种效应的情况下估算直接效果、如何为时间序列数据应用HAC标准误差、如何计算自助法置信区间,以及如何利用已知真实值来验证所有估算结果。

通过学习本教程,你将能够自行在自有的大语言模型平台上进行这类分析,并且能够识别出那些会破坏测试准确性的四种情况。

目录

为什么在共享的大语言模型基础设施上进行用户级A/B测试会失败

标准的A/B测试通过随机化来实现因果推断。当通过抛硬币来决定将每个用户分配到实验组还是对照组时,两组用户在所有干扰因素上的分布通常是相同的。结果之间的差异就可以归因于所接受的不同处理方式。这种逻辑在用户之间相互独立行动的情况下是成立的。

然而,共享的大语言模型基础设施破坏了这种独立性。以查询路由为例:如果50%的用户被分配到了使用AI进行路由的组别,他们就能优先使用高级模型,从而更快地完成任务且效率更高;而剩下的50%用户则处于较低效的环境中,因为实验组用户的请求占用了更多的资源,导致高级模型的等待时间变长。对照组用户之所以体验更差,并不是因为AI路由功能出现了故障,而是因为实验设计人为地造成了这种资源短缺的情况。

这里的核心问题在于“稳定单元处理效应假设”(SUTVA)并不成立——该假设认为一个单元的结果完全取决于它所接受的处理方式。

在共享的大语言模型基础设施上,SUTVA是无法成立的。被分配到实验组的用户的操作会占用系统资源,进而影响对照组用户是否能够使用高级模型。因此,对照组已经不再是一个合适的“反事实对照组”了。

在用户级别进行随机化处理时,所估计的处理效应为:

Naive ATE = E[outcome | AI-on user] - E[outcome | AI-off user, degraded capacity]

但实际上你需要的反事实情况是:如果没有使用AI进行路由,且系统资源没有下降,那么对照组用户会遇到什么情况。但在50%的用户被分配到实验组、50%的用户被分配到对照组的这种设计下,你是无法观察到这种反事实情况的。你的估计结果将AI路由的直接效果与资源消耗带来的负面影响混在了一起,而要区分这两者,就需要知道完整的资源利用情况,但这几乎是不可能的。

其他共享资源的大语言模型平台也会出现类似的问题:例如,缓存机制可能会让实验组用户的操作速度更快,但同时占用对照组用户的缓存空间;经过微调的模型版本会消耗大量的GPU内存,从而导致对照组用户的处理速度变慢;批量处理调度系统则会优先处理使用AI进行路由的请求,从而使其他请求产生等待时间。

如何通过“切换设计”恢复公平的对比结果

由于标准的随机化机制会通过共享资源影响对照组的结果,因此“切换设计”改变了随机化的对象。在这种设计中,你不再随机分配用户,而是随机分配时间槽。

在任何给定时间内,整个平台都处于相同的处理状态:所有用户的AI路由功能要么是开启的,要么是关闭的。

治疗指标会按照预定的时间表在不同的时段之间切换,在整个实验过程中循环出现。在实验结束时,你会得到一系列时间段的数据,每个时间段都包含一个治疗指标以及相应的综合结果,比如平均任务完成率或每次会话的平均成本。你可以通过将结果与治疗指标进行回归分析,得到的系数就是你对平均治疗效果的估算值。

64756f6a-bfac-4fd3-b014-21d6ef724df4

图1:三时段切换设计的概念示意图。蓝色区域表示人工智能路由开启的时段,橙色则标出了每个周期中第一个人工智能路由关闭的时段——在这段时间内,之前时段产生的影响会人为地提升结果数值。
绿色线条代表真正的6个百分点直接效应。如果简单地比较所有人工智能路由开启的时段与所有人工智能路由关闭的时段,就会高估实际效果,因为这种方法无法区分其中直接的效应与先前时段产生的延续性影响。

由于该平台在任何一个给定的时间段内都会执行相同的操作方式,因此才能进行公平、准确的对比。同一个时间段内的所有用户都会接受相同的治疗方案。只要各时间段之间的需求条件保持一致,那些人工智能路由关闭的时段就可以作为人工智能路由开启时段的可靠对照组。

关键的问题在于“延续性效应”。如果由于某些因素,比如缓存机制的作用、在人工智能路由模式下开始但在切换后完成的会话,或者用户行为的变化,在后续的人工智能路由关闭时段中仍然存在这些效应,那么这些残留的影响就会人为地提高那些人工智能路由关闭时段的结果数值。

如果简单地对比这些受先前影响而产生的结果与直接的治疗效果,就会导致估算值偏高。如何估计并消除这种延续性效应,正是这类实验中分析上的核心挑战——这也是本教程所重点讲解的内容。

识别假设

只有满足以下四个条件,切换设计得出的估算结果才具有因果意义。

1. 不同时间段之间的延续性效应为零或有限。

一个时间段内的人工智能路由效应不会持续到后续的时间段中,从而影响对比结果的准确性。本教程中的模型仅考虑了一阶延续性效应;如果效应会持续多个时间段,那么在回归分析中就需要加入更多的滞后项。

2. 不同治疗时间段内的需求状况保持稳定。

人工智能路由开启的时段与关闭的时段应面临相似的需求条件。如果周一早上的时段总是属于人工智能路由开启的时段,而周日下午的时段总是属于关闭的时段,那么这种需求差异就会干扰对比结果,而且这种影响是无法通过滞后校正来消除的。

3. 在区块边界处不存在加速效应。

系统在每个时间槽内都会达到稳态行为。如果某个包含路由AI的时间槽的性能由于路由模型的缓存尚未被填充而低于后续时间槽的性能,那么这个加速期就会导致人们对稳态直接效应的估计出现偏低的结论。

4. 已解决了残差自相关问题。

由于需求周期、容量变化以及跨多个时间段的平台级冲击,各个时间槽内的数据残差可能会存在相关性。使用HAC标准误差或自助法置信区间可以纠正这种问题(因为普通的OLS标准误差是不够准确的)。

“当切换策略失败时”这一部分将每种失败模式与其所违反的具体假设相对应起来进行了说明。

本教程中的所有代码都可以在06_switchback/switchback_demo.ipynb这个配套笔记本中端到端运行。

先决条件

  • Python 3.11及以上版本

  • pandas 2.x版本(通过pip install pandas安装)

  • numpy 1.26及以上版本(通过pip install numpy安装)

  • statsmodels 0.14及以上版本(通过pip install statsmodels安装)

  • matplotlib 3.8及以上版本(通过pip install matplotlib安装)

请克隆配套代码仓库以获取合成数据集:

git clone https://github.com/RudrenduPaul/product-experimentation-causal-inference-genai-llm
cd product-experimentation-causal-inference-genai-llm
python data/generate_data.py

生成脚本会创建一个名为data/synthetic_llm_logs.csv的文件,其中包含50,000条关于SaaS型大语言模型产品运行情况的记录。该文件的主要列包括user_idtask_completed(二元结果)、cost_usdsession_minutes

在完成第一步的时间槽分配后,48个时间槽中每个时间槽大约包含1,042次会话记录。这个数据集能够真实反映大语言模型平台的运行情况:查询到达率、模型成本分布以及会话时长都是根据实际生产环境中的数据分布来设定的。

步骤1:构建切换时间序列

在开展切换实验时,会使用一个实时处理分配控制器,在生产环境中于每个时间槽的边界处切换路由AI的开启状态。

在本教程中,你需要通过将会话日志中的每一行对应到某个合成时间槽中,然后再将这些数据汇总到时间槽层面来构建这个时间序列。

import pandas as pd
import numpy as np

df = pd.read_csv("data/synthetic_llm_logs.csv")
print(f"数据集的形状为:{df.shape}")
print(df[["user_id", "task_completed", "cost_usd", "session_minutes"]].head(3).round(3))

# 在进行时间槽分配之前,通过随机打乱数据顺序来消除排序偏差
df = df.sample(frac=1, random_state=42).reset_index.drop=True)

# 为每个时间槽分配对应的小时编号:共有48个时间槽,每个时间槽包含约1,042次会话
df['hour_slot'] = df.index % 48

# 处理方案安排:将数据分为3个时间段的块序列(开启、开启、开启、关闭、关闭、关闭……)
# 这种安排有助于让平台有足够的时间适应每种状态,并避免ai_on与其一个时间周期后的结果之间存在完全线性相关的关系
ai_on_schedule = np.tile([1, 1, 1, 0, 0, 0], 8)   # 共48个时间槽,包含8个完整的循环
df['ai_on'] = ai_on_schedule[df['hour_slot']]

# 将数据汇总到时间槽层面:计算平均结果、平均成本、处理状态指示以及会话数量
slots = df.groupby('hour_slot').agg(
    mean_task_completed = ('task_completed', 'mean'),
    mean_cost           = ('cost_usd',       'mean'),
    ai_on               = ('ai_on',          'first'),
    n_obs               = ('user_id',         'count')
).reset_index()

print(f"\n时间槽层面的数据统计结果:共有{len(slots)}个时间槽")
print(slots[['hour_slot', 'ai_on', 'mean_task_completed', 'mean_cost', 'n_obs')).head(8).round(4))
print(f"\n启用AI的時間槽数量为:{slots['ai_on'].sum()}, 关闭AI的時間槽数量为:{(1 - slots['ai_on']).sum()}")

预期输出:

数据集结构:(50,000, 16)
   user_id  task_completed  cost_usd  session_minutes
0        0               0     0.022             7.03
1        1               1     0.008             4.07
2        2               1     0.040             8.34

时段级数据:共48个时段
   hour_slot  ai_on  mean_task_completed  mean_cost  n_obs
0          0      1               0.5950     0.0222   1042
1          1      1               0.5806     0.0223   1042
2          2      1               0.5950     0.0224   1042
3          3      0               0.6353     0.0218   1042
4          4      0               0.6017     0.0222   1042
5          5      0               0.6094     0.0218   1042
6          6      1               0.5912     0.0218   1042
7          7      1               0.5931     0.0219   1042

启用AI的时段:24个,未启用AI的时段:24个

在进行时段分配之前,首先会对数据集进行随机打乱处理,以此消除数据生成过程中可能出现的排序偏差。50,000行数据中的每一行都会通过取模运算被分配到48个虚拟时段中的一个中,整个处理流程会以3个时段为一个单元进行循环,共完成8个完整的循环。

这种3个时段为一组的结构具有双重作用:首先,它为系统提供了适应每个处理状态所需的时间;其次,它打破了当前处理结果与其前一时段结果之间的完全线性关系——如果按照纯粹的交替模式进行处理,就无法对后续时段的结果进行准确预测。经过汇总处理后,每个时段大约包含1,042个会话数据。

需要注意的是,在开始实验之前,各个时段的平均值并不会根据不同的处理方案而出现明显的分离。在原始数据中,未启用AI的时段3、4和5的完成率略高于启用了AI的时段0、1和2。这是意料之中的结果:因为在实验开始之前,处理方案的分配是随机的,因此实验结果并不具备实际意义。后续的“注入”步骤才会真正确定各处理方案的实际效果。

# 已知的实际效果被纳入模拟模型中
TRUE_EFFECT = 0.060   # 启用AI技术可使任务完成率提高6个百分点
CARRYOVER = 0.030   # 剩余的效应会延续到下一个时段

# 用合成后的平衡基础比率替换各个时段的平均值
# 这个噪声标准差是根据约1,042次伯努利分布实验得出的,能够模拟出真实的时段间需求变化情况,
# 同时确保不同处理组之间的数据不会出现失衡。
BASE_RATE = df['task_completed'].mean()
slot_noise_std = np.sqrt(BASE_rate * (1 - BASE RATE) / slots['n_obs'].iloc[0])
rng = np.random.default_rng(42)
slots['mean_task_completed'] = BASE_RATE + rng.normal(0, slot_noise_std, size=len(slots))

# 计算处理效果的滞后值:上一个时段是否启用了AI技术?
slots['ai_on_lag1'] = slots['ai_on'].shift(1).fillna(0).astype(int)

# 实际结果 = 基础结果 + 处理效果 + 上一个时段的剩余效应
slots['mean_task_completed'] = (
    slots['mean_task_completed']
    + TRUE_EFFECT * slots['ai_on']
    + CARRYOVER * slots['ai_on_lag1']
)

print("实验后的时段数据:")
print(slots[['hour_slot', 'ai_on', 'ai_on_lag1', 'mean_task_completed]].head(8).round(4))

预期输出:

注入处理后的数据结果:
   时间段  AI开启状态  AI开启滞后状态  平均任务完成率
0          0      1           0               0.6606
1          1      1           1               0.6701
2          2      1           1               0.6973
3          3      0           1               0.6402
4          4      0           0               0.5663
5          5      0           0               0.5761
6          6      1           0               0.6579
7          7      1           1               0.6811

这种注入处理方法用方差为1,042次的伯努利试验所产生的噪声来替代原始数据中的数值,从而产生时间段之间的波动,这些波动能够真实反映生产需求的变动情况,而不会导致实验组之间存在不平衡现象。

ai_on这一指标反映了哪些时间段紧接在AI开启状态之后。注入处理公式会在每一个AI开启的状态对应的时间段中添加0.060的数值;而对于那些紧跟在AI开启状态之后的时间段,则会添加0.030的数值,无论这些时间段本身的状态如何。

以第3个时间段为例:ai_on=0,但ai_on_lag1=1,因此即使AI路由功能处于关闭状态,这个时间段的结果也会增加0.030的数值。这种“延续效应”是简单模型无法察觉的。

每个周期中第一个AI关闭的状态对应的时间段确实代表了真正的关闭期,但其结果仍然会受到前一个阶段剩余的路由状态的影响而有所提升。如果简单地比较所有AI开启的状态对应的时间段和所有AI关闭的状态对应的时间段,就会将这种被提升的结果视为AI关闭状态下的正常结果,从而扭曲了实际的直接效应。

a2e1458b-751e-4e64-9e76-ea269f09de5d

图2:左图:在模拟数据集中,注入了0.6个百分点的处理效应和0.3个百分点的延续效应后,48个时间段的时间序列变化情况。橙色点标记了每个周期中第一个AI关闭的状态对应的时间段(此时ai_on=0ai_on_lag1=1),这些时间段的结果仍然会受到前一个AI开启状态对应时间段的影响而有所提升。
右图:简单的OLS分析方法(红色曲线)由于将直接效应和延续效应混为一谈,因此其计算结果比实际效应高了0.9个百分点;经过调整后的OLS分析方法(蓝色曲线)则能够得到正确的结果。两条95%置信区间都包含了代表真实效应的绿色虚线。

步骤2:简单估计(忽略时间结构的影响)

在添加任何复杂的分析方法之前,先计算一个简单的估计值:忽略时间结构的影响,仅用二进制的AI开启状态指标来回归平均任务完成率。

import statsmodels.api as sm

# 简单的OLS分析:结果 = 常数 + AI开启状态
# 不考虑滞后项,也不进行时间控制
X_naive = sm.add_constant(slots['ai_on'])
naive_model = sm.OLS(slots['mean_task_completed'], X_naive).fit()

naive_ate = naive_model.params['ai_on']
naive_se  = naive_model.bse['ai_on']

print("=== 简单估计结果(未考虑延续效应) ===")
print(f"  ATE估计值:{naive_ate:.4f}")
print(f"  标准误差:{naive_se:.4f}")
print(f"  95%置信区间:[{naive_ate - 1.96*naive_se:.4f},  {naive_ate + 1.96*naive_se:.4f}]")
print(f"\n  真实效应:{TRUE_EFFECT}")
print(f"  偏差:{naive_ate - TRUE_effect:+.4f}")

预期输出:

=== 未经调整的估计结果 ===
  真实效应量估计值:0.0607
  标准误差:0.004
  95%置信区间:[0.053, 0.068]

  真实直接效应:0.06
  偏差:+0.0007

未经调整的OLS回归仅考虑了“AI开启”这一因素对任务完成率的影响,将48个时间段视为48个相互独立的观察值,忽略了它们之间的时间顺序关系。这种估计方法得出的真实效应量估计值为0.0607,而实际直接效应为0.06,因此存在+0.0007的偏差,这一偏差几乎相当于一个百分点。

产生这种偏差的原因在于“滞后效应”在两组之间的分配方式。在“3个时间段开启/3个时间段关闭”的设计中,每个“AI开启”组中的第1和第2个时间段既会受到直接的处理效应影响(+0.060),也会受到前一个开启时间段产生的滞后效应影响(+0.030),因此它们的结果会增加到基线值的+0.090。

未经调整的模型无法区分这两种效应:它将“AI开启”时间段的高完成率完全归因于直接的处理效应。在24个“AI开启”的时间段中,有16个时间段受到了这种复合效应的影响,从而导致整个组的平均完成率远高于实际直接效应。

而对于“AI关闭”时间段而言,每个组中的第一个关闭时间段会受到+0.030的滞后效应影响,从而使该组的基线值上升。这一效应在一定程度上可以抵消“AI开启”时间段带来的影响,但由于有16个时间段受到了复合效应的影响,而只有8个时间段受到了滞后效应的影响,因此最终还是存在大约+0.009个百分点的偏差。

如果根据0.0688这个数值来制定决策,而实际真实效应仅为0.060,那么就会高估这种效应的实际影响程度,从而相对优先考虑“路由功能”这一举措。

步骤3:经过滞后效应调整的OLS回归

为了解决这个问题,需要在回归分析中加入滞后的处理指标。此时,ai_on这个变量的系数所反映的就是在保持前一期处理效果不变的情况下,当前时期处理措施所产生的直接效应,这才是我们真正想要得到的结果。

# 经过滞后效应调整的OLS回归:结果 = 常数 + ai_on + ai_on_lag1
X_adj = sm.add_constant(slots[['ai_on', 'ai_on_lag1']])
adj_model = sm.OLS(slots['mean_task_completed'], X_adj).fit()

adj_ate      = adj_model.params['ai_on']
adj_carryover = adj_model.params['ai_on_lag1']
adj_se        = adj_model.bse['ai_on']

print("=== 经过滞后效应调整的估计结果 ===")
print(adj_model.summary().tables[1])

print(f"\n  直接效应量估计值:{adj_ate:.4f}  (实际值:{TRUE_EFFECT})")
print(f"  滞后效应估计值:{adj_carryover:.4f}  (实际值:{CARRYOVER})")
print(f"  剩余偏差:{adj_ate - TRUE_effect:+.4f}")

# 我们消除了多少偏差?
removed = naive_ate - adj_ate
print(f"\n  经过调整后消除的偏差:{removed:.4f}")

预期输出:

=== 经过滞后效应调整的估计结果 ===
==============================================================================
                 系数          标准误差        t值       P值        [0.025      0.975]
------------------------------------------------------------------------------
常数项         0.5996      0.003       222.975      0.000       0.594       0.605
ai_on          0.0607      0.004       16.830      0.000       0.053       0.068
ai_on_lag1     0.0244      0.004       6.754      0.000       0.017       0.032
==============================================================================
  直接效应量估计值:0.0607  (实际值:0.06)
  滞后效应估计值:0.0244  (实际值:0.03)
  剩余偏差:+0.0007

  经过调整后消除的偏差:0.0081

经过调整的回归模型中,既包含了ai_on(当前时段的处理措施),也包含了ai_on_lag1(前一个时段的处理措施)作为自变量。

该模型现在能够分解出每个时段结果提升的各种影响因素:部分提升效果源于当前时段的人工智能调度机制,而另一部分则源自前一个时段的剩余影响。其中ai_on的系数仅能反映当前时段的直接效应。

直接效应的估计值从0.0688下降到了0.0607,这一结果与真实值0.060相差仅0.0007,且残差偏差小于标准误差。

滞后效应的估计值为0.0244,而实际滞后效应应为0.030。出现这种低估现象也是意料之中的——由于采用了三时段的区块结构,某些时段中ai_onai_on_lag1的值都为1,这就导致了轻微的共线性,从而使得滞后效应系数被削弱了。加入ai_on_lag1这一变量后,0.0088的原始偏差中有0.0081得到了纠正,因此大约92%的误差被消除了。

这两个系数的含义对于产品决策而言非常重要。ai_on的系数(0.0607)代表的是**直接效应**:即当前时段人工智能调度机制所带来的具体影响,这一影响与前一个时段的情况无关。ai_on_lag1的系数(0.0244)则代表的是**滞后效应**:即在人工智能调度机制停止作用后,这种影响仍会持续到下一个时段。在真实的大型语言模型平台上,滞后效应可能会受到会话状态、预热的推理缓存,或者跨越多个时段的用户行为变化等因素的影响。

如果添加ai_on_lag2ai_on_lag3这些变量后,AIC值依然有所下降,说明你的时间段长度短于系统的处理能力,因此还需要增加更多的滞后项。继续添加滞后项,直到AIC值不再下降为止;同时,根据你所使用的平台架构,结合领域知识来设定一个合理的滞后效应上限。

步骤4:时间序列数据的HAC标准误差

经过调整的OLS模型能够给出准确的点估计值。但它计算得出的标准误差是基于残差在时间上不相关这一假设的。

实际情况下,残差中存在一些系统性的变化因素,这些因素并未被处理指标所涵盖,比如需求周期的变化、产能变动、模型版本的更新,以及跨越多个时段的用户行为模式。这种自相关性会导致OLS计算出的标准误差偏小,从而使得t统计量的值看起来比实际情况更精确。

为了解决这个问题,应该使用异方差性和自相关性一致的标准误差,也就是Newey-West标准误差。这类方法会根据你认为具有重要影响的滞后项数量来设定带宽参数,从而纠正残差中的序列相关性。

from statsmodels.stats.sandwich_covariance import cov_hac
from statsmodels.stats.stattools import durbin_watson

# 首先检查残差是否存在自相关性
dw_stat = durbin_watson(adj_model.resid)
print(f"Durbin-Watson统计量:{dw_stat:.4f}")
print("  如果DW接近2.0,说明残差中的自相关性较弱;")
print("  如果DW小于1.5,说明存在正序列相关性;")
print("  如果DW大于2.5,说明存在负序列相关性;")
print("  不管怎样都应使用HAC标准误差——因为Durbin-Watson仅能检测AR(1)结构。")

# 应用HAC校正(Newey-West方法),滞后3个时段
hac_cov = cov_hac(adj_model, nlags=3)
hac_se  = np.sqrt(npdiag(hac_cov))

print("\n=== 标准误差对比 ===")
print(f"  OLS标准误差:{adj_model.bse['ai_on']:.4f}")
print(f"  HAC标准误差:{hac_se[1]:.4f}")
print(f"  OLS t统计量:{adj_model.tvalues['ai_on']:.2f}")
print(f"  HAC t统计量:{adj_ate / hac_se[1]:.2f}")

# 手动计算基于HAC的置信区间
hac_ci_lower = adj_ate - 1.96 * hac_se[1]
hac_ci_upper = adj_ate + 1.96 * hac_se[1]
print(f"\n  HAC 95%置信区间:[{hac_ci_lower:.4f},  {hac_ci_upper:.4f}]")
print(f"  真实效应{TRUE_EFFECT}是否在置信区间内:{hac_ci_lower < TRUE_effect < hac_ci_upper}")

预期结果:

Durbin-Watson统计量:1.9628
  当DW接近2.0时,残差中的自相关性很小;
  当DW小于1.5时,存在正序列相关性;
  当DW大于2.5时,存在负序列相关性。
  无论哪种情况都应使用HAC标准误差——因为Durbin-Watson统计量仅用于检测AR(1)结构。

=== 标准误差比较 ===
  OLS方法计算得到的ai_on标准误差:0.0036
  HAC方法计算得到的ai_on标准误差:0.0037
  OLS方法的t统计量:16.83
  HAC方法的t统计量:16.41

  HAC方法计算的95%置信区间为[0.0535, 0.0680];
  真实效应值0.06落在该置信区间内,因此调整后的估计结果是有效的。

Durbin-Watson统计量接近2.0(实际值为1.9628),这说明在这个合成数据集中,残差中的AR(1)自相关性非常小,因此HAC方法和OLS方法计算得到的标准误差几乎相同。HAC方法计算的95%置信区间[0.0535, 0.0680]包含了真实效应值0.060,这进一步证实了调整后的估计结果是有效的。

在那些需求会在连续几个小时内发生变化的生产型大语言模型平台中(例如早晨需求激增、午餐时间需求下降、晚上需求再次上升),正序列相关性会导致OLS方法计算得到的标准误差低估实际不确定性。我见过有些团队跳过这个步骤,直接报告那些经不起检验的t统计量结果,而这些结果的t值往往超过20。

在这些情况下,使用HAC方法进行校正后,这些t统计量数值会降至更为合理的水平,有时甚至会使原本“显著”的结果变为不显著的。这种变化恰恰说明HAC方法运用得当。在任何时间序列回归分析中,都应默认使用HAC方法:当不存在自相关性时,使用它不会带来任何损失;而当存在自相关性时,它才能真正起到保护作用。

nlags参数的选择需要慎重考虑。一个合理的默认值应该是你预计最大的需求周期所覆盖的时间长度。如果你的平台显示出现明显的小时时段性变化模式,并且你使用的是30分钟为间隔的时间段,那么可以将nlags设置为4或6,这样就能涵盖两到三个小时的时间范围;如果你使用的是2小时为间隔的时间段,nlags设置为2或3通常也足够了。

步骤5:使用自助法计算置信区间

HAC方法计算的标准误差是在假设自相关结构遵循某种特定参数形式的前提下进行校正的。而自助法计算的置信区间并不做这样的假设。它通过有放回地重新抽样数据,并每次都重新计算估计量,从而量化估计结果的不确定性。

def bootstrap_ci(slots, B=500, seed=7):
    """使用自助法计算置信区间,将每个时间段视为一个独立的观测值。

    每个时间段的ai_on_lag1值取自原始的数据处理方案。
    通过有放回地重新抽样这些时间段,并保持它们原有的滞后值,可以准确量化估计结果的不确定性,同时不会破坏原有的自相关结构。
    """
    rng = np.random.default_rng(seed)
    n = len(slots)
    naive_ates, adj_ates, carryover_ests = [], [], []

    for _ in range(B):
        idx = rng.integers(0, n, size=n)
        s = slots.iloc[idx]  # ai_on_lag1值保持不变

        X_n = sm.add_constant(s['ai_on'])
        naive_ates.append(sm.OLS(s['mean_task_completed'], X_n).fit().params['ai_on'])

        X_a = sm.add_constant(s[['ai_on', 'ai_on_lag1']])
        m = sm.OLS(s['mean_task_completed'], X_a).fit()
        adj_ates.append(m.params['ai_on'])
        carryover_ests.append(m.params['ai_on_lag1'])

    naive_ci = np.percentile(naive_ates, [2.5, 97.5])
    adj_ci = nppercentile(adj_ates, [2.5, 97.5])
    carryover_ci = np_percentile(carryover_ests, [2.5, 97.5])

    print(f"\n=== 使用自助法计算的95%置信区间(B={B}, seed={seed}) ===")
    print(f"  未调整的估计值:[{naive_ci[0]:.4f}, {naive_ci[1]:.4f}]  "
          f"(是否包含真实效应:{naive_ci[0] < true_effect < naive_ci[1]})")
    print(f"  调整后的估计值:[{adj_ci[0]:.4f}, {adj_ci[1]:.4f}]  "
          f"(是否包含真实效应:{adj_ci[0] < trueeffect < adj_ci[1]})")
    print(f"  滞后效应:[{carryover_ci[0]:.4f}, {carryover_ci[1]:.4f}]  "
          f"(是否包含滞后效应:{carryover_ci[0] < carryover < carryover_ci[1]})")

    return naive_ci, adj_ci, carryover_ci

naive_ci, adj_ci, carryover_ci = bootstrap_ci(slots)

预期输出:

=== Bootstrap 95%置信区间(B=500,seed=7) ===
  朴素平均处理效应        : [0.0596,  0.0783]  (覆盖范围为0.06:符合预期)
  调整后平均处理效应     : [0.0541,  0.0683]  (覆盖范围为0.06:符合预期)
  残留效应            : [0.0175,  0.0320]  (覆盖范围为0.03:符合预期)

每次自助法迭代都会重新抽取48个样本,并重新拟合朴素平均处理效应模型和调整后平均处理效应模型,同时记录各项关键估计值。这500次重复实验中第2.5百分位数和第97.5百分位数的数值,构成了Bootstrap置信区间。

每个样本都携带了原始处理计划中的ai_on_lag1值,因此每次自助法抽样都能保持原有的滞后结构。这种重新抽样的方法能够真实反映估计值的不确定性,而不会人为制造原本并不存在的时间关联关系。

这三个95%置信区间都覆盖了它们所对应的真实数值。朴素平均处理效应的置信区间[0.0596, 0.0783]虽然覆盖了真实的效应值0.060,但其范围偏大,这与+0.009的正偏差是一致的;调整后平均处理效应的置信区间更接近真实值,且范围更窄;残留效应的置信区间[0.0175, 0.0320]确实覆盖了真实的残留效应值0.030,同时排除了零值,从而证明了这种残留效应在统计学上是可以被区分出来的。

排除零值这一结果对于决策来说非常重要:如果置信区间包含了零值,那就无法排除所有观察到的AI效果提升现象只是抽样误差,而非真正的持续效应。

根据真实数值进行验证

将这三个估计值与已知的真实数值进行对比:

print("=" * 52)
print(f"{'估计方法':<30} {'估计值':>8}  {'真实值':>6}  {'偏差':>7}")
print("-" * 52)
print(f"{'朴素OLS(无滞后)':<30} {naive_ate:>8.4f}  {TRUE_EFFECT:>6.4f}  {naive_ate - TRUE EFFECT:>+7.4f}")
print(f"{'调整后OLS':<30> {adj_ate:>8.4f}  {TRUE_effect:>6.4f}  {adj_ate - TRUE_EFFECT:>+7.4f}")
print(f"{'残留效应系数':<30> {adj_carryover:>8.4f}  {CARRYOVER:>6.4f}  {adj_carryover - CARRYOVER:>+7.4f}")
print("=" * 52)

预期输出:

====================================================
估计方法                        估计值    真实值     偏差
----------------------------------------------------
朴素OLS(无滞后)                   0.0688  0.0600  +0.0088
调整后OLS                       0.0607  0.0600  +0.0007
残留效应系数                     0.0244  0.0300  -0.0056
====================================================

对比表清楚地显示了各种估计方法与已知真实数值之间的差异。

朴素OLS的估计值偏高了0.0088个百分点,这是因为它无法将直接的AI效应与导致相邻时段AI效果提升的残留效应区分开来;调整后OLS的估计值与真实值的偏差仅为0.0007,完全处于合理的置信区间范围内;残留效应系数的实际值为0.0244,而真实值为0.030。

这是一种系统性的低估:在3个时隙的块结构中,ai_onai_on_lag1之间的相关性会导致这种效应出现在所有此类设计中。

这种实际影响并不仅限于这个模拟例子。在真实的大型语言模型平台中,这种延续性效应可能会比处理效果本身更为显著。如果人工智能路由系统从根本上改变了推理集群在用户之间分配缓存时隙的方式,那么即使在路由系统关闭之后,下一个周期的计算资源分布仍然会受到该系统的影响。

在这种情况下,简单的估算方法很可能会高估当所有功能都处于开启状态时的实际效果——因为在这种情况下,不会存在任何切换行为,也不会有延续性效应的累积。

因此,在进行任何部署决策时,都必须对这种延续性效应进行准确的估算。如果它的统计显著性较高,且其数值超过了你直接通过实验获得的效应估计值的20%,那么使用简单的估算方法就是不可靠的。

当“切换回原始设置”策略失败时

“切换回原始设置”策略在四种条件下能够有效解决市场干扰问题,但在另外四种情况下则会失效。

1. 延续性效应持续的时间超过了时隙的长度。

违反的假设是:(1)延续性效应为零或有限。

如果人工智能路由系统改变了推理集群在多小时时间跨度内分配缓存的方式,那么这种延续性效应的半衰期可能会超过60分钟甚至90分钟。由于30分钟的时隙长度远远短于系统的总内存容量,因此仅仅添加一个滞后项是无法完全反映这种持续效应的。这样一来,你对实际效果的估算就会偏低,从而产生偏差。

为了诊断这个问题,你可以逐步增加滞后项的数量,然后观察AIC值是否还会继续改善。如果ai_on_lag3ai_on_lag4这些滞后项的添加仍然能使模型拟合效果得到提升,那就说明你设定的时隙长度相对于系统的内存容量来说太短了。延长时隙长度或增加更多的滞后项,实际上是在用同样的资源来换取不同的结果:即减少有效的观测数据量,从而导致置信区间变宽。

2. 需求量的非稳定性导致了时隙效应的混淆。

违反的假设是:(2)在整个处理期间,需求量应该是稳定的。

工作日早晨流量会激增,周末晚上也会出现高峰,而系统部署后的采用速度曲线也会带来不同的负载情况。如果你的处理方案将人工智能开启的时隙安排在流量较大的时段,而将人工智能关闭的时隙安排在流量较小的时段,那么处理效应系数就会同时反映出需求量的变化以及人工智能路由系统的效果。

为了解决这个问题,可以在每天内部随机调整处理方案的安排;或者在回归分析中加入时间段的固定效应——比如设置代表早晨、下午、晚上和夜间的指标,这样就可以消除那些会干扰处理效果评估的日内需求波动因素。

3. 在每个开启时段的第一个时隙会出现效应递增的现象。

被违反的假设:(3) 在区块边界处不存在性能提升现象。

在真实的大型语言模型平台上,第一个时间段内运行的AI模块通常表现不佳,后续时间段的运行效果则会更好。这是因为路由模型的缓存处于“冷启动”状态,需求预测层也没有掌握当天的查询分布情况。

如果将初始阶段的“冷启动时段”与后续的稳定运行阶段合并计算,那么初始化阶段的性能会显得较差,而稳定运行阶段的性能则相对较好;因此,通过平均值来估算效果会低估在实际全面推广后所观察到的稳定状态下的表现。通常的做法是忽略每个周期内的第一个时间段,将其视为一个“磨合期”,然后根据每个区块中第2个和第3个时间段的数据来估算最终效果。

4. 周期性自相关性会导致p值被高估。

被违反的假设:(4) 残差自相关性问题已经得到解决。

Durbin-Watson诊断方法可以初步检测是否存在自相关性,但它只能检测AR(1)阶的自相关性。而真实的大型语言模型平台的时间序列数据往往存在日周期性变化、特定时间段的日内自相关性,以及在模型版本更新后出现的结构性变化。

如果绘制出模型残差的全自相关函数图,会发现那些对应于明显需求周期的滞后值会出现峰值;这说明你在cov_hac模型中设置的nlags参数需要增加,或者你应该改用不假设任何特定自相关结构的自助法置信区间估算方法。

如果不纠正这种自相关性问题,就会导致在大型语言模型平台的回归分析中出现大量的误报结果。

还有两种与设计相关的错误机制也值得关注。

如果每个时间段的长度小于15分钟,那么平台在切换模式之间就无法完成状态清零;排队长度、正在进行的会话数量以及缓存状态都会从上一个时间段延续下来,这些因素都会加剧数据偏差,使得非AI运行阶段的数据无法真实反映系统的稳定运行状态。

如果每个时间段的长度超过4小时,那么可用于进行对比分析的处理组与对照组的数量就会减少,从而导致有效样本量变小,置信区间也会扩大,进而使得我们无法检测到真正显著的效果。

对于大多数大型语言模型平台来说,每个时间段的最佳时长为30分钟到2小时;最终确定的最佳时长需要根据早期试点数据估算出的数据延续时间来决定。

何时使用回归分析与集群随机化方法

回归分析和集群随机化方法虽然采用不同的机制来解决干扰问题,但它们能够达到相同的目的。

集群随机化方法是根据地理位置、租户ID或组织账户将用户划分为互不重叠的组别,然后同时将这些组别分配到处理组和对照组中;而回归分析方法则是在不同的时间点将所有用户分别分配到处理组和对照组中。

当可划分的组别数量足够多,并且组与组之间的相互影响可以忽略不计时,集群随机化方法效果较好。对于那些为 Enterprise 客户提供专用计算资源的大型语言模型 SaaS 平台来说,按照租户来划分组别是完全可行的:因为一个租户的路由决策不会影响到另一个租户的会话运行。

对于那些所有用户共享同一推理资源的消费级大语言模型平台来说,当容量溢出现象跨越任何你划分的用户群体边界时,集群随机化方法就无法将其有效隔离。

当这种溢出效应突破了群体边界,或者当你没有足够数量的独立集群来进行具有足够统计效力的实验时,采用“切换策略”就显得十分必要。

大多数大型平台都会同时使用这两种方法:对于那些不存在明确群体边界的平台级基础设施变更,会采用切换策略;而对于那些可以针对特定租户或地理区域进行划分的功能模块,则会使用集群随机化方法。

最终的选择取决于你能够在哪里合理地切断这些干扰因素。当系统处理请求的速度快于某个时间窗口的长度时,时间本身就可以作为划分界限;而当资源池确实不存在重叠部分时,群体身份也可以作为划分依据。如果这两种界限都不适用,那么你就需要采用因果推断方法,比如合成控制法、匹配对照组的差分分析法,或者对干扰机制进行结构建模分析。

下一步该怎么做

如果你的“切换策略”分析结果显示存在显著的正面直接效应,并且这一效应还能通过明确的滞后效应项得到解释,那么接下来就需要考虑这样一个问题:考虑到人工智能路由基础设施的建设成本,这种效应是否足以证明全面推广这一方案是合理的。高端模型的每次查询成本确实高于标准模型,因此,6个百分点的完成率提升是否能够弥补这部分额外的推理成本,这取决于你的产品的盈利模式。

对滞后效应的估算也会影响这一决策的结果。

如果滞后效应系数较大,那就意味着一旦你改用全天候运行的路由机制,部分原本观察到的效果就会消失,因此这种切换带来的不对称性也就不复存在了。在进行因果成本效益分析时,你需要使用的是直接的平均处理效应值,而不是未经滞后效应调整后的粗略估算值——也就是说,你需要考虑完成率提升所带来的收入增加、在满负荷运行状态下额外的推理成本,以及这些数值的置信区间,才能做出最终的投资决策。

如果人工智能路由机制在不同类型的查询或不同用户群体中表现出不同的效果,那么下一步的分析工作就应该是对这些差异进行建模分析,构建出一个能够预测哪些查询类型能从高端路由服务中获得最大收益的模型,这样你就可以有针对性地选择使用高级路由服务,从而以较低的成本获得最大的任务完成率提升效果。

你之前所进行的因果识别工作,包括“切换策略”的设计、滞后效应的调整以及HAC校正等,这些都能够为你提供无偏的平均处理效应值,这些数据正是校准上述建模模型所必需的基准依据。

完整的配套代码可以在这里找到:06_switchback/。其中包含了包含所有五个分析步骤的笔记本文件、用于生成图表的数据脚本,以及用于生成数据集的代码。

Comments are closed.