实战报告:AI Coding 已经能做交付了,但前提苛刻

首先,为什么我想认真做这次AI Coding验证?现在大家对AI Coding已经不陌生了,代码补全、写工具函数、起一个页面、做一个Demo,这些事情它大多都已经能做,而且很多时候做得还不错。所以如果只是讨论“AI会不会写代码”,这个问题其实已经没有太多讨论价值了。至少在局部编码这件事上,它的可用性基本已经被......

实战报告:AI Coding 已经能做交付了,但前提苛刻

本文来自微信公众号: 叶小钗 ,作者:叶小钗,原文标题:《【万字】实战报告:AI Coding 已经能做交付了,但前提苛刻》

首先,为什么我想认真做这次AI Coding验证?

现在大家对AI Coding已经不陌生了,代码补全、写工具函数、起一个页面、做一个Demo,这些事情它大多都已经能做,而且很多时候做得还不错。

所以如果只是讨论“AI会不会写代码”,这个问题其实已经没有太多讨论价值了。至少在局部编码这件事上,它的可用性基本已经被反复验证过了。

但我一直更想确认的是另一件事:

如果把问题往前和往后都拉长一点,不再只是让它补某一段代码,而是让它真正进入一段完整的交付过程。

从理解需求开始,到实现功能,再到验证和收口,那它现在到底能做到什么程度?

换句话说,我这次真正想验证的,不是AI能不能辅助写代码,而是它有没有可能从局部参与走到完整参与交付。

这个问题,看起来只差一点,实际差得很远。

局部参与,验证的是它的编码能力;完整参与交付,验证的其实是它在一整段任务里的稳定性:AI Coding能不能理解边界,能不能按要求推进,能不能在约束下把事情做完,或者只是产出几段看起来还行的代码。

为了把这个问题看得更清楚一点,我没有直接拿现有业务项目来试。

一方面,业务项目本身不适合公开展示;另一方面,我也不希望把业务复杂度、历史包袱和真实约束全部搅在一起,最后很难判断,到底是AI的能力到了边界,还是测试条件本身不够干净。

所以这次我专门搭了一个脱敏的演示项目,并刻意把验证拆成了三个场景:

第一个场景,是从0到1,直接让AI按要求搭一个新项目,并完成一个最小功能;

第二个场景,是在已经搭起来的项目里继续加需求,模拟更接近日常开发的迭代过程;

第三个场景,则是模拟一个文档不全、约束不清的老项目,先补规范,再继续做功能;

这三个场景连起来,基本就是我想验证的那条完整问题链:AI今天到底能不能真正参与一段完整交付,而不只是把局部编码做得更快?

如果先给结论,我现在的判断是:

AI Coding已经不只是适合写Demo、补函数、起页面了。

在约束清楚、边界明确、验收方式可执行的前提下,它已经可以参与相当一部分真实交付工作

但这个结论不是无条件成立的。从这次实践看,它更适合三类项目场景:

新项目:适合先搭基线,再落最小功能

结构相对清楚的已有项目:适合在边界明确的前提下继续迭代

约束缺失但仍可运行的老项目:适合先补最小规范,再继续往下做

如果换一种更直接的说法,我现在会把AI编程的可用范围理解成这样:

做Demo、局部编码、小功能实现:已经比较成熟

做新项目初始化和最小功能闭环:可以承担

做已有项目中的小到中等规模迭代:可以承担,但前提是边界要先框清楚

做老项目直接功能开发:不建议直接开始,更适合先补规范、再做功能

做高不确定性、强业务耦合、约束大量依赖隐性经验的任务:仍然不够稳

也就是说,它已经可以参与交付,但更像一个需要被定义清楚、被约束好、被验证到位的协作对象,而不是一个把需求丢过去就能自动收口的全能开发者。

下面这三个场景,就是我这次为了验证这个判断,专门拆出来的一条完整实验链路。

unsetunset输入>模型unsetunset

——真正影响结果的,往往不是模型,而是输入

刚开始做这次实践的时候,我的想法其实也比较直接:既然是想验证AI能不能承担更多工作,那最自然的方式,就是把任务交给它,让它尽量往下做,我再回来检查、修正和收尾。

说得简单一点,就是先看看它到底能接住多少。这种用法在很多小任务上其实没有问题,甚至会让人觉得很顺。

尤其是那些上下文比较简单、边界也比较清楚的事情,比如补一个函数、写一个局部页面、改一个相对独立的小功能,AI往往能很快给出一个还不错的结果。

也正因为如此,最开始的时候,很容易形成一种判断:好像只要需求描述得差不多,后面的事情就可以交给它了。

但等任务稍微完整一点,问题就开始慢慢冒出来:

有时候是功能虽然做出来了,但一些关键细节和预期并不一致;

有时候是代码能跑,但实现方式和项目原本的组织方式并不统一;

还有一些情况是,你以为理所当然的前提,其实根本没有写出来,于是AI会自己去补全,而它补出来的东西未必就是你真正想要的;

后来我慢慢意识到,这里面最麻烦的地方,往往并不是“它把代码写错了”,而是很多偏差其实在写代码之前就已经出现了。更准确地说,很多问题发生在“理解任务”这一步。

当任务只是局部编码时,这个问题还没那么明显。因为上下文很短,目标也比较集中,AI就算理解得不够完整,影响范围通常也有限。

但一旦任务开始变长,开始涉及需求边界、功能约束、验证标准、回退方式这些东西,输入本身如果不完整,后面就很难稳定。

也就是从这个时候开始,我越来越确定:决定结果的关键,不只是模型,而是我们到底给了它什么样的输入。

如果输入只是一个方向性的描述,那AI很多时候做的,其实不是执行,而是在猜。它一边写代码,一边补我没有明确说出来的前提。

而问题恰恰在这里:那些没被写出来的前提,很多时候才是真正决定结果稳不稳的东西。

所以后来我调整的重点,不再是“怎么把提示词写得更花”,而是另一件更基础的事情:怎么把任务定义得更完整一点。

我开始不满足于只给它一个需求描述,而是尽量把目标、边界、规则、验收方式这些内容提前写清楚,让它面对的不是一个模糊任务,而是一份相对明确的执行说明。

我后来会把模糊描述和结构化任务定义放在一起看,差异会非常直观:

左边这种描述,人能靠经验补全很多前提;但放到AI面前,往往就会变成大量默认假设。

右边这种写法看起来更“啰嗦”,但真正能明显提升稳定性的,通常恰恰是这些被显式写出来的边界、规则和验收条件。

做完这次实践之后,我越来越确定,真正影响结果的,往往不是模型本身,而是你给它的输入,到底是不是一份足够清楚的任务定义。

unsetunset去配合模型unsetunset

——如果想让AI参与交付,需求就不能只写给人看

以前写需求,更多是为了让人看懂。业务目标说清楚,流程大致讲明白,关键页面和规则列出来,通常也就可以往下推进了。

很多默认前提不一定会专门写出来,因为团队里的人大多知道背景,也知道哪些地方该怎么处理。即便文档里没写得特别细,很多信息也能在协作过程中慢慢补齐。

但这次实践让我越来越明确地感受到,AI面对的不是这样的协作环境。

人和人协作时,很多东西是可以靠经验、上下文和来回沟通补上的。但AI不一样。它能依赖的,基本就是你明确给到它的那部分信息

这就意味着,以前那些“默认大家都知道”的内容,放到AI面前,其实很多都应该重新写出来。否则它只能根据现有输入去猜,而一旦进入猜的阶段,结果就会变得不稳定。

所以后来我开始要求自己,把需求写得更具体一些:

不只是写“这次要做什么”,还要写“哪些事情这次不做”;

不只是写功能本身,还要写规则和边界;

不只是写页面和交互,还要写最后怎么验证算完成;

如果有上线风险,还要提前考虑有没有回退空间;

说得更直接一点,我后来更希望需求文档像一份能直接执行的说明,而不只是一个方向性的描述。

比如这次实践里,其中一个很小的功能,是新增一个用户表单。

如果只是写一句“做一个新增用户表单页面”,那这个任务其实非常松。字段怎么设计、校验做到什么程度、有没有开关控制、要不要补测试、做到什么算完成,这些全都没有边界,AI只能自己去补。

但如果把这些前提往前补一点,事情就会清楚很多:

在当前项目中实现一个“新增用户表单”功能,并通过feature flag控制新表单是否启用。

##Feature Flag要求

新增`newUserForm`开关,并满足:

-关闭时展示占位内容或旧版占位区域

feature flag要显式、易定位。

1.feature flag开关对应的渲染分支

如果实现方式合理,可补更多,但不要为了数量堆测试。

3.feature flag生效

4.`pnpm lint`通过

5.`pnpmtest`通过

6.`pnpm build`通过

PS:其实从这里大家就可以直观感受到了,代码不再是必须,自然语言就是代码

这也是我这次实践里最深的一个感受之一:

如果目标只是让AI帮忙写点代码,那以前那种偏描述性的需求文档也许还能凑合;但如果目标是让它真正参与交付,需求本身就不能只写给人看。

需求一旦写清楚,AI不一定会突然变得更“聪明”,但它的表现通常会明显稳定很多。

unsetunset从0到1unsetunset

——拿一个新项目做验证

第一个场景,我故意从一个最“空”的状态开始。

原因很简单。如果AI连从0到1都接不住,后面讨论它怎么参与已有项目的迭代,其实意义并不大。因为那种情况下,它更多还是在已有上下文里“顺着写”,而不是在真正承担一段完整的起步工作。

而且从0到1还有一个好处:很多问题会暴露得更直接。

没有现成目录,没有既有约定,也没有默认上下文。这种情况下,如果要把一个项目真正搭起来,AI面对的就不只是“写代码”本身,而是要先处理一层更基础的东西:项目结构怎么组织、脚本怎么约定、验证方式怎么建立、后面还能不能继续迭代。

所以这个场景里,我一开始并没有把目标设成“让它快速生成一个页面”,而是先给自己定了一个更明确的判断标准:这个项目至少要像一个能继续发展的项目,而不只是一个跑起来的壳子。

也就是说,它要满足的不只是“能启动”,还包括:

后面还能继续往里加需求

整个项目要适合拿来做后续两个场景的继续验证

从这个角度看,我要验证的其实不是“AI会不会初始化一个Vite项目”,而是:在没有现成上下文的情况下,它能不能按照要求,把项目先搭到一个可继续交付的状态。

所以这个阶段,我最先写的不是业务功能,而是一份初始化规格:

基于以下技术栈初始化一个现代化前端项目:

-TypeScript

-React Testing Library

-Commitlint

项目初始化后,应具备继续承接后续功能迭代的能力。

2.建立清晰的基础目录结构

4.配置Vitest与React Testing Library

6.配置Commitlint

7.补齐基础scripts

8.提供最小可用的README

9.提供至少1条可运行的样例测试

-`src/app/`

-`src/pages/`

-`src/components/`

-`src/lib/`

-`src/tests/`

目录不要过度设计,也不要创建无用目录。

-`pnpm lint`

-`pnpm format`

-`pnpmtest`

-`pnpm build`

-`pnpm dev`

README至少说明:

-如何执行lint/test/build

1.`pnpm lint`可执行

2.`pnpmtest`可执行

3.`pnpm build`可执行

这份规格里没有太多复杂内容,主要就是把一些项目级别的约束先说清楚,比如技术栈、目录结构、脚本要求、测试基线,以及README至少需要说明哪些东西。

换句话说,我希望AI面对的不是一句“帮我起个项目”,而是一份更明确的初始化要求。

这个动作看起来有点“慢”,但后来回头看,它其实非常关键。

因为从0到1的场景里,最容易出的问题往往不是代码写错,而是方向一开始就偏了。

比如依赖引得太多、目录切得太散、脚本不统一、测试环境缺失,或者为了图快临时拼了一套不太能继续维护的结构。短期看它们都不是大问题,但如果一开始没有收住,后面每加一个需求,都会不断放大这些问题。

所以在真正让它动手之前,我没有直接要求它开始改代码,而是先让它把计划说出来:

它觉得要新增或修改哪些文件

这个阶段,我关注的重点不是“它写得快不快”,而是“它理解得对不对”。

因为很多偏差,其实在真正写代码之前就已经出现了。如果方向一开始偏了,后面代码写得再认真,也只是沿着一个不够理想的方向越走越远。

等这些都看起来没问题了,后面的实现反而是顺下来的。到了这个阶段,我真正关心的,也不再是它一共写了多少代码,而是最后这些东西能不能顺利通过验证。

Checked 6 filesin2ms.No fixes applied.

Test Files 1 passed(1)

Tests 2 passed(2)

$pnpm build

✓builtin397ms

这个场景做到这里,其实只能证明一半:AI已经能把项目基线搭起来,并把最基本的工程约束先立住。

但如果只停在这里,还不能完全说明它已经具备继续交付功能的能力。

所以项目基线搭起来之后,我没有马上进入第二个场景,而是先在这个新项目里落了一个最小功能:新增用户表单。

这一步很重要,因为如果只做到bootstrap,其实还只能说明AI能把项目框架搭起来,还不能完全说明它已经具备继续交付功能的能力。

只有当它能在这个基线上继续完成一个具体功能,并把校验、开关、测试这些细节一起接住时,场景一才算真正闭环。

所以我给它的第一个功能,不是很复杂,但故意保留了几个很“像真实项目”的要素:

feature flag

新增一个用户表单,包含以下字段:

-提交可使用mock逻辑,不接后端

-提交成功后有明确成功提示

##Feature Flag要求

新增`newUserForm`开关,并满足:

-关闭时展示占位内容或旧版占位区域

feature flag要显式、易定位。

1.feature flag开关对应的渲染分支

如果实现方式合理,可补更多,但不要为了数量堆测试。

3.feature flag生效

4.`pnpm lint`通过

5.`pnpmtest`通过

6.`pnpm build`通过

从实际改动文件看,这次实现也基本控制在了一个很小的范围内:页面层负责接住功能入口,组件层负责表单本身,lib层负责规则和开关,测试则分别覆盖页面分支和表单行为。

其中一个我刻意要求保留的点,是feature flag。因为我不想验证的只是“页面能不能做出来”,而是“这个能力是否具备最小回退空间”。

//featureFlags.ts

exportconst featureFlags={

newUserForm:false,

校验逻辑我也要求尽量集中,而不是散落在组件内部。这样一方面更贴近真实项目的写法,另一方面也更容易测试和复查。

//validation.ts核心代码

exportconst roleOptions=['admin','editor','viewer']as const

exportfunctionvalidateUserFormValues(values:UserFormValues):UserFormErrors{

const nameRequiredError=validateRequired(values.name,'姓名为必填项')

const emailRequiredError=validateRequired(values.email,'邮箱为必填项')

const roleRequiredError=validateRequired(values.role,'角色为必填项')

const nameError=

nameRequiredError??

validateLengthRange(values.name,2,20,'姓名长度需在2到20个字符之间')

const emailError=

emailRequiredError??validateEmailFormat(values.email,'邮箱格式不正确')

const roleError=

roleRequiredError??

validateOneOf(values.role,roleOptions,'角色不合法')

name:nameError,

email:emailError,

role:roleError,

测试这一步,我没有要求它堆很多数量,而是要求它把关键行为覆盖清楚。

页面层的测试主要验证feature flag开关前后的渲染分支:开关关闭时显示占位内容,开启时显示新表单。表单层的测试则覆盖了必填校验、长度和邮箱格式校验、非法角色值,以及最终提交成功提示。

it('renders placeholder when newUserForm flag is off',()=>{})

it('renders new user form when newUserForm flag is on',()=>{})

it('shows required errors when submitting empty form',()=>{})

it('shows success message after valid submission',()=>{})

做到这里,其实已经足以说明一件事:只要规格写得足够清楚,AI接住的就不只是页面本身,还包括规则、校验和测试这些过去往往需要人工补齐的部分。

但我还想再往前推一步。

项目基线和第一个最小功能都跑通之后,我没有只停留在单测和构建通过,而是又加了一层自动化验收:要求Claude Code借助Playwright CLI,从用户视角把关键流程再跑一遍。

这一步对我来说很重要。因为如果只看到代码、单测和构建结果,虽然已经能证明功能基本成立,但它仍然更偏“工程内部视角”。

而我这次想验证的是,AI能不能更完整地参与交付,那它就不应该只负责把代码写出来,还应该尽可能把“功能到底有没有真的跑通”这件事一起验证掉。

所以我给Playwright的范围控制得很小,只覆盖关键路径:

空表单提交时是否能展示必填校验错误

非法输入时是否能展示对应错误

合法输入后是否能出现成功提示

这一步做完后,我对场景一的判断才真正完整起来:

从0到1,AI不只是能把项目基线搭起来,也不只是能把第一个功能写出来,而是已经能够借助自动化工具,把这个功能从实现一路推进到最基本的验收。

如果一开始只是追求“先跑起来”,那很容易得到一个表面很快、后面却越来越难接着做的项目。反过来,如果先把项目级别的规则说清楚,再让AI往下执行,并在最后补上一层真正从用户视角出发的自动化验收,整个过程会稳很多。

unsetunset难点:加需求unsetunset

——项目搭起来之后,真正难的是继续往里加需求

第一个场景做完之后,项目至少已经站住了。基线有了,最小功能也有了,单测、构建和一轮自动化验收也都跑通了。

做到这里,已经足以说明AI不只是会“起一个项目”,也不只是会“写一个页面”,而是已经能在明确约束下,把一段从初始化到最小验收的链路接起来。

但这还不够。因为真实开发里,大多数时候并不是从0开始,而是在一个已经存在的项目里不断往下加需求、补功能、做调整。

这也是第二个场景更接近日常开发的地方。

项目已经有了基础结构,页面也已经起来了,测试和构建也都在。这个时候继续往里加功能,表面上看比从0到1更简单,但实际上,真正麻烦的地方反而开始出现了。

因为在已有项目里,问题的重点往往不再是“能不能写出来”,而是“会不会把原来的东西带乱”。

这类场景里最容易出现的问题,大概有三种。

改动范围失控。本来只是一个不大的需求,但做着做着会牵出越来越多修改;

顺手碰了不该碰的地方,为了实现这次需求,把原有结构也一起改了;

功能虽然加进去了,但实现方式和项目原本的组织方式并不一致,表面上完成了,实际上留下了新的不协调。

所以到了这个阶段,我自己的使用方式会有一个明显变化。

在场景一里,我更关心的是“它能不能先把项目立起来”;但到了场景二,我先问的就不再是“怎么实现”,而是“会改哪些地方”:

在现有项目基础上新增一个用户列表页面,用于模拟已有项目中的功能迭代。

1.展示mock用户列表

3.支持按`name`或`email`过滤

这个任务的重点不只是实现列表,而是控制改动边界。

开始实现前,需要先明确:

不要顺手调整无关结构。

也就是说,我不会让它一上来就直接给我代码,而是会先要求它把边界说清楚:

这次会影响哪些页面或模块

哪些文件需要新增,哪些文件需要修改

这个动作非常重要。因为在已有项目里,很多问题不是功能本身做错了,而是本来一个很小的需求,最后却牵扯出一片不必要的改动。范围一旦失控,后面的验证成本就会明显上升,整个过程也更难收口。

换句话说,在已有项目里,我更关心它打算动哪里,而不是它打算怎么写。

这背后其实是一个很现实的考虑:如果边界先清楚了,后面的过程通常都会稳很多。你知道它准备碰什么地方,也知道哪些地方最好不要动,那么后面的验证、回看和继续迭代,都会容易很多。

反过来,如果一开始没有把边界框出来,AI很容易凭它自己的理解去“顺手优化”一些东西。单看每一处修改,可能都不算离谱,但整体上很容易让结构慢慢变形。尤其是在一个已经有既有做法的项目里,这种“顺手改一点”的累计代价会非常高。

这次我在已有项目上新增的是一个用户列表页,功能本身并不复杂,主要包括:

这个功能刻意选得比较克制。因为我的目标不是用一个复杂需求去证明AI有多强,而是想验证:在一个已经成型的项目里,它能不能在边界明确的前提下,继续把事情做稳。

从最终落地结果看,这次改动基本也保持在了一个比较小的范围内:

App.tsx只做了最小的页面切换入口,用useState在“创建用户”和“用户列表”两个页面之间切换;

UserListPage.tsx作为独立页面承接列表、搜索框和空态展示;

mockData.ts和userFilter.ts则把数据和过滤逻辑放回到了lib层,没有直接揉进页面组件里。

这一步让我比较满意的一点,不是它把列表做出来了,而是它没有为了一个小需求去撬动原有结构。

它没有顺手引入路由库,也没有借机扩展分页、排序、编辑删除这些spec里根本没要求的内容。整个改动看起来更像一次正常的功能迭代,而不是一次失控的“顺便升级”。

从页面代码本身看,这种边界感也比较明显:App.tsx只负责页面切换和渲染,不承载列表业务逻辑。

例如这次实际就是用一个很轻的本地状态做页面切换:

typePage='create'|'list'

const[currentPage,setCurrentPage]=useState('create')

{currentPage==='create'?:}

而列表数据和过滤规则也没有直接写进页面里,而是都放在了lib层。数据本身只是一个很轻的mock集合,但结构已经是清楚的:

exportinterface User{

name:string

email:string

role:string

exportconst mockUsers:User[]=[

{id:'1',name:'张三',email:'zhangsan@example.com',role:'admin'},

{id:'2',name:'李四',email:'lisi@example.com',role:'user'},

{id:'3',name:'王五',email:'wangwu@example.com',role:'admin'},

{id:'4',name:'赵六',email:'zhaoliu@example.com',role:'viewer'},

对应的过滤逻辑也被单独抽成了纯函数:

//userFilter.ts

exportfunctionfilterUsers(users:User[],query:string):User[]{

if(!query.trim()){

returnusers

const lowerQuery=query.toLowerCase()

returnusers.filter(

(user)=>

user.name.toLowerCase().includes(lowerQuery)||

user.email.toLowerCase().includes(lowerQuery),

这一点很关键。因为它意味着这次迭代不是简单把功能“糊上去”,而是尽量沿着已有结构,把页面职责、数据职责和规则职责分开。这种分层看起来并不复杂,但它会直接影响后面这个项目还能不能继续往下演化。

测试这一步,我也仍然保持了和前面一样的思路:不追求数量,而是优先覆盖关键行为。

这一轮最重要的测试点其实很直接:

mock用户列表是否能正常渲染

按姓名搜索时过滤逻辑是否生效

按邮箱搜索时过滤逻辑是否生效

当没有匹配结果时,空态是否会正确出现

//UserListPage.test.tsx测试点摘要

it('renders user list with mock data',()=>{})

it('filters users by name',()=>{})

it('filters users by email',()=>{})

it('shows empty state when no results',()=>{})

另外,这次我还额外保留了一层更细的验证:把过滤逻辑单独抽出来后,又给它补了独立测试。这样一来,页面层验证的是“用户能看到什么”,规则层验证的是“过滤逻辑本身对不对”,两层职责会更清楚。

//userFilter.test.ts测试点摘要

it('returns all users when query is empty',()=>{})

it('filters by name',()=>{})

it('filters by email',()=>{})

it('returns empty array when no match',()=>{})

it('is case insensitive',()=>{})

完成后,我依然会要求它输出一份简短的交付说明,把这次到底改了什么、风险点在哪里、验证方式是什么,统一交代清楚。

从这次AI给出的总结里,信息其实已经比较完整了:新增了mock数据和过滤工具函数,新增了用户列表页和测试,App.tsx只做了最小页面切换,最终pnpm lint、pnpm test和pnpm build都通过了。

这一点看起来只是“补一段说明”,但它其实很关键。

因为在已有项目里,真正接近交付的,不只是代码本身,还包括你能不能把这次改动讲清楚。

改了哪些地方、影响范围多大、验证到什么程度,这些信息如果不能被稳定产出,就很难说它已经真的进入了交付链路。

所以第二个场景做下来,我最大的感受是:在已有项目里,AI的可用性并不只取决于它写得快不快。更重要的是,你有没有先把边界框出来。

边界一旦清楚,验证方式也提前明确,它在存量迭代里的表现会稳定很多。某种意义上说,AI真正开始变得“能用”,不是因为它更会写了,而是因为它开始能在边界里做事。

这也是我对场景二最核心的判断:从0到1,关键是先立约束;而在已有项目里继续往下走,关键则是先控边界。

unsetunset最后问题:老项目unsetunset

——老项目最先要补的,不是功能,而是约束

如果只做前两个场景,这次验证其实还不完整。

因为新项目也好,相对干净的存量项目也好,本身都还带着一种“理想环境”的前提:结构至少是清楚的,约束至少是写得出来的,验证方式至少能补得上。

但真实世界里,很多项目其实并不是这样。

真正更常见的情况是:项目已经跑了很久,功能不少,代码也不一定不能用,但很多最基础的东西其实是缺的。文档不全,脚本不统一,测试没有形成基线,目录结构也未必一致,很多规则都存在于人的脑子里,而不是写在项目里。

所以第三个场景我一定要单独测。因为对很多团队来说,这才是更接近现实的问题。

而且这种项目真正麻烦的地方,往往并不是“代码旧”,而是规则没有被写出来。

什么地方能改,什么地方最好别碰;平时怎么跑验证,做到什么程度算能交付;目录是怎么分层的,哪些约束是明确存在的,哪些只是过去协作中慢慢形成的默契。

这些事情,人和人协作的时候还可以靠经验补齐。但AI看不到这些隐性约定。它能看到的,只有代码表面和你显式给它的内容。

这也是为什么我觉得,老项目如果直接让AI上来就加功能,体验通常不会太好。

不是因为它一定做不出来,而是因为在约束缺失的情况下,它只能根据表面结构去猜。一旦进入“猜”的阶段,结果就会忽高忽低,很难稳定。

所以在这个场景里,我没有让AI一开始就做功能。我先让它做的是“补地基”。

针对一个“可运行但约束不完整”的前端项目,先补齐最小工程规范,再为后续功能迭代建立稳定基础。

在不大规模重构的前提下,优先补齐以下内容:

2.统一scripts

3.lint/test/build基线

-以“先建立最小秩序”为目标

本次完成后,项目至少应满足:

-至少存在1条最小测试样例

这里说的补地基,不是大规模重构,也不是借机把历史项目重新整理一遍。那样事情会立刻变大,也不符合这次验证的目标。我更关注的是另一件事:能不能先把最低限度的秩序补起来。

这次我故意把项目退化到了一个很常见的状态:README只剩下标题和一句非常泛的描述,虽然项目本身还能跑,但一个新接手的人几乎拿不到任何可执行信息。不知道怎么启动,不知道怎么验证,也不知道目录结构该怎么理解。

而在真正开始改动之前,我先让Claude Code做了一次问题盘点。它先总结当前项目的主要问题,再区分这次应该优先解决什么、明确不解决什么,最后给出最小变更策略。

这个顺序很重要,因为到了老项目场景里,最容易失控的地方,就是一上来就借题发挥,把“补规范”做成“顺手重构”。

这次它最终收敛下来的改动其实非常少,只动了3个文件:

package.json

.husky/pre-commit

其中最核心的变化,是把README从一个几乎没有信息的状态,补成了一份最小可执行说明。至少把下面这些内容补回来了:

这一步看起来不“炫”,但价值非常大。因为从这一刻开始,这个项目不再只是“能跑”,而是开始变成一个别人也能接手、AI也能稳定理解的项目。

除了README,这次还顺手补了一层很轻但很有用的门禁:把pre-commit从只跑pnpm test,补成了同时执行pnpm lint和pnpm test。这不是在做复杂治理,而是在补最基础的提交前约束,让最小工程基线真正形成闭环。

如果把这次变化抽象成一张表,大概就是这样:

更重要的是,这次我还刻意控制住了“顺手多做一点”的冲动。比如盘点里其实还发现了eslint残留依赖这类可以继续清理的问题,但我最后没有继续扩下去。

原因很简单:这一轮的目标不是把项目彻底整理干净,而是先把最低限度的秩序补起来。如果这个边界守不住,场景三就会从“补约束”滑向“借机治理一切”。

完成后,我还是用最直接的方式做了一次验证:

Checked 11 filesin3ms.No fixes applied.

Test Files 2 passed(2)

Tests6 passed(6)

$pnpm build

✓builtin401ms

到这里,我对场景三的判断就很明确了:老项目并不是不能让AI参与。但如果约束长期缺失,直接做功能的体验通常会比较差。

更合理的顺序,往往是先补最小规范,再逐步把后面的功能迭代交给它。换句话说,在老项目里,最先要补的,不是功能,而是约束。

unsetunsetAI Coding方法论unsetunset

——做完这三个场景之后,我真正沉淀下来的东西

把这三个场景都跑完之后,我回头再看,会觉得真正值得留下来的,并不是某一个页面或者某一段实现。

更有价值的,是我慢慢形成了一些更稳定的做法。

这些做法看起来都不算复杂,甚至很多都不新鲜。但它们一旦固定下来,确实会明显影响整个过程的稳定性。

也就是从这个意义上说,这次实践留下来的,不只是代码,而是几套后面还可以继续复用的工作方式。

我最后沉淀下来的几条固定做法,大致是这几项:

先把需求写清楚,再让AI动手

新能力尽量保留回退空间

老项目先补规范,再做功能

对界面功能尽量补一层接近真实使用的自动化验收

1.先把需求写清楚,再让AI动手

这听起来像一句正确的废话,但真正做起来,其实很容易偷懒。尤其是当你已经大概知道自己想要什么时,会下意识觉得有些前提不用写,边做边补就行。

但这次实践里,我越来越确定,如果需求本身还是散的,后面所有结果都会跟着一起变得不稳定。

所以我现在会更愿意先花一点时间,把目标、边界、规则和验收写清楚。这一步看起来慢,实际上是在减少后面来回修正、反复返工的成本。

2.先看计划,再看实现。

现在我基本不会一上来就盯着代码。我更先看它准备怎么做,会影响哪些文件,边界在哪里,依赖会不会加多,验证方式是否合理。

因为一旦方向偏了,后面代码写得再认真,也只是沿着一个不够理想的方向越走越远。很多时候,真正省时间的,不是更快动手,而是更早发现方向问题。

3.尽量让改动保持在较小范围内,并且尽快验证。

无论是新项目还是已有项目,我都会越来越在意“这一步是不是太大了”。任务一旦被拆小,很多问题都会更早暴露,也更容易收住。

相反,如果一次性交给AI一个过长的任务链,表面上看省了中间步骤,实际上往往只是把问题延后了。

4.尽量给新能力保留回退空间。

这次实践里,我对这一点的感受也很明显。尤其在AI参与度比较高的情况下,能不能快速回退,其实会直接影响你用它时的安全感。

像feature flag、fallback这种做法,不只是上线手段,本质上也是一种开发过程中的风险缓冲。它让你在推进的时候,有更多收手和调整的空间。

5.遇到老项目时,先看约束是不是存在,再决定要不要继续做功能。

以前很多时候,看到一个项目结构不太清楚,也会默认“先把需求做出来再说”。

但这次实践之后,我会更倾向于先停一下,看看这个项目是不是连最小规范都没有。

如果没有,那就先把这些基础设施补起来。因为很多所谓的“AI不稳定”,最后追根结底并不是模型的问题,而是输入环境本身就很模糊。

6.对界面功能尽量补一层接近真实使用的自动化验收。

单测、lint和build当然重要,但它们更多还是工程内部视角。

至少对表单、列表这类前端功能,我现在会更倾向于再补一层从用户视角出发的自动化验收。这样验证结果会更接近真实交付,而不只是停留在代码层面。

unsetunset结语unsetunset

这次实践做完之后,我对AI Coding的理解,和一开始相比,确实有了一些变化。

以前更容易把它看成一个生成代码的工具。你给它一个任务,它返回一段实现;你给它一段报错,它再继续帮你修。整个过程更多是在围绕“代码本身”打转。

但现在我会更倾向于把它看成一个需要被约束、被验证、被管理的协作对象。

这并不是说它变得更复杂了,而是因为当你开始希望它完整参与交付时,关注点自然就会从“它会不会写”转移到“它能不能稳定地按要求把事情做完”。

从这个角度看,我现在其实没那么在意“它到底有多聪明”。我更在意的是,在边界清楚、约束明确、验收可执行的前提下,它能不能把一段任务稳定地接住。

因为对真实开发来说,可预期往往比偶尔惊艳更重要。

至少在我这次实践覆盖的几个场景里,AI已经不只是一个局部编码工具了。在明确约束、明确边界、明确验收的前提下,它确实已经可以承担相当一部分完整实现工作。

但这个前提始终都在:问题得先被定义清楚。

如果问题本身还是散的,很多约束都停留在默认状态,需求也只是一个方向性的描述,那AI参与进来,大概率只是把原本已经模糊的东西更快地放大而已。

所以如果让我用一句话来总结这次实践,我大概会这样说:

AI能不能真正参与交付,最后比拼的,往往不是生成速度,而是问题定义的质量。

至少对我来说,这次实践最大的收获,不是少写了多少代码,而是我开始更认真地对待“把事情说明白”这件事。

THE END
免责声明:本文版权归原作者所有;旨在传递信息,不代表鲸媒智集的观点和立场。
相关阅读
  • 350位CEO:有关AI的三重真相和投资逻辑

    350位CEO:有关AI的三重真相和投资逻辑

    分钟咨询公司Teneo调查了350位年营收超过十亿美元的上市公司CEO,针对有关于AI的支出(CapEx)等关键问题做了调研,得出了一份非常具备参考性,同时也在某些方面与大众认知出现反差的信息报告。核心信息及数据如下:68%的CEO计划在2026年…

    2025年12月19日 14:43
  • MIT仅录取2人,斯坦福、密歇根狂撒20枚录取...今年的美国早申, 中国学生卷到新高度

    MIT仅录取2人,斯坦福、密歇根狂撒20枚录取...今年的美国早申, 中国学生卷到新高度

    早申放榜只是美本申请的开端,数据显示,ED录取率通常只有20%左右,有超过70%的申请者最终都是在RD阶段竞争。所以即使在早申里没有获得理想的结果,也不意味着申请就结束了。

    2025年12月19日 14:37
  • 防治骚扰电话要感谢美国?

    防治骚扰电话要感谢美国?

    2025年12月8日,美国联邦通信委员会执法局向中国移动、中国联通和中国电信在香港的运营实体公司发出“合规令”,认定其在反自动拨号骚扰电话数据库中的认证存在“重大缺陷”,并要求在14天内完成整改或作出说明,否则将面临被移出RMD、直…

    2025年12月17日 16:41
  • AI时代最重要的技能

    AI时代最重要的技能

    如果说这辈子最需要掌握的技能是啥,我觉得有俩:1、搜索能力;2、输出能力。这里说的“搜索”当然跟之前搜索引擎时代不太一样了,不过逻辑是相同的,都是从浩如烟海的网络信息里查找自己需要的。大家应该注意到了,自从大模型出现后,信…

    2025年12月17日 16:37

栏目精选