← 全部文章

理解代码,是为了更好地生成代码

做 FiboCode 的过程中,我越来越在意自己能否看懂项目的架构和功能流程。这篇文章记录了我对自然语言编程、模糊需求,以及如何长期用 AI 修改代码的一些思考。

我不喜欢看不懂自己项目的感觉。

需求是我提的,功能也是我想要的,AI 生成以后,我也确认过它能正常运行。但如果有人问,这个功能经过了哪些模块,为什么要这样实现,再改一个地方会影响什么,我却未必能回答清楚。

做 FiboCode 的过程中,我越来越在意这件事。

我当然希望 AI 多帮我写一些代码。可项目是要一直做下去的,今天加完功能,明天还会有新的想法。我不希望每次修改,都得先祈祷它别把其他地方弄坏。

修好了 A,B 又出问题。再去修 B,A 又出现了新的 Bug。碰到这种情况,我们很容易继续把现象发给 AI,等它给出下一份解释和修复方案。

但这样反复几轮,心里其实会越来越没底。每一次修改看上去都有道理,合在一起,系统到底变成了什么样子?

我想弄清楚这个问题。也是沿着这个问题,我开始觉得,理解代码和生成代码之间的关系,比我最初想的要紧密。

自然语言编程,让我想到了一件事

从机器语言到汇编,再到高级语言,我们一直在做抽象。

很多过去需要自己处理的细节,后来都交给了工具。我们可以用更少的代码表达更复杂的功能,也不必每次都从底层重新开始。

现在又向上走了一步。我们用自然语言描述需求,让 AI 去生成代码。

乍看起来,好像只是换了一种表达方式。但我觉得,这中间有个变化不能忽略。

汇编器、编译器按照明确的规则转换代码。程序当然可能有 Bug,但转换的过程不需要猜测我究竟想干什么。

模型需要猜。

它根据我说的话、看到的代码和上下文,推断我想要什么,再选择一种实现。同一句话,可能得到不同的结果;即使生成结果相同,也可能一直在实现一个我没有表达清楚的需求。

后来读到 Birgitta Böckeler 的一篇文章,她也谈到了这个区别:大语言模型依靠推断生成结果,不能直接当成自然语言的编译器。这和我在想的问题很接近。

那我们能做什么?

把需求写清楚,把规范留下来,让测试检查关键行为,应该都能减少一部分偏差。如果设计里已经说清了模块之间怎么协作,涉及哪些类和方法,AI 实现时也会多一些依据。

不过,文档得和代码对得上。写得很详细,却描述着一个早就改掉的设计,可能比没有文档还麻烦。

我也不觉得这些办法能彻底消除生成过程的不确定性。但至少可以少留一些让模型临时猜测的东西。

可我真的知道自己想改什么吗?

这件事想下去,又会碰到另一个问题:人也很难把需求说清楚。

尤其是在不了解代码的时候。

我可能很清楚界面上想多一个按钮,或者某个操作应该方便一点。至于它应该放进现有的哪个模块,会经过哪些处理,又有什么旧行为需要保留,我未必知道。

这时候,就算我反复告诉 AI“别影响其他功能”,它又该怎样理解这句话?

比如,“把这里统一一下”,可能只想统一展示,也可能是要合并数据处理逻辑。这两种理解,最后涉及的修改范围会差很多。

如果连我自己都说不清范围,模型就只能根据它看到的内容补全。它可能做得比我预想的多,也可能绕过了某个我根本不知道的约定。

改完以后发现问题,再让它修。修改越积越多,最初的要求、中途加上的限制和代码里原有的联系,未必都还能完整地留在上下文里。

我担心,最后会堆出这样一份代码:功能局部能用,到处都是为了解决上一个问题留下的补丁。看上去只有 AI 愿意继续改,人已经不太敢碰了。

这种代码让我想到 AI slop。更麻烦的是,就算继续交给 AI,它也未必真正掌握了整个系统。它还能写出解释,却不代表我们终于理解了它。

所以我越来越觉得,想把需求说得准确,还是得先知道项目大致是什么样子。

不必一开始就懂所有细节,但至少要知道,自己提出的变化会落在哪里。

我希望在架构这一层和 AI 对话

我并不主张把 AI 生成的代码一行行全部审查一遍。

项目复杂以后,人的精力很有限。我更希望先看清框架:有哪些核心模块,各自负责什么,一个功能从哪里进入,经过哪些处理,最后在哪里改变数据和状态。

也就是在高级语言的实现之上,再整理出一层对系统的认识。暂且叫它“架构层”吧,我还没有特别在意这个名字。

关键是,这一层要能和实际代码对应起来。

我看到一个模块,能找到它的实现;看到一条功能流程,能知道它经过哪些类和方法;想调整一个行为,能顺着关系找到需要关心的位置。

这样,我跟 AI 讨论修改时,就有了更具体的话可以说。

比如我知道某个操作本来就经过统一的任务队列,那新增能力时,就可以讨论它能否沿用这条流程。看到 AI 又写了一套状态管理,也可以追问:原来的为什么不能用?这两套状态以后会不会互相影响?

如果这些问题始终没想过,拿到一份能运行的代码,我可能就接受了。现在至少有机会提前看出,它是不是把事情做复杂了。

我觉得,代码水平也会在这种比较中提高。

一个功能有几种做法,为什么当前项目选择了这一种?某个抽象看起来很漂亮,实际修改时却要同时碰很多地方,它真的有帮助吗?另一个模块写得很朴素,几次需求变化都没有牵动其他部分,它又做对了什么?

这些问题值得花时间琢磨。慢慢有了自己的判断,就更容易告诉 AI 哪些地方应该调整,也能理解它给出的另一种方案。

当然,我理解的架构也可能是错的。图里连着的关系,需要回到代码里核对;我以为不会影响的功能,也需要通过测试和实际运行来检查。遇到关键状态、复杂边界,再往具体实现里看。

我希望自己能逐渐做到:大部分时候知道该从哪里看,也知道什么地方还没弄懂。

这也是我做 FiboCode 代码分析功能的原因。

我想让代码、文档和关系图联系起来,帮助人从陌生的实现里看清框架,沿着一个功能找到它的流程。有了这层认识,再去和 AI 讨论怎么改,心里会踏实一些。

Unmesh Joshi 在《What Is Code?》里也谈到,好的抽象、类型、测试和明确的规则,可以成为模型理解和修改代码的依据。我很认同这个方向:我们对系统的理解,应该尽量留在项目里,让下一次修改还能用得上。

有些时间,我还是愿意花在读代码上

AI 写得很快,会让人忍不住把下一个任务也交给它。

功能一个接一个完成,项目看起来一直在前进。但我也会在意,做完这些以后,自己到底多懂了什么。

如果只是不断提出需求、接收结果,等到项目复杂得无法把控,再花很多时间修补,甚至推倒重来,那前面省下的时间,可能又花回去了。还有模型调用的费用,以及重新熟悉一套实现的精力。

我更愿意拿其中一部分时间,静下来看看代码。

不一定看很多。可以先弄懂一个模块为什么这样拆,一个功能为什么要经过这些步骤,或者 AI 这次用了什么我还不熟悉的办法。看不明白就问,再去代码里找依据,试着改一点,看看自己的理解对不对。

我找资料时,看到 Anthropic 做过一个学习实验。参与者借助 AI 完成陌生编程任务后,短期测验的平均成绩低于手写组。这个小样本实验不能回答长期能力会怎样变化,但它让我更在意,任务完成以后,我是否真的学会了其中的东西。

我希望这些理解能一点点积累起来。以后遇到类似的设计,自己能看出门道;AI 给出方案时,也能提出有依据的问题。

FiboCode 能不能把这件事做好,还需要继续摸索。但我想做代码分析的原因,确实很大一部分就在这里。

我希望继续用 AI 写代码,也希望项目越做越大以后,自己还能说清它是怎么工作的。


文中提到的思路,简单画出来是这样:

另外一些相关资料