我的Agent系统研发原则
做 Agent 系统这些日子,踩过很多坑,自己反思,让CC,Codex一块跟着总结,总结了十六条原则和想法。
前十二条,是当时我以为自己想明白了。后四条,是把这这十二条作为红队原则实施之后碰到的的,后四条如果离开前面那句被打回来的话,就看不出它在改什么。后面的这些原则和经验也许会变,但是我认为这是经验所得,值得记录和分享。
一、不要爱上自己的理论
一个设计理论一旦成形,就会开始解释越来越多的事情。新现象进来,它总能找到一个位置安放。解释得越顺,人就越不愿意动它,工程师很容易为自己的设计找借口。
危险在这里。理论开始替你看世界的时候,你就不再看世界了。
真正该反复问的只有一句:
什么证据能够证明我是错的?
一直在找支持自己的证据,不去找能反驳自己的证据,理论就会越来越封闭。
二、抽象不是创新
很多工程师会把重命名当成一次设计。
原来是:
1 | 模块A |
换一套名字,就成了:
1 | Event |
图画得更整齐,会上也更好讲。可是系统能做的事,一件都没多出来。
这不叫创新。真正的创新只有一个标准:
解决了以前解决不了的问题。
重新组织已有概念,不算。名字换得再漂亮,问题还在原地,就还是原来那个系统。
三、没有数据,就没有架构
所有设计落到纸上之前,先问四句。
有没有实验?有没有 Benchmark?有没有 A/B?有没有生产数据?
四句里一句都答不上,就不要叫它架构。它是假设。假设可以先写下来,也可以很漂亮。它还没有资格用「架构」这个词。
四、复杂性必须买回价值
复杂不是免费的。
每增加一层,团队以后都要读它、改它、为它排障。这些都是利息,而且不会随着上线停掉。
所以复杂性必须买回对应的价值。买不回来的那一层,就是负债。负债不会因为图画得好看就消失。
五、先证明存在,再证明优雅
这是这次最大的教训。
以前的顺序是:
1 | Idea |
想法出来,先把架构画完,然后写代码,最后才验证。架构排在验证前面。验证不理想的时候,前面三步已经花出去了。
以后应该倒过来:
1 | Idea |
顺序必须反过来。先看别人怎么做,再问为什么没人这么做,把真正的创新点收成一条假设。假设只用几十行代码去做实验。实验拿出数据,才谈 Benchmark。架构放在最后。框架是实验活下来之后的容器。
六、任何设计都必须有删除证明
以后每新增一个模块,先回答:
如果删掉它,会发生什么?
答得出来,而且答案是系统会缺掉一块真正被用到的能力,它才有资格留下。
如果认真想完,答案是「没什么」,那它本来就不该存在。删除证明是加进来之前就要交的,不是上线之后再看的优化项。
七、别人都没这么做,第一反应是警惕
以前我们的第一反应是兴奋:
我们发现了别人没发现的东西。
这个反应要换掉。别人都没有这样设计,先问:
为什么别人都没有这样设计?
一种可能,是他们没想到。另一种可能,是他们想到过,做过,踩过坑,然后拿掉了。先把这两种可能研究清楚,再决定要不要做。兴奋留到研究之后。
八、不要设计系统,要设计实验
这一条我自己觉得特别重要。
一个真在做原创的人,每天的工作不是设计框架,是设计下一个实验。这个实验要打掉哪一条假设,怎样算成功,怎样算失败。
实验一次次做成,框架会从里面长出来。顺序倒过来,先有一个完整的框架,再去找能证明它的实验,框架不会自己长出来。后面第十五条会讲,这种倒过来的做法,最后会把交付整个换掉。
九、工程永远优先于理论
设计理论可以解释一切。给它足够的词,任何结果都能被说圆。
工程不行。工程必须把一个具体的问题解决掉。
所以任何理论,最后都要接受四重审判:代码、生产、用户、数据。四重里有一重过不去,它就还是理论。解释得圆,不能代替这四样。
十、不要追求最完整,要追求最不可替代
这是定位,不是完整度上的竞赛。
比如做记忆。mem0 的 Provider 比你多,那就不要在 Provider 的数量上跟它比。比完整,比的是一份别人已经铺开的清单。
真正要问的是:
有没有一个能力,别人没有,而用户又真正需要?
没有这种能力,就不要开源。开源一份更全的清单,别人不会因此换过来。换过来的理由只有一个:有一件事,只有这里做得成。
十一、最大的风险不是做错,而是做得越来越对
这是我昨天想到的一句。
很多失败的项目,不是第一天就失败的。它们越来越完整,越来越优雅,做的人也越来越相信自己,直到没人再质疑。
这时候,风险最大。
十二、永远保留一个反对者
以后任何重大设计,至少有两个角色。
Builder,建设者。 负责提出想法,把设计做出来。
Destroyer,摧毁者。 负责证明这个设计根本不应该存在。
只有 Destroyer 没能打倒它,Builder 才能进入下一阶段。没被打倒,意思是打过了,它还在。
十三、活着不是有效
第三条说,没有数据,就没有架构。后来我们发现,数据本身会说谎。更准确地说,太容易把「在动」读成「有用」。
线上有一项计数,ingest_ok = 843。看起来这条链路在跑,而且跑了不少。换算一下,recall_rate 大约只有 8.9%。context_keys 百分之百是空的。这组数放了三个月。三个月里,没有人问过这 8.9%,也没有人问过那些空键。
闲聊的主路径是同一个形状。走 SSE 的时候,memoryos = True,run_done 正常,没有 failed。可是也没有 ingest_ok。模块报的是健康,主路径并没有在写。
这样的子系统审计我们连续做了五次。五次都是这一形:模块跑了,结果没有进到下游关心的地方。
所以判据要换一句:
判据必须是下游真的用了,不是自己跑起来了。
裸计数是活动。消费率才是效果。843 次写入如果几乎没被用上,就只是 843 次活动。
十四、写下来的原则,准入当天不能让 CI 变红,就等于没写
第十二条留下一个反对者。空转之后,我们把「要有人反对」收成三道门,写进文档:falsifier,一条能证伪它的判据;sunset,flag 上的到期日;还有独立评审。
下一案照样活了 47 天。
消费端先落地了。commit 里写着「待生产侧 PR」。生产端从未存在。缺键的时候,代码静默走旧路径,端到端看起来正常。单测喂进去的是手写样本。埋点写在功能路径里面,功能没激活,埋点也不激活。这条路径上发生了什么,记录里不会有。
三道门都在文档里。发现它的方式,仍然是人走读。复盘还没写完,同一周,又塞进一个新的 shadow。
所以原则要换一种形态:
原则要在合入当天让 CI 变红。
消费键没有生产端,拒绝。flag 没有 sunset,拒绝。拿来当效果的指标仍是裸计数,拒绝。
靠「下次记得看」,就是当年允许它在线上空转的那句话。人会忘。门如果不会在合入时自己关上,就等于没有这道门。
十五、实验装置会取代交付
第八条说,不要设计系统,要设计实验。后来有一轮,实验做着做着,把交付换成了实验装置本身。
这一轮用了 8 天。150 个提交,46 个验证脚本,关掉的任务是 0。验收标准换了六代。第一代标准出现之前,没有一行字写过:什么叫关闭。
证据还会被做成看起来更强的样子。同一个用例循环跑 3 次,写成稳健。换一个进程再跑,两臂重叠,稳健就不在了。FAT 上 0 条命中,分母是 QA 脚本打出来的流量。这个 0 差点被读成「没人用」。全量召回 44%,严格按人工标准看是 17%。报出去的是 44%。
测试每次都是绿的。因为测试和那句声明,是同一个人、在同一时刻写下的。测试证明的,是作者刚才决定相信的那句话。
所以要先钉死一件事,再动手造测量的东西:
先钉死什么叫关闭,再造测量装置。
测量脚本先喂已知答案。它能把已知答案报对,再让它碰真实流量。对外只报最保守的那个数。44% 和 17% 同时在的时候,报 17%。
十六、影子态的理性选择,是永不转正
第六条的删除证明,发生在画图的时候。框还在纸上,删掉它几乎没有成本。代码一旦进了仓库,激励就反过来。
转正失败,要有人担责。永远停在影子态、永不转正,成本是零。重构可以写进 PR。删除没有人立项。
红队点过名,四个模块该砍。两周之后,四个都还在,而且是在重构里继续加行。
同一种激励还有一个技术上的样子。缺口留在上游,补丁打在下游。上游差五个字符。下游用两百多行,把这个差距变成制度。
所以,模块一时删不掉的时候,要删的是让它继续活下去的激励:
删不掉模块的时候,要删的是让它活下去的激励。
到期没转正,就告警。下游那一层适配,自己也要有删除证明。
最后
十六条放在一起,我想再留一句我认为最重要的认知。
优秀的架构,不是设计出来的。
它是在一次又一次的否定里,最后仍然没有被删掉的那部分。
所以:
真正的架构能力,不是创造复杂性,而是识别什么复杂性是不可避免的。