对于基于大语言模型的功能而言,因果推断始于编辑们在发布任何内容之前会提出的一个问题:这一变更确实改变了相关指标的值,还是这些指标的变化只是偶然发生的呢?
假设你的团队构建了一个路由层,该层会将传入的查询分派到两个不同的模型中:置信度低于0.85的查询会被发送到高级模型,而置信度高于0.85的查询则会被发送到成本较低的简化模型。其中,高级模型的成本是低成本模型的5倍。
你的上司希望得到一个能够终结这场争论的答案:对于那些被路由到高级模型的查询来说,这种配置是否真的值得?
你无法进行严格的A/B测试,因为查询的路由规则是确定性的:置信度为0.84的查询一定会被发送到高级模型,而置信度为0.86的查询则一定会被发送到低成本模型,因此你无法随机分配这些查询。
同时,你也不能简单地比较那些被路由到高级模型的用户与那些被路由到低成本模型的用户的表现。从设计初衷来看,高级模型就是用来处理更复杂的查询的(这就是你们构建这个路由机制的原因),因此在任何模型开始处理这些查询之前,这两组用户在查询的难度上就已经存在差异了。
实际上,这个置信度阈值本身就可以被视作一个免费的实验工具。当置信度恰好为0.85时,查询的分配规则会发生改变,但位于这一阈值两侧的查询在本质上来说是完全相同的。置信度为0.849的查询与置信度为0.851的查询之间并没有实质性的差异。这两组查询在结果上的任何差异都仅仅源于路由决策本身。这就是回归不连续性设计方法所揭示的本质。
在本教程中,你将使用Python结合局部线性回归分析,通过精确的回归不连续性设计方法来估算高级路由机制对任务完成率所产生的因果效应。你会测试不同的带宽设置以验证估计结果的稳定性,还会进行操作干预诊断,以及利用二次模型来检验方法的稳健性,并为每一个估计值计算95%置信区间。
我们使用的这个包含50,000个用户的合成数据集已经预先设置了6个百分点的高级路由效果,因此你可以确认回归不连续性设计方法确实能够准确捕捉到这一效应。
配套代码:所有的代码块都可以在配套笔记本中从头到尾运行。
目录
为什么阈值路由是一种天然的实验设计
采用这种路由规则的根本目的,就是帮助团队将高级模型的预算用在最能产生效益的地方。那些置信度较低的查询才是真正需要高精度模型来处理的难题,而在这些场景下,高级模型才能发挥出最大的作用。而对于置信度较高的查询,低成本的模型同样能够轻松应对。
在问答助手中,你会看到这种基于置信度分数的路由机制;在像OpenRouter这样的多模型系统中,也会根据查询的复杂程度来决定路由方式;在内容审核环节,会依据安全评估结果来进行路由调整;而当低成本模型会导致延迟超过预设阈值时,系统也会重新进行路由分配。
无论在哪种情况下,这种机制都是一样的:使用连续的评分系统、设定明确的阈值,并根据这些规则来确定具体的路由路径。
这种设计之所以适用于因果推断,是因为用户本身无法选择使用哪种模型。当一个查询被提交后,系统会计算其置信度,然后由路由层来决定最终使用哪种模型进行处理。就在那个临界阈值处,用户的体验会从使用高级模型突然转变为使用低成本模型,而这种转变所导致的差异其实微乎其微。
需要再次强调的是,一个置信度为0.849的查询与一个置信度为0.851的查询,在本质上并没有什么区别。如果这两组查询在处理结果上存在差异,那肯定是因为路由决策的不同造成的,而查询本身是相同的。
这种“局部随机性”正是RDD在进行因果分析时所依赖的关键因素。我们不需要设置随机对照组,也不需要使用倾向得分进行分析;更重要的是,我们只需要一个明确的阈值,确保没有人能够通过操纵这个阈值来影响实验结果。
回归不连续性究竟起到了什么作用
在临界阈值处出现的这种变化,其实就是我们所关注的因果效应。产品团队就可以根据这一效应来采取相应的行动。RDD通过为两种不同的用户群体分别拟合两条回归线来分析这一现象:一条适用于置信度低于阈值的用户群体,另一条适用于置信度高于阈值的用户群体。这两条回归线在临界阈值处的垂直距离,就代表了该点上“高级模型处理”与“低成本模型处理”所带来的局部平均效果差异。
从图形上看,可以把任务完成率放在y轴上,查询的置信度放在x轴上。通常情况下,任务完成率会随着置信度的提高而增加(因为较简单的查询更容易被成功处理)。然而在恰好为0.85这个临界值时,低于阈值的用户会使用高级模型进行路由处理,而高于阈值的用户则会使用低成本模型。
如果高级模型确实能带来更好的效果,那么我们就会看到,在置信度略低于0.85的区间内,任务完成率会急剧上升;而当置信度超过0.85时,这一上升趋势又会消失。从左向右观察这个变化过程,当置信度逐渐增加时,在0.85这个临界点处,任务完成率会出现明显的下降,因为此时用户已经从使用高级模型的区域进入了使用低成本模型的区域。
图1. 概念性示意图。两条结果轨迹——一条代表使用高级模型处理的查询(置信度低于0.85),另一条代表使用低成本模型处理的查询(置信度高于0.85)——在临界阈值处相交,但它们的最终结果并不相同。这两条曲线在0.85这个临界点处的垂直距离,就是“高级模型处理”所带来的局部因果效应。
这一差异是基于以下两个假设而产生的:
-
不允许人为调整运行中的变量: 用户或系统无法精确地调整查询的置信度分数,使其恰好落在分界线之下。如果有人能够通过操纵分数让自己所处的数值略低于0.85从而获得高级路由服务,那么分界线的设定就不再具有随机性,从而导致RDD的结果出现偏差。
-
在分界点处,各种潜在结果的变化应当是连续的: 所有影响任务完成的因素(如查询类型、用户经验、使用工作区的时长、时间等因素)在置信度为0.85这一数值附近都应该呈现出连续的变化趋势。只有路由分配的结果在恰好达到这个阈值时才会发生突变。如果还有其他规则在置信度为0.85时被触发(比如不同的日志记录级别、不同的用户界面处理方式,或者重试策略等),RDD就会将这些规则的影响归因于路由决策结果。
在相信这些估计结果之前,你必须先验证这两个假设。下面的第三步就是用来检验第一个假设的;而第二个假设则是你的系统所具备的一种固有特性,你必须事先了解这一点。
有两个实际因素会影响到每个RDD的设计:带宽(即分析范围距离分界线的远近)和函数形式(线性、二次函数还是局部多项式函数)。
较窄的带宽范围能够通过将分析范围限制在接近随机分配的区域来减少潜在的偏差,但同时也会导致样本量变小。线性函数模型具有稳定性,但其前提是认为相关关系在分界线的两侧都可以用直线来近似表示。
你可以在不同的带宽设置下分别尝试使用线性和二次函数模型,以此验证这一结论是否成立。
本文中使用的RDD模型属于确定性类型,因为路由分配结果完全取决于置信度数值:当置信度低于0.85时总是会获得高级服务,而高于0.85时则总是会得到廉价服务。然而,如果分界线是概率性的,并且合规性也存在一定的不确定性,那么就需要使用工具变量方法来进行分析,这时你可以使用rdrobust这个Python包来实现这一功能。
先决条件
你需要安装Python 3.11或更高版本,并且需要熟悉pandas和statsmodels这两个库,同时还需要对线性回归以及交互项有一定的了解。
请安装本教程中提到的所有软件包:
pip install numpy pandas statsmodels matplotlib scipy
具体操作步骤如下:只需安装四个标准的科学计算Python库,再加上matplotlib用于数据可视化分析。这些都不是什么复杂的工具。
接下来,请克隆配套的代码仓库并生成合成数据集:
git clone https://github.com/RudrenduPaul/product-experimentation-causal-inference-genai-llm.git
cd product-experimentation-causal-inference-genai-llm
python data/generate_data.py --seed 42 --n-users 50000 --out data/synthetic_llm_logs.csv
数据生成过程如下:该工具会从Beta(5,2)分布中随机抽取50,000个用户,然后根据他们的query_confidence置信度分数来应用路由规则(routed_to_premium = query_confidence < 0.85),最终在task_completed字段中体现出来+6个百分点的高级路由效果。每次运行时都会使用相同的种子值和数据生成参数,因此得到的数据集都是完全一致的。
设置工作示例
该数据集模拟了一个SaaS产品,该产品会根据置信度将查询请求分配到高级模型或廉价模型中。置信度的阈值设置为0.85,而高级模型在任务完成率方面确实能带来6个百分点的提升效果。你知道这个真实结果,因此可以用来验证RDD是否能够正确地再现这一现象。
请加载数据并查看查询请求的分配情况:
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
df = pd.read_csv("data/synthetic_llm_logs.csv")
print(f"已加载{len(df):,}行数据,共{df.shape[1]}列")
print("\n查询请求的分配情况:")
counts = df.routed_to_premium.value_counts().to_dict()
print(f" 被分配到高级模型的请求(置信度<0.85):{counts.get(1, 0):,}")
print(f" 被分配到廉价模型的请求(置信度≥0.85):{counts.get(0, 0):,}")
print("\n查询请求的置信度分布:")
print(df.query_confidence.describe().round(3))
预期输出结果:
已加载50,000行数据,共16列
查询请求的分配情况:
被分配到高级模型的请求(置信度<0.85):38,874
被分配到廉价模型的请求(置信度≥0.85):11,126
查询请求的置信度分布:
count 50000.000
mean 0.715
std 0.159
min 0.078
25% 0.611
50% 0.736
75% 0.838
max 0.998
实际情况是:大约78%的查询请求的置信度低于0.85,因此这些请求被分配到了高级模型中;而Beta(5,2)分布的形态显示,其置信度值大多集中在0.85以下。剩余的22%的查询请求则是模型已经对其结果有较高把握的请求,因此它们会被分配到廉价模型中。
在进行任何回归分析之前,先来看看每个团队都可能会想做的那种简单对比:
naive = (
df[df.routed_to_premium == 1].task_completed.mean()
- df[df.routed_to_premium == 0].task_completed.mean()
)
print(f"简单的高级模型与廉价模型效果对比:{naive:+.4f} (真实值=+0.06)")
预期输出结果:
简单的高级模型与廉价模型效果对比:+0.0632 (真实值=+0.06)
实际情况是:这种简单的对比得出的结果是+0.0632,这个数值与真实值非常接近。这其实只是因为在这个特定的合成数据集中,高级模型与廉价模型的区别仅仅取决于查询请求的置信度这一因素,而且最终的结果也与置信度本身没有直接关系,而是通过分配规则来决定的。
在实际情况中,你几乎不可能这么幸运。用户的操作经验、提示语的表述方式、时间等因素都会影响查询请求的置信度以及任务完成率。因此,在真实系统中进行简单对比时,结果可能会出现50%甚至更大的偏差。而RDD能够帮助你得出不受隐藏因素影响的准确结论。
步骤1:使用局部线性回归的Sharp RDD
基本的锐化RDD估计方法实际上是一种局部线性回归分析。对于那些置信区间位于某个临界值带宽范围内的用户,可以在该区间的两侧分别拟合不同的线性斜率,然后读取在0.85这个临界点处出现的数值变化。
cutoff = 0.85
bw = 0.10
near = df[(df.query_confidence > cutoff - bw) &
& (df.query_confidence < cutoff + bw)].copy()
near["below_cutoff"] = (near.query_confidence < cutoff).astype(int)
near["rc"] = near.query_confidence - cutoff
rdd_model = smf.ols(
"task_completed ~ below_cutoff + rc + below_cut:rc",
data=near,
).fit(cov_type="HC3")
effect = rdd_model.params["below_cutoff"]
print(f"在临界点处的RDD效应(LATE):{effect:+.4f}")
print(f"标准误差(HC3): {rdd_model.bse['below_cut']:.4f}")
print(f"p值: {rdd_model.pvalues['below_cut']:.4f}")
print(f"置信区间在[0.75, 0.95)内的用户数量:{len(near):,}")
预期输出结果:
在临界点处的RDD效应(LATE):+0.0548
标准误差(HC3): 0.0131
p值: 0.0000
置信区间在[0.75, 0.95)内的用户数量:21,689
具体原理如下:该模型会在0.85这个临界点的两侧分别拟合不同的截距和斜率。below_cutoff用于表示这一分界点,而rc则表示置信区间位于临界点附近的数值。通过分析below_cut这一变量对应的系数,就可以得到在临界点处出现的数值变化,这个数值实际上代表了那些置信区间接近0.85的用户所体验到的局部平均处理效应(LATE)。最终计算得到的结果为+0.0548,这个数值处于+0.06这个真实值的抽样误差范围内。
关于模型设定需要说明三点:首先,task_completed这一变量属于二元变量,因此这里使用的是线性概率模型。对于那些在临界点处具有二元结果的RDD数据集来说,线性概率模型是标准做法,因为无论采用哪种模型,局部线性假设都是成立的。如果需要得到全局范围内的有界预测结果,也可以选择使用logit模型。
其次,之所以使用cov_type="HC3"这种设定来计算标准误差,是因为这样做可以放宽同方差性假设——而对于二元变量数据来说,这一假设几乎总是不成立的。
第三,由于这个数据集中每个用户只有一条查询记录,且不存在用户内部的聚类现象,因此这里不需要使用基于聚类的标准误差计算方法。如果数据集中每个用户有多条查询记录,那么就可以按照user_id来进行聚类分析。
接下来需要分析的是临界点附近用户的置信分布情况。图2展示了在0.80到0.90这个置信区间内,50,000条查询记录的实际分布情况:

图2. 来自这个包含50,000条记录的合成数据集的实际置信分布情况。与图1中的示意图不同,这张图直接显示了各置信分数对应的查询记录数量分布情况,并标出了临界点的位置。下方的统计结果展示了在0.80到0.90这个区间内,每个2个百分点的置信区间内分别包含多少条查询记录(具体数值分别为2,461、2,481、2,335、2,229和2,048)。这种大致均匀的分布情况说明,在临界点附近并没有任何因素导致用户数量在某一侧过度集中。
步骤2:尝试不同的带宽
选择合适的带宽非常重要。如果带宽过窄,可获得的观测数据就会太少,从而导致置信区间变得过大;而如果带宽过宽,那么你就会在对那些线性模型不再适用的区域进行推断。
最稳妥的做法就是尝试多种不同的带宽,然后判断这些估计结果是否可靠。
results = []
for bw in [0.05, 0.10, 0.15, 0.20]:
sub = df[(df.query_confidence > cutoff - bw)
& (df.query_confidence < cutoff + bw)].copy()
sub["below_cutoff"] = (sub.query_confidence < cutoff).astype(int)
sub["rc"] = sub.query_confidence - cutoff
m = smf.ols(
"task_completed ~ below_cutoff + rc + below_cutoff:rc",
data=sub,
).fit(cov_type="HC3")
results.append({
"bandwidth": bw,
"n": len(sub),
"effect": m.params["below_cutoff"],
"se": m.bse["below_cutoff"],
"p": m.pvalues["below_cutoff],
})
print(pd.DataFrame(results).round(4).to_string(index=False))
预期输出结果:
bandwidth n effect se p
0.05 11554 0.0635 0.0183 0.0005
0.10 21689 0.0548 0.0131 0.0000
0.15 29137 0.0618 0.0112 0.0000
0.20 34074 0.0614 0.0107 0.0000
具体操作过程如下:我们选择了四种不同的带宽,范围从±0.05到±0.20,然后使用相同的模型对数据进行了重新分析。这些估计值的范围都在+0.06这个真实值附近;而且随着带宽的增大,标准误差会减小,而随着带宽的缩小,标准误差会增大。所有的p值都远低于0.05。这些估计结果是否“稳定”,取决于它们对应的置信区间,而这些置信区间将通过第5步中的自助法计算得出。
步骤3:检查阈值处是否存在人为操控行为
只有当用户无法精确地调整位于阈值附近的变量值时,这种分析方法才有效。如果你的用户(或系统)能够将置信分数略微调低至0.85以下,从而迫使系统选择更高级别的路由方式,那么在阈值处就会出现数据分布的异常峰值,进而导致RDD估计结果出现偏差。
常用的诊断方法是McCrary密度检验,该方法用于检测变量值在阈值处是否会出现急剧变化。具体操作方法是将数据分为间隔为0.02%的几个区间,然后比较这些区间内的数据数量是否存在显著差异。
print("位于0.85附近、间隔为0.02%的数据区间内的人数分布:")
for lo in [0.80, 0.82, 0.84, 0.86, 0.88]:
hi = lo + 0.02
cnt = ((df.query_confidence >= lo) & (df.query_confidence < hi)).sum()
print(f" [{lo:.2f}, {hi:.2f}): n = {cnt:,}")
预期输出结果:
位于0.85附近、间隔为0.02%的数据区间内的人数分布:
[0.80, 0.82): n = 2,461
[0.82, 0.84): n = 2,481
[0.84, 0.86): n = 2,335
[0.86, 0.88): n = 2,229
[0.88, 0.90): n = 2,048
具体情况如下: 随着置信水平的提高,Beta(5,2)使得数据分布在较高置信区间内的占比增加,因此整体密度曲线会逐渐下降;当置信水平接近1.0时,密度曲线趋于平缓。在位于临界值0.84–0.86之间的区间内,并没有出现密度突然上升或下降的情况。所有5个区间中433名用户的分布情况,都符合这种密度逐渐变化的规律。
当不存在任何人为干预时,数据就应该呈现这样的模式。为了进行更严格的测试,rddensity这个Python包实现了经过偏差校正的标准误差计算方法,从而完成了McCrary程序的正式验证。
而当确实存在人为干预时,数据会呈现出不同的特征:在置信水平略低于0.85的区间内,用户数量会突然增加(因为这些用户被引导选择了更高级别的路由方式);而在置信水平略高于0.85的区间内,用户数量又会减少。如果观察到这种模式,那么使用RDD得出的估计结果就会高估实际的因果效应,因为置信水平低于0.85的用户与那些高于0.85的用户在行为动机上存在差异——前者会主动调整自己的评分,因此即使在随机路由的情况下,他们的表现也会不同。
步骤4:利用二次回归模型进行稳健性检验
near = df[(df.query_confidence > cutoff - 0.10)
& (df.query_confidence < cutoff + 0.10)].copy()
near["below_cutoff"] = (near.query_confidence < cutoff).astype(int)
near["rc"] = near.query_confidence - cutoff
near["rc2"] = near.rc ** 2
rdd_quad = smf.ols(
"task_completed ~ below_cutoff + rc + below_cutoff:rc"
" + rc2 + below_cutoff:rc2",
data=near,
).fit(cov_type="HC3")
print(f"线性RDD模型(带宽为0.10):效应系数 = +0.0548,p值 < 0.0001")
print(f"二次RDD模型(带宽为0.10):效应系数 = "
f"{rdd_quad.params['below_cutoff']:+.4f}, "
f"p值 = {rdd_quad.pvalues['below_cutoff']:.4f}")
预期输出结果:
线性RDD模型(带宽为0.10):效应系数 = +0.0548,p值 < 0.0001
二次RDD模型(带宽为0.10):效应系数 = +0.0569,p值 = 0.0036
具体解释如下:采用二次回归模型后,模型中会包含与临界值相关的平方项及交互项,这使得关系曲线在临界值的左右两侧呈现出不同的变化趋势。尽管如此,below_cutoff这个系数仍然能够捕捉到临界点处的变化,而且这种模型设定也更加灵活。
这两种模型的估计结果相差0.0022,但都接近真实的效应值+0.06,并且都在p值小于0.01的水平上具有显著性。由此可见,即使允许模型出现非线性变化,结论也不会发生变化。
当线性模型与二次模型的预测结果出现明显差异时,那就说明存在真实的可解释因素。然而,在样本量较小(例如在较窄的带宽范围内只有几千个样本)的情况下,二次模型可能会因为需要识别额外的四个参数而降低其解释能力。
标准的处理方法是增加带宽,然后重新计算这两种情况下的数值。如果即使在更宽的带宽下,这两个数值仍然不一致,那就说明线性近似方法是错误的,此时你应该报告这两个数值。
步骤5:使用自助法计算置信区间
本文中提到的每一个点估计值实际上都来自于一个有限的样本。自助法能够量化这个数值在重新抽样后会发生多大的变化,而置信区间正是用来描述这种变化的。
def bootstrap_ci(df, cutoff, bw, quadratic=False, n_reps=500, seed=7):
rng = np.random.default_rng(seed)
near = df[(df.query_confidence > cutoff - bw)
& (df.query_confidence < cutoff + bw)].copy()
near["below_cutoff"] = (near.query_confidence < cutoff).astype(int)
near["rc"] = near.query_confidence - cutoff
if quadratic:
near["rc2"] = near.rc ** 2
formula = ("task_completed ~ below_cutoff + rc + below_cutoff:rc"
" + rc2 + below_cutoff:rc2")
else:
formula = "task_completed ~ below_cutoff + rc + below_cutoff:rc"
n = len(near)
estimates = np.empty(n_reps)
for i in range(n_reps):
sample = near.iloc[rng.integers(0, n, size=n)]
m = smf.ols(formula, data=sample).fit()
estimates[i] = m.params["below_cutoff"]
return (nppercentile(estimates, 2.5), nppercentile(estimates, 97.5))
print("线性RDD(带宽=0.10):")
lo, hi = bootstrap_ci(df, cutoff, bw=0.10)
print(f"效应值 = +0.0548 95%置信区间: [{lo:+.4f}, {hi:+.4f}]")
print("\n带宽敏感性:")
for bw, eff in [(0.05, 0.0635), (0.10, 0.0548), (0.15, 0.0618), (0.20, 0.0614)]:
lo, hi = bootstrap_ci(df, cutoff, bw=bw)
print(f"带宽 = {bw:.2f} 效应值 = {eff:+.4f} "
f"95%置信区间: [{lo:+.4f}, {hi:+.4f}]")
print("\n二次项RDD(带宽=0.10):")
lo, hi = bootstrap_ci(df, cutoff, bw=0.10, quadratic=True)
print(f"效应值 = +0.0569 95%置信区间: [{lo:+.4f}, {hi:+.4f}]")
预期输出结果:
线性RDD(带宽=0.10):
效应值 = +0.0548 95%置信区间: [+0.0278, +0.0817]
带宽敏感性:
带宽 = 0.05 效应值 = +0.0635 95%置信区间: [+0.0244, +0.0986]
带宽 = 0.10 效应值 = +0.0548 95%置信区间: [+0.0278, +0.0817]
带宽 = 0.15 效应值 = +0.0618 95%置信区间: [+0.0381, +0.0823]
带宽 = 0.20 效应值 = +0.0614 95%置信区间: [+0.0420, +0.0808]
二次项RDD(带宽=0.10):
效应值 = +0.0569 95%置信区间: [+0.0205, +0.0959]
具体操作过程如下:使用自助法对那些受到带宽限制的数据进行500次有放回的重新抽样,每次抽样后都重新拟合RDD模型,并记录下每次计算得到的below_cutoff系数。这500个估计值的第2.5百分位数和第97.5百分位数共同构成了95%置信区间。每个置信区间都会包含真实的效应值+0.06,同时都不会包括0;此外,随着带宽的变化,这些置信区间也会出现重叠。
这种稳定性是通过在整个带宽范围内进行重复实验来验证的。当带宽减小时,置信区间会变宽;而当带宽增加时,置信区间会变窄。之所以二次项模型的置信区间比线性模型更宽,是因为额外的四个参数消耗了更多的自由度。
在这个数据集上,区间估计方法并没有起到排除那种简单的“+0.0632”这种估算结果的作用。这是因为数据生成机制并没有将查询置信度这一因素纳入考虑范围。在预期值方面,高级组和低级组之间唯一的差异就是那+6个百分点的路由效应本身,因此那种简单的比较其实已经接近了真实情况。
但在现实系统中,情况要复杂得多。在生产环境中,那些未被观察到的查询特征会同时影响路由分配和任务完成过程,因此那种简单的估算方法所得出的结果就会与RDD估计值产生偏差,而自举区间估计方法则能帮助我们判断哪种结果更可靠。
当回归分析出现异常时
RDD模型看起来似乎很完美,但事实上有几种特定的情况会破坏这种模型的识别能力,而每种情况都对应于上述两个假设中的一项被违反。
用户对变量值进行人为操控(这违反了假设1)。整个分析流程的前提是用户或任何上游系统都无法精确控制自己的得分结果会落在分界线的哪一侧。如果某个系统暴露了这一分界线,或者给了用户某种影响自己得分的方法(比如重试机制、提示设计之类的手段),那么RDD模型的有效性就会受到破坏。
每次在进行步骤3中的密度检查时都务必执行这项操作。如果发现有人进行了人为操控,就应该改用将阈值视为概率变量的模糊RDD模型,或者干脆放弃这种分析方法。
其他规则也在相同的分界点发挥作用(这违反了假设2)。如果你的产品还存在其他在0.85这个分界点生效的规则(比如不同的用户界面处理方式、不同的日志记录级别或重试策略),那么RDD模型就无法将路由效应与其他规则的影响区分开来。因此,你需要仔细审核所有的规则设置,找出那些与0.85这个分界点相关的规则。
阈值存在随机波动或被其他因素覆盖(这同样违反了假设1)。也许在0.85这个分界点上,路由分配并不是一个完全确定性的过程——它可能会产生一些随机的波动,或者在其他某些情况下,其他规则会优先于主规则发挥作用。
如果被分配到高级模型的结果并非由query_confidence这一变量完全决定,那么你就需要使用模糊RDD模型进行分析,这时就需要借助工具变量方法。`rdrobust`这个包能够处理这两种情况。
曲率被误认为是突变(这种情况会破坏在分界点处进行线性近似分析的前提)。RDD模型假设在线性范围内进行分析是合理的,但当实际的结果与置信度之间的关系呈现出强烈的曲线特征时,线性模型就会将这种弯曲误解为突变。
步骤4中的二次项稳健性检验是一种标准的诊断方法。如果线性模型和二次项模型的预测结果不一致,就需要扩大分析的区间范围,然后重新进行两次测试。
外推偏差(这实际上也是一个连续性问题)。RDD模型的估计结果仅适用于分界点附近的样本数据。在0.85这个分界点上观察到的+0.06效应,并不能帮助我们了解对于置信度为0.30或0.99的查询来说,高级路由机制会带来怎样的效果。
<如果你想要了解全球范围内的平均效应,那么你需要使用其他方法:倾向性评分分析、经过混杂因素调整的回归分析,或者进行实际的实验研究。>
下一步该怎么做
当你的AI功能受到连续分数及明确阈值的限制时,RDD正是合适的工具。
如果该功能的启用与否由用户自行控制,那么倾向得分方法更为适用;如果是通过在不同工作区分阶段逐步启用该功能,差异分析法就能解决这个问题;而如果这些限制规则你无法直接观察到,但其中包含随机因素,那么工具变量法就是最佳选择。
对于生产环境中的RDD分析,建议使用rdrobust这个Python包。它能够帮助你确定最优的带宽选择方案(Calonico、Cattaneo和Titiunik在2014年提出了相关方法),提供经过偏差校正的标准误差值,并内置了绘图工具。配套的rddensity包则实现了你在第三步中提到的McCrary密度检验方法。
本教程中的从头开始实现的示例展示了相关操作流程,而实际部署到生产环境时应该使用rd-packages系列工具包。
LATE方法有一个局限性:它无法告诉你那些远低于临界值的用户会受到什么影响。例如,如果在0.85这个分数下+0.06的LATE值就足以使高级路由功能继续被启用,那么这个问题就已经解决了;但如果你想了解高级路由功能会对目前被分配到低级路由渠道的简单查询产生什么影响,或者想研究那些接近最低阈值的复杂查询的情况,那么接下来就需要在这些区域进行小规模的随机试验,并根据RDD的估算结果来验证这些试验的结果。在没有足够证据的情况下,切勿盲目推广LATE方法的应用。
本教程配套的笔记本文件可以在GitHub上找到。你可以克隆这个仓库,生成合成数据集,然后运行rdd_demo.ipynb文件来重现本教程中的所有代码示例。
在实际的生产环境中,基于阈值进行路由分配是LLM系统中最为常见的做法之一,而你的系统中所采用的任何基于置信度来进行决策的路由机制,都有可能适用于RDD方法。因此,请务必运行相应的分析测试。



