一个用户询问你的智能体(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 仅仅是一个口号。
展示数据账龄 —— 面向 Agent 和用户
测量出的延迟(lag)只能告诉运维人员索引平均陈旧程度。它在当下无法帮助 Agent,也无法帮助正在阅读那个言之凿凿的错误答案的用户。为此,你需要在使用的那一刻展示数据的账龄(age)。
每个数据块(chunk)都应携带其来源追溯作为可检索的元数据:源内容的最后修改时间、Embedding 的计算时间,以及理想情况下隐含保质期(shelf life)的内容类别。当 Agent 检索到一个数据块时,它应当能“看到”底层文档是在 11 天前最后更新的。这一点至关重要,因为 Agent 随后可以采取不同的行为。它可以含糊应对:“根据 5 月 12 日的最后一次同步,价格为 X —— 请允许我对比实时系统进行确认。”它可以选择调用新鲜的 API,而不是信任索引中快速变化的实事。它也可以拒绝回答那些因过时而可能产生危险的问题。如果数据块只是作为没有时间戳的匿名文本到达,那么这一切都不可能实现。
用户同样理应获得这种信号。过时检索最具破坏性的特性在于,用户无法区分一个实时事实还是一个星期前的快照 —— 两者都以同样自信的句式、同样的字体呈现。一句简单、诚实的“基于 5 月 12 日同步的数据”,在建立信任方面比再进行一轮模型微调更有用。用户对那些承认自身延迟的系统表现得异常宽容。而对于那些将过时数字当作实时数字展示并被拆穿的系统,他们是无法原谅的。
这里可以借鉴缓存领域的一个有用原则:将检索到的上下文视为具有生存时间(time-to-live, TTL)。如果一个数据块的账龄超过了其所属内容类别的阈值,系统就不应再默默地提供它。系统应当拒绝提供、刷新数据,或者降级为含糊式的回答。超过声明保质期的过时上下文应被视为缓存未命中(cache miss),而不是有效的命中(hit)。
并非所有文档的衰减率都相同
一旦你接受了“新鲜度需要成本”这一事实,本能反应往往是更频繁地重新索引所有内容。这就是陷阱。在紧凑的周期内对整个语料库进行重新 Embedding 的成本极高 —— 从业者报告称,每周对大型语料库进行全量重新 Embedding 的月度账单高达五位数,而且成本是随着语料库规模增长的,而不是随着实际变化的内容增长。这其中的大部分支出都是浪费,因为大多数文档并没有改变。
杠杆点在于意识到衰减率并不是统一的。价格页面、库存清点、值班轮换和 Feature Flag 的状态是不断变化的 —— 它们的有效保质期以小时计。而入职指南、复盘报告、架构概览、法律政策则很少变动 —— 它们的保质期以月计。在两者身上投入同样的重新索引预算才是真正的错误,而不是预算本身。
行之有效的模式是分层处理,有时被称为冷热分桶(hot/cold bucketing)。根据内容变动的速度对语料库进行分区。热桶(hot bucket)—— 即那一小部分变化快、重要性高的文档 —— 采用事件驱动的更新:通过变更数据捕获(CDC)钩子或 Webhook,在编辑发生后的几秒钟内触发重新 Embedding。冷桶(cold bucket)则采用廉价的每日或每周批处理。这并不是什么稀奇的工程方案;它就是应用在 Embedding 上的缓存分层逻辑。它比统一的快速重新索引要便宜得多,又比统一的慢速重新索引要新鲜得多,因为它将新鲜度预算花在了过时真正会造成损害的地方。
针对热层的事件驱动刷新值得投入集成成本,正是因为批处理刷新存在结构性缺陷:当你接受了“批处理之间的间隔内也足够好”的那一刻,你就已经接受了系统在每次运行间隔期间都会默默退化。对于那些确实每月才变动一次的文档,这种间隔是无害的。但对于价格页面,这种间隔就是一个 Bug。
像监控复制延迟一样监控它,因为它本质上就是
回到副本(replica)的类比,因为这个类比本身就是实施方案。你已经知道如何运维一个带有延迟的副本了。对向量索引也做同样的四件事。
持续测量延迟。 将源修改时间与索引更新时间之间的差值作为指标发送,并包含百分位数。观察 p50 的漂移情况和 p99 的尾部。建立告警。 批处理失败、队列堵塞、Webhook 停止触发 —— 所有这些都会表现为延迟超过阈值,而且在用户察觉之前,这些都是可以触发告警(pageable)的。将其放在负责团队真正会查看的仪表盘上,紧挨着他们已经信任的其他指标。追踪覆盖率漂移:即语料库中超过陈旧阈值的比例,这样你就能在缓慢的“腐烂”变成积压问题之前发现它。
重新定义视角才是关键。索引的新鲜度不是内容问题或模型问题。它是一个分布式数据系统的运维属性,拥有延迟、尾部、负责人和告警机制 —— 就像它在功能上所属的副本一样。
目前,在大多数团队中,SLO 是存在的,但未被书面化、未被测量、也无人负责。Agent 正在默默地根据上周的数据做决策,直到有一天客户拿着你家 Agent 给出的一个已经过时数天的价格来质问你,大家才察觉。请有意识地设定这个指标。测量延迟。告诉用户答案的时效。如果不这样做,结果并不是“没有 SLO” —— 而是你绝不会同意签署的那种糟糕 SLO。
会员专享
余下内容仅对会员开放。
会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
- —完整文章,包含未公开存档的部分
- —可落地的工作框架,附带权衡与决策依据
- —新文章抢先看,先于公开发布
随时取消 · 一次订阅,畅读全部