Feature Stores from Scratch: A Minimal Working Implementation
TL;DR · AI 摘要
从零开始构建一个最小可用的特征存储系统,涵盖训练和推理场景,并适用于LLM上下文需求。
核心要点
- 特征存储系统包含五个核心组件:特征注册表、离线存储、在线存储、材料化管道和检索API。
- 使用DuckDB、Parquet、Redis和FastAPI实现特征存储的最小可用版本。
- 特征存储不仅解决训练-推理偏差问题,还支持LLM个性化推荐场景。
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 特征存储系统
- 核心组件
- 特征注册表
- 离线存储(Parquet)
- 在线存储(Redis)
- 材料化管道
- 检索API(FastAPI)
- 应用场景
- 训练-推理偏差问题
- LLM个性化推荐
金句 / Highlights
值得收藏与分享的关键句。
特征存储系统定义特征一次,存储为两种形式(训练用和推理用),并保持两者同步。
LLM在推理时需要结构化用户上下文,特征存储的在线存储和检索API正好满足这一需求。
特征存储的五个组件同时支持预测性机器学习用例和LLM上下文用例。
从零开始构建特征库:一个最小可行实现 - 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、DuckDB、Parquet、Redis和FastAPI从零开始构建一个最小的特征库。然后,我们将探讨AI应用如何改变我们实际使用它的目的。
完整的代码足够简短,我们将逐步讲解每一个组件。
# 特征库实际上解决的问题
经典的卖点是训练-服务偏差:构建训练集的SQL与推理时运行的代码路径不同,因此值会漂移。这个问题是真实的,离线加在线的拆分是标准的解决方案。
现代的卖点更为广泛。大型语言模型(LLM)代理和检索增强生成(RAG)管道需要在每次请求时,在10毫秒内提供结构化的用户上下文。LLM没有用户是谁的记忆。如果我们想要个性化的输出,我们必须将用户的计划级别、最近的活动和账户状态注入提示中,并且我们需要一个系统能够快速且一致地返回这些值。这就是特征库的在线存储和检索API为我们提供的。
因此,我们为两者构建。相同的五个组件处理预测性机器学习用例和LLM上下文用例。
# 五个组件
- 一个特征注册表,用代码定义特征。一个基于Parquet的离线存储,使用DuckDB进行查询,用于训练和回填。一个基于Redis的在线存储,用于推理时的低延迟查找。一个材料化管道,将最新的值从离线存储推送到在线存储。一个FastAPI服务,提供一个类型化的检索API。
# 运行示例:一个个性化的LLM推荐系统
我们正在运行一个流媒体服务。当用户打开应用程序时,LLM会生成一条简短的个性化“接下来观看什么”消息。LLM需要用户的以下三个信息:
特征
类型
新鲜度
user_segment字符串
每日
watch_count_30d整数
每小时
last_genre每事件
实体是user_id。我们将注册这三个特征,材料化它们,并在请求时将它们提供给LLM。
#### // 1. 定义特征注册表
注册表只是一个地方,特征在那里被声明一次,带有其实体、dtype和源。我们使用一个数据类。
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"),
}完整的代码可以在这里找到。
当你运行它时,输出显示如下:
已注册的特征:
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 文件,这意味着不需要运行单独的数据库。
以下是一段代码示例:
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 是标准选择,因为哈希查找的速度低于毫秒。
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)}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 或流处理作业)。在这里,它是一个函数。
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)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 应用程序所调用的。
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."
)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