乐于分享
好东西不私藏

围棋、AI 与软件设计:真正重要的也许不是多算几步

围棋、AI 与软件设计:真正重要的也许不是多算几步
最近对围棋产生了一点兴趣。
这个"兴趣"离"会下"还差得很远。我知道规则,知道怎么吃子,听过布局、定式、死活这些词,但真给我一盘稍微复杂点的棋,让我说哪里重要、哪边占优,我大概完全看不出来。
最先让我困惑的倒不是围棋为什么值得研究,而是:亲自下棋到底有什么乐趣?
如果下棋就是脑子里不停预测——我走这里,对方可能走那里,再往后推十步——那它更像一场纯脑力比赛。业余玩家在这件事上天然吃亏:别人每天做死活题、研究定式、复盘棋谱,算得自然比我深。我偶尔下一盘,投入量摆在那里,还没开始好像就已经输了。
带着这个疑问查了点资料(也问了不少 AI),慢慢发现,投入练习固然重要,但是我仍把"计算"放到了太中心的位置。
围棋当然很需要计算。但在展开一个局部之前,棋手其实已经做了很多别的判断:现在是什么局面,哪里重要,哪里更急,哪块棋弱,这一仗有没有必要打,这个定式放到今天的棋局里合不合适。计算只是整套决策里的一部分。
更关键的是:面对一个根本不可能穷举的空间,先把它压缩成少数真正值得研究的问题,再把有限的计算花在那些地方。
想到这儿,我很自然地想到了做设计——尤其是 AI 已经能把"展开方案"这件事变得很便宜之后。

第一手之前,棋局其实已经开始了

围棋落下第一颗子之前,条件就定下来了:执黑还是执白,什么规则,多少时间。熟悉对手的话,还可能知道他喜欢复杂战斗还是稳稳拿实地,自己又擅长哪种局面。
我离考虑"棋风"还远,但这个顺序很有意思。做软件系统的时候,我们也很容易过早进入"下一步怎么走"。
拿到一个问题,很快有人开始讨论拆不拆服务、缓存怎么做、数据结构选什么、协议怎么设计。AI 让这件事更明显了:问题扔进去,几秒后给你三套架构、五种方案,再附一份看起来相当完整的优缺点分析。
但这些都是解法了。
再往前还有更基本的东西:我们到底在解决什么问题?谁会用它?要的是延迟、吞吐、成本还是可靠性?哪些条件改不了?哪些能力现在必须有,哪些只是以后也许需要?团队愿意为它长期承担多大的复杂度?
这些没弄清楚,后面的"最优方案"就没有明确的意义——换一组约束,评价标准就全变了。
所以第一颗棋还没落下,棋局其实已经开始了。设计也是。

布局:先决定棋往哪里走

布局是我以前对围棋最没有概念的部分。棋盘那么空,能落子的地方那么多,如果围棋真的是比谁向后算得多,开局应该是最难算的——搜索空间大得离谱。
实际显然不是这么下的。
棋手会先看角上怎么占,几颗棋怎么配合,往哪个方向发展,哪里空间大,哪里虽然以后要下但现在不急。看入门讲解时经常听到"这一边更大""这个方向不好",对一个刚知道规则的人来说这挺神奇:棋盘上并没有写着这里值 18、那里值 13,这些判断来自棋子之间的关系,是对未来发展的粗粒度预判。
也就是说,精确计算之前,棋手已经把巨大的选择空间压缩了一次。能下的地方很多,值得认真考虑的只剩几个。
这个过程和软件设计很像。系统稍微复杂一点,可选的设计方向几乎永远比我们有精力研究的多。以前实现成本会帮我们做一层自然过滤:一个方案光 prototype 就要写两个星期,大家通常不会随便往下试。
AI 正在迅速拆掉这层过滤。于是过去不那么显眼的问题冒了出来:我们真的需要把所有变化都展开吗?
很多时候并不需要。先判断哪个方向值得投入,比生成更多方案更重要。

定式:成熟经验并不会替你下棋

我一开始把定式理解成"标准答案"。围棋研究了几百年,一个角走成这样,后面最正确的下法应该早就定了吧,照着下就行。
后来才知道不是。定式更像长期实践后局部双方都能接受的变化:一边拿角上实地,一边得外势,谁也没明显吃亏,于是稳定下来。问题在于,棋盘不只有这个角。外面已经有什么棋,外势朝向哪边,附近有没有弱棋,这一盘更需要实地还是发展——都影响同一个局部到底值不值。
更有意思的是,定式本身也不是一张永远正确的答案表。AI 出现以后,一些职业棋手用了很多年的变化被重新评价,以前看着有点奇怪的下法反而开始流行。
所以我现在更愿意把定式理解成:被长期经验压缩出来的成熟方案。它告诉你这类局部以前的人通常怎么处理,却不会替你决定今天这盘棋需要哪种结果。
软件工程里的成熟模式也一样。设计模式、架构模式、经典算法和协议值得学,是因为它们省掉了从头发明的成本。但"这是一个成熟方案"和"我的系统现在该用它",中间始终隔着具体上下文。
这种感觉在大多数软件系统里应该都成立。不过要举例子,我更熟悉的是数据库。
数据库领域尤其不缺"定式":B+ Tree、LSM Tree、WAL、MVCC、Buffer Pool、同步复制、Raft,都有大量成熟经验,论文、实现、最佳实践一应俱全。
现在了解这些东西比以前容易太多了。想比较 B+ Tree 和 LSM,很快能拿到一份完整分析;想了解某种复制协议,也能马上得到几套不同实现。
真正麻烦的反而是另一些问题:现在面对的到底是什么 workload?这个系统真正害怕什么?某部分复杂度值不值得承担?一个局部机制单独看收益不错,放进完整数据库以后还是不是这样?

中盘:哪里都能下,但不是哪里都该下

进入中盘,棋盘上的事一下子多了起来。门外汉最直观的感觉就是:哪里都有事。这边几颗棋正在接触,那边还有大片未确定的空间,中间某块棋似乎不太安全,另一侧又好像有机会攻击。
如果每个地方都重要,下一手反而最难决定。
围棋里讲"急场"和"大场",这个区分我特别有感触。一个地方大,说明最终价值高;另一个地方最终价值未必最大,但更急。朴素点理解就是:大场可以以后再抢,急场如果错过时机,棋本身可能马上受损。所以下一手并不总是去棋盘上"价值最大"的地方,而要同时判断大小和紧迫程度。价值和优先级,不是一回事。
中盘还有一个概念叫"先手",我第一次听到这个名字时完全没意识到它有多重要。一个局部变化结束,如果对方必须回应,你还能继续掌握主动;如果对方可以不理,转身去别处,那你就算局部得了些利益,也可能失去下一步选择重要位置的机会。所以评价一段变化,不能只算各自拿了多少,还要看交换之后下一手重要棋轮到谁下。有时候宁可少拿一点眼前利益,也要保住主动权——"局部最优"一下子复杂了很多。
做数据库项目久了,这两种感觉都非常熟悉。索引还能优化,Checkpoint 还能更平滑,缓存命中率还能提高,复制延迟还能降,恢复、监控、异常处理,几乎每个方向都还能往前走。这些事都不是"没价值",难的是决定现在先做什么。一个模块当然也可以继续打磨,把 API 做得更漂亮、性能再提一点,但为了把这一块做到最好,整个项目一直没有精力进入下一个真正重要的问题,那局部的最优也就没那么重要了。
以前实现成本本身会挡住不少想法,一个优化要几周,大家自然会认真掂量。AI 让"先实现一个看看"越来越便宜之后,这层约束在减弱,排序反而比以前更重要了。

弱棋和厚薄:看不出来,但感觉得到

围棋里还有弱棋。一块棋自身没完全安定,就可能持续被攻击。但攻击弱棋不一定要把它杀掉,只要逼对方不断回应,攻击方就能顺便获得外势、把自己的棋走厚,甚至借机抢到别处的主动。一个局部弱点,会不断影响整盘棋。
系统里那些早期看起来不严重的设计问题,也有类似效果。一个接口边界模糊,一个状态没说清归谁管,一套缓存机制能工作但失效规则很别扭——一开始都不是致命问题。真正麻烦的是系统继续生长以后,新功能开始不得不围绕这些地方设计,后来很多复杂度不是新功能带来的,而是在不断迁就早期留下的那个弱点。有些技术债之所以贵,大概就是因为它不会安静地待在原地。
围棋里还有一个概念叫"厚"和"薄"。以我的水平当然判断不出一块棋厚不厚,粗略理解:厚,就是本身稳固、难被严重攻击,还能支持周围行动;薄,就是里面有缺陷、断点,藏着未来可能被利用的地方。
而厚薄很难直接换算成"现在有多少目"。厚棋眼下不一定给你多少实地,但它足够安全,你以后不用老回来补,可以放心去别处行动。它给的是行动自由。
系统设计里也常有这种难以量化的感觉。有些设计没有明显 bug,但第一眼就是让人不舒服:状态由好几个模块同时维护,缓存失效规则很难解释,正常路径很顺畅,但 crash 到某个位置后很难说清该相信哪个状态。这时候还没有证据说明设计一定错了,但你会觉得这里值得再看一眼,然后才开始往下推:并发发生会怎样?请求重复会怎样?机器恰好在这时掉电呢?恢复过程中再失败呢?
这种第一反应当然不能替代证明。但经验确实会压缩成这样的判断。以前读过的源码、踩过的 bug、分析过的 crash,不会一直以完整推理链的形式留在脑子里,最后可能只剩一种感觉:这个地方有点薄,最好再看看。

真正战斗的时候,还是要老老实实算

讲了这么多判断,很容易走到另一个极端:好像只需要大局观,计算不重要了。围棋显然不是这样。
进入局部战斗,一块棋能不能活,这里能不能断,征子成不成立,双方对杀谁先没气,最后还是要把具体变化展开。认真学棋的人大量做死活题,大概就是练这个。
只是我现在开始理解,局部计算和大局判断解决的是不同的问题。一个人可以局部算得很深,却在开头就为一个不值得打的地方投入了大量棋子;反过来,方向判断得再好,关键死活算错一步,整块棋死掉,前面的判断也全失去意义。
所以围棋要的是两件事同时成立:先找到值得计算的地方,到了那里,又有能力真的把变化算清楚。
这也是我觉得它和 AI 时代做设计最像的地方。AI 特别擅长展开后半段:搜代码、枚举方案、写 prototype、补测试,把过去很耗时的工作变得便宜。于是人的注意力很自然地往前移了一点:到底让 AI 算什么,为什么值得算这个,算出来的结果又怎么判断。

形势判断与官子:越到后面,才越算得精确

下棋过程中还要不断做形势判断:现在谁领先,哪里已基本确定,哪里还有潜力,有没有棋有危险。领先时更愿意把局面走简单,不主动制造复杂战斗;落后时反过来,主动找变化。这意味着布局不是开局定个计划,然后一路执行到底。棋盘变了,之前的判断也要跟着更新。
系统设计当然也一样。一个项目最初基于某种数据规模、workload 和硬件条件做的选择,几年后可能面对完全不同的环境。如果因为"这是最初的设计"就一直坚持,反而会错过真正需要变化的地方。有时候最难的不是做出一个好设计,而是发现当初那个好设计已经不再适合今天的棋局。
有意思的是,顺序其实和我原来以为的完全反过来:不是从第一手就开始精确计算,而是越接近棋局末尾,才越算得精确。布局阶段全是很难量化的全局判断,中盘还要处理强弱急缓、攻击取舍,到了官子,棋盘越来越确定,很多地方的价值才开始能被精细计算:这里几目,那里交换后差多少,先手官子和后手官子价值差在哪。整盘棋像是一个从模糊判断逐渐走向精确计算的过程。这个顺序我很喜欢。
做设计的时候我经常有相反的冲动:大方向还没定,就开始把某个 API、某个数据结构甚至某段实现设计得无比精确,最后可能只是精确地解决了一个后来根本不重要的问题。很多细节并不是越早确定越好。到了真正需要精确的时候再算清楚,可能反而更合理。

复盘:真正要修正的是当时的判断

最后是复盘。一盘棋下完,如果只记住"AI 说第 73 手该下在这里",下一盘周围棋子配置稍微变一下,大概还是不知道怎么办。更有价值的追问是:为什么当时觉得那里最大?为什么认为那块棋已经安全?为什么选这个定式?为什么没发现另一边其实更急?
需要复盘的不仅是最后那步棋,还有产生那步棋的判断。
做设计也一样。系统出了问题,当然要先修 bug,但如果每次最后只留下"这里少了一把锁""那里缺了个 fsync""这个缓存漏了一次失效",下次换一套代码,我们可能还犯同样的错。继续追问为什么当时觉得两个状态可以独立更新,为什么认为这个 failure 不需要处理,为什么判断这种 workload 不会出现——经验才有可能真正留下来。
最后,它们会慢慢压缩成一些很难写成规则、但很有用的判断:这个地方有点薄,那个问题比较急,这种优化现在可以先放着。
这大概就是所谓的"棋感"。
我当然还没有围棋的棋感。这篇文章也不是我下了很多棋之后总结的心得——恰恰相反,正是因为不理解"如果只是比谁多算几步,亲自下棋到底有什么意思",我才跑去查了资料。结果发现围棋真正要训练的,比"计算未来"丰富得多。
棋还没开始,要先理解自己面对的条件;进入布局,要从巨大的空间里判断方向;遇到定式,可以借用前人的经验,但不能把判断也交出去;到了中盘,要处理强弱、急缓、先手和取舍;真进入关键战斗,又必须老老实实把变化算清楚;过程中还要不断重新判断形势,最后靠复盘修正自己当时为什么那么想。
这样回头看 AI 时代的设计,问题也变得有趣了一些。AI 会让"展开变化"越来越容易,软件如此,我更熟悉的数据库更是如此。当方案、prototype、甚至局部推演都越来越便宜以后,需要训练的可能反而是前面那部分能力。
先看清棋局,再决定什么值得算。