AI项目砸了?因为你可能忽略了这件事
说个真实的故事
去年Q4,我们团队接了个推荐系统的活,离线指标漂亮得很——AUC 0.89,团队内部 demo 效果拔群。然后上线,第一周业务方就跑来找我了:线上效果怎么差这么多?
排查了一周,发现问题贼蠢:训练的时候用户特征是用 Spark 批量算的7天平均,推理的时候 Python 实时算,用的却是最近3天。就这么点差异,直接把模型干懵了。
这种事在AI落地过程中太常见了,后来我跟其他团队的同行聊,发现大家踩的坑都差不多——不是模型不够好,是特征那边出了问题。
所以今天想聊聊 FDE(Feature Data Engineering),这玩意儿做好了你就不至于像我一样大半夜在线排查问题。
FDE 是什么
FDE 就是从原始数据到模型特征之间的那一套工程体系。听起来简单,但实际上是AI落地最容易被忽略、出了问题最难看清楚的环节。
核心就三件事:
- 训练推理一致性
就是字面意思——训练时用的特征怎么算的,推理时必须一模一样。听起来简单,做到其实挺难的。
- 特征复用
团队里通常不止一个模型,如果每个模型都自己写一套特征加工逻辑,一来重复劳动,二来同一特征定义不一致,最后就是你用你的我用我的,谁也说不清到底哪个是对的。
- 特征治理
线上特征有没有失效、数据漂移了没有、某个特征的 SLA 有没有达标——这些得有人管。
训练推理不一致这个坑
先说清楚这个问题有多容易出。
典型场景:
训练 pipeline:数据仓库 → Spark 任务 → 特征存储(Hive/Iceberg)
推理 pipeline:业务接口 → Python 计算 → 直接进模型
这两边但凡有一个细节对不上,就等着踩坑吧。
举几个我们踩过的:
时间窗口不一致。训练用T-1的数据,推理用实时数据,听起来"实时性更强",但模型学的是T-1的分布,推理却给了实时的,分布漂移直接导致效果雪崩。
计算精度差异。Python的float64和Spark的float64在某些边界值上结果不一样,特别是涉及到大量累加的操作。排查起来贼痛苦,特征值就差0.0001,但模型就是效果差。
缺失值处理逻辑不同。训练时用-1表示缺失,推理时用0。模型学到的 pattern 全乱了。
怎么解决这个问题
我们后来搞了套特征注册机制,所有特征的计算逻辑必须注册到平台,训练和推理共用同一套代码。
大概长这样:
@feature_def(
name="user_active_duration_7d_avg",
version="v2",
owner="推荐团队",
sla={"p99_latency": "10ms", "availability": "99.9%"}
)
def compute_user_active_duration_7d(user_id: str, ctx: FeatureContext) -> float:
"""
计算用户最近7天平均每日活跃时长
重要:这里不要写死任何默认值或特殊处理
"""
daily = ctx.get("user_active_duration_daily", days=7)
if not daily:
return 0.0
return sum(daily) / len(daily)
这个装饰器做的事情:
注册特征的定义到平台,训练和推理代码都要走这个入口
自动生成监控埋点
统一管理特征的元信息
特征中台这个事
当团队里模型多了,特征复用的需求就出来了。
我们之前有个问题,同一个用户画像特征,推荐团队、风控团队、营销团队各自算了一版,定义还略有差异。最后对数据的时候发现三方报表对不上,打了整整两天仗才知道原来大家算的就不是同一个东西。
后来我们把特征做了统一注册管理,每个特征有唯一的 ID 和版本:
features:
-
id: "USER_001"
name: "user_age_days"
type: int
description: "用户注册天数"
owner: "用户中台"
source:
online_store: redis
offline_store: hive
血缘:
上游: "dw.user_info"
字段: "create_time" -
id: "USER_002"
name: "user_active_duration_7d_avg"
type: float
description: "用户7天平均日活跃时长"
owner: "推荐团队"
依赖:- feature: "USER_001"
- feature: "BEHAVIOR_003"
好处就一个字——不用撕了。谁想用直接查,用了之后变更历史清清楚楚。
实时特征的工程难题
这个是我们踩坑踩得最多的地方。
业务场景往往是:模型需要特征,但特征计算需要时间。这两边怎么平衡?
方案一:实时计算
用户请求来了再算,延迟高但准确度高。适合对实时性要求不那么极端的场景。
方案二:特征缓存预加载
提前把能算的特征算好存起来,请求来了直接读缓存。我们用的比较多的是 Redis,KV 访问延迟能压到 1ms 以内。
方案三:缓存 + 实时融合
缓存命中高的特征走缓存,缓存miss的走实时计算。这是目前我们觉得最实用的方案。
class FeatureLoader:
def init(self, cache_store, compute_engine):
self.cache = cache_store
self.compute = compute_engine
async def load(self, feature_names: List[str], entity_id: str) -> Dict[str, float]:
results = {}
先查缓存
cached = await self.cache.mget(feature_names, entity_id)
need_compute = []
for fname in feature_names:
if fname in cached and cached[fname] is not None:
results[fname] = cached[fname]
else:
need_compute.append(fname)
缓存 miss 的走实时计算
if need_compute:
computed = await self.compute.batch_compute(need_compute, entity_id)
results.update(computed)
# 回填缓存
await self.cache.mset(computed, entity_id, ttl=300)
return results
有个血的教训:预加载特征的时候一定要设 TTL。之前没设,结果线上特征跟历史数据混在一起,查了半天不知道模型拿到的是哪天的数据。
特征质量监控
特征上线之后不管了?早晚会出事的。
我们后来加了几个监控项:
1. 特征空值率
突然空值率飙升?大概率上游数据源出问题了。有一次我们空值率从0.1%飙到30%,一查发现是上游Kafka消费延迟,数据没来得及过来。
2. 特征分布漂移
每天跑一遍PSI检测,特征分布偏移超过阈值就告警。这个能提前发现问题,而不是等模型效果崩了才发现。
3. 特征延迟
特征计算到返回超过SLA了吗?这个直接影响模型响应时间。
class FeatureMonitor:
def check_drift(self, feature_name: str, current: np.ndarray,
reference: np.ndarray) -> Optional[DriftAlert]:
"""PSI 检测特征漂移"""
psi = self._compute_psi(current, reference)
PSI > 0.2 表示显著漂移
if psi > 0.2:
return DriftAlert(
feature=feature_name,
psi=psi,
severity="critical",
message=f"特征{feature_name}分布漂移,PSI={psi:.4f}"
)
elif psi > 0.1:
return DriftAlert(feature=feature_name, psi=psi, severity="warning")
def _compute_psi(self, current: np.ndarray, reference: np.ndarray,
buckets: int = 10) -> float:
"""Population Stability Index"""
# 分位数切分
breakpoints = np.percentile(reference, np.linspace(0, 100, buckets + 1))
ref_counts = np.histogram(reference, bins=breakpoints)[0]
cur_counts = np.histogram(current, bins=breakpoints)[0]
ref_ratio = ref_counts / len(reference)
cur_ratio = cur_counts / len(current)
避免除零
ref_ratio = np.clip(ref_ratio, 1e-10, None)
cur_ratio = np.clip(cur_ratio, 1e-10, None)
psi = np.sum((cur_ratio - ref_ratio) * np.log(cur_ratio / ref_ratio))
return psi
冷启动这个问题
新用户、新商品进来的时候没历史数据,特征全是默认值或者0。模型在这种情况下基本等于瞎猜。
我们试过几种处理方式:
用群体统计值兜底。新用户的特征用同类型用户的平均值填充。
用简单模型替代。冷启动阶段跑一个不用历史特征的简化模型,等数据积累够了再切到主模型。
用规则引擎保底。实在没数据的,用业务规则先顶着,不让模型在完全无信息的情况下做决策。
没有银弹,就是组合着来,看哪个对业务指标影响小就用哪个。
最后说几句
FDE 这个事,说重要大家都说重要,但真到干活的时候,往往是最先被压缩时间的部分。觉得"特征嘛,不就是查个数"。
然后上线就等着踩坑。
建议是:特征平台的建设要跟模型训练同步推进,甚至更早。等模型调好了再补特征工程,补着补着发现原来的离线数据要重新跑,整个人都不好了。
好了,废话够多了。有问题评论区见。
以上经验来自真实项目,踩过的坑都是真坑。如有雷同,说明你也踩过 😃

浙公网安备 33010602011771号