设计原则:自包含
上一节说到卷二要把原本对话承载的东西都装进 spec,让它变成一份独立的载体。这一节回答一个具体的问题。载体应该长什么样。什么样的载体才能真的支持多个 Agent 独立执行。
答案是一条原则。
Agent 消费的每个单元,在注意力承载力内自足承载本单元任务所需的全部意图。
原则包含四个成分。单元大小和意图自足性是两个变量。注意力承载力是物理约束。完成本单元任务所需是功能标准。四者同时满足,载体才能被 Agent 独立消费。
先说这条原则从哪里推出来。再说它内部两个变量为什么不能分开讨论。再说它满足之后载体的形态。最后说规模上会有什么后果。
两条物理事实
原则来自 Agent 在子空间里的两条物理事实。
第一条事实是 Agent 只能读它拿到的东西。
它没办法在做任务的中间去查其他文档。它没办法给你打电话问一句。它没办法跟别的子空间同步。它拿到什么就用什么。这条听起来像常识,但常识背后的含义是明确的:Agent 完成任务所需要的每一个信息片段,都必须装进这次任务的输入里。少装一样,任务里遇到那个点的时候 Agent 就要自由发挥。自由发挥的方向可能对,也可能不对,但你不会立刻知道,因为你不在场。多装一样,Agent 的注意力就分散一点。分散得太厉害,任何一个具体的点上它都不能集中处理。
这条事实的操作性推论是明确的。载体每一次投给 Agent 的量,必须精心挑选。既要装齐,也要克制。
第二条事实是 Agent 一次能处理的量有上限。
这个上限来自注意力机制的物理属性。让 Agent 读一份几十页的长文档,它会开头结尾认真读,中间大幅跳读。这不是模型偷懒,是 attention 分布的自然结果。当前主流模型的注意力权重在长上下文里呈现明显的边缘偏置。开头几页和末尾几页拿到的权重高,中间几页拿到的权重低。让 Agent 复述一份 60 页文档的中间段的关键决策,它经常复述不出来,或者复述得含糊。这不是它没看,是它的注意力权重让它对那部分内容的处理质量低于对头尾的处理质量。
拆成多份短文档也不解决问题。三份 20 页的文档喂给 Agent,它需要在三份之间来回切换。切换本身消耗承载力。它切到第二份的时候,第一份的信息已经开始褪色。等它读完三份回头做任务,前两份的很多细节已经不在有效注意力里了。
Ryan Yang 在开发过程里观察到这个现象。他的原话:"虽然上下文窗口越来越大,但过多冗余信息下注意力还是不集中,然后漂移。"上下文窗口从 32K 涨到 1M,物理容量是够了,但 Agent 的处理质量并没有跟着上下文容量线性上升。有效注意力这个变量的天花板远低于上下文容量。
胥克谦对这个现象有一个更具体的描述:"大模型读一份 PRD 文档的话,都是开头和结尾。他读的他是逐字读逐句读了,中间都是跳读的。你把它拆成更小的文档,也得全读完了才能形成决策。你这里边涉及到二十个文档,他也得把二十个文档都读一遍,他仍然会爆上下文。"这段话覆盖了两个关键点:读长文档中间跳读,读多份文档全读需要切换。两种模式下,Agent 的有效处理量都受限于同一个物理上限。
两条事实合起来。Agent 需要的信息必须都在它拿到的东西里,同时它拿到的东西不能超过它能有效处理的量。这两个约束共同定义了原则。
单元大小和自足性不能分开
原则里有两个变量:单元大小、自足性。这两个变量互相耦合,脱离对方讨论就失去定义。
自足是相对性质。给你一块 spec 问它够不够自足,你必须先知道这块 spec 要完成什么任务。任务定了,才能判断意图装齐了没有。任务的范围由单元大小圈定。改变单元大小,够不够自足这个问题的答案就变了。
举个具体的例子。同样一段关于用户会话的架构说明,装在一个负责数据库 schema 的子空间里可能不够自足,因为写 schema 的 Agent 需要知道会话怎么读写、什么时候过期、并发写入怎么处理,架构说明可能只给了一个大方向。装在一个负责服务端 API 的子空间里,架构说明可能刚好自足,因为 API 层需要的抽象比 schema 层高一级。装在一个负责前端的子空间里,同样的架构说明就多余了,因为前端只需要知道 session token 怎么用,不需要知道 session 内部是怎么组织的。同一段文字,在三个不同大小的单元里,够不够自足的答案是三个。自足这个性质本身没变,变的是单元。
这个例子还揭示了一个操作性问题。你在写载体的时候,脑子里想的可能是一份完整的架构说明。你把它写下来,觉得挺完整。但直到你决定这份架构说明要装到哪个单元里去、给哪个 Agent 消费,你才能判断它够不够。同样的写作在不同的目标场景下是不同的产出。这也是为什么载体不是"设计"出来的,是在目标场景确定后逐步收敛出来的。
反过来单元大小也是相对性质。给你一份完整 spec 问切成多大合适,你必须先知道每一块要装什么。装的东西少就切小,装的东西多就切大。多少算合适取决于自足性要求。
这就是为什么两个变量必须联合优化。你不能先定死一个再定另一个。给定一个候选的单元大小,检查这个大小内能不能自足承载所需意图。如果不能自足,要么扩大单元让更多依据进得来,要么把任务拆得更小让所需依据变少。这个来回可能要走几轮。走完之后单元大小和自足性都定型。这就是为什么载体的原则只有一条。表面看是两个约束,实际上是同一个约束的两面。
原则满足之后载体是什么形态
原则满足之后,载体的形态就固定了。它必然是分块的。它的每一块必然自足。它的切分线必然落在决策边界上。这三个特征是同一条原则的操作层展开。
分块的必然性来自注意力上限。
一份完整意图(vision、架构、所有 feature、所有边界情况、所有边缘处理策略)加起来一次读不完。就算你用最大的上下文窗口硬塞进去,Agent 也没法有效处理。所以载体必须切成多块,每一块单独消费。这不是可选设计,是原则的推论。
每一块必然自足的必然性来自意图完整性。
原则要求本单元任务所需的意图都在里面。这就是自足。自足意味着某种冗余。如果 A 块和 B 块都需要用户会话表达方式这条决策依据,那这条依据要在 A 里写一次,在 B 里也写一次。传统软件工程告诉你同一条信息只存一份,改一次就够,这是 DRY 原则。载体的自足性违反 DRY。
后面会专门讲这一点。这里先记住:这不是坏设计,是原则要求的。
切分线必然落在决策边界上的必然性来自自足性。
想想反面。按代码模块切:前端一块,后端一块。前后端交互怎么表达用户会话这个决策,落在两块中间。切成两块的时候,这条决策没进任何一块。前端 Agent 猜一个方向,比如 session 用 cookie 传。后端 Agent 猜另一个方向,比如 session 用 header 里的 token 传。两个 Agent 各自的代码都能跑。合起来的时候前端发的 cookie 后端不读,后端读的 header 前端不发。矛盾在集成的时候才暴露。
要让每一块自足,切分线必须画在决策彼此独立的地方。跨切分线的决策要么被上提到公共层,要么被复制到两侧。决策边界正好是这样的地方——两组决策彼此独立,一组决策的变动不影响另一组。按决策边界切,跨切分线的决策在切分之前就已经定好,切完之后每一块内的决策彼此耦合,跨切分线之间不耦合。合起来的时候不会矛盾。
用另一个例子说明决策边界跟代码边界的差别。假设你在做一个电商系统的订单模块。按代码边界切,可能是订单前端、订单服务、订单数据库、订单通知这四个代码模块。按决策边界切,可能是订单状态机(承载订单从创建到关闭的所有状态和转移条件)、订单数据模型(承载订单相关的所有数据字段和约束)、订单通知策略(承载什么状态变化触发什么通知)、订单支付集成(承载跟支付系统的所有交互)。两种切法看起来接近,但每一块的内容不一样。按代码边界切,订单状态机被拆到订单前端和订单服务两块,因为两边都要处理状态。按决策边界切,订单状态机是完整一块,前端消费的时候读整块的状态机定义,服务消费的时候也读同一块。两种切法哪个更好,取决于两块之间的对齐怎么保证。按代码边界切,前端跟服务的状态机定义要靠人保持一致,容易出错。按决策边界切,只有一份状态机定义,两边同一个来源,对齐直接保证。
反 DRY 的论证
自足性违反 DRY 是显性的。同一条决策依据在多个块里各写一份,从 DRY 视角看是坏设计。这需要专门论证。
DRY 服务的是人的文档维护成本。同一条信息只写一份,改一次就够,不会漏。这个优化在人是消费者也是维护者的时候成立。你改文档的时候,只需要改一处。你读文档的时候,只需要读一处。DRY 减少了双向的工作量。
载体的场景变了。载体的消费者是 Agent 不是人。Agent 的物理事实决定了它不能像人那样"记住某处提到过某条约束然后去别处找"。它拿到什么就用什么。同一条约束在两个块里各写一份,两个块的 Agent 都能独立消费。写一份放在公共层,两个块的 Agent 都要去公共层查,查完还要正确记住这条约束和自己任务的关系——而它做不到。
所以载体的优化目标从"文档维护成本"变成了"分发后的对齐准确性"。两个优化目标不同,权衡结果也不同。DRY 在传统场景下节省的成本,在这里换来的是对齐失败。反 DRY 在传统场景下多出的冗余成本,在这里换来的是对齐成功。哪个更划算取决于你的场景是什么。载体的场景是后者。
有一个具体的场景可以体会这个权衡。假设一个 harness 里五个块都需要"用户 ID 用 UUID 而不是自增整数"这条决策。DRY 的做法是这条决策写在公共层的"数据模型规范"里,五个块里各自引用"参见公共规范第 3.2 节"。DRY 的做法在维护上省事——将来这条决策改成用 snowflake ID,只改一处就够。DRY 的代价在分发时暴露——五个 Agent 拿到自己的块,每个 Agent 要主动跳到公共规范去查,查完还要正确记住这条决策跟自己任务的关系。查这个动作本身消耗它的注意力,还可能查错或者查漏。反 DRY 的做法是这条决策在五个块里各写一份完整的("用户 ID 用 UUID。理由:兼容外部系统。边界:只用于用户实体,其他实体的 ID 可以用别的形式。")。五个块的 Agent 都在自己的输入里直接看到这条决策,不需要跨块查询。代价是将来改这条决策需要改五处,多出的维护成本。
冗余的成本要用别的机制管理。这个机制在演进章讲。这里先接受一个事实:载体的每一块都自足,冗余是必然的。
规模量级的变化
一份满足这条原则的载体比卷一的 spec 大得多、结构化得多。凡是卷一里人在现场即时补充的东西都要预先编码。决策的推理过程要写下来。边界条件要写下来。边缘情况的处理策略要写下来。多个块之间的冗余也要保留。
规模上升是数量级的。
卷一的一份 spec 可能十几页够用。你在场,很多东西可以放到对话里补,spec 本身只承载相对稳定的骨架。骨架里面的决策点大概几十个:主要功能选什么、字段怎么定、错误怎么处理。这个规模一个人做一下午能想清楚。
卷二的一份载体可能几十页甚至上百页。它承载所有决策、所有理由、所有边界、所有边缘处理策略。它带内部分块,每一块有明确的边界和内部内容。它带跨块引用关系(哪一条决策是哪几块共同依赖的)。它带一致性检查目标(多个块里的重复决策不能相互矛盾)。它带产出验收标准(每一块喂给 Agent 之后要能产出什么才算通过)。决策点从几十个上升到几千个甚至上万个。
胥克谦在他的项目里做过一个统计。教育项目里,一个功能模块的 spec 完整展开之后大约有两百到三百个决策点。这些决策点分布在产品交互、数据模型、集成契约、边缘处理这几个层。他的开发工具项目里同类模块的决策点数量翻了一倍多。理由是开发工具的边界更复杂,需要预判的场景更多。
这个规模超出了一个人的能力边界。产出这样一份载体的方法是接下来要回答的问题。
Harness Engineering Playbook · AgentsZone Community