Skip to content

pstack 拆解 14 · 设计:先把形状摆出来 ​

模型给的第一个形状,往往会被锁进代码。要避免,就在写实现之前把形状摆出来,而且刻意做两个结构上不同的版本。

这一篇讲 architect 和它内部用的 arena。

architect ​

设计一旦写进代码就很难反悔,所以它的价值在于它是唯一便宜的反悔窗口:在写实现之前把形状先摆出来,而且刻意做两个结构上不同的版本,让选错的风险在代价最低的时候暴露。

architect 在写代码之前,先把类型、函数签名、类形状、模块边界画出来,用 not implemented 的函数体和伪代码。这些草图先在多个模型视角之间综合,然后在选定的草图上把代码填进去。如果实现证明草图是错的,就把草图扔掉重来。

它有一个五阶段的循环,每个阶段都有明确的进入和退出条件。

Phase A: Ground。 先给新代码触碰到的每一个系统建立真实模型,跑 how。如果设计重新定义了所有权或分层,再加 why,让已有的理由变成约束,而不是猜测。

这里有一句判据:命名一个文件不叫 grounding。 你要产出 how 规定的那份被追踪过的模型。只有真正从零开始、周围没有系统可整合时,才跳过这一步。

Phase B: Sketch。 它跑 arena,把设计任务和 Phase A 的 grounding 产物一起交出去。每个候选拿到一份 runner prompt,产出一个按 rationale 模板组织的设计包。

它要求至少两个结构上不同的候选,哪怕第一个看起来就够用。这是 exhaust-the-design-space 原则的具体化,要的是整体形状的备选,不是同一个形状里的点修。

候选在合成之前要先过一遍设计红线清单。这份清单的假设写得很实在:下一个贡献者是一个 agent,它只看到自己打开的文件,照抄最近的一个例子,走最短的能编译的路。所以在两个候选之间,选那个"从一个文件里做对的事,对整个仓库也对"的设计。

比较候选的轴是接口深度:优先那个把更多复杂度藏在更小的公共面后面的设计。一个很厚的接口,如果它把能力集中起来、让调用链保持短,这是好事;一个很宽的接口,如果它藏不住复杂度,那是坏事。

Phase C: Agree。 默认不设检查点,直接从合成出来的设计进入实现。只有调用方明确要求("先给我看再实现")时才停下来等你签收。如果你在检查点里对形状提出异议,它把这当成 Phase A 的证据,回到 Phase B 重跑。

Phase D: Implement。 用草图当契约,把 not implemented 换成代码,把伪代码换成逻辑。偏离草图是要浮出来的信号,不是要默默吸收的摩擦。 如果一个函数需要一个草图没预料到的参数,回来问:是草图错了,是需求漏了,还是实现越界了。

Phase E: Scrap。 这是最容易被跳过、也最重要的一步。如果实现持续产生草图吸收不了的摩擦,就把草图扔掉,不要往上打补丁。

它判断信号看的是一个模式,不是单个例子。它列出的征兆很具体:同一种 workaround 在不相干的代码里反复出现;多个不相干的边界情况都需要特殊分支;类型需要逃生舱(any、cast、实际上总会被赋值的可选字段)才能编译;草图说状态不共享,实现里却本能地想要一把锁;调用方必须知道抽象的内部规则才能用它;以及 Phase D 里出现了两个或以上同形状的偏离。

推翻的时候不是重来一遍了事。它按固定顺序做:先用 how 重新理解已经建出来的东西,再按新约束重画(redesign-from-first-principles),再先减后加,让新草图比旧草图更小,然后回到 Phase B 重跑 arena。

它的输出有一条固定纪律:调用方的用法先写,类型从用法里派生出来。 小改动是一份新类型和签名,大改动是模块图加类型定义,再附一份 rationale,里面记下用法草图和合成决定。

arena ​

同一个问题,不同的模型会想到不同的形状,也带着不同的盲区。让多个候选同时上场、互相取长补短,得到的不是平均,而是一个比任何单个候选都更强的合成:选一个做底,再把别人身上最好的那一小部分嫁接进来。

arena 对同一个任务并行起 N 个候选,读完每一个,选最强的做底,再把其他候选里最好的想法嫁接进来,验证合成结果。

它的阶段是这些。

Phase A: Frame。 N 个候选会收到同一个 prompt,所以这个 prompt 就是契约。它先声明每个候选要产出什么,再从"这个任务的成功长什么样"里推导出三到六条可打分的标准。这个 rubric 是挑选工具,候选看不到它。

然后是分配输出位置。每个候选写到自己的地方:一个 git worktree,或者 /tmp/arena-<slug>/candidate-<n>/。这是 separate-before-serializing-shared-state 的直接应用,它保证 N 个候选不会互相覆盖。

Phase B: Fan out。 一次消息里并发起全部候选,后台运行。每个候选不只产出作品,还要产出一段简短的理由,写明它考虑过哪些备选、否决了什么。如果某个候选没产出东西,就带 N-1 个继续,并在合成记录里记下这个掉队的。

Phase C: Cross-judge。 全部候选完成后,在不同模型族上起一个只读的 judge。它看到 rubric 和按路径标签区分的候选,逐条打分,推荐一个底。它和你的阅读并行,但不要和候选并行:候选还在写的时候不要起 judge。

Phase D: Pick a base。 读完每一个候选,逐条对着 rubric 打分,不靠整体感觉。 挑那个"未来的维护者最容易在不破坏不变式的前提下扩展"的候选。两个打平时,偏好边界更干净、API 更小的那个(这是 laziness 在起作用)。它把你的判断和 cross-judge 的判断对比:一致就是确认,不一致说明有一方有偏见,或者 rubric 本身含糊,两边都要读。

Phase E: Graft。 再走一遍每个落选的候选,找出值得移植进来的东西。信号通常是一个候选一两样,不是大半。手工融合,不要机械粘贴,结果必须在一个心智模型下保持连贯。嫁接了什么、来自哪个候选、否决了什么、为什么,都要记录。

Phase F: Verify。 合成出来的产物要像任何其他产出一样接受审查。如果验证暴露了一个 arena 没抓到的问题,要么是 Phase A 错了(重新框定重跑),要么是某个候选抓到了但你漏了嫁接(回到 Phase E)。不要糊过去。

它有两个很有用的观察。候选们收敛到同一个形状,是强一致信号,直接发共识形状,不用嫁接。候选们疯狂发散,说明 Phase A 规格没写清,重新框定再跑一次,不要把它们平均掉。

它的输出是一份合成作品,加一份简短的合成记录:底是谁、嫁接了哪些(来自哪个候选)、否决了什么、有没有掉队的、验证结果如何。

参考 ​

原文都在 pstack 上游仓库(cursor/plugins):