用 AI 写项目,先把模块边界划清楚
从“加个悔棋”说起
假设我们正在做一个棋类网页,落子、回合切换和 AI 对手都能运行。现在想加一个悔棋按钮,却发现点击事件里既有合法性判断,又有动画、历史记录和 AI 调用。撤回一步之后,该撤销哪些状态、已经开始思考的 AI 怎么处理,都得沿着代码重新找一遍。
下面就用这个简化场景讨论模块边界。它是说明性的例子,不对应某个已上线项目的实现。
用 AI 写功能时,很容易把注意力放在眼前的效果上:按钮能点,页面能打开,任务就像完成了。等到第二次、第三次修改,代码之间的依赖才逐渐显出来。为了少走到“哪里都不敢动”的地步,值得在开工前先写清每部分的职责。
按职责和变化原因划分
给文件夹起名叫 core、ui、utils,还不足以说明边界。更有用的问题是:它负责什么,谁管理状态,对外提供什么能力?
调整棋子的移动动画,主要应该影响界面;修改合法落子的条件,应能找到统一的规则入口;更换 AI 的选步方法,也应能定位到决策部分。如果每次都要在几个角落各改一遍同样的判断,就需要检查职责是否散开了。
这里借鉴了 Martin Fowler 对 Bounded Context(限界上下文) 的介绍:模型内部的概念要一致,不同上下文之间的关系要明确。它讨论的尺度比一个小组件大,不能直接等同于“多拆几个文件”。对个人项目而言,可以先在同一个应用里落实这些约定,不必一上来就拆微服务。
规则调整也可能影响 AI 或界面。合理的边界应让影响路径容易判断,并不要求每个模块永远互不影响。
先写一张职责表
对于这个棋类例子,可以先尝试下面的划分:
| 模块 | 负责什么 | 如何与其他部分协作 |
|---|---|---|
| 规则 | 检查动作,计算落子后的局面及结束条件 | 接收局面和动作,返回处理结果 |
| 对局流程 | 保存当前局面,安排回合操作,管理历史 | 调用规则,把新状态交给界面 |
| AI 决策 | 根据局面选择候选动作 | 提交动作,仍经过规则校验 |
| 界面 | 显示棋盘,播放动画,接收操作 | 发出操作意图,按状态更新显示 |
这张表需要随玩法调整。它先解决一个很实际的问题:当前局面由对局流程保存,规则负责计算新局面,界面不应在动画结束时顺手改掉轮到谁。
AI 也一样。它可以采用不同的选步算法,但不能因为是“程序自己走的棋”,就绕过普通玩家使用的合法性检查。以后换算法时,调用方仍可以沿用约定好的动作入口。
接口要写到失败时怎么办
我在整理开发流程时,会把契约先行作为一个约束:先说清模块之间怎么交接,再让 AI 实现各自的内部逻辑。这里的契约可以很短,例如:
applyMove(state, move)合法:返回新局面 nextState非法:返回拒绝原因 reason两种情况都不修改传入的 state这只是接口示意。除了参数名字,还要约定“合法”由谁判断、回合变化在哪里计算、调用失败时旧局面是否保留。比如规定规则模块在 nextState 中计算下一回合,对局流程保存结果,就能避免两边各自推进一次。
如果只写“返回一个局面”,实现者很可能作出不同理解:一个模块原地修改对象,另一个却还把它当作历史快照。历史记录也需要稳定的快照或不可变数据,不能只是把同一个可变对象反复塞进数组。
遇到悔棋这样的新功能,还要补充流程约定:恢复到哪一步,如何处理正在执行的 AI 任务,旧任务返回后是否允许落子。这些问题适合在接口讨论时暴露出来。
给 Agent 一张范围清楚的任务卡
多开几个 Agent,并不会自动得到低耦合的代码。大家如果各自理解一遍需求、各自发明一套数据结构,最后仍得重新拼接。
可以把“完善棋盘逻辑”改写成下面这样的任务:
目标:补充非法落子的处理。允许修改规则模块及其测试;沿用现有局面和动作类型。暂不调整棋盘布局或 AI 策略。验收时检查越界、占用位置、非当前回合操作;被拒绝后,局面和回合都保持不变。
任务卡让实现者和审查者对“完成”有共同理解。如果过程中发现共享类型不够用,应先列出需要调整的接口及调用方,再统一修改,避免在某个模块里悄悄追加一套兼容逻辑。
Google 的 Small CLs 建议也适合放在这里:把修改组织成范围集中、能够独立理解的一次改动,并带上相关测试。小步修改不只是限制行数;新增接口时,合理的调用示例也应一起出现,让审查者知道它怎么用。
若需要大幅移动文件或重命名模块,尽量与新增功能分开处理,否则很难看出行为到底在哪里发生了变化。
用行为检查边界是否有效
职责表写完,还需要实际检查。对于这个例子,我会先关注这些行为:
- 非法动作被拒绝后,棋盘和当前回合都不变。
- 合法动作只应用一次,回合不会被重复推进。
- AI 提交的动作也要经过规则校验。
- 纯界面改动不会影响规则计算的结果。
这些检查不能证明架构已经完美,但能帮助发现哪些约定没有被遵守。规则可以单独测试,再用集成检查验证界面、流程与规则连在一起时的行为。只测某个函数返回正确,还不足以证明点击按钮后没有重复提交。
边界也需要调整。如果两个模块长期互相读取内部数据,或同一条业务规则总在几处同时修改,就值得重新看职责划分。反过来,一个简单动作若要经过许多只做转发的模块,继续细拆也可能只是在增加理解成本。
下一次让 AI 加功能前,我会先写下:这次改哪里,依赖哪份接口,怎样检查完成。先让这几项约定在一个小功能里跑通,再根据实际修改中遇到的问题调整结构。
参考资料
- Martin Fowler:Bounded Context。用于理解概念边界与上下文之间的关系。
- Google Engineering Practices:Small CLs。用于理解改动范围、审查与相关测试的组织方式。
