AI特征仓库:重复不够用了之后

我们团队当时在做几个独立项目:

表面上看是三个系统,但底层特征大量重复:用户活跃度、购买力、设备稳定性等等。

我们团队当时在做几个独立项目:

表面上看是三个系统,但底层特征大量重复:用户活跃度、购买力、设备稳定性等等。

为什么需要特征仓库

先说清楚遇到了什么问题。我们团队当时在做几个独立项目:

  • 商品推荐系统:需要用户最近 30 天点击、购买、收藏行为特征
  • 风控系统:需要用户最近 7 天登录、交易、设备变更特征
  • 营销系统:需要用户最近 90 天活动参与、优惠券使用特征

表面上看是三个系统,但底层特征大量重复:用户活跃度、购买力、设备稳定性等等。每个项目都要自己算一遍,结果就是:

  1. 特征定义不一致:同样是"最近 30 天购买次数",有人算的是自然月,有人是滚动窗口,还有人忽略了退款订单
  2. 特征漂移:离线训练时用的特征工程逻辑,线上推理时重新写一遍,两边对不上
  3. 重复计算资源浪费:同一个聚合特征在多个系统里独立算,存储和计算成本翻倍
  4. 新模型上线慢:每次要重新对接特征管道,特征从算到用至少要两周

这些问题的本质是:特征工程缺少一个中心化的管理机制。大家各自造轮子,轮子质量参差不齐。

什么是特征仓库

特征仓库的概念其实很简单,就像代码仓库管理代码一样,特征仓库负责统一管理、存储和提供特征。

核心解决三个问题:

  • 特征定义统一:一个特征的计算逻辑只定义一次,离线训练和在线推理共用
  • 特征共享复用:不同模型、不同系统可以复用同一套特征
  • 特征版本可追溯:特征定义变更可追踪,方便回滚和问题排查

业界开源方案里,Feast 算是使用最广的之一。它支持离线和在线特征存储,提供统一的特征服务接口,还能和常见的训练框架直接集成。

搭建 Feast 特征仓库

环境准备

先说环境:当时用的是 Feast 0.40 版本,存储层用了 Redis(在线)和 Parquet(离线),数据源是 PostgreSQL 业务数据库。

安装比较简单,直接用 pip:

pip install feast[redis,postgres]

初始化项目

创建一个 Feast 项目:

feast init my_feature_repo
cd my_feature_repo

生成的目录结构里,feature_store.yaml 是核心配置文件。里面要指定存储类型、数据源和缓存策略。

定义特征

特征的定义在 features/ 目录下,用 Python 写。以用户特征为例:

from datetime import timedelta
from feast import Entity, Feature, FeatureView, Field
from feast.types import Float32, Int64, String
from feast.data_source import PostgreSQLSource

# 定义实体:用户
user = Entity(
    name="user_id",
    join_keys=["user_id"],
    value_type=String,
)

# 数据源配置
user_events_source = PostgreSQLSource(
    name="user_events_source",
    query="""
        SELECT
            user_id,
            event_time,
            event_type,
            event_value
        FROM user_events
        WHERE event_time >= '{{ start_date }}'
        AND event_time < '{{ end_date }}'
    """,
    timestamp_field="event_time",
    created_timestamp_column="created_at",
)

# 定义特征视图
user_features = FeatureView(
    name="user_features",
    entities=[user],
    ttl=timedelta(days=30),
    schema=[
        Field(name="click_count_last_7d", dtype=Int64),
        Field(name="click_count_last_30d", dtype=Int64),
        Field(name="purchase_count_last_7d", dtype=Int64),
        Field(name="purchase_amount_last_30d", dtype=Float32),
    ],
    source=user_events_source,
)

这里有几个关键点要说明:

  • Entity 是特征的唯一标识,通常是用户、商品这类主键
  • FeatureView 把多个特征组合在一起,定义存储策略和数据源
  • ttl 是特征的有效期,超过这个时间窗口的数据不提供
  • 数据源用 SQL 查询定义,Feast 会自动处理增量刷新

特征注册

定义完特征后,需要注册到 Feast 仓库:

feast apply

这步会把特征定义写进存储层,初始化相应的索引和缓存结构。

物化特征

特征定义好只是逻辑上的,还需要把数据物理化到存储层:

feast materialize 2026-06-01T00:00:00 2026-07-17T00:00:00

这会执行数据源查询,把结果写到离线存储(Parquet)和在线存储(Redis)。时间窗口可以按天或小时切,支持增量更新。

离线训练特征获取

训练时直接用 Feast 获取特征:

from feast import FeatureStore

store = FeatureStore(repo_path=".")

entity_df = pd.DataFrame({
    "user_id": ["user_001", "user_002", "user_003"],
    "event_timestamp": [datetime(2026, 7, 15), datetime(2026, 7, 15), datetime(2026, 7, 15)]
})

training_data = store.get_historical_features(
    entity_df=entity_df,
    feature_views=["user_features"],
).to_df()

Feast 会自动按时间点获取当时有效的特征,避免穿越问题。

在线推理特征获取

在线推理时,通过特征服务获取实时特征:

from feast import FeatureStore

store = FeatureStore(repo_path=".")

features = store.get_online_features(
    feature_views=["user_features"],
    entity_rows=[{"user_id": "user_001"}],
)

print(features)
# 特征值会直接从 Redis 读取,延迟通常在毫秒级

数据流向大致是这样:

graph LR A[业务数据库] --> B[数据源查询] B --> C[离线存储 Parquet] B --> D[在线存储 Redis] C --> E[离线训练] D --> F[在线推理] E --> G[模型训练] F --> H[模型预测]

这张图想说明的是:特征从数据源出来后,分别写入离线和在线存储。离线侧用于模型训练,在线侧用于实时推理。两边共用同一套特征定义,确保逻辑一致。

踩坑记录

搭建过程不顺利,踩了不少坑。这里说几个典型的。

时间窗口问题

最开始做的时候,特征值总是对不上。查了很久,发现是 ttl 和数据源查询的时间窗口不一致。

特征视图的 ttl 决定了特征从 Redis 过期的时间,但数据源查询里的时间过滤是单独控制的。如果两边逻辑没对齐,就会出现:请求时间在特征有效期外,但数据源里其实有数据。

解决办法是在数据源查询里明确时间过滤条件,让 ttl 只作为缓存控制,不做逻辑过滤。

特征漂移仍然存在

用了 Feast 之后,特征定义统一了,但特征漂移问题没有完全消失。

原因在于:离线训练用的是历史快照,而在线推理用的是实时特征。如果特征计算逻辑依赖实时数据(比如最近 1 小时的行为),两边天然存在时间差。

这个问题的解决方案是:训练时显式标注特征的时间点,推理时用相同逻辑对齐。另外就是避免过度依赖实时特征,适当加长特征的时间窗口。

存储成本增加

一开始以为集中管理特征会节省存储,结果发现存储成本反而上升了。

原因主要是:离线和在线存储各存一份,而且每个特征版本都会保留历史副本。如果特征更新频繁,磁盘占用增长很快。

后来加了几个优化:

  • 冷特征从在线存储降级到离线存储,减少 Redis 占用
  • 特征版本只保留最近 7 天的历史数据,更早的通过归档处理
  • 对重复度高的特征做压缩,Parquet 本身对列式存储压缩效果不错

性能调优

在线推理时,特征获取的延迟一度成为瓶颈。问题出在:

  • 单次请求获取的特征太多,网络往返次数增加
  • Redis 连接池配置不当,高并发时连接不够用
  • 特征值没有做序列化优化,数据体积偏大

优化措施:

  • 特征按使用频率分组,高频特征单独缓存
  • 调整 Redis 连接池大小和超时时间
  • 把特征值从 JSON 改成 Protobuf 序列化,体积减少约 40%

效果和边界

折腾了几个月后,效果是有的,但也看清了一些边界。

好的方面:

  • 新模型上线时间缩短:特征直接从仓库取,不用再重新对接数据管道,时间从两周降到三天左右
  • 特征质量提升:统一的特征定义减少了因计算逻辑不一致导致的特征漂移
  • 复用性增强:不同项目可以共用特征,开发效率明显提高

但同时也发现:

  • 特征仓库不是万能药:如果特征本身就设计得不好,统一管理只会放大问题
  • 维护成本不低:特征定义、数据源配置、存储优化都需要持续投入
  • 过度集中有风险:所有模型都依赖同一个特征仓库,单点故障影响面很大

结语

特征仓库这件事,本质上是在解决重复造轮子的问题。它让特征从"一次性资源"变成了"可复用资产",这当然是好的。

但技术方案从来不是万能的。特征仓库能解决特征工程里的重复和一致性问题,但解决不了特征本身的业务价值问题。如果特征设计本身有问题,再好的管理机制也只是让问题更集中。

实际使用中,更像是在集中和分散之间找平衡:核心特征集中管理,边缘特征允许一定程度的冗余。完全统一会失去灵活性,完全分散又会陷入重复造轮子的泥潭。

技术的边界往往不是工具本身,而是我们在工具和业务之间做出的选择。

版权声明: 本文首发于 指尖魔法屋-AI特征仓库:重复不够用了之后https://blog.thinkmoon.cn/post/286-ai-feature-store-duplicate-shared-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!