atelier 是被这些错误判断磨出来的

💡 原文中文,约4600字,阅读约需11分钟。
📝

内容提要

Atelier 项目在开发中通过推翻自身的错误判断而成长,主要经验包括:删除没有消费者的抽象层;不合并两个正交字段;压缩无效果时必须留痕,而非静默丢弃;拆分 548 行代码应先理清控制流再分文件;flaky 测试比明确失败更危险,恒真或恒假的用例更糟。最终提炼出四条判据:抽象看删掉什么、字段看是否同变、无效果要可观测、全绿须连跑三次。

🔎

延伸解读

抽象层的价值在于能删掉什么

作者最初设计 RunProjection 和 outcomeOf() 想统一两个翻译器,结果零消费者。关键转折是验收标准从“代码变简单了吗”改为“哪一行代码会被删掉”。这个判据很实用:加抽象前先问它会删掉哪些具体行,说不出来就别加。它把主观的“清晰”变成了可验证的删除清单,避免为统一而统一。

正交字段合并前先看是否同变

TurnEndReason 想替代 StopReason,但两者回答不同问题:run 是否结束 vs 回合内是否继续。作者用“会不会一起变”作为判据,并刻意不给 reason 设默认值,让编译器强制每个构造点说清收尾原因。这提醒读者:字段能否合并不看名字像不像,而看变化条件是否一致;不设默认值是把遗漏变成编译错误。

无效果要留痕,静默丢弃会制造隐蔽差异

压缩后没变短就丢弃结果,看似合理优化,却让单条超长消息的“检查点+原消息”被误判为没压缩,导致摘要成功但历史未替换。修法是原样返回 next,由调用方按 size 判定 noop 并留痕。核心教训是“没有效果”和“没做”是两件事,前者必须可观测,否则一次成功操作会看起来像没发生。

全绿要连跑三次,恒真断言比 flaky 更糟

一条用例三次跑一红两绿,作者认为 flaky 比明确失败更危险,因为人容易重跑一次绿了就走,而背后往往是产品侧竞态。修法是用“先登记再投递”的顺序消除窗口,而非加长超时。自查时还发现恒真断言,比如 run 前后状态都是 Idle,这类用例永远绿、从未测过任何东西,比 flaky 更糟。

❓

Q&A

atelier 项目从错误判断中提炼出了哪四条可执行的判据?

判据一:抽象要看「删掉什么」——加抽象前先问它会删掉哪些具体行,说不出来就别加。判据二:字段要看「会不会一起变」——两个字段能否合并看是否同时变化,变化条件不同就是正交,不能合。判据三:无效果不等于没发生——操作执行了但没效果必须可观测(noop 留痕),不能静默丢弃。判据四:全绿要看连跑——报告全绿前至少连跑三次,且每个断言都要问它在操作之前是否也成立,成立就是恒真等于没测。

为什么 atelier 删除了 RunProjection 抽象层?

因为 RunProjection 和 outcomeOf() 零消费者。A2A 和 AG-UI 的消费端是逐事件调用的,不需要「跑完之后的总结」,而 outcomeOf() 需要完整事件序列才能算,形态根本不匹配。删除后验收标准从「代码变简单了吗」改为「哪一行代码会被删掉?」,有具体行会消失才是真消费者。

TurnEndReason 和 StopReason 为什么不能合并成一个枚举?

因为它们是两个正交的问题:TurnEndReason 回答「这个 run 结没结束」,StopReason 回答「这个回合内要不要继续」。例如模型调了工具然后又调了工具,此时 StopReason = ToolUse,但如果 reason 认为任务已完成就会提前下发终态,导致工具还在跑而协议终态不可再更新。最终两个字段并存,且 TurnEndEvent.reason 刻意不设默认值,用编译器强制每个收尾点说清原因。

压缩策略中「没变短就不写回」导致了什么问题?

导致 3 个压缩用例变红。边界情况:单条消息超长(18000 字符)超过保留预算 2000,retainTokens 切点算出来是 0,压缩产出「检查点 + 原消息」= 1 条,原消息也是 1 条,size 相等被判定为「没变短」而丢弃,结果什么都没发生。但原实现会返回新列表,因为「检查点 + 原消息」虽然条数相同但内容不同(插入了摘要)。修法是必须原样返回 next,压缩「没变短」由调用方按 result.size < messages.size 判定为 noop 并留痕。

atelier 的主循环 chatWithEnv 是如何从 548 行拆分成功的?

前两次尝试都失败:第一版按「阶段类」拆,5 个类共享 RunState(8 字段)撞墙;第二版用 extend 按主题拆成多个文件又撞墙。真正原因是顺序反了——问题不在「怎么拆」,在「先拆什么」。正确顺序是先理清控制流,再按控制流的七个阶段切分:openTurn、compactBeforeRequest、invokeProvider、executeToolBatch、admitToolCall、admitAfterHooks、admitByAsking。跨回合状态收进 RunScope,阶段方法只收一个引用。

为什么说 flaky 测试比明确失败更危险?

明确失败你知道有问题会去修;flaky(时红时绿)你倾向于认为「这次是偶发」,重跑一次绿了就走。而 flaky 背后往往是产品侧的竞态,这次留在测试里,下次就留在生产里。更糟的是恒真的假用例,它们永远绿,从来没测过任何东西。atelier 中一条用例三次跑一红两绿,还挖出 2 条恒真用例。

atelier 如何解决子 Agent 通信中的竞态窗口?

子 Agent 等待回复存在时序窗口:父提问已投递,子还没登记等待,窗口内的回复被判为「回合中消息」,子一直等到超时。修法是让投递带上「我会等」的标记,先登记再投递——用顺序消除窗口,而不是靠加长超时。

🏷️

标签

➡️

继续阅读