结构反压胜过更聪明的代理
Reuben Brooks 在 HN 上发了一篇 182 分的文章,标题就是结论:结构反压胜过更聪明的代理。
他从一件无聊的事说起。
最严重的软件 bug 往往也是最无聊的那种。用户不应该能读另一个租户的数据。没人会反对这条规则,也没人会替 Alice 读 Bob 的记录辩护。然而,访问控制漏洞常年稳居 OWASP Top 10 的第一名。
为什么?因为规则被放在了系统的错误位置。它活在提示词里、在评审清单里、在"每个未来的工程师——现在加上每个未来的模型调用——会记得这个不变量并正确应用它"的共享期望里。
这个假设本来就弱。在 AI 生成大部分代码的时代,它直接失效。
你可以做所有显而易见的事:把规则写进 CLAUDE.md,写个仔细的系统提示,在 agent 指令里加上"授权非常重要"。这些都应该做。但模型写了一万六千行代码之后,真正的问题是:你怎么知道代码做了你想做的事?
Brooks 的答案是换一个杠杆。
大多数提示词层面的约束是"行为门"。你告诉模型"不要跳过授权"、"验证输入"、"用共享辅助函数"。模型大部分时间会遵守,偶尔会违反。这个"偶尔"就是问题所在。行为门依赖模型记住规则、识别它适用的位置、抵抗局部上下文的引力,然后再靠人工评审在整个代码库里维持同一个不变量。
结构门不一样。编译器、类型检查器、测试运行器、lint 工具——每一个对眼前的代码给出一个具体的答案。答案不是完美的,但是真实的。在它的范围内,代码错了它就拒绝。
这个拒绝就是关键。它让你把验证的工作从模型的指令空间移到模型的底层架构中。不用花 token 去求模型记住一个不变量,你把代码组织成"偶然违反就很难"的样子:取你最关心的属性,用机器可以检查的形式表达它,把它投射到实现里去,让迭代在这个检查上反弹,直到生成的代码满足它。
Brooks 的工具叫 Shen-Backpressure。他用 Shen——一个带sequent calculus类型系统的静态类型 Lisp——来写规则。
以多租户访问控制为例。规则很简单:用户只有在认证通过、是租户成员、且资源属于该租户时才能访问。他把这个规则写成一串推导链:jwt-token 推导 authenticated-user,再推导 tenant-access,再推导 resource-access。
每一层推导都有一条水平线。线上面的前提必须满足,线下面的结论才能构造。要拿到一个 resource-access 类型的值,你需要一个 tenant-access 的证明,加上资源归属的证明。
然后代码生成器 shengen 把每个规则降成目标语言的守卫类型。在 Go 里,字段不可导出,生成的构造函数是唯一填充值的路径:
func NewTenantAccess(principal, tenant, isMember) (TenantAccess, error)
如果 isMember 是 false,构造函数直接返回错误。外部代码不能绕过——不能直接写 TenantAccess{isMember: true},因为字段未导出。
这就是"结构反压"。AI agent 写代码时,遇到的是一个机械的"不"。不是提示词里的"请记住授权",而是编译器的报错:
cannot use tenantID (variable of type string) as shenguard.TenantId value
这短短一行的机械拒绝,比提示词里的任何一段话都有效。
想把这件事跟 Hashimoto 论"AI 精神病"串起来。
Hashimoto 说整个行业正处在一个危险状态中——相信 MTTR 是唯一需要关注的指标,认为代理会修好所有 bug。他经历过基础设施领域的 MTBF vs MTTR 之争,知道结果是什么:你会自动化出一个局部指标健康、整体不可理解的灾难机器。
Brooks 的方案是另一种回应。他不试图让 agent 更聪明,不是"等下一代模型就更好了"。他的论点是 agent 已经够聪明了,能写几乎所有代码。限制因素不是 agent 的智能水平,而是你能不能知道它做了你想做的事。而这个知识来自底层架构,不来自等待更聪明的模型。
这跟前几天写的"瓶颈在上游"也是同一个脉络。AI 编程的瓶颈不在代码生成速度,在验证和信任。Brooks 的方案把验证从"人工读代码"变成"编译器拒绝"。这是在上游解决问题。
还有一个更实际的对比。
传统写多租户 API 的方式是在每个 endpoint 里写一个 if:
if !user.IsMemberOf(tenantID) { http.Error(w, "forbidden", 403); return }
这个模式本身合理。但问题在于它"恰好"会被遗忘在第七个 handler 里,或者在第三次重构时被丢掉。Brooks 的方案把成员检查集中在 TenantAccess 的构造边界。handler 操作的是一个已经遍历过证明链的值。证明跟着值走,不需要在每个地方重复。
给一个实际的落地建议。
你不需要去学 Shen。但你应该问自己两个问题:代码库里最重要的不变量是什么?访问控制?数据一致性?某个业务规则?然后问:这个不变量活在提示词和人工评审里,还是活在编译器和类型系统里?
如果答案是前者,你就在 Brooks 说的"行为门"的脆弱区里。每一次 AI 生成代码、每一次新工程师加入、每一次重构——不变量都在被重新回忆,而不是被机械执行。
迁移到"结构门"不需要形式化验证。Go 的 sealed interface、TypeScript 的 branded type、Python 的 dataclass 加验证逻辑——这些就够你把最重要的几个不变量变成编译时错误。
Brooks 的方案展示了上限。但即使只做到它的一小部分,也已经是质的提升。