flowchart LR
D(("已知扰动<br/>Agent 会怎么错")) -.->|"前馈:落笔前就堵上"| A
A["Agent 工作"] --> O[产出]
O --> M["测量<br/>三层判定"]
M -->|"不合格"| A
M -->|"合格"| MERGE[合并]
U(("未知扰动<br/>你没预料到的错法")) -.->|"只能靠反馈接住"| O
style D fill:#eef2fb
style U fill:#fdf3e3
style MERGE fill:#e8f4e8
5 环境为什么先于检查
这一部讲四块环境。在进入具体内容之前,得先回答一个问题:为什么是环境先, 而不是检查先。这不是排版顺序问题 —— 它决定你把有限的投入放在哪一侧, 而两侧的回报率差得不是一点。
5.1 把因果讲反了的那个说法
这套东西我最初叫它「三层基建」,指的是测试、结构检查和路径不变量。 用了一段时间之后我发现这个说法把因果讲反了:
检查本身不产生质量,环境才产生质量;检查只是让质量变得可以确认。
一个 Agent 之所以能把事情做对,主要不是因为有人在后面挑错, 而是因为它工作的地方本身就把”该怎么做”表达清楚了。
这个自我修正值得单独说一句,因为它比结论本身更能说明问题:一个先建成、 后被重新理解的系统,说明它是长出来的,不是设计出来的。长出来的系统 有一个特点 —— 它的每一部分都对应过一次真实的失败,所以里面很少有装饰。 后面还会出现一次同样形态的修正(章节 2 里那个 编解码器模型),而两次的教训是同一个:实践可以是对的, 而解释是后补的;补上正确的解释之后,你能看到一些原本看不到的东西。
5.2 环境靠什么把话说清楚
靠把规则写进结构 —— 不写在文档里,写进目录层级、依赖方向、 类型定义和 owner 归属。
这两种写法的区别在于生效的时刻。写在文档里的规则,要等人想起来去查 才生效;写进结构里的规则,在 Agent 落笔的那一刻就已经在场了 —— 它不是一条需要被记住的条文,而是 Agent 一读代码就接收到的事实。
这个区别在人的团队里也存在,但没这么致命,因为人会积累:一个人在仓库里 待了半年,那些没写下来的规矩他也知道了。** Agent 每次都是新来的。** 它不会积累,也不会因为上次被指出来而在下次记得。所以一条”需要被记住”的规则, 对它来说的生效概率约等于它出现在当前上下文里的概率 —— 而这个概率会随着上下文变长而下降。
5.3 四块环境,四种结构
四块环境各自回答一个问题,而它们分别是四种不同东西的结构: Codebase 回答”新代码该放哪、能依赖谁”,那是目录的结构;架构回答 “一次改动会穿过哪些层”,那是依赖的结构;工作指导回答”规则怎样在 正确的时刻到达”,那是到达时机的结构;工具链回答”Agent 够得着什么”, 那是可达性的结构。
第四块最容易被漏掉。前三块决定 Agent 知道什么,第四块决定它能做什么 —— 而这一块光靠约束长不出来,只能靠有没有人一件一件把工具造出来。
5.4 地面与墙:为什么这不只是比喻
前四块是 Agent 站的地面和它手里的工具,后三层是围着它的墙。 只有墙没有地面,Agent 会不停撞墙,却不知道该往哪走。
这个比喻可以再精确一点,而精确之后它就不是比喻了。
在控制论里,一个系统对付扰动有两条路。前馈是你事先知道扰动的结构, 在它产生影响之前就补偿掉;反馈是你测量输出,发现偏差之后再修正。 四块环境是前馈 —— 你已经知道 Agent 会往哪些方向出错(会把文件放错目录、 会自己造一个日志器、会绕过配置层直接写偏好设置),所以你在它落笔之前 就把这些路堵上。三层检查是反馈 —— 测量它交出来的东西,不合格就退回去。
控制论里有一条结论,它把上面那句经验之谈变成了一个可以证明的事实:
前馈决定性能,反馈只提供鲁棒性。
一个纯反馈的系统,它的性能上限由回路延迟决定,再怎么调都超不过去。 翻译成这里的语言就是:如果你的判定延迟是十五分钟,那么无论检查多严, Agent 每次犯错的成本下界就是十五分钟 —— 除非你在它犯错之前就拦住它。 章节 17 会把这条结论展开,并用它解释我撞过的那两堵墙。
5.5 那检查还有什么用
前馈有一个根本限制:它只能处理你已经知道的扰动。你没预料到的失败方式, 前馈补偿不了,因为你没有为它设计补偿。这类失败在 Agent 场景下不会少 —— Agent 出错的方式和人不一样,你的直觉不覆盖它们。
反馈的价值就在这里:它不需要你事先知道扰动是什么,只需要能测量输出。
所以两者的分工是清楚的。前馈处理已知的失败方式,便宜、快、在错误发生之前; 反馈处理未知的失败方式,贵、慢、但不需要预知。两者之间有一条流动的路径, 后面会反复讲到:每一次反馈抓到的新失败,都是一个可以被转化成前馈的候选。
举个具体的。第一次撞到”Agent 会自己造日志器”,是反馈抓到的 —— 我在评审时 发现的。第二次之后它变成了一条结构检查,仍然是反馈,但便宜了很多。 真正的终点是把日志器的构造放进一个只有 owner 能访问的地方 —— 那时候它变成前馈,问题彻底消失。章节 15 会讲这条路径的完整形态。
5.6 容易被误读的那个推论
“前馈决定性能”这句话有一个错误的推论:那我们应该把所有精力放在前馈上, 检查可以少建一点。
这是错的,而且它错在一个具体的地方:前馈只能处理你已经知道的扰动, 而你怎么知道有哪些扰动?大部分是反馈告诉你的。一条规则的完整历程是 反馈发现一个新的失败方式,然后它变成一条自动检查(还是反馈,但便宜了), 最终变成结构(前馈,问题消失)—— 没有第一步,就没有第三步。
所以正确的推论不是”少建反馈”,而是反馈的产出应该被持续地转化成前馈。 这个转化如果不发生,会有一个明确的症状:规则的数量单调增长, 却没有任何一条因为”问题已经在结构上消失了”而被删除(小节 15.10)。
5.7 前馈的三个层次
“把规则写进结构”这句话可以再分层,因为三个层次的强度差别很大。
第一层是信息在场。 规则以信息的形式出现在 Agent 的上下文里 —— 常驻文件里的一条、路径清单里的一段。它降低了做错的概率,但没有消除 做错的可能:Agent 仍然可以读了之后不照做,通常是因为上下文里 有别的东西压过了它。
第二层是默认路径正确。 正确的做法同时是最省事的做法。比如”共享的 测试替身放在协议旁边的第四格”—— 如果那个替身已经存在、已经是公开目标、 已经被别人用着,那么复用它比自己写一个更省事。这一层不靠约束,靠成本, 它把”做对”变成了阻力最小的那条路径。
第三层是做错不可能。 两个冲突的构建目标在加载期就报重复;已校验的 配置类型在模块外无法构造;哨兵(小节 13.4.3)的类型是非零整数所以关不掉。 这一层不需要 任何人遵守,因为它不存在”不遵守”这个选项。
flowchart TB
L1["① 信息在场<br/>写进常驻文件 / 路径清单"]
L2["② 默认路径正确<br/>复用比重写更省事"]
L3["③ 做错不可能<br/>类型 / 构建图 / 可见性"]
L1 -->|"成本↑ 可靠性↑"| L2 -->|"成本↑ 可靠性↑"| L3
L1 -.->|"仍可能被忽略"| X1["靠上下文竞争"]
L2 -.->|"仍可能被绕过"| X2["靠成本引导"]
L3 -.->|"无法违反"| X3["编译器就是执行者"]
style L3 fill:#e8f4e8
三个层次的成本递增、可靠性也递增,而适用范围递减:第一层适用于任何规则, 第二层适用于有替代方案的场景,第三层只适用于少数能被类型或构建系统表达的东西。 大部分规则只能停在第一层,这没问题 —— 重要的是知道第二层和第三层存在, 并且在设计一条新规则时问一句:这条能不能往上走一层?
这个问题在实践中有一个很省事的问法:如果我不写这条规则, 有没有办法让做错这件事变得更麻烦?
5.7.1 三个层次怎么在一条规则上依次走完
拿一条真实的规则走一遍,因为”往上走一层”这句话 只有在看过一次完整的路径之后才具体。
那条规则是”界面测试的 bundle 名字必须由包装器推出,不由调用方起”。 第一层的形态是把它写进常驻文件:一句话,说明为什么 —— 两个人各自起名,就会撞出两个同名的 bundle,而加载期会失败, 失败信息指向的是加载器,不是起名的那个人。 这一层的成本是十分钟,效果是让读到它的会话不犯这个错。
第二层的形态是提供一个包装器,让它比手写更省事: 调用方只需要给一个目标,名字、宿主、依赖全都由包装器填。 这一层的成本是半天,效果是做对成了阻力最小的路径—— 一个 Agent 在两种做法之间,会选参数更少的那种。
第三层的形态是包装器成为唯一的入口: 名字由它推出,于是两个冲突的目标在构建图加载期就报重复, 根本拆不出来。这一层的成本接近零 —— 因为第二层做完之后,第三层只是把”可以绕过”变成”绕不过”。
这条路径上值得注意的是第二层和第三层之间的成本落差极小。 真正贵的是第二层:你得设计那个包装器、迁移已有的调用、 处理那些不符合默认形状的特例。一旦第二层建成, 第三层往往只是一次收紧 —— 把一个公开的构造函数改成内部的, 把一个可选参数删掉。
所以”往上走一层”这个问题最该问的时机, 是你正在做第二层的时候。那时候多花的半小时, 换的是一条从此不需要任何人遵守的规则。 如果你在第二层停下来,半年后再想往上走, 成本会因为已有的调用方而翻几倍。
5.8 反馈的成本必须被算进来
前面说”前馈决定性能”,容易被读成”反馈不重要”,所以补一句平衡。 反馈的价值是不可替代的,但它的成本是持续的,而且有四项。
机时是最显性的一项,每次改动都要跑一遍。延迟次之,每次失败都要等一个回路。 注意力第三,每条规则都要有人维护。第四项最容易被忽略、也最难恢复:信任。
一个总是误报的检查系统,它的实际效果不是”拦住了一些问题”, 而是”教会了所有人忽略它的输出”。一旦这个习惯形成,那些真正的失败 也会被一起忽略。小节 14.4 那一节之所以要花那么多篇幅 讲”为什么不禁掉某个语句” —— 那不是在讲宽容,是在保护这套系统里 唯一不可再生的资源。
5.9 环境与检查的投入比例
有一个可以参照的实际数字,虽然它不该被当成目标。在我那个仓库里, 工具链(环境层的一部分)有 225,192 行 Rust,而检查器运行时 (检查层的全部)是 67,113 行 —— 工具链是检查器的三倍多。
这个比例和大部分人的直觉相反。“给 Agent 建规矩”听起来是主要工作量, 实际上”给 Agent 造手”才是。
环境层的其余部分是零代码的 —— 目录结构、架构约束、载体安排, 它们不体现为行数,体现为没有被写出来的代码:没有被写重的机制、 没有被放错的文件、没有被绕过的边界。这个”看不见的投入”是环境层最大的 一块,也是最难被计量的一块。
它的存在有一个间接的证据:这套系统的检查器只有六万七千行, 而它要管三百多万行代码,比例是 1:46。一个环境很差的仓库, 需要的检查器会大得多,因为它要拦的错误路径更多 —— 检查器的大小,某种程度上是环境质量的一个反向指标。
5.10 一句容易被记错的话
“检查不产生质量,环境才产生质量”这句话,最容易被记成”检查不重要”。 准确的表述要长一点:
检查不产生质量,它让质量变得可以确认。 而不可确认的质量,在一个几十个 Agent 并行的系统里,等价于不存在。
后半句是这句话的另一半,而它同样重要。一个环境完美但没有任何检查的仓库, 它的代码可能真的很好 —— 但没有人(也没有任何机器)能知道这一点, 所以每一次合并仍然需要人来看,小节 1.1 那堵墙照样撞上。
所以正确的理解是:环境决定质量的上限,检查决定你能利用多少这个上限。 两者的关系是乘法,不是加法 —— 环境为零时,再多的检查也只能挡住格式问题; 检查为零时,再好的环境也没法被规模化利用。
5.11 环境四块为什么是这四块
不是随便切的。这四块对应 Agent 在动手之前必须回答的四个问题, 而这四个问题在实践中是穷尽的:我要写的东西放哪(Codebase)、 它会影响谁(架构)、有什么我必须知道的(载体)、 我能做到什么(工具链)。
“穷尽”的意思是:任何一个 Agent 做不好的事情,都可以被归到这四个问题 之一没被回答好。它把代码放在了错误的位置,是第一块的问题;它没意识到 这个改动会影响另一个模块,是第二块;它不知道这条路径上有一个特殊约定, 是第三块;它没法验证自己的界面改动,是第四块。如果四个问题都答好了, 剩下的错误就是”它算错了”—— 那正是判定层(第三部)的对象。
(这个说法当然是粗糙的,它假设了错误可以被这样分类, 而 章节 4 那七个形状说明真实的失败经常跨越多块。 但作为一个组织框架,它够用。)
5.12 环境层最容易被跳过的一块
四块里工具链最容易被跳过,而原因很具体:前三块的投入都可以被”顺手做”。 调整一次目录结构、加一条依赖规则、改一份常驻文件 —— 这些都可以在做别的 事情的时候顺带完成。
工具链不行。它需要专门的时间、专门的代码,而且它的收益在膝点之后 才出现(小节 9.10.3)。所以它在资源紧张时永远是第一个被砍的 —— 砍掉它不会违反任何规则,不会让任何检查变红,它的缺席是静默的。
这是形状 A 在投入决策上的形态:
一块没有判定覆盖的投入,会被系统性地低估。
5.13 前馈做不到的那一半
这一章的重心在前馈,所以得说清楚它按定义做不到什么 —— 否则”环境先于检查”会被读成”建好环境就不需要检查了”。
前馈的定义是在测量到误差之前就施加控制作用, 依据是对扰动的先验知识。这句话里”先验知识”四个字 就是它的边界:你只能前馈掉你已经知道形状的扰动。
三类扰动前馈够不着。没见过的失败够不着 —— 一条规则守的是一个已经付过代价的问题, 而第一次撞上的问题按定义没有对应的前馈。 依赖外部世界的失败够不着 —— 一个外部接口改了返回格式、 一台机器磁盘满了、一个证书过期了, 这些不是你的代码能提前避免的。 组合产生的失败够不着 —— 两个各自都符合所有约束的改动, 合在一起可能出问题,而前馈是作用在单次改动上的。
三类合起来说明了反馈为什么不可能被消除, 也解释了 小节 5.5 那一节存在的理由: 反馈的作用不只是兜底,它还是前馈的唯一来源。 每一次被反馈抓到的新失败,都是一个可以被转化成前馈的候选, 而没有反馈,前馈就没有输入 —— 它会停在你今天知道的那些问题上, 然后随着系统演化慢慢过时。
所以两者的关系不是”前馈更好、反馈是无奈”, 是前馈决定当下的性能,反馈决定长期的适应能力。 一个只有前馈的系统在遇到第一个新问题时就失效了, 而它甚至不知道自己失效了 —— 因为没有东西在测量。
5.14 这一部的读法
四章的顺序不是重要性排序,是依赖顺序。章节 6 定义”东西放在哪”, 那是后面所有讨论的坐标系;章节 7 定义”东西之间怎么连”, 它建立在坐标系之上;章节 8 定义”规则放在哪”,它需要前两章 来判断”能不能写进结构”;章节 9 定义”手能伸到哪”, 它是唯一一块不依赖前三块的。
章节 8 那一章零基建成本,能立刻改善任何一个正在用 Agent 的团队, 包括一个人的项目。 如果你的团队还很小,跳过 小节 7.2 那一节,那是这一部里 唯一需要重基建的地方。
5.15 四章各自压成一句
作为导航,先把四个结论给出来,读完之后再回来对照:
章节 6 是「目录结构是你写给 Agent 的第一份文档, 而且是唯一一份它必然会读的」。
章节 7 是「每一条架构边界都必须换来一种可测性,换不来的那条是装饰」。
章节 8 是「问题不是这条规则重不重要,是它什么时候需要到达」。
章节 9 是「前三块降低 Agent 做错的概率,这一块决定它能不能 知道自己做对了」。
四句话的共同底色是同一个:把需要被记住的东西,变成一读就接收到的事实。 目录让”这是什么”成为事实,架构让”会影响谁”成为可查的,载体让规则 在正确的时刻在场,工具让”我做对了没有”从猜测变成观察 —— 而它们都不依赖任何人记得什么。