LLM 应用的特征存储模式:停止检索那些你可以预计算的内容
大多数构建 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)”问题。
他们构建的其实就是一个特征存储:离线计算、在线服务以及两者之间的某种同步。他们缺失的是规范化:一个同时用于训练和服务端的单一特征定义、模式版本控制、新鲜度监控以及数据质量告警。
规范化意味着:
- 统一的转换函数:计算特征的函数只有一个,同时用于批处理流水线和服务路径。
- 模式版本控制:让模型产物知道它们是基于哪个版本的特征模式训练的。
- 新鲜度监控:当流式层的延迟超过 5 分钟时发出告警。
- 分布监控:当在线特征值与离线存储在统计上发生偏差时进行标记。
这并不是要你采用 特定的平台(Feast、Tecton、Hopsworks 都可以)。这是关于意识到你正在进行特征工程,并应用该实践所需的运维规范。
传统特征存储中不存在的 LLM 特定模式
传统的特征存储模式需要针对 LLM 用例进行扩展。
上下文组装作为一等操作
传统的特征存储提供标量特征(例如:user_age: 34, spend_30d: 1200.50)。LLM 上下文组装生成的是结构化文本块:一个 500 token 的用户画像摘要、一组相关的文档片段、一个系统提示词模板。LLM 应用的“在线存储”不仅需要提供特征值,还需要提供在请求时进行组合的预计算上下文片段。
这意味着用户的在线存储条目可能是他们预先格式化好的画像摘要,可以直接插入到提示词中,而无需任何额外处理。组装层只需进行拼接:系统提示词 + 用户画像 + 最近交互 + 检索到的文档 + 当前查询。
Token 预算作为约束维度
传统特征有大小(Redis 中的字节数),但这不会影响模型行为。LLM 上下文片段具有 token 计数,这直接限制了上下文窗口中还能容纳什么。LLM 应用的特征存储需要跟踪每个特征的 token 数量,并实现感知 token 预算的组装:如果用户画像占用了 600 token 且上下文窗口为 8K,组装层就会知道文档检索还剩下多少 token。
用户嵌入作为压缩上下文
Google 的 USER-LLM 研究展示了一个重要的模式:与其在每个提示词中附加数千 token 的原始用户历史记录,不如将用 户的行为序列编码为一个紧凑的嵌入(Embedding,32 token),模型通过交叉注意力(cross-attention)来消耗它。用户嵌入变成了传统意义上的特征——一个预先计算并存储在在线存储中的固定大小的向量表示——但它携带了语义信息,否则这些信息将需要巨大的上下文窗口。
这就是 LLM 领域的特征工程:将高维原始数据转化为模型能够实际使用的紧凑表示。
延迟与成本:数据说明
实际的利害关系是非常具体的。
一个没有预计算的典型 RAG 流水线:
- 查询嵌入(Query embedding):100–500ms
- 向量搜索:50–300ms
- 源文档检索:不确定
- 生成:500–2000ms
- 总计:650–2800ms,波动较大
有了预计算的上下文(批量画像 + 流式近期活动 + 缓存嵌入):
- 缓存查找:命中时 0.35ms
- 向量搜索(缓存未命中):100ms
- 生成:500–2000ms
- 总计:500–2100ms,可预测
一个实时语音助手的实现报告称,在缓存命中时速度提升了 316 倍——0.35ms 对比 110ms——在生产流量中缓存命中率为 75%。对于总延迟预算为 200ms 的应用来说,预计算上下文与实时上下文组装之间的区别,就是“可用”与“不可用”的区别。
成本也遵循类似的模式。预先对 100 万个产品目录进行嵌入大约花费 0.10 美元(一次性)。而以每秒 100 次请求调用 API 进行嵌入,每月成本达数千美元。每个请求 500 token 的预计算用户画像摘要, 其推理成本比从 5000 token 的原始历史记录中组装相同信息的成本低 10 倍。在生产规模下,批计算成本被分摊到每一个消耗该特征的请求中。
什么时候不该进行预计算
批处理层并不适用于所有场景。实时检索在以下情况下仍然是合适的:
- 高度动态的数据:库存数量、市场价格、实时传感器读数 —— 任何几秒钟前的数据就会导致错误输出的场景
- 安全敏感的上下文:必须反映当前系统状态的授权状态或合规性数据
- 单次使用的特征:仅供单个模型使用的上下文,且物化流水线(Materialization Pipeline)的开销超过了节省的成本
- 低流量应用:如果一个特征在更新前被调用的次数少于 1 万次,那么摊销计算就不划算了
目标并不是要预计算一切。而是要针对每个上下文来源做出深思熟虑的架构选择,而不是因为实时检索是最简单的路径就默认选择它。
清单
如果你的 LLM 应用在推理时检索上下文,请进行以下审计:
- 此上下文对多个请求是否相同? 如果是,它就是批处理特征的候选者。
- 此上下文的变化频率是否快于每小时一次? 如果不是,流式物化就绰绰有余了。
- 在训练流 水线和推理(Serving)流水线中,计算的是否是相同的聚合? 如果不是,你将面临训练-推理偏差(Training-serving Skew)的风险。
- 你上次验证缓存的上下文是否真实反映源系统状态是什么时候? 如果你没有监控数据作为答案,你将面临幻象失败(Phantom Failure)的风险。
- 你是否知道每个上下文来源贡献了多少个 Token? 如果不知道,你并没有在管理你的上下文窗口预算。
完成此清单核对的团队通常会发现,70–80% 的实时检索可以在不损失质量的情况下迁移到批处理层 —— 而剩下的 20–30% 终于获得了所需的延迟预算。
特征存储(Feature Store)模式并不是为 LLM 发明的,但它几乎完美地契合了 LLM 上下文组装问题。及早识别出这种模式的团队可以将工程时间花在模型质量上。而那些在事故发生后才发现这种模式的团队,则要把时间花在梳理杂乱无章的临时流水线上。
- https://www.hopsworks.ai/dictionary/feature-store
- https://www.databricks.com/blog/what-feature-store-complete-guide-ml-feature-engineering
- https://unified.to/blog/index_time_rag_vs_real_time_rag_choosing_the_right_retrieval_strategy
- https://arxiv.org/html/2603.02206v1
- https://research.google/blog/user-llm-efficient-llm-contextualization-with-user-embeddings/
- https://arxiv.org/html/2601.12078
- https://medium.com/@vesaalexandru95/the-role-of-feature-stores-in-fine-tuning-llms-22bd60afd4b9
- https://aws.amazon.com/blogs/machine-learning/personalize-your-generative-ai-applications-with-amazon-sagemaker-feature-store/
- https://weaviate.io/blog/context-engineering
- https://serokell.io/blog/design-patterns-for-long-term-memory-in-llm-powered-architectures
- https://building.nubank.com/dealing-with-train-serve-skew-in-real-time-ml-models-a-short-guide/
- https://www.featurestore.org/benchmarks
- https://odsc.com/speakers/personalizing-llms-with-a-feature-store/
