跳到主要内容

LLM 应用的特征存储模式:停止检索那些你可以预计算的内容

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数构建 LLM 应用的团队最终都会趋向于同一种临时架构:散乱的计算用户摘要的定时任务(cron jobs),每次请求都要重新查询的向量数据库,因延迟到了令人尴尬的地步而添加的 Redis 缓存,以及三个对“用户偏好”定义略有不同的代码库。通常只有在生产事故发生后,他们才会意识到自己构建了什么:一个特征存储(feature store)—— 而且是一个拼凑出来的劣质品。

特征存储是传统机器学习(ML)基础设施中经过实战检验的最成熟模式之一。当有意识地将其应用于 LLM 上下文组装时,它可以消除困扰大多数检索流水线的延迟、成本和一致性问题。本文将解释其原理。

特征存储究竟解决了什么(以及为什么它能直接对应 LLM 的问题)

特征存储是实现特征 计算 与特征 服务 分离的基础设施。它由两部分组成:

  • 离线存储(Offline store):以列式格式(如 Parquet、BigQuery)存储的历史特征值。用于构建具有时间点准确性(point-in-time correctness)的训练数据集——也就是说,你可以精确地重构过去某个时间点可用的特征值,而不会让未来的信息污染训练数据。
  • 在线存储(Online store):存储在低延迟键值数据库(如 Redis、DynamoDB、RonDB)中的当前特征值。针对推理时的亚毫秒级查找进行了优化。

核心见解是:特征只需计算一次,却可以多次提供服务。在传统机器学习中,这可能是用户的 30 天购买历史,每晚预计算并持久化到两个存储库中。在预测时,你直接从在线存储中获取该行数据,而不是实时运行聚合计算。

LLM 应用也面临同样的问题,只是术语不同。你不再是为“分类模型组装特征”,而是为“生成模型组装上下文”。约束条件是相同的:上下文窗口空间有限且昂贵,检索存在延迟,且每次请求都运行相同的聚合计算是非常浪费的。解决方案也是一样的:预计算稳定的部分,只检索变化的部分。

三层新鲜度模型

并非所有上下文对过时程度的容忍度都相同。将特征存储思维应用于 LLM 上下文,可以揭示出一种自然的三层架构:

批处理层(每天到每周)

这是变化缓慢的上下文,受益于昂贵的计算,你不想在每次请求时重复执行:

  • 从数月的活动中提炼的用户偏好摘要
  • 实体概况(公司信息、产品属性、文章摘要)
  • 索引到向量库中的知识库嵌入(embeddings)
  • 从历史交互模式中构建的微调数据集

这些内容由批处理作业(Spark、dbt、定时 SQL)计算并持久化到离线存储,然后同步到在线存储。在请求时,你只需获取预先构建的用户概况——比如 500 个 token——而不是检索 50 条原始交互并进行实时摘要。

流处理层(秒级到分钟级)

变化速度快到无法忍受每日计算的滞后,但又没快到需要实时查询的上下文:

  • 最近的会话活动(最近 10 次交互)
  • 活动文档的编辑历史
  • 实时行为信号(如“正在浏览类别 X”)

这通过流式流水线(Kafka + Flink,或 Spark Structured Streaming)处理,并持续写入在线存储。特征始终足够新鲜,而无需在每次请求时都从源系统查询。

实时层(按需)

必须反映当前确切状态的上下文:

  • 购买建议中的库存可用性
  • 对缓存敏感的合规性数据
  • 当前用户消息本身

这一层在设计上应该是精简的。失败的模式是将所有内容都放在这里:每次额外的源系统调用都会增加 50–300ms 延迟。响应预算为 200ms 的对话应用,每次请求无法负担超过一两次的实时查找。

对于大多数生产系统,合理的分布比例大约是 80% 批处理、15% 流处理、5% 实时处理。反转这一比例的团队——即每次请求都重新查询所有内容——会在延迟和成本上付出 10 倍的代价,而质量却得不到成比例的提升。

时间点准确性:隐蔽的训练陷阱

在 LLM 语境下,最被低估的特征存储概念是时间点准确性(point-in-time correctness),它的缺失导致了一类难以诊断的模型失败。

当你根据历史案例微调 LLM 时,每个训练示例通常包含一些检索到的上下文:当时的用户概况、其最近的活动以及相关文档。如果你在组装这些训练数据时不够细心——为 历史 交互获取 当前 的用户概况——你就会用未来的信息污染训练数据。模型会从那些包含了在原始交互发生时还不存在的信号的“上下文”中学习。

其结果是,模型在你的评估集上表现良好(评估集也存在同样的污染),但在生产环境中却会莫名其妙地性能下降(因为生产环境没有污染)。

特征存储通过为特征值添加事件时间戳(event timestamps)版本来解决这个问题。在构建训练数据集时,你使用 交互发生时 存在的特征值来重构每个示例,而不是使用今天的特征值。离线存储维护完整的历史记录,使这成为可能。

在实践中:如果你正在为客户支持摘要模型构建训练数据,你正需要将每张支持工单与 提交该工单时 的客户概况和交互历史配对,而不是现在的状态。这需要时间点检索,也就需要一个带有历史快照的离线存储。

无意识的特征存储(以及为什么规范化它至关重要)

这就是 TODO 框架识别出的失败模式:团队在没有将其命名为特征存储(Feature Store)的情况下构建了特征存储,结果既承担了成本,又没有获得收益。

这种模式显而易见。一个团队从实时检索开始。由于延迟是个问题,他们增加了 Redis 缓存。缓存变得陈旧,于是他们增加了一个定时任务(cron job)来刷新它。定时任务和训练流水线对用户偏好聚合的定义不同,导致他们微调后的模型在推理时看到的输入与训练时不同。一次模型更新失败了,因为没有人对特征模式(feature schema)进行版本控制。一次隐性的 API 失败意味着 2% 的请求获得了“用户无历史记录”的上下文,因为上游超时被解释为空结果——这就是“幻象零值(phantom zero)”问题。

他们构建的其实就是一个特征存储:离线计算、在线服务以及两者之间的某种同步。他们缺失的是规范化:一个同时用于训练和服务端的单一特征定义、模式版本控制、新鲜度监控以及数据质量告警。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates