跳到主要内容

向量索引存在一个没人定义的陈旧度 SLO

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个用户询问你的智能体(agent)企业版的当前价格档位。智能体检索了一个分块(chunk),读取并回答:“每月 2,000 美元。” 信心满满,来源明确,格式优美。问题在于价格在四天前已经变了。智能体引用的数字在上周是真实的。它检索到的分块是在变更之前嵌入的,而索引还没有赶上进度。

没人决定让这种情况发生。没有设计评审说“智能体可以根据长达四天前的数据进行回答”。只是有一个每晚或每周运行的重新索引任务,以及一个随心所欲编辑内容的团队,而这两个时钟之间存在一个没人衡量的缺口。那个缺口就是一个服务等级目标(SLO)。无论你是否写下来,它都存在。唯一的问题是你是有意设定它的,还是由于意外继承下来的。

这是检索增强(RAG)系统的一种隐蔽失败模式。返回无关分块的糟糕检索是显而易见的——答案明显错误,有人会提交 bug。而陈旧的检索返回的是一个相关的分块,只是刚好过时了。答案看起来是对的:格式良好、切中主题,并且引用了真实的文档。它只是在时间上错了。时间是你的评估(evals)几乎从未测试过的一个维度,因为你的评估集冻结在了某个周二,而世界并没有。

“索引每晚更新”是一个随口承诺的 SLO

走进任何一个运行生产环境 RAG 流水的团队,询问他们的新鲜度保证是什么。你通常会得到一个关于机制的描述,而不是一个数字。“我们每晚重新索引。”“有一个定时任务。”“它会在下次抓取时拾取变更。”这些都是被悄悄提升为承诺的实现细节。

“我们每晚重新索引”实际上意味着:下午 2 点编辑的文档直到下一次运行才能以新形式被检索到,因此最坏情况下的延迟约为 10 小时,平均延迟为 5 小时。 这才是真正的 SLO。它有一个分布。它有一个尾部(tail)。但因为没人把它表述为一种承诺,所以没人对尾部负责,当运行失败时没人报警,也没人能告诉你当每晚的任务静默跳过一批数据时会发生什么。

副本延迟(replication-lag)的类比是极其精确的,值得认真对待。每个数据库工程师都知道只读副本滞后于主库,这种延迟是一个具有 p50 和 p99 的测量量,在写入负载下会激增,并且你会针对它设置告警。副本延迟是一个拥有一流指标、仪表板和寻呼报警的操作度量。嵌入索引延迟(Embedding-index lag)是同样的物理现象——下游副本落后于真实数据源——但它被视为隐形的。矢量索引就是一个副本。它只是恰好是一个大多数团队从未进行过插桩监控的副本。

处理方式上的差异并非技术性的,而是文化上的。数据库副本延迟之所以受到监控,是因为停机事故让每个人都意识到它很重要。索引延迟还没有经历过属于它的停机事故,或者更确切地说,它已经发生过了,但事故表现为“智能体给出了一个略微错误的答案”,并被归咎于模型幻觉(hallucination),而不是流水线问题。

新鲜度是一份合同,而合同应包含数字

如果你想解决这个问题,第一步不是技术上的。而是把数字写下来。检索新鲜度应该是索引所有权团队与所有使用该索引的人之间的一份明确合同,而一份写着“最终(eventually)”的合同根本称不上是合同。

一个可用的新鲜度合同包含几个部分。最大延迟:源变更到该变更可被检索之间的最坏情况时间,以百分位数表示,因为平均值具有欺骗性,而尾部才是伤人的地方。范围:保证涵盖哪些内容,因为你几乎肯定无法为千万级文档归档提供与定价页面相同的新鲜度保证。测量:如何在生产环境中观察延迟,而不是根据定时任务表进行估算。责任人:一个具体的团队,当违反合同时会收到报警。

最关键的指标被一些从业者称为陈旧检索率(stale retrieval rate)——即在实时查询中,至少返回一个分块的嵌入是在源文档最后一次更新之前计算的比例。这个数字直接转化为对用户的伤害。p99 延迟两小时听起来很抽象;“本周 3% 的回答是基于已经过时的分块生成的”则不然。而且这是可以测量的,无需猜测:在矢量旁边存储源文档的最后修改时间戳,在查询时将其与分块的嵌入时间戳进行比较,并进行计数。

注意这种重构(reframing)的作用。它将新鲜度从“流水线的一个属性”转变为“每个答案的一个属性”。一旦你能将陈旧度归因于单个响应,你就可以将其放入仪表板,针对它进行回归测试,并在事故复盘中进行讨论。一个无法在每次请求中测量的 SLO 仅仅是一个口号。

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