T
traeai
登录
返回首页
KDnuggets

Feature Stores from Scratch: A Minimal Working Implementation

8.5Score

TL;DR · AI 摘要

从零开始构建一个最小可用的特征存储系统,涵盖训练和推理场景,并适用于LLM上下文需求。

核心要点

  • 特征存储系统包含五个核心组件:特征注册表、离线存储、在线存储、材料化管道和检索API。
  • 使用DuckDB、Parquet、Redis和FastAPI实现特征存储的最小可用版本。
  • 特征存储不仅解决训练-推理偏差问题,还支持LLM个性化推荐场景。

结构提纲

按章节快速跳转。

  1. 介绍特征存储系统的重要性及其在实际工程中的必要性。

  2. 解释特征存储如何解决训练-推理偏差问题,并支持LLM个性化推荐场景。

  3. 列出特征存储系统所需的五个核心组件,并简要说明其作用。

  4. 通过一个具体的例子展示特征存储系统如何支持LLM推荐场景。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • 特征存储系统
    • 核心组件
      • 特征注册表
      • 离线存储(Parquet)
      • 在线存储(Redis)
      • 材料化管道
      • 检索API(FastAPI)
    • 应用场景
      • 训练-推理偏差问题
      • LLM个性化推荐

金句 / Highlights

值得收藏与分享的关键句。

  • 特征存储系统定义特征一次,存储为两种形式(训练用和推理用),并保持两者同步。

    第 2 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • LLM在推理时需要结构化用户上下文,特征存储的在线存储和检索API正好满足这一需求。

    第 3 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • 特征存储的五个组件同时支持预测性机器学习用例和LLM上下文用例。

    第 3 段

    ⬇︎ 下载 PNG𝕏 分享到 X
#特征存储#机器学习#LLM#MLOps
打开原文

从零开始构建特征库:一个最小可行实现 - KDnuggets

publ: 2026年6月11日

  • 博客热门文章
  • 主题 AI 职业建议 计算机视觉 数据工程 数据科学 语言模型 机器学习 MLOps NLP 编程 Python SQL
  • 数据集
  • 活动
  • 资源 快速参考 推荐 技术简报
  • 广告

加入新闻通讯

#header end

/ad_wrapper

从零开始构建特征库:一个最小可行实现

构建每个特征库都需要的五个组件,然后看看AI如何改变设计。

作者:

Nate Rosidi

,KDnuggets市场趋势与SQL内容专家,2026年6月11日发布于

机器学习

<div class="addthis_native_toolbox"></div>

# 引言

大多数团队都是通过艰难的方式发现他们需要一个特征库。欺诈模型在笔记本中运行良好,但在生产环境中却悄然失效。支持代理给出一个通用的答案,因为它不知道用户是谁。推荐管道在三个作业中重复计算相同的“30天消费”指标,其中两个作业的结果不一致。

特征库是解决这些问题的基础设施。它定义一次特征,以两种形式存储(一种用于训练,一种用于服务),并保持两者同步。我们将使用Python、DuckDBParquetRedisFastAPI从零开始构建一个最小的特征库。然后,我们将探讨AI应用如何改变我们实际使用它的目的。

完整的代码足够简短,我们将逐步讲解每一个组件。

# 特征库实际上解决的问题

经典的卖点是训练-服务偏差:构建训练集的SQL与推理时运行的代码路径不同,因此值会漂移。这个问题是真实的,离线加在线的拆分是标准的解决方案。

现代的卖点更为广泛。大型语言模型(LLM)代理和检索增强生成(RAG)管道需要在每次请求时,在10毫秒内提供结构化的用户上下文。LLM没有用户是谁的记忆。如果我们想要个性化的输出,我们必须将用户的计划级别、最近的活动和账户状态注入提示中,并且我们需要一个系统能够快速且一致地返回这些值。这就是特征库的在线存储和检索API为我们提供的。

因此,我们为两者构建。相同的五个组件处理预测性机器学习用例和LLM上下文用例。

# 五个组件

  • 一个特征注册表,用代码定义特征。一个基于Parquet的离线存储,使用DuckDB进行查询,用于训练和回填。一个基于Redis的在线存储,用于推理时的低延迟查找。一个材料化管道,将最新的值从离线存储推送到在线存储。一个FastAPI服务,提供一个类型化的检索API。

# 运行示例:一个个性化的LLM推荐系统

我们正在运行一个流媒体服务。当用户打开应用程序时,LLM会生成一条简短的个性化“接下来观看什么”消息。LLM需要用户的以下三个信息:

特征

类型

新鲜度

code
user_segment

字符串

每日

code
watch_count_30d

整数

每小时

code
last_genre

每事件

实体是user_id。我们将注册这三个特征,材料化它们,并在请求时将它们提供给LLM。

#### // 1. 定义特征注册表

注册表只是一个地方,特征在那里被声明一次,带有其实体、dtype和源。我们使用一个数据类。

code
from dataclasses import dataclass
from typing import Literal

@dataclass(frozen=True)
class Feature:
    name: str
    entity: str
    dtype: Literal["int", "float", "str"]
    source: str  # path to a Parquet file or a SQL view

REGISTRY: dict[str, Feature] = {
    "user_segment": Feature("user_segment", "user_id", "str", "data/user_segment.parquet"),
    "watch_count_30d": Feature("watch_count_30d", "user_id", "int", "data/watch_count_30d.parquet"),
    "last_genre": Feature("last_genre", "user_id", "str", "data/last_genre.parquet"),
}

完整的代码可以在这里找到。

当你运行它时,输出显示如下:

code
已注册的特征:
user_segment  entity=user_id  dtype=str  source=data/user_segment.parquet
watch_count_30d  entity=user_id  dtype=int  source=data/watch_count_30d.parquet
last_genre  entity=user_id  dtype=str  source=data/last_genre.parquet

这就是契约。其他所有组件都会从 REGISTRY 中读取,因此重命名一个特征、更改其 dtype 或将其指向新的源只需要在一个地方进行。在生产系统中,这会是 YAML 或一个被提交到 Git 仓库的 Python 模块,每次更改都需要代码审查。

#### // 2. 使用 DuckDB 和 Parquet 构建离线存储

离线存储保存了每个特征值的完整历史记录。我们使用 Parquet 文件作为存储层,使用 DuckDB 作为查询引擎。DuckDB 可以直接读取 Parquet 文件,这意味着不需要运行单独的数据库。

以下是一段代码示例:

code
import duckdb
import pandas as pd

def get_historical_features(
    entity_df: pd.DataFrame, features: list[str]
) -> pd.DataFrame:
    con = duckdb.connect()
    con.register("entities", entity_df)
    base = "SELECT * FROM entities"
    for fname in features:
        f = REGISTRY[fname]
        src = f.source.replace("'", "''")
        con.execute(f"CREATE VIEW {fname}_src AS SELECT * FROM '{src}'")
        base = f"""
            SELECT t.*, s.{fname}
            FROM ({base}) t
            ASOF LEFT JOIN {fname}_src s
              ON t.user_id = s.user_id
             AND t.event_timestamp >= s.event_timestamp
        """
    return con.execute(base).df()

user_id

event_timestamp

user_segment

watch_count_30d

last_genre

8a2f

2026-05-05 12:00:00

casual

22

NaN

b13c

2026-05-07 20:00:00

5

thriller

2026-05-07 22:00:00

power_user

47

documentary

AsOf 连接是一种时间点连接。对于每个实体行,它会选择特征值中时间戳在事件时间戳之前或相等的最新特征值。这就是防止信息泄露的方法——即训练行中不会包含预测时刻尚未存在的特征值。

对于任何计划训练或微调的模型,时间点连接仍然是正确的选择。对于一个纯粹的推理阶段 LLM 使用场景,我们可能永远不会调用这个函数。我们仍然需要离线存储,因为它是回填、评估数据集和审计的来源。

#### // 3. 在 Redis 上设置在线存储

在线存储只保存每个实体的最新值。Redis 是标准选择,因为哈希查找的速度低于毫秒。

code
import json
import fakeredis  # 在生产环境中使用 redis.Redis() 连接到真实服务器

r = fakeredis.FakeRedis(decode_responses=True)

def write_online(entity: str, entity_id: str, values: dict) -> None:
    r.hset(
        f"{entity}:{entity_id}",
        mapping={k: json.dumps(v) for k, v in values.items()},
    )

def read_online(entity: str, entity_id: str, features: list[str]) -> dict:
    raw = r.hmget(f"{entity}:{entity_id}", features)
    return {f: json.loads(v) if v else None for f, v in zip(features, raw)}
code
read_online -> {'user_segment': 'power_user', 'watch_count_30d': 47, 'last_genre': 'documentary'}
missing key -> {'user_segment': None}

键的格式是 entity:entity_id。值是一个哈希,每个特征对应一个字段。一次 HMGET 可以在一次往返中获取所有请求的特征。在本地 Redis 实例上,对于三个特征,这可以在不到 1ms 的时间内完成。

#### // 4. 运行 Materialization 管道

Materialization 将值从离线环境转移到在线环境。在实际系统中,它按照计划运行(如 Airflow、cron 或流处理作业)。在这里,它是一个函数。

code
def materialize(features: list[str]) -> None:
    by_entity: dict[str, dict] = {}
    for fname in features:
        f = REGISTRY[fname]
        src = f.source.replace("'", "''")
        df = duckdb.sql(f"""
            SELECT {f.entity}, {fname}
            FROM '{src}'
            QUALIFY ROW_NUMBER() OVER (
                PARTITION BY {f.entity}
                ORDER BY event_timestamp DESC
            ) = 1
        """).df()
        for _, row in df.iterrows():
            by_entity.setdefault(row[f.entity], {})[fname] = row[fname]
    for entity_id, values in by_entity.items():
        write_online("user_id", entity_id, values)
code
user_id:8a2f -> {'user_segment': 'power_user', 'watch_count_30d': 47, 'last_genre': 'documentary'}
user_id:b13c -> {'user_segment': 'casual', 'watch_count_30d': 5, 'last_genre': 'thriller'}

QUALIFY 子句保留每个实体的最新行。我们将相同用户的全部特征组合成一次 Redis 写入,以减少往返次数。按照每个特征所需的频率运行此过程:watch_count_30d 每小时运行一次,last_genre 几乎实时运行,user_segment 每天运行一次。在实际实现中,注册表是编码该频率的正确位置。

#### // 5. 暴露 FastAPI 获取服务

获取服务是生产环境的接口。这是 LLM 应用程序所调用的。

code
f = resp.json()["features"]
print("\nPrompt the LLM would receive:")
print(
    f"  System: You recommend shows for a streaming service.\n"
    f"  User context: segment={f['user_segment']}, "
    f"watched {f['watch_count_30d']} titles in last 30 days, "
    f"last genre watched: {f['last_genre']}.\n"
    f"  Task: suggest 3 titles in a friendly, short message."
)
code
POST /get-online-features -> 200
body: {'user_id': '8a2f', 'features': {'user_segment': 'power_user', 'watch_count_30d': 47, 'last_genre': 'documentary'}}
Prompt the LLM would receive:
  System: You recommend shows for a streaming service.
  User context: segment=power_user, watched 47 titles in last 30 days, last genre watched: documentary.
  Task: suggest 3 titles in a friendly, short message.

特征存储是将 "user 8a2f" 转换为 LLM 可以使用的结构化上下文的部分。

# 特征存储的终点与向量数据库的起点

向量数据库(Pinecone、Weaviate、pgvector)并不是特征存储,尽管两者都在模型推理时位于模型前面。它们解决的是不同的检索问题。

一个真正的大型语言模型(LLM)堆栈会同时使用两者。向量数据库返回最相似的三个过去的浏览会话。特征存储返回用户的分段信息和最近的计数。提示信息将它们结合起来。

# 常见的反模式

我们经常看到一些模式失败:

  • 在模型服务中计算特征。相同的逻辑最终出现在训练笔记本和API中,两个定义在不到一个季度的时间内就会出现偏差。
  • 将在线存储视为真相来源。Redis在重启失败时会丢失数据。离线存储是规范的来源,而在线存储只是一个缓存。
  • 跳过注册表。三个团队各自独立地定义了active_user,导致仪表板不再与模型匹配。
  • 将向量数据库称为特征存储。它无法进行实体键的结构化查询,而需要同时使用两者提示信息最终仍然需要连接到两个系统。
  • 在没有时间点连接的情况下进行回填。训练集看起来很好,但生产模型看起来却损坏了,差距就在于数据泄漏。

# 与Feast、Tecton和Databricks的比较

我们大约200行代码以微型版本完成了相同的工作。

如果我们想继续沿着相同的模式进行扩展,Feast是最接近的比较,它是一个自托管的解决方案。Tecton和Databricks是托管路径,并且有明确的LLM功能(Tecton的LLM特征检索API,Databricks的复合生成式AI系统的特征服务)。在它们之间选择,主要取决于我们希望自己运营多少,以及我们堆栈的其余部分是否已经在Databricks中。

# 结论

一个功能齐全的特征存储包含五个组件:一个注册表、一个离线存储、一个在线存储、一个材料化步骤和一个检索API。一次构建它会让我们了解为什么生产系统看起来是那样的。它还展示了AI设计的变化:在线检索路径是LLM接触的表面,在训练或评估时时间点连接很重要,而向量数据库位于特征存储旁边,而不是内部。

一旦我们有了这些组件,将我们的最小版本替换为Feast、Tecton或Databricks基本上只是注册表的迁移。系统的结构保持不变。

Nate Rosidi是一位数据科学家,也从事产品战略工作。他还是一个兼职教授,教授分析课程,同时也是StrataScratch的创始人,这是一个帮助数据科学家通过真实面试问题准备面试的平台。Nate撰写有关职业市场最新趋势的文章,提供面试建议,分享数据科学项目,并涵盖所有与SQL相关的内容。

更多关于此主题的内容

  • 关于特征存储的一切
  • 如何为Python应用程序创建最小的Docker镜像
  • 克服AI实施挑战:早期采用者的经验教训
  • 使用Hugging Face进行多模态RAG实现
  • Python中与SQLite数据库交互的指南
  • 用于处理日期和时间的10个Python一行代码

<hr class="grey-line"><br> <div><h3>我们推荐的前5个免费课程</h3><br> </div>

Mailchimp for WordPress v4.13.0 - https://wordpress.org/plugins/mailchimp-for-wp/

/ Mailchimp for WordPress 插件

你可以从这里开始编辑。

如果评论已关闭。

<= 上一篇

下一篇 =>

#content end

<script type="text/javascript">kda_sid_write(kda_sid_n);</script>

最新文章

  • 将 Claude Code 与本地模型配对 3 个 NumPy 技巧提升数值性能 从零开始构建特征存储:一个最小可行实现 为初创想法获取资金的 7 种最佳方式 低成本实现本地智能代理编程:Claude Code + Ollama + Gemma4 5 个有用的 Python 脚本自动化无聊的 PDF 任务

热门文章

  • Anthropic 完整指南:Claude 技能构建
  • 低成本实现本地智能代理编程:Claude Code + Ollama + Gemma4
  • 5 个有用的 Python 脚本自动化无聊的 PDF 任务
  • AI 工程师必须知道的 5 个 Python 概念
  • Python 网络开发的 10 个 GitHub 仓库
  • 将 Claude Code 与本地模型配对
  • 5 篇有趣论文清晰解释大语言模型
  • 智能代理时代对数据科学的意义
  • 现代数据库系统和工具的 10 个 GitHub 仓库

#content_wrapper end

© 2026

Guiding Tech Media

|

关于

联系我们

广告合作

隐私政策

服务条款

发布于 2026 年 6 月 11 日,作者 Nate Rosidi

blank

不,谢谢!

/.main_wrapper

<script defer type="text/javascript" src="https://s7.addthis.com/js/300/addthis_widget.js#pubid=gpsaddthis"></script>

noptimize

/noptimize

AI 可能会生成不准确的信息,请核实重要内容