跳至主要内容
氛围编程的门槛在边界,不在语法

氛围编程的门槛在边界,不在语法

有一次我让 AI 给一个看板加筛选功能。指令写得不短,需求、字段、交互都说了。它改完之后,页面白屏。

问题不在它写错了某一行,而在它顺手把原来的全局状态管理替换掉了——因为在它的上下文里,那看起来像是可以优化的一处冗余。我贴上报错,它又改了两轮,每一轮都在错的假设上往下走,最后改到我自己都认不出这个文件。

那是我们之间第一次真正的分歧:它认为它在优化,我认为它在拆房子。

一次白屏,让我改了工作方式

事后复盘,原因很普通。我把三件事塞进了一条指令:筛选器的界面、数据的过滤逻辑、图表的渲染更新。三件事互相依赖,模型没有足够的信息判断哪一件是主体,只能自己排一个顺序。它选了一个对我最不利的顺序。

另一层原因是上下文。那个文件我之前让它改过几次,历史里堆着一堆”再调一下””把刚才那个改回去”。它读到的不是一个干净的项目,是一段关于这个项目的混乱记忆。在这样的输入上,再强的模型也只能猜。

我原来以为氛围编程的门槛是”会不会写提示词”。那之后我不这么看了。门槛在于一个人能不能把一件事切成几件互不牵连的小事,并且说清楚每件事的边界在哪。这个能力跟语法无关,跟架构经验有关,跟有没有被人踩过也有关。

那之后我梳理过自己用它的方式,大致有三类。

一类是补全。写一半,它接一半,我逐行看。这种最没有风险,收益也最有限,大概就是把打字时间省掉。

一类是对话式修改。我描述一处改动,它给 diff,我审。这是我现在大部分时间的状态。它不快,但它把”想”和”敲”分开了,我只需要保留前者。

一类是全权代理。给一个目标,它自己找文件、自己改、自己跑测试、自己修。这一类在演示里最惊艳,我用得最少。原因很简单:它跑得越久,我要验证的东西越多。十分钟的自动执行,可能要花二十分钟确认它没顺手改别的地方。

我不认为第三类没有前途。它在一种情况下确实好用:验证成本极低的任务。改文案、批量重命名、把一批文件按规则转格式。这类事错了立刻看得见,回滚也干净。凡是验证要靠判断的,我都会退回第二类。

自动化省下的是操作时间,不是判断时间。判断没有变少,只是被推迟到最后。

先定验收,再分四步推进

有一件事我改得很晚,但改了之后收益最大:写代码之前,先把”算做完”的条件写出来。

这件事跟测试驱动开发是老关系,我只是把执行者从自己换成了模型。它带来的变化很直接:以前我给的任务是”加一个筛选器”,现在给的是”按日期和类型筛选,两个条件可以叠加,清空后回到全量,切换类型时保留日期”。前者它要做很多猜测,后者它只要照着填。

我不一定真的先写测试文件。多数时候只是几行验收条件,写在对话里。但我会坚持让它先复述一遍,确认它和我理解的是同一件事。它复述错的时候,往往不是理解能力的问题,是我那几行本身有歧义。

这件事反过来也在改我的习惯。以前我习惯边做边想,需求在实现过程中逐渐清晰。现在不行,因为模型不会等我清晰,它会先把一个版本做出来,然后我所有的修改都变成在它的版本上打补丁。宁可多花十分钟把条件想清楚,也不要在它的错误结构上返工。

验收条件定下来之后,我才开始让它动手。我不再写长指令,现在固定四步,顺序不乱。

第一步是把范围收窄。我会明确说改哪个目录、参考哪个文件、不要动哪些东西。参考文件这一条最有用——说”照 settings 页的卡片样式”比说”用 Tailwind 做得好看一点”有效十倍,因为它给的是一个具体的样本,而不是一句形容词。

第二步是让它先给方案,不写代码。我要看它认为该改哪几个文件、按什么顺序。这一步经常能拦住灾难:它列出来的文件里如果有一个我根本没打算动的,我就知道它理解偏了。

第三步是分批生成。一次只让它做一个文件或者一个函数。它很喜欢一次交出五个文件,看起来很爽,但我审不动。审不动的代码等于没有审。

第四步是把真实结果喂回去。不是”报错了修一下”,而是把完整的报错、我做了什么、期望是什么一起给。差别很大:前者会让它开始猜,后者能让它定位。

我很少因为模型不够聪明而卡住,多数时候是我给的信息不够它判断。

这套循环很慢。一个下午能推完的东西,可能只有三四个文件。但它有个好处:每一步我都能停下来,知道我停在哪里。

规则文件是活文档

我一开始把规则文件当成一次性配置:把技术栈、命名规范、目录结构写进去,之后就不管了。

结果是它慢慢失效。不是文件变了,是项目变了。新加了一层缓存,规则里没写;换了一种错误处理方式,规则里还是旧的;某个目录的职责早就扩大了,说明还停留在半年前。模型照着过期的规则做事,做出来的东西不能怪它。

我现在的用法是,遇到一次具体的错误,就往里加一条。它用了 console.log,我就加一条”日志统一走项目里的 logger”;它擅自改了环境变量,我就加一条”环境配置只允许在指定文件里改”。规则不是从架构文档里抄来的,是从踩过的坑里长出来的。

长度上我有个硬要求:短。超过两三百行,我自己都不想读完,模型更不会认真对待。写不进去的规则,通常说明项目本身有问题,或者这条规则根本不该由文档来保证。

有一点我坚持不让模型做:修改规则文件。它可以在对话里建议,但不能自己写进去。规则是约束它的东西,让被约束者改约束,这个循环没有尽头。

规则变多之后,代码库也跟着变了,这是我没有预料到的收获。

为了让它少出错,我把文件拆得更细了。一个文件里塞三个不相关的功能,它改一个的时候很容易碰到另外两个;拆开之后,每次给它一个文件,出错的范围就可控。

我也开始讨厌隐式约定。以前觉得”这段逻辑大家都知道在哪儿”,现在必须写成一个明确的函数,否则它会在别处重新实现一遍。以前能忍的重复,现在不能忍,因为重复的地方它只会改一处。

副作用是代码变得有点啰嗦。很多地方多了一层封装,几个小文件代替了一个大文件,一些本来靠约定跑通的东西现在要显式声明。从纯粹的美学看,这不算更好看。但我用它换来了一个很实际的东西:我可以把一块代码单独交给模型,而不用把整个项目塞给它。

这笔账我算得过来。代码是给两个人看的,一个是我,一个是模型。让后者的阅读成本降下来,前者的维护成本通常也会跟着降。这话我以前不信,现在有点信了。

失控时先回滚,不要谈判

我踩过最贵的一个坑,是试图和出错的模型讲道理。

那次它虚构了一个不存在的 API,我贴了报错,它道歉,换了个写法,又虚构了另一个。三轮之后,代码里多了两个不存在的依赖和一个被删掉的状态管理。我在它制造的错误上继续对话,等于让它在一个错误的世界里做推理。

现在的做法很机械:复杂改动之前先提交。出问题就回退到上一个能跑的版本,然后重新开一轮对话,从干净的状态开始。回退三十秒,谈判半小时,这个账很好算。

如果非要修,我有一条自己的规矩:连续两次没修好,就停止让它改代码。转而让它加日志,把运行时的真实数据打出来。它看不到运行时,只能靠猜;日志能把它从猜里拉出来。这个办法笨,但成功率比继续对话高得多。

回滚做得多了,我对边界的判断也在变。我用得越久,交给它的东西反而越少。

适合交出去的是那种”结构清楚、边界明确、错了能看出来”的活:搬一个组件、补一个空状态、把一段重复逻辑抽成函数、写一个我懒得写的脚本。这些事它做得比我快,而且我能在 diff 里一眼看出对不对。

不适合的是三类。一类是涉及状态流转的核心逻辑,改错之后不会立刻报错,而是在某个边缘场景里静默出错。一类是需要判断”该不该做”的事,它永远会给一个看起来合理的方案,因为它不会拒绝。还有一类是我自己都没想清楚的地方——我没想清楚,它只会把模糊放大成一个更庞大的模糊。

我也不再相信”不用看代码”这种说法。我可以不写,但不能不看。不看的那部分,会在三个月后的某个深夜回来找我。到那时候,我面对的是一堆我不认识的代码,和一个记不清当初为什么这么写的人。

有一种情况我会例外:一次性的脚本。跑完就丢,不进仓库,不留维护责任。这种活我完全不看,让它随便写,能跑就行。分清”会留下来的代码”和”用完就丢的代码”,比在两者之间找一个折中的审查强度要实际得多。

MCP 这类东西我一直在跟。它解决的是一个真实的问题:模型看不到我的数据库、我的日志、我的工单,只能等我粘贴。接上之后,它拿到的是一手信息,幻觉会少很多。

但它解决的是获取,不是判断。它能看到表结构,不代表它知道哪张表在生产上不能写;它能读工单,不代表它理解那条工单背后是谁在施压。接口越顺,我越容易忘记这一点。

我也不确定这套工作方式能维持多久。工具在变,模型的自主性在变,今天需要我切四步的事情,明年可能一句话就够。但有一件事我猜不会变:写代码的人要为结果负责。不管代码是谁敲的,出问题的时候,站在那儿解释的人是我。

关于本文

由 Lu's Blog 撰写,采用 CC BY-NC 4.0 许可协议。

#AI 编程 #上下文工程 #工程习惯

评论