基于LLM的AI功能的因果推断不再是理论问题。Airbnb、Netflix、Lyft和Uber都发布了详细的工程博客文章,精确描述了它们如何衡量产品变更对用户行为的因果影响。
它们提到的技术(双重差分、断点回归、双重稳健估计等)都是标准工具。
有趣的是这些团队如何在规模化生产中落地这些方法:哪些方法在生产环境失效,他们围绕每种方法构建了什么来让估计可信,以及他们如何将这些数字与实际产品决策联系起来。
如果你正在构建LLM功能,并依据点赞率和会话时长来做产品决策,这些文章将改变你对度量的看法。
大多数团队仍然用30天A/B测试和点赞率来衡量功能影响。这种办法在需要判断指标变动是因为你的功能还是同一周发生的其他诸多因素之前是有效的。
下面这四个团队在大多数团队还没开始用LLM构建时就遇到了这个问题。在你犯同样的错误之前,理解他们最终采用的模式是值得的。我见过有的团队花数周发布功能,又花数周争论数字是否真实。这是可以避免的。
对这些组织来说,因果度量不是事后才有的想法,而是产品实验的基础组成部分,直接集成到其部署架构中。本文所做的综合梳理详细介绍了一套适用于AI产品实验的完整工具包,覆盖传统A/B测试与部署模型不兼容的各种场景。
无论你在管理全球模型切换、基于阈值的路由、分阶段上线,还是观测性主动加入数据,每种场景都需要特定的方法论。不使用这套工具包带来的不止是模糊不清。它会导致产品决策被混杂数据驱动,这比完全没有度量更有害。
本文中的每个代码块都可以在配套Notebook中端到端运行,地址为 product-experimentation-causal-inference-genai-llm/tree/main/13_case_studies/。Notebook:case_studies_demo.ipynb。
你需要:
Python 3.11或更高版本
熟悉pandas、scikit-learn和基础回归
无需事先阅读因果推断方法相关内容:每个案例研究都会在文中解释所用技术
安装本文所需的包:
pip install numpy pandas scikit-learn scipy 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
本文中的四个案例研究代码块都使用 pd.read_csv("data/synthetic_llm_logs.csv") 加载该文件。该数据集包含50,000行和16列,涵盖用户身份、会话行为和模型元数据,其中包括 user_id、session_minutes、task_completed、model_used、latency_ms 和 query_complexity 等字段。
关于衡量AI功能影响的标准说法通常是这样的:运行A/B测试并报告提升幅度。如果p值低于0.05,就可以上线。但这种说法在三个方面失效。
首先,随机化并不总是可行的。企业级SaaS产品按批次向工作区推出AI功能,绕过了A/B测试所假设的个体用户随机分配。消费类产品按地区、按队列或按平台逐步推出功能。涉及安全敏感的功能只会推送给风险画像达到一定阈值的部分用户。
当随机化不存在时,A/B测试的逻辑就失效了。你无法在非随机数据上运行同样的分析,并期望得到的估计值具有任何意义。那些既与谁获得该功能相关、又与其行为方式相关的混杂因素,会使你计算的每一个系数产生偏差,而且这种偏差往往是对该功能有利的方向。
其次,短期指标并不总能预测长期价值。一个今天让点赞评分提高8个百分点的提示词改动,可能会以某种方式增加用户对AI助手的依赖,进而在三个月后导致用户流失。一个本周提升了任务完成率的模型路由改动,在下季度出现新的查询分布时可能会表现恶化。
我最初以为短期代理指标能够可靠地反映长期趋势,但它们却并不总能做到这一点。短期A/B测试的局限性在于,它只关注即时的指标变化,却对下游用户行为变化视而不见,而这些行为变化恰恰是最关键的因素。
最后,观测数据是不可避免的。A/B测试只覆盖产品决策中很小的一部分。六个月前上线的路由阈值调整、第三季度的模型版本切换,或在闸门关闭前选择进入代理模式的用户——这些都无法在事后作为实验来运行。
对于任何需要回顾过去的问题,或者任何在伦理上无法对路由决策进行随机化的系统,你都只能基于观测日志来分析,而没有实验设计可以依赖。
观测性因果推断并不是一种退路,而是一项核心能力。那些把它视为可有可无的团队,当利益相关者质疑上季度发布的数据为何经不起推敲时,就会深刻地体会到这一点。
下面的四个团队分别构建了应对上述三个问题中一个或多个的系统。
正如Jenny Chen在Airbnb技术博客文章《Airbnb如何衡量未来价值以标准化权衡》中所描述的,Airbnb的工程团队在其实验基础设施上遇到了一个根本性问题。标准的A/B测试在实验窗口结束时(通常为14到30天)衡量结果。
对于影响用户行为长达数月甚至数年的市场功能来说,这个窗口太短了。一个让30天预订量上升的功能,可能只是加速了用户本来就会表现的行为,也可能只是提前释放了需求,或者确实增加了新的长期参与。仅凭30天指标无法区分这些情况。
这个问题在LLM场景中的体现就是助手依赖问题。一个让你的AI助手更加简洁和自信的提示词重新设计,通常会立即提高点赞评分和任务完成率。用户更喜欢自信、直接的答案。但如果这种重新设计同时也降低了用户独立核实验证答案的可能性,那么你可能是以牺牲准确性和长期信任为代价来改善短期体验。
当用户因为助手两次给出自信的错误答案而开始流失时,提示词改动早已上线,它与流失信号之间的关联也已不可见。我见过这种差距让团队花费数月的诊断工作,试图将提示词改动与模型更新和季节性行为区分开来。
你不需要等待长期结果的出现。你需要的是从之前的队列中估计出,哪些短期信号能够可靠地预测长期的留存和收入。Airbnb的解决方案是利用基于这种历史关系训练出的预测模型,将短期信号转化为预估的长期价值。
在他们的语境中,该指标是一个“未来价值”分数,根据用户当前的参与模式来估计其长期预订贡献。一旦你有了这个模型,你就可以通过实验对未来价值的预期影响来评估任何实验,将30天指标作为多个输入之一。实验窗口保持较短,而评估的时间跨度则可以延伸到预测模型所能达到的最远范围。
参考实现中的DiD步骤需要一个识别假设:处理前的平行趋势。在功能上线之前,两个队列必须处于等价的行为轨迹上。如果第一批用户在没有该功能的情况下已经趋向于更高的留存率,那么DiD估计就会将功能效应与队列之间预先存在的差异混在一起。大多数团队都会跳过对这一假设的验证,因为它需要绘制前期趋势图,而这只需20分钟,在结果不合理之前大家都会觉得没必要。
对于LLM团队来说,相应的做法需要两件事。首先,你需要长期用户价值的先行指标:第7周留存率和回访查询率。其次,你需要将这些先行指标与你真正关心的长期结果(收入和用户生命周期)联系起来的历史数据。这个关联模型在历史队列上训练一次,然后应用于新的实验。
下面的代码展示了结构模式:根据短期信号计算每个用户的未来价值代理指标,然后将其作为DiD或IPW分析中的结果变量,替代即时的任务完成信号。
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
# Synthetic LLM telemetry with retention signal
df = pd.read_csv("data/synthetic_llm_logs.csv")
# Step 1: Train the future-value proxy model on a historical cohort.
# In production this model is trained on users old enough that
# their long-term outcome (e.g., 90-day retained revenue) is known.
historical = df[df.signup_week < 10].copy()
feature_cols = ["task_completed", "thumbs_up", "session_minutes"]
X_hist = historical[feature_cols].fillna(0)
y_hist = historical["retained_7d"].values # 7-day retention as long-term proxy
fv_model = LinearRegression().fit(X_hist, y_hist)
# R² computed on training data; use a holdout cohort in production
print("Future-value model R²:", round(fv_model.score(X_hist, y_hist), 3))
# Step 2: Score all users with the future-value proxy.
X_all = df[feature_cols].fillna(0)
df["future_value_score"] = fv_model.predict(X_all)
# Step 3: Compare future_value_score by wave (this is the real experiment outcome).
print("\nMean future-value score by wave:")
print(df.groupby("wave").future_value_score.mean().round(4))
# Step 4: The DiD effect on future value (rather than on task_completed).
# This is where you would plug future_value_score into your DiD regression.
analysis = df[df.signup_week < 30].copy()
analysis["post"] = (analysis.signup_week >= 20).astype(int)
analysis["treated"] = (analysis.wave == 1).astype(int)
cells = analysis.groupby(["treated", "post"]).future_value_score.mean()
did_fv = (
(cells.loc[(1, 1)] - cells.loc[(1, 0)])
- (cells.loc[(0, 1)] - cells.loc[(0, 0)])
)
print(f"\nDiD effect on future-value score: {did_fv:+.4f}")
预期输出:
Future-value model R²: 0.024
Mean future-value score by wave:
wave
1 0.6325
2 0.6271
Name: future_value_score, dtype: float64
DiD effect on future-value score: +0.0059
情况是这样的:你在一个已知长期结果的历史队列上训练一个轻量级线性模型,将可观察的短期信号映射到7日留存率,作为未来价值的代理指标。
你用该模型对所有用户进行评分,然后将未来价值评分作为标准双重差分(DiD)中的结果变量。7日留存率是一个不完美的代理指标,但它迫使分析根据短期互动与持久价值的历史相关性对其进行加权,这是点赞率所做不到的。
0.024的低R²值是有意为之,因为它凸显了将即时会话数据与7日留存率关联时存在的固有噪声。虽然生产系统理想情况下应利用具有更高预测能力的信号(如回访率或查询深度),但即使是精度较低的关联模型仍然可以提供价值。
主要目标是确定校正的正确方向,而非实现绝对精确。
Airbnb框架是对测量时间跨度问题的直接回应。当你以30天或14天的时间窗口评估AI功能时,你奖励的是那些能快速推动用户行动的功能,而不论用户被推向何方。
为长期价值的先行指标构建度量体系,并不需要更长的实验周期,而是需要更丰富的测量模型。已经构建了这种能力的团队,会运行更少的那些在第2周表现出色但在第4个月令人失望的实验。
如果关联模型尚未成为你基础设施的一部分,开发一个关联模型应当优先于扩展你的评估仪表盘,成为你的当务之急。
Netflix技术博客上的文章《Netflix准实验的关键挑战》是关于产品团队因果推断的实用文章之一。其核心贡献在于一套分类体系:针对每种部署场景,都有对应的因果方法,并且文章指出了与之相伴的识别假设和失败模式。
这种框架很重要,因为大多数团队并不是根据部署结构来选择方法。他们选择的是自己已经熟悉的方法,而这往往并不合适。
图1:部署结构决定了哪种识别策略是可信的。阈值路由系统需要断点回归设计(RDD),而自愿参与的分析需要倾向性方法。分配机制主导着方法选择,团队偏好的估计量则居于其次。
Netflix的分类体系涵盖了四种场景,这些场景几乎与LLM团队遇到的情况完全对应:
分阶段发布(他们的场景:逐步进入市场)对应双重差分法。当你先于工作区队列B向工作区队列A发布AI功能时,你就拥有了跨时间的自然处理组和对照组。识别策略从结果差异中减去共同的时间趋势。
关键假设是两个队列在治疗前具有平行趋势。如果某个队列在治疗开始前就已经呈现上升趋势,该方法就无法将其与真实效应区分开来。
基于阈值的路由(他们的场景:地理评分截断)对应断点回归设计。当一个连续分数决定了用户接收哪个模型或功能时,刚好低于和刚好高于阈值的用户在所有方面几乎相同,除了所接受的处理不同。
断点处的跳跃识别出局部平均处理效应(LATE):即仅针对阈值附近用户的因果效应,而所有用户的平均处理效应不在其范围之内。关键假设是用户无法精确操控该分数。
全量用户升级(他们的场景:平台范围的政策变更)对应合成控制设计。当所有用户同时获得新模型,且没有留出对照组时,你需要构建历史或合成反事实的加权组合,来估计如果没有升级会发生什么。
关键假设是合成控制能够很好地拟合治疗前时期。事前拟合不佳并不是一个小麻烦,它会使整个反事实推断失效。
匹配比较(他们的场景:自愿采用功能)对应倾向性评分方法。当用户自我选择使用AI功能时,你通过重新加权或重新匹配对照组,来在可观测变量上近似随机分配。
关键假设是所有相关的混杂因素都被观察到。如果选择使用功能的用户同时也在某些你未测量的方面倾向于重度用户,那么你的混杂因素调整就不完整,你的估计会以事后难以检测的方式产生偏差。
这套分类体系使方法选择成为一种结构化的查找过程:描述你的部署结构,然后找到你的设置最有可能满足其假设的方法。
我见过一些团队跳过这一步,花了两周时间在明显属于阈值路由问题的数据上运行双重差分法。两种估计结果相差40%。两者都没有错,只是他们在回答不同的问题。
下面的代码将分类体系实现为一个决策函数:给定部署场景描述,输出相应的方法及其关键假设。
TAXONOMY = {
"staged_rollout": {
"method": "Difference-in-Differences (DiD)",
"assumption": "Parallel pre-treatment trends between treated and control cohorts",
"check": "Plot weekly means by cohort before treatment starts; "
"run pre-trend placebo regression",
"failure_mode": "Non-parallel pre-trends, time-varying confounders, "
"staggered adoption without Callaway-Sant'Anna correction",
},
"threshold_routing": {
"method": "Regression Discontinuity Design (RDD)",
"assumption": "Users cannot precisely manipulate their score across the cutoff",
"check": "McCrary density test; bandwidth sensitivity; "
"quadratic spec robustness",
"failure_mode": "Score manipulation, other policies firing at same cutoff, "
"extrapolation bias away from the cutoff",
},
"full_population_upgrade": {
"method": "Synthetic Control",
"assumption": "Pre-treatment fit between actual and synthetic counterfactual is good",
"check": "In-time placebo tests; in-space placebo tests; "
"plot pre-period fit",
"failure_mode": "Poor pre-period fit, interference between donor units, "
"post-treatment structural breaks",
},
"opt_in_feature": {
"method": "Propensity Score Methods (IPW / Matching)",
"assumption": "All confounders that drive opt-in and affect outcome are observed",
"check": "Standardized mean difference before and after weighting; "
"propensity overlap histogram",
"failure_mode": "Unmeasured confounders, positivity violations, "
"propensity model misspecification",
},
}
def select_method(scenario: str) -> None:
if scenario not in TAXONOMY:
valid = ", ".join(TAXONOMY.keys())
print(f"Unknown scenario. Valid options: {valid}")
return
entry = TAXONOMY[scenario]
print(f"Scenario: {scenario}")
print(f"Method: {entry['method']}")
print(f"Assumption: {entry['assumption']}")
print(f"Key checks: {entry['check']}")
print(f"Failure modes: {entry['failure_mode']}")
# Example: staged AI feature rollout across enterprise workspaces
select_method("staged_rollout")
print()
# Example: confidence-threshold routing between model tiers
select_method("threshold_routing")
预期输出:
Scenario: staged_rollout
Method: Difference-in-Differences (DiD)
Assumption: Parallel pre-treatment trends between treated and control cohorts
Key checks: Plot weekly means by cohort before treatment starts; run pre-trend placebo regression
Failure modes: Non-parallel pre-trends, time-varying confounders, staggered adoption without Callaway-Sant'Anna correction
Scenario: threshold_routing
Method: Regression Discontinuity Design (RDD)
Assumption: Users cannot precisely manipulate their score across the cutoff
Key checks: McCrary density test; bandwidth sensitivity; quadratic spec robustness
Failure modes: Score manipulation, other policies firing at same cutoff, extrapolation bias away from the cutoff
每种部署场景都有对应的分析方法、一个主要的识别假设、用于检验该假设是否成立的诊断方法,以及会导致分析失效的故障模式。
该函数是一个决策辅助工具,它让方法选择这一步骤变得明确,从而使团队在编写任何回归代码之前就达成识别策略的一致。如果缺乏这种一致,你通常会在分析中途发现团队中有两个人实际上是在同一份数据上隐式运行着不同的因果模型。
大多数团队会挑选自己最熟悉的因果方法。这是错误的启发式,Netflix 分类学正是为了打破这种惯性而存在的。
一个具备双重差分经验的 LLM 团队,即使他们正在运行的是一个阈值路由系统——在这种情况下断点回归能给出更清晰的答案,以及一个有依据的局部处理效应估计,而不是一个平均化的猜测——他们还是会倾向于使用双重差分。
这一分类学凸显了一个至关重要的原则:方法的选择是由分配机制本身决定的,而不是由团队的熟悉程度决定的。如果你的分配机制是一个切分分数线,那么断点回归就是首先应当尝试的工具,无论团队已经会运行什么方法。
搞错这一点不仅会产生更嘈杂的估计,还会产生一个结构上无效的估计,而更干净的数据也无法修复它。
Shima Nassiri 在 Lyft 工程博客上发表的文章《信任不可测试之物:双重稳健模型的验证与诊断》以一个实际观察为起点:在大多数真实的生产环境因果分析中,至少有一个干扰模型带有设定误差。
当你运行一项观察性因果分析时,几乎总是要拟合两个模型:一个倾向性模型(根据协变量预测处理分配)和一个结果模型(根据处理与协变量预测结果)。
这两个模型都是未知真实函数的近似。如果其中任何一个在你尚未考虑到的方面出错了,你的因果估计就会有偏差,而单凭标准输出你不会意识到这一点。
双重稳健估计,特别是增强的逆概率加权估计量,正是对这一问题的回应。增强的逆概率加权估计量将倾向性加权与回归调整相结合:只要倾向性模型或结果模型中有任何一个被正确设定,增强的逆概率加权估计量就是一致的。一个设定良好的模型就足够了。
尽管如此,增强的逆概率加权估计量并不能抵御未观测混杂因素的影响,它仍然需要无混杂性假设:所有同时影响处理分配和结果的因素都必须被观测到并纳入模型。如果你的数据中缺少一个关键的混杂变量,增强的逆概率加权估计量也救不了你。
Nassiri 的文章所涉及的远不止估计量本身。它之所以具有实际重要性,在于文中描述了一套诊断工具,用于在对观察性分析采取行动之前对其进行验证。
在干净的随机化实验中,你可以检查协变量平衡并进行功效计算。而在观察性研究中,你必须付出更多努力,因为设计本身不带有随机化保证。我见过一些团队跳过了这一诊断步骤,然后花了数周时间解释为什么他们的因果估计偏差了两倍。
该流程会执行四项检查:
拟合倾向性模型后,绘制 IPW 权重的分布图。极端权重(例如超过 20 或 30)表明某些用户的倾向性接近于零,这违反了积极性假设:每个单元都必须具有非零的概率被分配到处理组或对照组。
这些用户缺乏可比较的反事实,如果让一个不寻常的观测主导你的因果结论,就会削弱分析的可靠性。跳过这一检查,恰恰就是一个行为异常的重度用户就能使平均处理效应偏移 15 个百分点的原因。
设定一个最大权重。任何权重超过修剪阈值的观测都会被降至阈值水平。常见的选择是权重分布的第 95 或第 99 百分位数。
修剪是用少量偏差换取方差的大幅降低,使估计在轻微的模型设定误差下更加稳定。如果你不进行修剪,就相当于任由数据中最诡异的边缘案例驱动你的头条数字。
绘制倾向性模型中每一个协变量在加权前后的标准化均值差。目标是加权后 |SMD| < 0.1。
加权后仍然高于该阈值的协变量表明,倾向性模型遗漏了该协变量对处理分配的影响。这一检查正是用来捕获“我们已经调整了所有变量”这一盲点的。
选取一个你的处理被证明不会导致的结果,例如处理存在之前的预处理指标,然后在其上运行完整的增强的逆概率加权估计量流程。
如果流程对安慰剂结果返回了显著效应,那么你就遇到了问题:未观测混杂因素、倾向性模型设定错误或数据泄漏。安慰剂失败是分析不可信的最清晰信号之一,而且你在发布任何东西之前就能获得这一信号。
下面的代码展示了 Lyft 流程在信任任何因果估计之前所应用的权重分布检查和修剪步骤。
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
df = pd.read_csv("data/synthetic_llm_logs.csv")
# Estimate propensity for opt-in to agent mode
X = pd.get_dummies(
df[["engagement_tier", "query_confidence"]], drop_first=True
).astype(float)
y = df["opt_in_agent_mode"]
ps_model = LogisticRegression(max_iter=1000).fit(X, y)
df["propensity"] = ps_model.predict_proba(X)[:, 1]
# ATE weights: 1/P(treat) for treated, 1/(1-P) for control
df["ipw"] = np.where(
df.opt_in_agent_mode == 1,
1 / df.propensity,
1 / (1 - df.propensity),
)
# Diagnostic 1: weight distribution
print("IPW weight percentiles:")
for p in [50, 75, 90, 95, 99]:
print(f" {p}th pct: {np.percentile(df.ipw, p):.2f}")
fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(df.ipw, bins=60, edgecolor="none", alpha=0.7)
ax.axvline(np.percentile(df.ipw, 99), color="red", linestyle="--",
label="99th pct (trim threshold)")
ax.set_xlabel("IPW weight")
ax.set_ylabel("Count")
ax.set_title("Weight distribution: check for extreme values")
ax.legend()
plt.tight_layout()
plt.savefig("weight_distribution.png", dpi=140)
print("Saved weight_distribution.png")
# Diagnostic 2: trim extreme weights at 99th percentile
trim_threshold = np.percentile(df.ipw, 99)
df["ipw_trimmed"] = df.ipw.clip(upper=trim_threshold)
# Compare ATE before and after trimming
def weighted_ate(data):
t = data[data.opt_in_agent_mode == 1]
c = data[data.opt_in_agent_mode == 0]
return (
(t.task_completed * t.ipw_trimmed).sum() / t.ipw_trimmed.sum()
- (c.task_completed * c.ipw_trimmed).sum() / c.ipw_trimmed.sum()
)
# Untrimmed ATE using ipw column
df["ipw_trimmed_orig"] = df["ipw"].copy() # backup before overwrite
ate_untrimmed = (
(df[df.opt_in_agent_mode==1].task_completed * df[df.opt_in_agent_mode==1].ipw).sum()
/ df[df.opt_in_agent_mode==1].ipw.sum()
- (df[df.opt_in_agent_mode==0].task_completed * df[df.opt_in_agent_mode==0].ipw).sum()
/ df[df.opt_in_agent_mode==0].ipw.sum()
)
ate_trimmed = weighted_ate(df)
print(f"\nATE (untrimmed): {ate_untrimmed:+.4f}")
print(f"ATE (trimmed): {ate_trimmed:+.4f}")
print(f"Trim threshold: {trim_threshold:.2f}")
预期输出:
IPW weight percentiles:
50th pct: 1.52
75th pct: 1.57
90th pct: 2.88
95th pct: 8.14
99th pct: 8.58
Saved weight_distribution.png
ATE (untrimmed): +0.0851
ATE (trimmed): +0.0852
Trim threshold: 8.58
图2:50,000用户合成数据集上的IPW权重分布。大部分权重集中在1.0到3.0之间。500个观测值超过了99%分位的截断阈值8.58。截断使ATE偏移了0.0001,确认极端权重对该估计的影响可忽略不计。与图1的概念图不同,该诊断直接运行在共享数据集上的真实数据上。
这里发生的事情是:你拟合一个倾向性模型,计算ATE权重,然后绘制权重直方图,以查看是否有任何用户的极端权重主导了该估计。
第99百分位线是可视化的截断阈值。你应用截断并比较未截断与截断后的ATE。如果它们接近,说明极端权重对结果的影响极小。如果它们相差较大,则说明存在一小簇有影响力的观测值,此时截断后的估计更可信。
当你从观测日志中衡量AI特征的因果效应时,几乎总是处于倾向性模型和结果模型都带有误差的情形。AIPW结构为你提供了一道防线,防止其中一个模型出错。Lyft诊断工具包会在你依据估计结果行动之前,告诉你每个模型各自承担了多少误差。
运行权重诊断和安慰剂测试可能会给因果分析增加大约2小时的时间。这2小时可以防止那种自信但错误的结论,这种结论会让一个工程团队花一个季度去追逐错误的功能,而我已经目睹过这种情况的发生。跳过诊断的代价并不抽象:那就是六名工程师在某个并非你所测量结果原因的事情上工作。
因果分析的标准输出是一个点估计和一个置信区间:AI功能将任务完成率提高了6个百分点,95%置信区间为[3.8, 8.2]。这个数字回答的是一个回溯性问题:发生了什么?
产品决策是前瞻性的。如果你正在考虑将模型路由阈值从0.85提高到0.90,你想知道下个季度的成本和质量权衡会是什么样子——这是基于你从上个月实验中学到的东西进行的前瞻性预测。
Totte Harinen和Bonnie Li在Uber工程博客上的文章“使用因果推断改进Uber用户体验”描述了Uber如何将因果推断应用于生产决策,为将因果效应估计嵌入前瞻性情景模型提供了基础。
结构性做法是将因果估计视为预测中的一个参数。分别预测成本和质量并假设它们之间存在稳定关系,会导致因果参数未被明确指定。结构性做法是直接对路由阈值在成本-质量权衡上的因果效应进行建模,然后在关于查询量、查询分布和模型能力的不同假设下,将该参数向前投影。
这对LLM系统尤为重要,因为路由决策与成本之间的关系是非线性的且依赖于分布。在当前查询量下具有成本效益的路由阈值,在3倍查询量下可能会失效。你在第一季度为其优化路由的模型可能在第三季度被更便宜的模型取代,从而完全改变成本-质量帕累托前沿。将因果估计嵌入预测中,可以在这些结构性变化到来之前使其可见。
路由阈值附近的局部比较依赖于两个识别假设。首先,工程师和用户无法精确操纵query_confidence,使其聚集在0.85分界线的某一侧。分配必须在阈值附近的窄带内近似随机。
其次,潜在结果函数必须在分界线上连续,因此在0.85处观察到的跳跃归因于路由分配,而不是任何其他在同一分数水平触发的策略。
下面的代码说明了这种模式:估计路由阈值变化对成本和质量的因果效应,然后将该效应投射到一系列未来的查询量场景中。
import pandas as pd
import numpy as np
df = pd.read_csv("data/synthetic_llm_logs.csv")
# Step 1: Estimate causal effect of premium routing on quality and cost
# (Using RDD logic: compare users near the routing threshold)
cutoff = 0.85
bw = 0.10
near = df[
(df.query_confidence > cutoff - bw)
& (df.query_confidence < cutoff + bw)
].copy()
# Low-confidence queries route to premium model (below-threshold queries need stronger handling)
near["routed_premium"] = (near.query_confidence < cutoff).astype(int)
# Causal effects from the local comparison near the threshold
quality_effect = (
near[near.routed_premium == 1].task_completed.mean()
- near[near.routed_premium == 0].task_completed.mean()
)
cost_effect = (
near[near.routed_premium == 1].cost_usd.mean()
- near[near.routed_premium == 0].cost_usd.mean()
)
print(f"Estimated quality effect of premium routing: {quality_effect:+.4f}")
print(f"Estimated cost effect of premium routing: {cost_effect:+.4f}")
# Step 2: Embed into forward-looking scenarios
# Suppose we're evaluating: what if we raise threshold from 0.85 to 0.90?
# Queries with confidence 0.85 to 0.90 would shift from premium to cheap routing.
threshold_change_users = df[
(df.query_confidence >= 0.85) & (df.query_confidence < 0.90)
]
n_shifted = len(threshold_change_users)
print(f"\nQueries that would shift at threshold 0.85 to 0.90: {n_shifted}")
# Volume scenarios (monthly queries)
monthly_query_volume = [500_000, 1_000_000, 2_000_000]
shifted_fraction = n_shifted / len(df) # fraction of total traffic shifted
print("\nForward-looking scenario: raise threshold from 0.85 to 0.90")
print(f"{'Monthly volume':>20} {'Quality change':>16} {'Cost change ($/mo)':>20}")
for vol in monthly_query_volume:
n_affected = vol * shifted_fraction
delta_quality = quality_effect * n_affected / vol # rate change in overall quality
delta_cost = -cost_effect * n_affected # negative: saving cost by de-premiuming
print(f"{vol:>20,.0f} {delta_quality:>+16.4f} {delta_cost:>+20,.0f}")
预期输出:
Estimated quality effect of premium routing: +0.0613
Estimated cost effect of premium routing: +0.0080
Queries that would shift at threshold 0.85 to 0.90: 5415
Forward-looking scenario: raise threshold from 0.85 to 0.90
Monthly volume Quality change Cost change ($/mo)
500,000 +0.0066 -436
1,000,000 +0.0066 -871
2,000,000 +0.0066 -1,742
情况是这样的:你通过路由阈值附近的一个局部比较,来估算高级路由对质量(任务完成率)和成本的影响。然后,你要确定如果将阈值从0.85提高到0.90,会有多少比例的查询改变路由分配。
最后,你将这些变化对质量和成本的影响,投影到不同的月度查询量场景中。最终产出一张场景表,产品或财务团队可以直接阅读:在当前流量下,提高阈值每月大约节省X美元,但代价是任务完成率下降约Y个百分点。
因果预测模式适用于成本和质量效应都很显著的路由与基础设施决策,并且你需要在你尚未达到的流量规模之前做出选择。将因果估计向前推导到流量场景,就把回顾性发现转化成了可操作的预测。
跳过这一步,因果估计就只能埋藏在分析文档里,与容量规划和定价决策脱节。我见过很多有价值的分析因为这个原因而无人问津。而有了这一步,测量团队产出的才是真正影响产品运营方式的输入。
这四个团队采用了不同的方法,却收敛到了同一种运营纪律上。
从分配机制(处理是如何分配的?)出发,逆向推导出识别策略。Airbnb超越了短期A/B测试,因为他们的功能会影响30天窗口之外的长期价值。Netflix对阈值路由系统使用RDD,因为截断点就是天然的识别策略。
选择某种技术,是因为你的系统设计让某种识别策略具有可信性。默认使用团队最熟悉的方法,正是识别错误产生的根源,而这些错误不会自我暴露。
在报告估计结果之前,先运行假设检验。Airbnb在历史队列上验证先行指标模型。Lyft在基于观测估计做出行动之前,会运行权重分布和安慰剂检验。
没有附带诊断层的估计结果,是无法辩护的估计。当产品团队在季度评审中质疑你的数据时,这一区别至关重要。
Airbnb估算长期价值,以支持功能发布决策。Netflix运行准实验来做发布决策。
不能改进任何具体产品决策的分析,不值得运行:它们消耗分析师的时间,在报告积压中制造误导信号,并随时间推移侵蚀利益相关方对测量职能的信任。
每种技术都有一系列已知的失效方式:DID的非平行趋势、RDD在截断点处的操纵、倾向得分法的未测量混杂变量,以及全量升级时的合成对照拟合不佳。
在发布估计结果时,要把对应的失效条件也明确标注出来。对于持怀疑态度的受众而言,分析的可信度并非来自置信区间本身,而是来自对特定假设的透明披露——即需要推翻哪些假设,估计才会失效。
大多数LLM团队并非从成熟的因果管线起步。以下步骤按影响力排序。
每项观测性因果分析中最大的制约因素,就是你所需的数据没有被收集过。在对分阶段上线运行DID之前,你需要两个队列的预处理数据。
要对一个可选功能运行AIPW,你需要一套丰富的协变量来预测用户是否选择启用。
现在就为六个月后想做的分析做好系统埋点:会话时长、查询复杂度、7天回访率,以及模型路由决策。埋点成本很低,但追溯性数据收集是不可能实现的。
将Netflix的分类法应用于当前产品中运行的每一个AI功能。对于每个功能,问:处理是如何分配的?这种分配机制支持哪种因果方法?
核心假设是什么?你有数据来检验它吗?这个练习通常会揭示出,大多数功能正在被用与其分配机制不匹配的工具进行测量。这种不匹配并非学术问题。它意味着你不知道这些功能是否真的有效。
选择一个功能,运行均衡性检验和安慰剂检验,对规格选择进行敏感性压力测试,并写出结果。把每一项检验都完整执行一遍,这种纪律会为后续分析建立模式。
它通常还会暴露一个让你不舒服的发现,尤其是关于你最有信心的那个功能。我在三个不同的团队中都见过这种情况:那个“明显有效”的功能,结果发现对照组存在混杂。
效仿Airbnb的做法,找出至少一个可以在30天实验窗口内测量的长期价值先行指标。7天留存率、第3周回查率或升级率轨迹都是候选指标。
在每份实验总结中,把该指标与即时互动指标一起报告。否则,你就是在优化一个代理指标,然后在下个季度的留存数据里发现差距。
当你产出一个因果估计时,加一行:“在3倍当前流量下,这个效应意味着X。”这种转换步骤会迫使分析去对接基础设施和产品规划,也会改变谁会阅读它。
生产环境中的因果管线会在几个可预见的地方失效。
首先,没有人对测量设计负责。在大多数团队中,数据科学家通常在功能上线后才写分析。由于这是标准工作流程,你总是在为并非为因果识别而设计的数据做回顾性分析。
解决办法是在功能上线前进行测量设计评审:谁是对照组,前期时段有多长,核心假设是什么,以及什么诊断能证伪它?30分钟的评审就能避免一整类无法挽回的分析。
其次,因果结果没有到达决策者手中。一个正确的因果估计如果没有为产品决策提供信息,那就是一次失败的分析,即使统计结果是正确的。你无法用更好的方法来解决这个问题。因果管线需要快速通道报告,同时也要有严谨的报告。
首先,仪器缺口是在事后才被发现的。最常见的技术失败是需要一个未被记录的协变量。当你在实验结束三周后尝试检查平衡性或运行倾向性模型时,才会发现这一缺口。
上述的“尽早仪器化”原则可以解决这个问题,但它需要基础设施团队的支持,以便将服务于因果分析的事件日志记录置于与产品仪表盘同等的优先级。获得这种支持比日志记录本身更难。
其次,合成数据集中存在处理泄漏。对于在合成数据或内部数据上测试因果方法的团队,数据生成过程可能无意中嵌入了你试图估计的因果效应,从而使任何方法看起来都有效。
在外部留存数据或生成窗口之外的队列上验证你的分析。这一点很容易被忽略,因为合成数据看起来很干净。数据行内的结构性污染可能很微妙,难以检测。
首先,将LATE与ATE混为一谈。RDD估计的是局部平均处理效应(LATE):即在临界点附近特定用户的效果。倾向性匹配估计的是ATT:即接受处理的用户的效果。而针对全人群的ATE则需要不同的方法。
当产品经理问“这个功能的效果是什么”时,他们通常指的是ATE。当你的因果分析给出的是LATE而没有解释两者差异时,他们就会将该估计应用于其并非设计来支持的决策,由此产生的产品选择将以你无法追溯至该分析的方式出错。
其次,外部效度假设不成立。基于上一季度用户群体的因果估计可能无法推广到下一季度,尤其是当你扩展到新细分市场或进入国际市场时。
该估算是针对早期选择加入的高活跃用户的效应,而此时功能正在向低活跃度用户推广。请记录你的估计所适用的人群。当它即将被应用于该人群之外时,应明确标记。
第三,报告精确度夸大了确定性。在存在残留混杂风险的观察性研究中,报告保留两位小数的因果估计,所传达的确定性超出了该分析实际所能支持的程度。
在决策者实际会看到的摘要中,同时报告点估计的置信区间、估计所依赖的假设,以及加权后的平衡性。只有当不确定性对依据其行动的人可见时,分析才算完成。
观察性分析的点估计带有抽样不确定性。以下自助法(500次重复,种子=7)为本文中的三个数值估计提供了95%置信区间:Airbnb双重差分法对未来价值评分的影响、Lyft逆概率加权ATE,以及Uber断点回归设计质量效应。
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression, LogisticRegression
rng = np.random.default_rng(7)
df = pd.read_csv("data/synthetic_llm_logs.csv")
n_boot = 500
# Bootstrap 1: DiD on future-value score (Airbnb)
historical = df[df.signup_week < 10].copy()
feature_cols = ["task_completed", "thumbs_up", "session_minutes"]
fv_model = LinearRegression().fit(historical[feature_cols].fillna(0), historical["retained_7d"].values)
df["future_value_score"] = fv_model.predict(df[feature_cols].fillna(0))
analysis = df[df.signup_week < 30].copy()
analysis["post"] = (analysis.signup_week >= 20).astype(int)
analysis["treated"] = (analysis.wave == 1).astype(int)
did_boots = []
for _ in range(n_boot):
s = analysis.sample(frac=1, replace=True, random_state=rng.integers(1e9))
c = s.groupby(["treated", "post"]).future_value_score.mean()
try:
did_boots.append((c.loc[(1, 1)] - c.loc[(1, 0)]) - (c.loc[(0, 1)] - c.loc[(0, 0)]))
except KeyError:
pass
ci_did = np.percentile(did_boots, [2.5, 97.5])
print(f"DiD future-value 95% CI: [{ci_did[0]:+.4f}, {ci_did[1]:+.4f}]")
# Bootstrap 2: IPW ATE trimmed (Lyft)
X = pd.get_dummies(df[["engagement_tier", "query_confidence"]], drop_first=True).astype(float)
ps_model = LogisticRegression(max_iter=1000).fit(X, df["opt_in_agent_mode"])
df["propensity"] = ps_model.predict_proba(X)[:, 1]
df["ipw"] = np.where(df.opt_in_agent_mode == 1, 1 / df.propensity, 1 / (1 - df.propensity))
trim_thr = np.percentile(df.ipw, 99)
df["ipw_trimmed"] = df.ipw.clip(upper=trim_thr)
ate_boots = []
for _ in range(n_boot):
s = df.sample(frac=1, replace=True, random_state=rng.integers(1e9))
t = s[s.opt_in_agent_mode == 1]
c = s[s.opt_in_agent_mode == 0]
ate_boots.append(
(t.task_completed * t.ipw_trimmed).sum() / t.ipw_trimmed.sum()
- (c.task_completed * c.ipw_trimmed).sum() / c.ipw_trimmed.sum()
)
ci_ate = np.percentile(ate_boots, [2.5, 97.5])
print(f"IPW ATE trimmed 95% CI: [{ci_ate[0]:+.4f}, {ci_ate[1]:+.4f}]")
# Bootstrap 3: RDD quality effect near routing cutoff (Uber)
cutoff = 0.85
bw = 0.10
near = df[(df.query_confidence > cutoff - bw) & (df.query_confidence < cutoff + bw)].copy()
near["routed_premium"] = (near.query_confidence < cutoff).astype(int)
qe_boots = []
for _ in range(n_boot):
s = near.sample(frac=1, replace=True, random_state=rng.integers(1e9))
qe_boots.append(
s[s.routed_premium == 1].task_completed.mean()
- s[s.routed_premium == 0].task_completed.mean()
)
ci_qe = np.percentile(qe_boots, [2.5, 97.5])
print(f"RDD quality effect 95% CI: [{ci_qe[0]:+.4f}, {ci_qe[1]:+.4f}]")
预期输出:
DiD future-value 95% CI: [+0.0023, +0.0093]
IPW ATE trimmed 95% CI: [+0.0727, +0.0966]
RDD quality effect 95% CI: [+0.0490, +0.0748]
这里说明一下情况:三个独立的bootstrap循环各自对分析数据集进行500次重采样,并共享同一个随机种子。
DiD bootstrap对完整分析队列进行重采样,并重新计算2x2单元格均值。区间[+0.0023, +0.0093]确认未来价值效应在统计上与零有显著区别。
IPW ATE bootstrap对所有50,000名用户进行重采样,并对每次抽取重新加权。区间[+0.0727, +0.0966]覆盖了真实值+0.08的选择效应,且不包含零。
RDD bootstrap仅对0.85阈值附近带宽窗口内的用户进行重采样。区间[+0.0490, +0.0748]确认局部质量效应不为零。
这三个区间既足够紧凑以保证可操作性,又足够宽以反映观测性估计的不确定性。如果你在报告点估计时没有附带这些区间,那你就是在低估利益相关者正在承担的风险。
本文配套的notebook位于github.com/RudrenduPaul/product-experimentation-causal-inference-genai-llm/tree/main/13_case_studies/。克隆该仓库,使用上面的“前置条件”命令生成合成数据集,然后运行case_studies_demo.ipynb即可复现本文中的每一个代码块,包括全部四个案例研究实现和bootstrap验证。其中还包含一个决策函数,将Netflix分类法扩展为更完整的方法选择指南。
四个案例研究的原始材料可直接从各团队的工程博客获取。
Jenny Chen关于未来价值的文章位于(Airbnb技术博客)。
准实验分类法位于(Netflix技术博客)。
Nassiri关于双重稳健验证的文章位于(Lyft工程博客)。
Harinen和Li的因果推断综述位于(Uber工程博客)。
阅读原文是值得的:它们详细描述了生产系统,这些细节是摘要无法完全呈现的。
那些能够可靠衡量AI影响的团队有一个共同做法:让方法与分配机制相匹配,在信任估计结果之前先运行诊断,并在决策窗口关闭之前将因果结果与决策相连接。
瓶颈几乎总是埋点。这些分析所依赖的数据必须在功能上线之前就存在。这是上述框架无法替你弥补的差距,也是“尽早埋点”这一步被放在首位的原因。
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。