跳到主要内容
DRIFT 那一行是错的,而 0.8626 那一行没人看

DRIFT 那一行是错的,而 0.8626 那一行没人看

全景
全景

· 阅读约 16 分钟

Nova Pro 那次跑批的记录里,EC2 On-Demand Standard vCPU 配额那一行长这样:record 5,superseded 32,live 16,判定 DRIFT。

三个数字并排,各带来源,一个 verdict。这是整个 demo 最漂亮的一帧,也是最经不起看的一帧。

16 不是“记录过时”的证据。GetServiceQuota 给你的是这个账户被批下来的 applied value,GetAWSDefaultServiceQuota 给的才是默认值。5 是默认,16 是这个账户被提过额。所以那一行真正说的是“这个账户和出厂设置不一样”,被一个叫 DRIFT 的词盖成了“文档和现实不一样”。这两件事隔着一整个数据模型,不是隔着一次 API 调用。

这篇是 Sanity Challenge Path One 的参赛作品,题目是 ship 一个能查真实内容的 agent,作者是个被 AWS 文档坑过的人——他自己开场就交代,曾经从文档里抄了个默认 vCPU 配额上线,生产里炸了,没有任何预警。所以他做的这个东西不是玩具:它从 Sanity 的知识库里读带类型的事实,用一条确定性规则选赢家,模型被明确禁止推翻,然后对 AWS 打一次只读的现场校验,看裁决后的记录和现实还对不对得上。

这个立意我认。但它最像工程的那一层,恰恰是语义最脏的那一层;而它最不像产品的那一层——一个普通函数拥有那个数字——才是唯一站得住的地基。这篇只走三段:先把那个 verdict 拆开,再看检索层那个没人看的数字,最后说这块地图上还空着什么。

一、一个 verdict 底下压着四个问题

评论区里 Arhan Canli 干的第一件事,是把“拿裁决后的记录去比应用到的配额”这句话本身判成了框架错误。原因很朴素:GetServiceQuota 返回的是账户的应用值,GetAWSDefaultServiceQuota 返回的是默认值,你把一个跟另一个比,比出来的差值到底是“文档陈旧”还是“这个账户被提过额”,从结果里读不出来。

作者认了,而且认得干净:EC2 那行 5 对 16,不是记录陈旧的干净证据,完全可能只是这个账户拿到过提额。

我在这里想多说一句,因为这件事比“一个 bug”重要。一个验证层报告“我发现了漂移”,你会信它,因为它背后有一次真实的 API 调用,有 region,有 HTTP 200;而一个验证层没发现漂移,你反而会去翻日志。假阳性比假阴性更消耗信任,因为它不触发怀疑。 这套东西如果上线给一个团队用,第一次误报之后,那一列 DRIFT 就会被所有人当噪音划过去,整个校验层等于不存在。

更要命的是粒度。一行 verdict 只能装一个布尔值,可那一行底下至少压着四个独立命题:文档里那个数字过时了吗;这个账户的值还是默认值吗;这个 region 和别的 region 一样吗;这个版本还在 standard support 里吗。四个问题,一个布尔,误报不是意外,是必然而已。

Canli 又往下补了两刀,一刀比一刀深。

第一刀是 region。Service Quotas 是按 region 回答的,同一个账户完全可以在 us-east-1 提了额,在 eu-west-1 还挂着默认值。那“5 对 16”这个对比,换个 region 跑可能就是“5 对 5”,一行 agreement。一个不带 region 的 verdict 在语义上是没有指称的,它没说自己在说哪儿。

第二刀是 support tier,扎在 RDS 那一行上。记录写 13 是最老的 major version,现场 describe-db-engine-versions 说 11 还活着,于是判了 DRIFT。可 11 活着是因为它在 extended support 里——仍然 runnable,但已经不在 standard support 里了。“能跑什么”和“在 standard support 里有什么”是两个问题,一个 11 一个 13,不是矛盾,是各答各的。作者的修法是给每条 fact 加一个 support-tier 字段,让这一行读起来是“两个问题上的各自一致”,而不是“一个问题的两个答案”。

到这里,那四个属性就齐了:region、applied value、default value、support tier。四个都很朴素,缺任何一个,校验层就从“在验证”退化成“在瞎判”。

而我想钉在这里的坐标是:live check 之所以看起来最硬,是因为它有一个外部 API 给它背书;但它恰恰是最容易把不同命题压成一个数字的一层。数字越具体,错得越像真的。 一个写着 16 的格子里,你没法看出它回答的是哪个问题。

顺便说一句 5 这个数。5 恰好是默认值,如果裁决记录里存的就是默认值,那它跟现场的应用值比较,从第一步就错配了,后面所有推理都是在比较两个不同主语。这个坑比“优先级规则选错了源”隐蔽得多,因为记录本身没错,5 就是控制台上写的那个数。

二、0.8626

现在说那个没人看的数字。

EBS general purpose SSD 每卷最大 IOPS,当前 gp3 是 80,000,出自 EBS 控制台。旧的 gp2 天花板 16,000 还留在一份老的 SSD 指南里。那份指南里有一句人真的会去搜的原话——关键词摆得整整齐齐,措辞完整,就是为被读到而写的。

TF-IDF 基线跑下来:旧的 16,000 得 0.8626,正确的 80,000 得 0.2026。

关键词检索把错的数字排在第一,还给它配了一个看起来非常自信的分数。

这不是 TF-IDF 的锅。打分打的是“这段文字和你这几个词有多像”,不是“这个数字现在对不对”。旧文档分数高,是因为它啰嗦、它完整、它把“EBS”“general purpose”“SSD”“IOPS”“maximum”这些词一个不落地铺在同一个句子里;控制台那一条是字段加值,词少,干巴,相似度自然低。写得越像人问问题,越容易被排上来——这个偏差是结构性的,不是语料脏。

换成 embedding 也不会好。稠密检索对“语义上像不像这个问题”更敏感,对“这个数字是哪一年的”一样不敏感。你把向量维度堆到再大,也堆不出一条时间轴来。相关性分数和时效性是两根正交的轴,而绝大多数检索栈只画了前一根。 你 top-k 拿出来,模型照着念,念出来的东西旁边挂着 0.86,读的人以为那是可信度。我见过太多 demo 把 retrieval score 印在答案下面当“置信度 86%”,那玩意的量级和星座运势差不多。

真正的置信度不该来自检索层,只能来自裁决层。这也是为什么这个 demo 里“search 再 read”这个两步调用值得单独拎出来说:先 knowledge_base_search 拿一个排序,再 knowledge_base_read 取全文——第二步不是为了多一次调用,是为了让证据链上有一环是“我确实读了这一条”,而不是“我搜到了一些相关的”。排序是猜测,全文才是证据。模型嘴里那句答案到底出自 Sanity 还是出自它自己的记忆,差别就在这一环上。

我觉得这篇里最该被同行抄走的,还不是这个。是它把上面那个失败做成了一条可以跑的命令。

repo clone 下来,装 requirements,跑 reconcile_offline,scope 到 us-east-1 的 EBS limits,返回 80000 IOPS,Service Quotas console 排在官方文档之上;另有一条 baseline 命令,专门复现关键词检索返回错误答案那一幕。一个 demo 愿意把自己的失败路径做成一等公民,比它展示十次成功都有信息量——因为评审没法评估一个只展示成功的方法,只能评估它的最好情况。作者把最坏情况也交出来了。

无凭证那条路径也值得一提:不用 Bedrock,不用 token,就能看到裁决和排序的全貌。可验证性这东西,门槛越低越值钱。

三、数字归函数,句子归模型

现在说地基。

这套东西的分工是:模型只干两件事,跑工具,写那句人话。数字归一个普通的函数。裁决用的是一条确定性规则,模型被明确禁止覆盖它。外面再套一个 guard:模型说的值如果和确定性裁决的值不一致,扔掉模型的话,用裁决值。

有一次 EBS 的跑批,Nova 的措辞糊了,guard 抓到,直接替换成正确值,没有人工介入。

我想把这件事的性质说清楚:这个设计的好处不在“准”,准是函数的本分;它的好处是把模型的失败模式变成了可见的。模型说错话,不是因为它算错了,是因为它把数字说糊了;guard 抓的正是“措辞和事实不一致”这个缝,然后缝上。整个系统的可信度不来自模型变强,来自那一次减法——把“能不能说这个数”从模型手里拿走。

这块地图上现在的风向是反的。所有人都在给 agent 加工具:浏览器、代码执行、一堆 MCP server、文件系统。加工具当然有用,但很少有人在做另一件事——收窄它的输出权。这个 demo 的全部可信度,就来自那一次收窄。

然后是两次事故,我觉得比 guard 更有价值。

第一次,Nova 往 reconcile 函数里塞了个空字符串,函数一查,零条 fact。这时候代码如果写成“没查到就猜一个最可能的”,整个东西就废了。它没有,它 fail-closed,返回 “Not verified”。作者后来在服务端缓存了上一次真实 fetch 来修——注意这个修法:它不是让模型更聪明,是让“零结果”这个状态不再可能由模型的参数错误产生。

第二次,Nova 猜错了 factType,拿 instanceType 去查一个其实叫 regionalAvailability 的字段,一条真 fact 就那么消失了。修法是:typed fetch 返回零行时,退回只用 service 加 region 再查一次。

这两次都指向同一件事:模型的参数错误会伪装成“数据不存在”。 这是 agent 系统里最阴的一类 bug,因为日志上一切正常——查询成功,返回 0 行,退出码 0。你不会去看,因为没报错。fail-closed 加降级重查,是把这类 bug 从“静默的错误答案”变成“可见的空”。

还有一条我特别想夸:EBS 最大 IOPS 和 Lambda 并发这两条,Service Quotas 里没有对应条目,agent 报“现场校验不可用”,而不是编一个差不多的出来。承认查不到,比查到一个像模像样的更值钱。这条原则我和写代码的人在这块地图上分歧最大——大部分实现宁可给一个近似值,因为界面上空一格看起来像没做完。

四、provenance 是地基,不是装饰

每条 fact 在 Sanity 的 production dataset 里长这样:service、factType、region、key、currentValue、unit、effectiveDate,加一个嵌套的 source,里面是 name、kind、url。

裁决规则按 source kind 排优先级:console 和 pricing page 高于 changelog,changelog 高于 official docs,official docs 高于 blog,同级用最新的 effectiveDate 破平。

这块 schema 才是全文的技术内容。比 Bedrock 重要,比 Strands 重要,比 MCP 也重要——那三个都是可替换的管道,这块是地图本身。

理由很直接:“哪个数是对的”这个问题,在没有 provenance 的数据模型里是没有答案的。 你手上只有一堆文本,文本之间没有优先级、没有生效日期、没有来源分级,那模型只能靠“哪段读起来更像”来选。那不就是 0.8626 吗。检索层的假置信度不是检索的缺陷,是数据模型没提供判断依据的必然结果。

很多人给 agent 喂知识的方式是:把 markdown 切块,塞进向量库,完事。这条路在这块地图上走不通,不是因为检索不行,是因为切块把 provenance 切碎了。一块 512 token 的文本里,通常没有“这条来自控制台还是来自 2021 年那份用户指南”,也没有 effectiveDate。你切完的那一刻,裁决所需的信息已经丢了,后面再怎么调 rerank 也补不回来。前阵子看人讨论 chunk size 讨论了几百楼,在这块地图上这是个次要问题——边界优化得再好,你也没法从一块文本里恢复出“它是 2021 年的”这个属性。

还有一个设计我要单独拎出来:每条 fact 同时显示当前值和它取代的那个值,各自带来源。32 这个数不是垃圾,它是“那份旧用户指南说的”,它证明那份文档在某个时间点是真的。留下 superseded,等于给这张地图留了历史图层。没有历史图层,你回答不了“这个数哪天变的”,也没法在客户说“我们文档上写的是 32”的时候告诉他是哪一年的事。

模型权重里同样没有时间轴。32、5、16 在它记忆里都“像”是对的,它没有任何依据排这三者的先后。整个系统里唯一带明确时间语义的字段就是 effectiveDate,这也是为什么它必须是一等公民,而不是元数据角落里的一个可选字段。

最后提一个便宜但少见的做法:这个 dataset 是公开的,project ID 0q5ohtvv,不用 token,直接 GROQ 端点就能查。评审不用信截图,自己查一下就知道那条 fact 长什么样。把底座数据开放给人查,是这类 demo 最便宜的可验证性——所有人都在给自己的 agent 做 dashboard,很少有人把下面那层摊开。

五、摊开:一张表,一棵树

把上面梳过的压成一张表。

层解决什么问题这篇的实现失效模式
检索找到相关的条目search 排序 + read 取全文分数当置信度,旧文档措辞更“像”问题
内容模型提供判断依据typed fact + source{name,kind,url} + effectiveDate切块后 provenance 丢失
裁决决定哪个值算数确定性优先级规则,模型禁止覆盖规则本身错,或来源分级不合业务
输出把数字说成人话模型只写句子,guard 比对后替换措辞糊(可被 guard 抓)
现场校验检测现实是否移动只读 API 调用混淆 applied/default/region/support tier

再画一棵树,一个 AWS 事实的四种状态:

一个"事实"
├── 文档声明值(多份文档,各自有 source kind + effectiveDate)
│   ├── console / pricing   ← 优先级最高
│   ├── changelog
│   ├── official docs
│   └── blog                ← 优先级最低
├── 裁决值(规则选出,附带被取代的旧值)
├── 账户应用值(GetServiceQuota,按 region)
└── 账户默认值(GetAWSDefaultServiceQuota,按 region)
    └── 另有 support tier 轴(standard / extended)

树一画出来,那个 DRIFT 错在哪就一目了然:它拿“裁决值”直接去比“账户应用值”,中间跨了两个节点,还漏了 region 和 support tier 两根轴。这不是实现 bug,这是坐标系没建完就开始测量。

上面那张表也有个读法:纵向看,越往下越接近“真值”,但越往下越依赖上下文(region、账户、支持级别);横向看,每一层的失效模式都不一样,而这一层的问题没法用上一层的办法修——检索调得再好,也修不了 verdict 的语义。这是我认为这块地图最该被理解的一条规律。

六、live check 应该退成观测层

作者自己在评论里交代过:知识库不会定期重新校验,reconciliation 是一个时间点上的决定,靠每条 fact 的 effectiveDate;检测移动靠 on-demand 的现场校验。他说打算让现场校验把 last_verified 时间戳写回每条 fact,让“陈旧”变成一个字段,而不是一种感觉。

方向对,但只走了半步。

last_verified 记的是“我什么时候看过”,不记“我看到了什么”。真正该写回的是观测本身:region、applied、default、support tier、observed_at,五个字段,一条 observation,append-only。这样“记录对现场”的比较,就变成了“两条 observation 之间的比较”,而不是“一条记录对一次调用”。差别在于:前者可回溯、可 diff、能回答“这个值是哪天变的”;后者每次都是即兴判断,判断完就没了。

我前面说“给 agent 更多工具不如收窄它的输出权”,这句话在这一层不成立,我得收回一半。收窄输出权的前提是输入权足够。现场校验这一层的问题恰恰相反——它只有一个工具、一次调用、一个数字,信息根本不够支撑它想下的那个结论。收窄和扩输入是两个方向的事,别混着用。

还有一个性质得说清楚:这一层是账户局部的。换个 AWS 账户跑,现场数字就不一样,所以它的结论天然不能当通用事实用。这也解释了为什么它不能和裁决层共用一套 verdict 词汇——裁决层说的是“文档之间谁算数”,现场层说的是“这个账户此刻是什么状态”,两个主语。

七、落子

摊开这张地图,我的落子是这样:这块地盘——给 agent 喂会过期的真相——还很早期,绝大多数实现停在“检索 + 让模型念”,这篇走到了“裁决 + guard + fail-closed”,但在现场校验那一层栽在了语义上。

地图上还空着两块大的。

第一块是观测的持久化。last_verified 只是半步,append-only 的 observation 才是那块地。谁把这块画清楚,“文档过时”和“账户被改过”才能在数据模型里彻底分开——现在它们是在 verdict 里混着的,混着就会误报,误报就会让整列报警变成噪音。

第二块是“同一份数据回答不同问题”这类维度的建模。RDS 的 11 对 13 不是孤例,凡是沾版本、支持级别、生命周期的数据都有这个问题:extended support 里能跑,不等于在 standard support 里。这类维度现在没有通用做法,每个服务一套,最后都变成人肉在 prompt 里解释,而 prompt 里的解释不算数,因为它不在数据模型里,guard 抓不到它。

这两块的共同点是,它们都不在模型侧。模型侧的收窄已经跑通了——数字归函数、句子归模型、guard 兜底,这套东西可以照抄。剩下能涨的地方全在数据模型和观测层。所以下一颗钉子大概率钉在 observation 的 schema 上,而不是钉在某个更强的模型上。

这是判断,不是共识。你要是觉得现场校验那一层根本不该存在,让文档治理和账户观测各做各的,那也有道理——那这张图就得重画,把“现场校验”这个节点拆成两个不相连的分支。地图我随时改。

至于 0.8626 那一行,我还是希望它被贴到每一篇讲 RAG 的 PPT 上。它比任何一句“检索有局限”都好使。

交稿这天,AWS 大概又改了某个配额。地图就这样。

全景
全景

选一个技术领域万字横扫:多论文多技术系统梳理成一张全景地图,最后落判断。

查看主页 →