跳到主要内容

你的 AI 功能可靠性受限于无人负责的上游 ETL 流水线

· 阅读需 10 分钟
Tian Pan
Software Engineer

AI 功能拥有仪表板。提示词(Prompt)有版本控制。评估套件(Eval suite)有轮值表。然后是一个写于 2022 年的上游定时任务(cron job),由一个在两次重组前就退出了分析部门的团队负责,它生成了构建你的检索索引所需的 CSV 文件。那个定时任务没有 SLA。那个 CSV 没有 Schema 契约。负责它的团队根本不知道它正在为一个 AI 功能提供数据。当它发生变化时——它一定会变——AI 团队将花费三周时间去调试一个完全没有出错的提示词。

你即将追踪的 AI 质量回退几乎从来不是 AI 问题。它是一个穿着 AI 外衣的 ETL 问题。需要落实的规范是两者之间的衔接点——契约、血缘(lineage)、新鲜度信号、成对的轮值——而没有将其正式化的团队,所交付的 AI 功能的可靠性将受限于公司里最不受待见的定时任务。

隐形的依赖

AI 功能是一个流水线。这条流水线的最后 20%——提示词、模型、评估框架(eval harness)——是工程投资集中的地方,也是每次复盘开始的地方。而前 80%——摄取任务、规范化步骤、去重处理、每日快照、从第三个上游系统合并进来的列——是在任何人在会议中提到“Agent”这个词之前就构建好的。它由那些认为自己拥有的是一个面向分析用户的数仓的人负责。他们不知道下游有一个 LLM。

这种不可见性就是故障模式。数据团队像对待 Tableau 后端一样对待他们的流水线:重命名一个列是常规清理,每日运行延迟到隔天运行是可接受的降级,Schema 的“改进”只是在 Slack 的分析频道发个通知。这些沟通都没有传达到 AI 团队,因为 AI 团队从未被注册为消费者。没有任何契约规定嵌入(embedding)流水线依赖于 customer_segment 是字符串而不是整数。没有任何消费者注册表会在下游 RAG 索引读取数据团队的输出时向他们发送告警。

与此同时,AI 团队将上游视为绝对真理。他们的评估是针对过去某个时间点拍摄的数据快照进行的。他们的检索之所以有效,是因为列都在他们预期的位置。他们的微调是在他们假设平稳的分布上训练的。每一个假设都是一份从未签署的契约,而上游团队今天下午就可以随意违反其中的每一项,因为没有人告诉过他们自己曾经达成过任何协议。

被记录为“模型回退”的故障模式

这种模式重复得如此频繁,以至于几乎成了一种典型。AI 团队注意到每周评估中的质量下降了四个百分点。延迟正常,错误率正常,模型版本没变,提示词也没变。他们花了一周时间调整提示词。又花了一周尝试不同的分块(chunking)策略。第三周对检索流水线进行消融实验。最终,有人将一批糟糕的输出追溯到某个特定文档,找到了该文档,查看了它最后一次摄取的时间,发现上游流水线在两周前开始过滤掉某一类记录,原因是进行了一次“无害的清理”,删除了标记为“内部”的记录。而该 AI 功能正依赖于这些记录。

第二种模式:上游流水线开始输出精度不同的列。以前是以毫秒为单位的时间戳,现在变成了秒。检索层原本使用时间戳来解决相关性排名的冲突。突然间,冲突解决变得不可预测,同样的查询在不同的日子返回不同的文档,评估套件开始出现波动。模型没问题。检索没问题。数据差了一个小数点。

第三种:由于缩减成本的计划降低了非关键任务的优先级,上游流水线的运行频率从每小时降到了每天。RAG 索引现在有长达 24 小时的滞后。AI 功能开始用落后一天的数据回答关于“最新”信息的问题。没有任何地方触发告警——流水线运行成功,索引更新成功,模型响应成功——唯一的信号是客户满意度在一个季度内悄然下降。

第四种:上游流水线由于有人更改了数仓列类型,静默地将一个长字符串字段截断为 256 个字符。RAG 索引现在包含的分块(chunk)在每个长文档的后半部分都缺失了。检索仍然返回分块。只是分块是不完整的。模型基于不完整的上下文进行回答。幻觉率上升。团队归咎于模型。

在所有这些案例中,AI 团队的前三个假设都是关于模型的。这些假设没有一个是正确的。第四个假设最终指向了数据。数据几乎总是答案,而几乎从来不是人们首先寻找的地方。

必须存在的契约层

上游流水线与 AI 功能之间的缝隙必须成为一等产物,而这个产物就是数据契约(data contract)。契约不是一条 Slack 消息,也不是一个 Wiki 页面;它是一个带有版本控制、可由机器校验的规范,定义了模式(schema)、新鲜度、完整性以及生产方承诺且消费方依赖的变更策略。

模式子句定义了 AI 功能读取的每一个列,包括其类型、允许值及语义。重命名列属于违约行为。拓宽类型属于违约行为。新增一个可为空(nullable)的列是可以的;但将一个非空列转变为可为空列则属于违约。生产方在不升级契约版本的情况下不能进行这些更改,而升级契约版本会触发对每个已注册消费方的通知。

新鲜度子句定义了允许 AI 功能看到的数据的最大账龄。“至少每四小时更新一次”是一个新鲜度 SLA。违反该条款的流水线即为违约流水线,无论文件内部的记录是否依然正确。消费方被允许——甚至是要求——拒绝提供超过新鲜度阈值的上下文,并将陈旧性展示给用户,而不是默默地基于旧数据提供回答。

完整性子句定义了必须到达的预期记录百分比。“至少 98% 的源记录必须出现在输出中,缺失的 2% 归因于已记录的过滤器”是一个完整性 SLA。即使产生了输出,仅达到 95% 阈值的运行也违反了契约。静默截断是上游引起的 AI 退化中最常见的情况,而完整性子句正是为了捕捉这类问题而存在的。

变更策略子句定义了生产方如何演进契约。“不向后兼容的更改需要提前 30 天通知并升级契约版本;小的增项可以随时进行”就是一种变更策略。如果没有它,每一次更改都会变成通过 Slack 解决的协调问题,而这种协调最终会失败,因为 AI 团队通常不在正确的频道里。

血缘、所有权与配对轮值

仅有契约是不够的。契约是一个承诺;血缘(lineage)则是谁向谁承诺了什么的地图。数据血缘图将喂给 AI 功能的每一个字节追踪回产生它的上游流水线,并将拥有该流水线的团队确定为责任方。如果没有血缘,当功能出现退化时,AI 团队甚至无法识别是谁的流水线出了问题;他们必须取证式地重建从“糟糕的输出”到“糟糕的列”到“糟糕的任务”再到“糟糕的团队”的路径,而每一步都要花费数小时。

血缘将 AI 功能变成了一个已注册且可见的消费者。上游团队可以在仪表盘上看到,他们的 daily_customer_facts 表供应给三个下游消费者,其中之一就是 AI 支持智能体。上游团队现在在社交和协作上都能意识到,“无害的清理”并非真的无害。消费者不再是隐形的了。

下一步是配对轮值(paired on-call)。当 AI 功能退化且根因在上游时,上游团队会被呼叫。这并非是对 AI 团队呼叫的补充——而是当根因在他们那边时,直接呼叫他们。AI 团队针对质量退化的运维手册(runbook)以“检查上游契约状态”开始,如果违反了契约,则将报警路由给上游负责人。这听起来很官僚,直到这种事发生过两次,上游团队内化了这样一个认知:他们的定时任务(cron job)有一个通过报警(而非仅仅通过仪表盘)来“付费”的客户。行为转变会非常迅速。

具备新鲜度感知的检索策略是消费端的补充。检索层知道它所提供的每份文档的时间戳,知道源数据的新鲜度 SLA,并拒绝提供已过时的上下文。它被允许向用户展示陈旧性——“我掌握的关于此项的最新数据是 36 小时前的;我可以据此回答,但如果你需要更新的数据请告知”——而不是默默地降级。这把静默失败转化成了明显的失败,而明显的失败是团队唯一能采取行动的失败类型。

架构层面的实现

AI 功能是两条流水线缝合在一起的产物,而这种缝合处在几乎所有组织中都是不可见的。上游流水线是 2010 年代的产物:为分析而建,由数据团队拥有,像基础设施一样运行。下游功能是 2020 年代的产物:为推理而建,由 AI 团队拥有,像产品一样运行。两者之间的缝隙是 2025 年的问题,在这两个十年中都没有人负责,而没有将其正式化的团队所交付的 AI 功能,其可靠性上限将由上游链路中插桩(instrumentation)最少的环节决定。

需要落地的纪律并不光鲜。它是具有约束力的数据契约,是标明所有权的血缘图,是穿透检索层传播的新鲜度信号,以及遵循数据流向而非组织架构图的轮值机制。这些都不是科研。这全都是介于一个可运行的 AI 演示(demo)与一个可运行的 AI 产品之间那些乏味的工程细节。能够大规模交付可靠 AI 功能的团队,是那些当上游数据团队提议进行“小幅清理”时会将契约摆在台面上,以及当上游团队忘记契约存在时会触发报警轮值的团队。

下次你的 AI 功能退化时,不要先忙着调整提示词(prompt)。先去读读血缘图,问问这周上游发生了什么变化。答案几乎总是在那里。而你原本打算花三周时间重写的提示词,几乎肯定是在准确执行它该做的任务——只不过是基于那些悄悄变得不再符合你评估套件假设的数据。

References:Let's stay in touch and Follow me for more thoughts and updates