
三年前我参加过一次内部策略评审会会议室里坐着算法、产品、运营和法务四拨人。产品经理在白板上写下一行目标把高价值用户的券后实付价提升 3%同时保证整体 GMV 不降。算法同学点头说这个可以做用历史下单数据估一下价格弹性就行。当时没人觉得这句话有问题——直到法务问了一句那你们怎么区分高价值用户和不敏感用户会议室安静了大概十秒。后来这件事的讨论持续了两个月也让我第一次真切意识到人工智能和大数据在工程落地层面的伦理问题往往不是出现在论文里而是出现在这样一句轻描淡写的技术方案里。以大数据杀熟这个被反复讨论的案例为切口我把这套链路从数据到决策拆开讲一遍它技术上是怎么成立的、伦理上在哪里断裂、以及如果我是这个项目的工程师我会用什么可执行的工程手段把它拦住。适合正在做推荐、定价、增长策略的同学也适合刚接触工程伦理、不知道怎么把伦理落到代码里的入门读者。1. 为什么一个发券策略比一段训练代码更值得工程师警惕1.1 大数据杀熟的真实边界它不是涨价而是差别对待很多人对大数据杀熟的理解停留在老用户看到的价格比新用户贵。这个理解不算错但太窄了。在实际的工程系统里直接改标价是最笨也最容易被抓到的做法绝大多数被讨论的争议都发生在更隐蔽的层面。我把常见的形态归成四类形态技术实现位置隐蔽程度用户感知难度直接差别标价商品/服务定价服务低低截图一比就露差异化优惠券发放营销策略引擎中中需要多账号对比配送费、打包费、服务费差异履约计价模块高高明细藏在订单页搜索结果与推荐排序差异召回与排序模型极高极高几乎无法自证真正值得工程师警惕的是后两类。因为它们不在价格这个字段里而在你能不能看到便宜的那个选项里。同一个商品一个账号在首屏看到的是 32 元的套餐另一个账号在第三屏才刷到 26 元的同款——从系统日志上看两次请求的商品池是一样的价格字段也是一样的但用户实际支付的成本完全不同。这里必须说清楚一点在这类争议中当事平台往往有自己的解释比如优惠券是随机发放的、看到的时间点不同、参与的活动不同、门店库存动态变化等等。这些解释在技术上确实可能是成立的。所以本文讨论的重点不是某一次争议谁对谁错而是一个更前置的工程问题如果一个团队真的打算做基于用户画像的差别定价现有工程体系里有哪些环节是拦不住它的1.2 工程伦理不是道德口号它是一组可以写进代码的约束我刚入行的时候也以为工程伦理是那种写在员工手册第一页、签完字就忘的东西。做久了才发现它其实非常具体具体到可以翻译成几条工程约束可解释约束任何一个对用户产生实质影响的决策都必须能回溯到具体特征和具体规则而不是模型就是这么输出的。一致性约束在相同条件下系统对相同输入应当给出相同输出差异必须能被某个明确的、非敏感的理由解释。可退出约束用户有权知道自己被如何对待并且有成本可控的退出路径。最小够用约束为了完成某个业务目标只采集和使用真正必要的数据。这四条听起来抽象但每一条都能对应到具体的技术动作特征白名单、决策留痕、影子账号审计、数据字段裁剪。伦理之所以在工程实践中容易失效不是因为工程师不在乎而是因为没有人把它翻译成这些动作。1.3 这个案例为什么成了 AI 工程伦理的经典样本我后来复盘过很多次觉得这个案例之所以被反复拿来讨论是因为它同时踩中了三个特征。第一它用到了典型的人工智能技术栈。用户画像、弹性估计、推荐排序、实验分流这些模块单独看都没有问题甚至每一个都是为了提升用户体验而建的。恶意不是集中在某个模块里的而是分散在一整条链路的分工里。第二它的因果链很模糊。用户投诉我被杀熟了平台回复价格是统一的你看到的是优惠券差异两边说的可能都是事实。这种模糊性让技术团队很容易滑进我们只是按规则执行的自我安慰里。第三它的收益是即时的代价是延迟的。短期内客单价确实涨了指标确实好看了而用户信任的流失要等到半年后才在留存曲线上显形。当一个系统的正反馈和负反馈之间有半年时差时工程团队几乎必然会做出错误的选择——这是结构性缺陷不是人品问题。2. 定价链路的完整解剖从原始日志到千人千价2.1 数据层哪些字段构成了价格弹性的原材料要理解这件事得先看清楚模型吃的是什么。一个典型的定价或发券策略模型输入特征大致能分成这几类交易类特征近 30 天下单次数、下单金额分布、退款率、取消率、订单间隔。优惠类特征历史用券率、券面额接受区间、是否只在有券时下单、对满减门槛的敏感度。行为类特征浏览时长、加购到下单的转化路径长度、比价行为在详情页停留时长、来回切换商品。设备与环境类特征设备型号与价位段、网络环境、常驻城市与商圈、下单时段。关系类特征是否会员、会员等级、是否绑定支付优惠、是否为平台入驻商家关联账号。单看每一个字段都能找到合理的解释。用券率高的用户给他发券的边际收益低从营销效率角度看确实不该发——这是标准的商业逻辑。问题出在下一步当这些特征被组合起来模型实际上在估计的不是这个人需要什么而是这个人最多能承受多少。历史用券率这个字段本来衡量的是用户对价格的关注程度但在一套以利润最大化为目标的模型里它会被等价解读成这个人对价格不敏感的比例有多高。这两个解释在数学上完全等价在伦理上却天差地别。同一组特征配上不同的目标函数就会导向完全不同的系统行为。这就是为什么我一直认为工程伦理最关键的介入点不在数据采集而在目标函数的定义阶段。2.2 模型层从预测到决策的四个跳跃很多同学以为定价模型的输出就是一个数字。实际上从原始数据到最终用户看到的那个价格至少跨了四道跳跃每一道都可能放大偏差。第一跳预测到因果。模型先做的是预测——预测这个用户在给定价格下的下单概率。但决策需要的是因果——如果我提价他会怎么做。这两者之间的差距是巨大的。一个用户在 30 元档位下单多不代表把价格提到 35 元他还会下单。工程上常见的做法是引入因果推断或者 uplift 建模但这类模型对实验数据的质量极其敏感一旦实验分流有偏结论就是错的。第二跳因果到决策。有了弹性估计还要转成具体策略。这一步通常是一个优化问题在约束条件下最大化某个目标函数。而约束条件里写了什么直接决定了系统是提高效率还是收割用户。如果约束里只有总单量不降没有任何公平性约束那算法必然会往差别定价的方向走因为那就是最优解。第三跳决策到投放。策略引擎会根据模型分数决定给谁发什么券、在什么位置展示什么价格。这一步有一个很隐蔽的机制投放频次也会被学习。系统会记住哪些用户催一催就会下单于是这些用户会被更频繁地触达最终形成一个自我强化的循环。第四跳投放到实验。灰度实验、A/B 测试是所有互联网团队的标准动作。但这些实验本身可能带有选择偏差如果分流是随机的那没问题如果分流是按用户 ID 尾号做的、而用户 ID 又和注册时间相关那实验组和控制组的用户本就是两类人。2.3 为什么这套链路天然难以被发现我见过不少技术同学的第一反应是真有问题的话跑个 SQL 一比不就出来了实际操作中远没有这么简单原因有三个。其一价格不是一个字段。用户实际支付的金额 标价 - 商品券 - 平台券 - 会员折扣 配送费 打包费 服务费 天气/时段附加费。你比了标价人家差在配送费上你比了配送费人家差在券的可用门槛上。要完整对比需要把整条计价链路都还原出来。其二样本不可复现。用户 A 的账号、历史、设备、位置都是唯一的你不可能拿同一个账号在两个不同的画像状态下各下一单。其三分布改变问题本身。这类策略一旦上线用户的后续行为会被策略影响历史数据里拿到的价格弹性其实是策略生效后的结果用它来评估策略本身会形成循环论证。3. 编码之外的四个伦理断点3.1 需求就是这么定的——技术中立论为什么站不住我听过最多的一句话是我只是按需求实现的。这句话在技术上有时候成立在伦理上几乎从来不成立。原因很朴素需求本身就是由能力和成本共同定义的。产品经理不会提一个做不到的需求所以你能做什么直接决定了对方会提什么。当一个团队具备了给每个用户算一个专属价格的技术能力之后统一定价这个选项就自动从需求池里消失了因为它看起来浪费了能力。技术中立论还有一个更实际的问题它让责任变得无法落地。当每个人都说我只是执行的时候整条链路上就没有人负责了。真正负责的做法不是让工程师去对抗业务而是在流程里设置一个明确的、必须有人签字的检查点让这件事该不该做变成一个必须被回答的问题而不是一个可以被默认跳过的问题。3.2 代理指标当用券敏感度变成你是不是好欺负机器学习模型本身不会区分敏感和不敏感它只在拟合目标。真正制造歧视的往往不是敏感特征本身而是代理特征。举个我实际遇到过的例子某次评审里风控同学提出要剔除性别年龄这类字段大家一致同意。但模型上线后公平性审计发现不同年龄段的用户拿到的券面额存在系统性差异。反查了半天发现是设备型号价位段这个特征在起作用——而设备价位段和年龄高度相关。你删掉了敏感特征却没有删掉敏感特征的信息。这类问题的排查不能靠直觉只能靠定量方法。我常用的做法是对模型做特征重要性分析再看每个高频特征的分布在不同用户群体间是否存在显著差异。import numpy as np import pandas as pd from sklearn.ensemble import GradientBoostingRegressor from sklearn.inspection import permutation_importance # X: 特征矩阵, y: 目标(如预估价格弹性), group: 用于审计的分组标签(仅用于审计, 不进入训练) model GradientBoostingRegressor(n_estimators300, max_depth4, random_state42) model.fit(X_train, y_train) # 用置换重要性找出对预测贡献最大的特征 result permutation_importance( model, X_valid, y_valid, n_repeats10, random_state42, scoringneg_mean_absolute_error ) importance pd.Series(result.importances_mean, indexX_valid.columns).sort_values(ascendingFalse) # 对被判定为代理特征嫌疑的字段, 检查组间分布差异 def group_shift(feature: str, df: pd.DataFrame, group_col: str) - pd.DataFrame: 检查某个特征在不同群体间的分布差异, 差异越大越可疑 return df.groupby(group_col)[feature].describe()[[mean, std, 50%]] for feat in importance.head(10).index: if feat in CANDIDATE_PROXY_FEATURES: # 人工维护的代理特征候选清单 print(feat, \n, group_shift(feat, X_valid, audit_group))这段代码的关键不是算法本身而是那句注释审计用的分组标签绝对不能进训练特征。很多团队把审计字段和训练字段放在同一张宽表里结果数据同学手一抖就带进模型了这是一类非常典型的事故。3.3 同意与告知的形式化一次勾选能不能覆盖无限次定价用户协议里那句我们可能根据您的使用情况提供个性化服务在工程上被翻译成了什么实际上被翻译成了一个非常宽泛的授权覆盖了从推荐排序到价格展示的全部环节。从工程角度看这里的问题是授权粒度和决策粒度严重不匹配。用户给出的是一个笼统的、一次性的、覆盖全部场景的同意而系统做出的是高频的、场景化的、实时变化的决策。这两者之间没有任何映射关系。我能想到的工程解法是分层开关把个性化能力拆成若干个明确的、可以独立关闭的层比如个性化推荐排序个性化营销触达个性化定价展示。前两个可以默认开启最后一个必须显式授权并且关闭之后要有一条真实的、可用的降级路径。听起来像是产品设计问题但它的落地完全依赖工程侧是否愿意把定价逻辑做成可开关的模块——如果你的定价代码里到处是if user.is_vip这种散落的判断那这个开关永远做不出来。3.4 指标失灵GMV 涨了信任却在流失最后一个断点在被讨论得最少但我觉得它最致命大多数团队的指标体系中没有公平这个维度也没有信任这个维度。一个典型的增长策略看板会包含GMV、订单量、客单价、转化率、留存率、ROI。这些指标有一个共同特点——它们都是短周期、可归因、可实验的。而用户觉得这个平台在坑我这件事周期长、难归因、而且没有对照组。更麻烦的是当你试图加一个长期指标时会遇到一个工程上的现实问题长期指标的反馈信号太弱很容易被短期噪声淹没。三个月后的留存差异可能在统计上不显著但业务已经在这三个月里做了几十次策略调整你根本说不清是哪一个造成的。我的建议是退一步用过程指标替代结果指标。不去追用户信任度这种测不准的东西而是监控几个可以直接观测的过程量过程指标观测方式异常信号价格一致性违规率影子账号定时探测超过阈值即告警券面额分布的组间差异每日离线审计任务组间基尼系数异常上升用户比价行为频次埋点统计短期内显著上升说明用户开始不信任客服价格类投诉占比工单分类环比持续上升这些指标不如 GMV 那么性感但它们能在问题变成危机之前发出信号。4. 把伦理做成可执行的技术约束4.1 需求阶段写进验收标准的公平性条款我现在的习惯是任何一个涉及差异化对待用户的策略项目评审文档里必须有三样东西缺一样就不进入开发排期。第一样是差异化的明确边界。哪些维度允许差异化哪些绝对不允许白纸黑字写清楚。比如允许按城市、按时段、按会员等级差异化不允许按历史用券频率差异化这条规则本身就可以写成代码里的配置。第二样是最坏情况说明。不要写预计提升客单价 2%而要写在最坏情况下单个用户看到的价格相对基准价最多上浮 X%。这个值必须在需求评审时由业务方明确承诺。第三样是回滚方案。不是如果出问题就下线而是具体的下线的开关在哪里、生效需要多久、回滚后历史订单怎么处理。这三样东西的价值在于它们把伦理从一个形容词变成了一个可以被验收的名词。评审时不会再有人问这个方案有没有伦理风险这种没法回答的问题而是问你的差异上限写成了多少。4.2 特征层敏感特征隔离与代理特征反查特征层的治理要做两件事隔离和反查。隔离指的是建立三级特征清单。黑名单是绝对不能进入模型的特征包括设备唯一标识、通讯录相关、精确位置轨迹等。灰名单是需要额外审批才能使用的特征包括设备价位段、消费能力评分等。白名单是可以自由使用的特征主要是商品、商家、时段这类与用户身份无关的字段。反查则是前面提到的那套定量方法。我通常会在模型上线前跑一次代理特征体检具体流程是先用置换重要性找出 Top 20 特征再用互信息或者简单的相关性分析看这些特征和审计分组标签之间的关联强度。关联强度超过某个阈值的强制进入灰名单复核。from sklearn.feature_selection import mutual_info_classif # 计算每个特征与审计分组标签的互信息, 值越高说明该特征携带的群体信息越多 mi mutual_info_classif(X_audit, y_group, discrete_featuresFalse, random_state42) mi_series pd.Series(mi, indexX_audit.columns).sort_values(ascendingFalse) # 超过阈值的特征自动进入复核清单 suspect mi_series[mi_series 0.05] print(需要人工复核的代理特征:\n, suspect)这里有个实操细节值得提醒互信息对连续变量很敏感如果你的特征里有大量长尾分布的字段比如订单金额最好先做分箱否则算出来的值会失真。我第一次跑这套流程的时候因为没做分箱把一个完全无辜的字段误判成了高危特征白折腾了两天。4.3 决策层给最优解套上缰绳决策层是最后一道能拦住问题的地方也是最容易被忽略的地方因为大家默认模型输出什么就执行什么。我的做法是在决策服务和最终计价服务之间插一个约束层所有定价决策都必须通过它。约束层做三件事上下限截断任何个性化价格必须在基准价的 [下限, 上限] 区间内超出部分直接截断。这个区间值来自需求阶段承诺的最坏情况。一致性校验同一用户、同一商品、同一时段、同一会员状态多次请求应当得到相同结果。如果出现抖动说明有随机性或缓存不一致需要告警。可解释性留痕每次决策记录使用的特征快照和主要贡献因子。不需要全量记录记录 Top 5 贡献特征即可存储成本可控但排查时非常有用。# 决策约束层的核心逻辑(简化示意) PRICE_FLOOR_RATIO 0.9 # 相对基准价的下限 PRICE_CEIL_RATIO 1.1 # 相对基准价的上限 def enforce_pricing_constraints(base_price: float, model_price: float, ctx: dict) - dict: floor base_price * PRICE_FLOOR_RATIO ceil base_price * PRICE_CEIL_RATIO final min(max(model_price, floor), ceil) # 一致性校验: 缓存中若存在同键决策, 直接复用, 避免同一场景多次请求结果不同 dedup_key f{ctx[user_id]}:{ctx[item_id]}:{ctx[time_bucket]}:{ctx[member_level]} cached decision_cache.get(dedup_key) if cached is not None and abs(cached - final) 1e-6: audit_log.warning(pricing_inconsistency, keydedup_key, firstcached, nowfinal) final cached decision_cache.set(dedup_key, final, ttlctx[time_bucket_seconds]) return {final_price: final, truncated: final ! model_price, base: base_price}这段代码里有两点是踩过坑之后加上的。第一去重键里必须带时间桶否则用户跨时段看到的价格会被永久锁定业务侧的时段调价就失效了。第二一致性校验要选复用旧值而不是采用新值因为在争议场景下系统给出过两个不同的价格这件事本身就是最大的风险点。4.4 上线后影子账号审计与常态化监控上线不是终点。我坚持每个涉及差异化定价的系统都必须配一套影子账号审计机制。具体做法是维护一组专用于审计的模拟账号这些账号的画像特征被刻意设置成不同组合有的表现为价格敏感的高频用券用户有的表现为从不看券的沉默用户还有完全空白的冷启动账号。审计任务定时用这些账号发起相同请求采集返回的完整计价明细做横向对比。审计项采集内容判定标准标价一致性商品标价组间必须完全一致券后价差异券面额、门槛、可用性差异需能归因到明确的公开规则履约费用配送费、打包费组间差异需与地址、时段强相关展示位差异搜索结果中的排位同款商品的曝光位次偏差需在阈值内这里有个很容易被忽略的细节审计账号的请求要模拟真实用户的行为路径不能直接调接口。因为很多策略是在客户端做分流的你绕过了客户端拿到的就是一个干净的价格审计结果全是绿的但真实用户看到的完全不是这么回事。我们第一次做审计的时候就栽在这里白跑了一个月的数据。5. 一次隐性杀熟的完整排查链路5.1 现象投诉是零散的数据是正常的假设你收到了一批零散的客诉说我是老用户反而比新用户贵。这时候最容易犯的错误是直接去拉订单表比价格。结果通常是客诉样本里的价格差异都能被解释成用了不同的券然后就不了了之。正确的起点是先把现象结构化。我会先做三件事收集至少 20 个具体案例每个案例记录用户 ID、下单时间、商品 ID、完整计价明细、以及用户自述的对比对象然后用这些案例反查系统日志看这几个账号在同一时间段内是否有共同特征最后才是拉全量数据看分布。5.2 第一步把价格拆成标价、券后价、履约费、会员价这一步的目标是把模糊的贵变成具体的差异项。我在实际操作中会写一个计价还原脚本把每个订单的最终支付金额拆解成最终支付 商品标价 - 商品级优惠(活动价/满减) - 平台券 - 会员折扣 配送费 打包费 时段/天气附加费拆完之后通常会看到几种情况。如果差异集中在标价上那是定价服务的问题如果集中在券上去查券的发放策略如果集中在履约费上去查计价规则引擎里的条件分支。每一种情况的排查路径完全不同所以第一步的拆分做得越细后面越省事。5.3 第二步影子账号与装置变量控制拆分完发现差异集中在券的发放上。这时候要验证的是券的发放是否和用户画像相关。工程上没法用同一个账号做两次实验所以只能构造可控对照。我的做法是构造一组画像可控的审计账号让它们在除了某一个目标特征之外其他特征尽量一致注册时间、设备型号、常驻商圈、下单时段都对齐只让历史用券频率这个特征在组间不同。然后观察券的发放结果。如果发现历史用券频率高的账号收到的券面额显著更低那基本可以确认存在基于用券行为的差别对待。这时候还需要排除一个混淆因素券池的时间变化。所以要保证所有账号的请求在同一分钟内发出最好做多轮采样取中位数而不是均值。5.4 第三步反查到模型的输入特征确认了现象之后接下来是定位到具体的模型和特征。这一步需要拉模型的决策日志看被差别对待的账号在特征空间里的位置。我通常用 SHAP 值来做归因因为它的可加性比较好解释业务侧也能看懂。import shap # explainer 基于训练好的策略模型构建 explainer shap.TreeExplainer(policy_model) shap_values explainer.shap_values(X_audit_samples) # 找出对这组被降券账号影响最大的特征 shap.summary_plot(shap_values, X_audit_samples, plot_typebar)实际排查中最常见的发现是模型并没有用任何显式的用户等级或消费能力字段但某个看似中性的行为特征比如最近 30 天打开 App 但未下单次数承担了这个角色。这个字段本身很合理——它衡量的是用户的犹豫程度——但在一个以提价为目标的模型里它变成了这个人还能再榨一榨的代理。5.5 第四步修复、回归与防复发定位到特征之后修复方案有几个层次选哪个层次取决于问题的严重程度。最轻的修复是调整特征权重。在目标函数里加入惩罚项让模型为使用这类代理特征付出代价。这个方案改动最小但效果也最弱因为模型的优化压力很快就会找到替代路径。中等强度的修复是特征隔离。把这类行为类特征从定价模型中移除只保留在推荐模型中。代价是模型效果会有一定下降通常 1 到 3 个百分点需要业务方接受。最彻底的修复是策略级下线。整个基于个体画像的差异化定价能力下线只保留基于城市、时段、会员等级这些公开规则的差异化。这个方案技术上最干净业务上阻力最大。我的经验是修复方案必须配套一个回归验证机制否则过几个月同样的问题会以另一种形式回来。具体做法是把排查过程中构造的审计账号固化成一套回归测试每次模型迭代上线前自动跑一遍输出组间差异报告。这份报告要成为上线审批的必附件。6. 我在实际操作里踩过和见过的几个坑6.1 均值没变是最容易骗过自己的结论我第一次做公平性审计的时候看到不同用户组的平均支付价格几乎一样就下了结论说没问题。后来做分布对比才发现两个组的均值一样但方差完全不同一组的价格集中在均值附近另一组一半人是低位、一半人是高位。均值相同恰恰掩盖了最严重的分化。所以审计一定要看分布不看均值。至少要输出分位数对比、基尼系数、以及分组直方图。这件事我后来做成了强制规范——任何一份无差异的结论如果只给了均值一律打回。6.2 实验分流和用户分层耦合是最隐蔽的偏差源有一类偏差非常难发现实验分流用的是用户 ID 的哈希看起来完全随机但用户 ID 的生成规则和注册时间相关而注册时间又和用户的生命周期阶段相关。结果就是实验组里老用户的比例略高而老用户恰好是这次策略的目标群体。最后策略效果看起来很好其实是一部分人本来就更容易被影响。排查方法很简单但必须做实验开始前对实验组和对照组做一次协变量平衡检验。把所有关键特征拉出来做组间显著性检验任何一个特征 p 值小于 0.05 就要重新随机化。这个检查只需要十几分钟但能避免后面几周的无效实验。6.3 一刀切同价的二阶效应比想象中更麻烦出问题之后很多团队的第一反应是那就所有人同价。这个决定听起来最安全但实际执行会遇到几个问题。一是补贴能力的丧失。很多业务的拉新本来就依赖定向补贴一刀切之后新用户获取成本会上升业务侧很快就会要求至少给新人留个口子。二是被伤害的用户不会因为修复而回来。信任的恢复和代码的回滚不是一回事。三是同价本身也可能不公平比如同一个标价对不同履约距离的用户成本结构完全不同强行同价会让远距离用户被交叉补贴。我现在倾向于规则公开的差异化这个中间态差异化本身不是问题问题是差异化不可见、不可预期、不可解释。如果规则是公开的——比如新用户首单立减 10 元会员每月三张运费券——那用户是能理解并接受的。真正让人愤怒的不是贵是不透明。最后分享一个小技巧。如果你所在的团队还处在没有专职伦理评审角色的阶段最实用的做法是从一个最小的检查项开始在每次策略上线的评审文档里加一行本次改动是否会造成同一用户在不同条件下看到不同价格的问答。这一行问题看着不起眼但它会强制要求有人给出明确回答而只要有人必须回答很多方案在写的时候就自己改掉了。我在两个团队推行过这个做法第一次加进去的时候评审时间多了大概十分钟三个月之后几乎没人再提要不要做个体化定价这种方案了。