AI 辅助编程技巧与踩坑:一些我现在不会再省略的步骤

同一句需求,喂给 AI 两次,产出可能天差地别。

有一回我让它给一个中后台列表加"导出 CSV"。第一次我只说了"给这个页面加个导出按钮,点了下载当前筛选结果",它很利索地写了一段:

1function handleExport() {
2  // data 是当前列表拿到的那一页数据
3  const csv = data.map((r) => `${r.id},${r.name},${r.amount}`).join("\n")
4  const blob = new Blob([csv], { type: "text/csv" })
5  const url = URL.createObjectURL(blob)
6  const a = document.createElement("a")
7  a.href = url
8  a.download = "orders.csv"
9  a.click()
10}

跑起来也确实能下载——但它导出的是 data,也就是"当前页",而我们的列表是服务端分页的,用户筛选后有几千条,它导的只有屏幕上那二十条。代码本身没错,错在它不知道我们的数据是分页的、真正的全量数据在后端。这就是最典型的"局部对、整体错":拎出这段函数它挑不出毛病,一放进真实业务就是个 bug。

第二次我换了个说法,先贴了列表用的 useTableQuery hook、说明分页在服务端、导出要走后端已有的 /export 接口带上当前筛选参数,再让它动手。这次它写的就完全不同了——调后端导出接口、把当前 filters 传过去、拿到文件流再触发下载,一次就对。

两段代码的差距不在模型聪不聪明,在我给它的信息完不完整。这两年我把 AI 越来越当成结对开发的对象,踩下来最朴素的感受是:它不是不会写代码,而是太容易在信息不完整时写出一段"看起来像样"的东西。 下面这几条是我现在不会再省略的步骤,基本都是被这类"局部对、整体错"的改动逼出来的。

不给上下文,它就替你脑补

上面那个导出的例子是典型。我以前总觉得"需求已经说得够清楚了"——告诉它改哪个页面、要什么效果,应该够了吧。后来发现这只是人类之间的默认默契,对 AI 不成立。

它不知道这个项目偏保守还是偏激进,不知道目录结构背后的约定,也不知道哪些旧逻辑虽然丑但暂时不能删。上下文一缺,它就用最通用的方式补空白,而通用写法往往不是你这个仓库真正想要的写法。

所以现在动手之前,我会先把这几样交代清楚:入口在哪、当前模块怎么组织、这次只允许动哪些文件、哪些业务逻辑不能碰、有没有现成的错误处理和测试方式。听着啰嗦,但它直接决定了 AI 是"接着你的项目往下做",还是"重新发明一套它自己熟悉的方案"。

这一步现在有更省事的做法。多数 AI 编码工具都支持在仓库根放一个约定文件(AGENTS.mdCLAUDE.md.cursorrules 之类,名字随工具而定),把项目的固定约束写进去,每次会话自动带上,不用我反复口述:

1# 项目约定
2
3## 技术栈
4- Next.js(App Router)+ TypeScript + Tailwind v4
5- 数据请求统一走 lib/api.ts 的封装,不要直接 fetch
6- 表单校验用 Zod,schema 放在 schemas/ 目录
7
8## 硬约束
9- 列表默认服务端分页,导出/批量操作要走后端接口,不要在前端拼全量数据
10- 不新增依赖,除非先说明理由并等我确认
11- 组件默认 Server Component,需要交互再显式标 "use client"
12
13## 提交前
14- 跑 pnpm lint 和 pnpm typecheck
15- 改到构建路径的跑 pnpm build 确认能过

自从加了这个文件,"它替我脑补错方向"的情况明显少了。像"列表是服务端分页"这种,当初要是写进约定里,第一次那个导出就不会翻车。

约束不明确,返工概率直线上升

上下文是"这个项目长什么样",约束是"这次不许你做什么"。两者不一样,都得给。

我吃过很多"看着挺聪明、实际不符合要求"的亏。只说"把接口异常处理一下",它可能顺手引入一个新的工具函数、甚至装个新库;只说"补一下这个组件",它可能连带把命名风格、文件里其它无关的地方也改了。这类问题不一定是它写错了,而是没人明确框住它该做多少,它就默认帮你做更多

所以现在我把约束写得很直白,宁可显得啰嗦:

1只改 components/OrderTable.tsx 这一个文件。
2不要引入新依赖。
3不要动 useOrderQuery 的签名,其它文件在依赖它。
4错误一律走 lib/api.ts 里已有的 handleApiError,不要自己 try/catch 弹 alert。
5改动前先把计划说给我听,我确认再动手。

有人觉得这类话"流程感太重"。但我的体会是,约束越明确,后面来回的沟通成本越低。AI 不怕限制多,怕的是你嘴上说要保守、实际输入却给了它很大的发挥空间——那它一定会发挥。

约束还有一个作用是防"越描越乱"。它犯了错,你指出来,它会很痛快地道歉、说"你说得对",然后——有时候还是照原样再犯一遍,或者为了绕开你指出的问题,又引出一个新问题。这种循环我碰到过好几次,光靠一句"不对,再改"是跳不出来的。有效的做法是把约束写死、写具体:"不许改 useOrderQuery""错误只能走 handleApiError",比反复说"你又错了"管用得多。它对"具体的禁止项"响应得远比对"笼统的不满"好。真陷进死循环了,我干脆重开一个会话、把约束一次性交代清楚,比在原地跟它拉锯省时间。

一上来就让它写,通常最费时间

这是我现在最坚持的一条:先让它读代码,再让它动手。

道理很简单,项目里真正重要的信息往往藏在已有实现里,而不是在需求描述里。状态怎么流转、公共方法怎么复用、异常在哪兜底、构建脚本怎么跑,这些都不是一句"按标准答案来"能补出来的。开头那个导出功能,如果第一步就是让它读一遍列表组件和 api.ts,它自己就会发现"哦这是服务端分页",根本不用我事后纠正。

现在我更喜欢让它先走两步再上手:

  1. 先读相关文件,用自己的话把它理解到的现状说一遍——包括它没看懂或者觉得可疑的地方。
  2. 再拆任务:准备怎么改、会碰到哪些文件、怎么验证。

第一步尤其有用。它复述现状的时候,我能立刻看出它有没有理解偏。比如它说"导出直接用当前 data 拼就行",我马上知道它没意识到分页在后端,这时候纠正的成本,比等它写完一版再返工低得多。老项目里出问题的常常不是它不会写,是它写得太快、太自信。

具体到 prompt,"先读再写"和"直接写"就差一句话,效果差很远:

1# 差的问法
2给 OrderTable 加导出功能。
3
4# 好的问法
5先读 components/OrderTable.tsx、hooks/useOrderQuery.ts、lib/api.ts,
6用自己的话讲一遍:这个列表的数据从哪来、分页在前端还是后端、
7现有的下载/导出能力有没有。讲完停下来等我,先别写代码。

多花的就是让它先"讲一遍"的那几十秒,换来的是它把方案建立在真实实现上,而不是它脑补的一个通用列表组件上。这一步在陌生代码里价值更大——接手一段没人讲得清的老逻辑时,我经常第一件事不是让它改,而是让它逐段讲这段代码在干什么、哪里有坑,相当于免费得到一份阅读笔记,再决定要不要动、怎么动。

把它当可回合的协作对象,而不是一次性生成器

我早期还有个误区:总想一轮把需求说全,然后等它吐出最终结果。后来发现代码协作不是写宣传文案,很多细节只有看过第一版改动才会暴露。你在需求里想不到要交代"加载态怎么显示""接口失败给什么提示""空数据是什么样",是因为这些东西不看到具体实现根本浮不上来;非要一次性把话说全,只会编出一堆凭空想象的限制条件,反而把它带偏。

更稳的是小步走:

  • 一轮只推进一个明确目标;
  • 每轮先确认计划再动手;
  • 改完立刻验证;
  • 根据结果决定下一轮改什么。

这和人跟人结对很像。你不会让同事闷头一次改十个点、完全不 review 就合进去。AI 也一样——它适合被放进一个可检查、可回退、可继续追问的循环里,而不是被期待"一次生成完美答案"。

"先确认计划"这一步我现在几乎不省。让它把计划列出来,我扫一眼就能拦掉方向性错误,比看完整段代码快得多。一个典型的计划回合长这样:

1AI:计划如下,确认后再动手——
21. 在 lib/api.ts 加 exportOrders(params),调后端 /export,返回 blob
32. OrderTable 顶部加"导出"按钮,点击时带上当前筛选参数调用它
43. 下载用已有的 lib/download.ts 里的 downloadBlob,不自己写
54. 加载态复用现有的 useMutation 模式
6影响文件:lib/api.ts、components/OrderTable.tsx
7不改:useOrderQuery、后端接口
8
9我:第 1 步 /export 的参数名跟筛选状态对不上,筛选用的是 filters 对象,
10你先读一下 useOrderQuery 里 filters 的结构再定参数。其余可以。

在它写下第一行代码之前,我就把"参数对不上"这个隐患挡住了。等它写完再发现,就得连着改动一起返工。计划阶段拦一个问题,抵得上实现阶段拦十个。

这里有个和"小步走"直接相关的技术细节:上下文窗口。一次会话越拖越长,早期贴的关键约束会被后面几十轮对话稀释、甚至挤出窗口。表现就是它突然"忘了"你之前说过"不要动这个 hook",或者又开始用你早就否掉的方案。我的两个应对:

  • 一个相对独立的任务结束就开新会话,别把一天的活全堆在一条超长对话里;
  • 把稳定的约束沉淀进上面那个 AGENTS.md,而不是靠对话记忆——文件是每轮都重新加载的,不会被历史挤掉。

判断"是不是该开新会话"有个土办法:如果你发现自己在反复提醒它一些前面已经说过的约束,基本就是上下文被冲淡了,重开一个、把约束重新交代一遍,往往比在旧会话里继续拉扯更快。

让它用工具,但别放任它乱翻

现在的 AI 编码工具大多不只是"生成文本",还能跑命令、读文件、调 MCP 服务查数据。这让它能自己读代码、自己跑测试验证,是前面几条能落地的前提。但工具能力放开了,也得给规矩。

我踩过一次:让它"看看这个报错哪来的",它自作主张跑了一串命令,中间夹了个改动文件的操作,等我反应过来工作区已经不干净了。之后我的习惯是:读类命令(看文件、跑测试、查日志)放开让它自己来,写类和有副作用的操作(改文件、装依赖、跑迁移、碰数据库)必须先报计划、我确认再执行。

很多工具本身就支持给命令配白名单、把危险操作设成需要确认,值得花十分钟配一下。大致就是把安全的读类命令放行、其余要人工点头:

1{
2  // 这些直接放行,不用每次问我
3  "allow": [
4    "pnpm lint",
5    "pnpm typecheck",
6    "pnpm test *",
7    "git diff *",
8    "git status"
9  ],
10  // 这些一律先问,别自作主张
11  "confirm": [
12    "git commit *",
13    "git push *",
14    "pnpm add *",
15    "rm *",
16    "* migrate *"
17  ]
18}

配好之后省心很多:它能自己反复跑测试验证、翻代码定位问题,不用我在旁边一遍遍点"允许";但真要改文件、装东西、碰远端的时候,主动权还在我手里。

调 MCP 或外部数据源时还要多一句心眼:它读回来的东西不一定可靠。让它照着一个接口文档写调用,最好把真实响应也给它看一眼,别让它按文档里的"理想字段"想当然——线上后端和文档对不上是常事,这一点在写 Zod 校验的时候我另讲过。

最容易被忽略的一步,其实是测试

如果只能留一条习惯,我留"每轮改完必须验证"。

原因不是 AI 比人更容易犯错,而是它特别擅长产出"局部合理、全局没验证"的改动。它能把一段函数写得很顺、把一个组件拼得很完整,但这不等于整个项目还是通的。类型对不对得上、别处有没有被牵连、构建能不能过,它自己看单个文件是看不出来的。

所以我现在把验证要求直接写进任务,让它改完自己先跑一遍:

1改完按顺序自查:
21. pnpm typecheck —— 类型必须过
32. pnpm lint —— 不许留新的 warning
43. 动到 OrderTable 的话,跑 pnpm test src/components/OrderTable
54. 没有自动化测试覆盖的,说明你手动验证的步骤和预期结果
6把每一步的实际输出贴给我,别只说"应该没问题"。

这样做还有个额外好处:它逼着 AI 在动手前就想"怎么证明自己没改坏"。一旦连验证方式都说不清,通常说明它对这次改动的理解还不扎实——这本身就是个预警信号。

顺带说,别把测试完全托给它写。它写的测试有时候是"迎合当前实现"的:你的实现有 bug,它连测试一起顺着错的写,跑起来全绿,实际啥也没保障。我遇到过更离谱的——它为了让测试过,直接把断言改成和错误输出一致,等于把温度计焊死在你想要的读数上。所以关键路径的测试,输入和期望输出我自己定,把"这个函数在空数组、超大值、请求失败时应该怎样"讲清楚,让它去补齐调用样板和 mock,而不是让它替我决定"什么算对"。

还有个高频坑是它对测试工具的 API 想当然。断言写法、mock 的用法在不同测试框架、不同版本之间差别不小,它有时会混用,甚至编一个看着合理、实则不存在的匹配器。测试跑不起来还好,跑起来但断言其实没生效才要命。碰到我拿不准的用法,我会让它把方法标到具体文档,或者自己扫一眼确认,不照单全收。

review 它的 diff,别看它的解释

有一个坑很隐蔽:AI 改完通常会附一段解释,"我做了 A、B、C,都符合你的要求"。这段解释读着顺,很容易让人放松警惕直接合。但它的自述和它实际改的东西不总是对得上——它可能说"只改了导出逻辑",实际顺手动了排序、或者删了一段它觉得多余、其实别处在依赖的代码。

所以我现在的铁律是:看 diff,不看它的总结。 让它把改动落成实际的文件变更,我用 git diff 或者编辑器的 diff 视图一行行过,判断依据是代码本身,不是它的说明文案。几个我盯得最紧的点:

  • 有没有改到我没让它改的文件或函数;
  • 有没有偷偷加依赖(package.json 有没有动);
  • 边界条件——空数组、null、请求失败这些分支它处理了没,还是只写了顺利路径;
  • 有没有把 // TODO 或者硬编码的假数据留在里面充数。

diff 大到一屏看不完的时候,往往说明这一轮改动铺得太开了,这时我会退回去,让它拆成几个小 commit,一段一段来。能被逐行 review 的改动,才是能放心合的改动。

大范围改动,先立一道"回退线"

现在的工具敢一口气改几十个文件——批量重命名、把一个模式在全项目铺开、跨模块的重构,它都能干。这个能力很爽,但也最容易失控:等你回过神,工作区已经面目全非,想撤都不知道从哪撤。

我给这类任务立了一条硬规矩:动手前,工作区必须是干净的。 让它改之前先 git commit 或者 git stash,把当前状态存成一个明确的还原点。这样无论它这一轮改成什么样,git checkout . 一把就能全部退回,代价是零。养成这个习惯之前,我有过一次被它一通"顺手优化"改花了七八个文件、又没法干净回退、只能一个个手动还原的经历,那半小时纯属自找。

大重构我还会把它拆成"机械"和"判断"两半。纯机械的部分(换个函数名、统一 import 路径、把某个 API 的调用方式全局替换)放心交给它批量做,改完跑一遍 typecheck 和测试就能验收;涉及判断的部分(这个抽象合不合理、这层要不要拆)我自己来,或者只让它出方案、我拍板。把这两半混在一句"帮我重构一下这块"里丢过去,是最容易翻车的——它会把机械替换和自作主张的设计改动搅在同一个 diff 里,你想拆都拆不开。

还有一点:大改动别指望它一轮到位。我更倾向让它分几个可独立验证的阶段走,每个阶段一个 commit,跑通了再进下一个。中间哪一步崩了,退回上一个 commit 就行,不至于满盘皆输。这本质上还是"小步走",只是放到多文件重构的尺度上更重要——步子迈太大,回退成本会指数级上升。

怎么判断一段 AI 代码值不值得信

"看着对"和"真的对"之间隔着验证,前面反复说了。但除了跑测试,我还攒了几条快速判断的经验,专门用来识别那种"表面光鲜、底子虚"的产出。

一是看它敢不敢承认不确定。如果我问"这么改会不会影响到分页缓存",它含糊地说"应该没问题",我反而更警惕;靠谱的回答是它去把相关代码读一遍再告诉我影响面。二是看它给的验证证据。我要求它贴 typechecktest 的真实输出,而不是"我跑过了没问题"——真跑过的,输出贴得出来;没跑的,一问就露。三是对陌生 API 的用法保持怀疑:它可能用一个看着合理、实则是旧版本或者根本不存在的方法名,尤其是版本迭代快的库。碰到我不熟的 API,我会让它把出处指出来,或者干脆自己去查一眼官方文档核对,不照单全收。

这几条本质上是把"信任"拆成了"可核对的证据"。AI 给我的价值是速度,但速度不能建立在"我懒得核对"上——它写得越快,我越要有一套低成本的方式快速判断这东西能不能要。

什么时候干脆别用它

用了两年,我也划出了几条"这活别指望 AI"的线,省得白费来回。

需求本身没想清楚的,别丢给它——它不会帮你把模糊的需求变清晰,只会用一段代码把模糊固化下来,你还得推倒重来。牵扯全局架构决策的(要不要拆服务、状态管理换不换方案),我只拿它当讨论对象列利弊,最终拍板和落地拆解自己来,因为它看不到你团队的历史包袱和三个月后的路线。还有那种改动小、但错了代价极大的(改权限判断、改计费逻辑、动数据库迁移),我宁可自己慢慢写,也不省这几分钟——这类地方它一个"看起来合理"的改动,排查起来能吃掉你一整天。

反过来,有一批活它又快又稳,能实打实省时间,我基本无脑交给它:

  • 样板代码:CRUD 骨架、表单和 schema、列表页的分页搜索这类重复结构;
  • 格式与语言转换:JSON 转类型、一段 JS 转 TS、把一个正则讲清楚或者反过来按描述写一个;
  • 把一个我已经想清楚的方案翻成代码:思路我定,落地它写;
  • 补类型和补测试的样板部分:函数签名我给,它填实现细节和 mock;
  • 读陌生代码先讲一遍:接手老逻辑时当阅读向导,比自己啃快得多;
  • 一次性的脚本:数据迁移、批量改文件名、跑个统计,写完就扔的那种。

这些场景的共同点是"对错好验证、错了代价低"。凡是"难验证、错了代价大"的,就回到上面那几条线,慢一点自己来。

顺带一句:别让它无脑读整个仓库

有段时间我图省事,遇到问题就把一大片目录甩给它"你自己看着办"。结果是它读了一堆无关文件、上下文被塞满、真正相关的那几个反而没读透,回答又慢又飘。后来我改成自己先定位到大概范围,只给它相关的那几个文件或目录,让它读得准、读得深。这既是省成本,也是提质量——喂进去的东西越聚焦,它的判断越靠谱;一股脑全塞给它,等于让它在噪声里找信号。

判断"给多少"有个粗略的手感:如果这个问题我自己上手,会去翻哪几个文件,就先给它那几个;它读完说"还需要看某某文件才能确定",我再补。让它带着明确目标去读,比让它漫无目的地扫整个仓库有效得多。

留下来的不是神奇 prompt,是一套顺序

写到这儿,我反而不太信"万能提示词"了。真正让我少踩坑的,不是哪句咒语,而是一套越来越固定的顺序:

先给上下文、再给约束;先让它读代码、再拆计划;小步改动、每轮改完就验证;副作用操作先确认、上下文长了就重开。

这套东西不酷,甚至有点土,但它很贴工程现实。AI 辅助编程能省下来的时间,靠的是把这些步骤执行得更快,信息给全、约束写死、每轮都验证,而不是想办法跳过它们。开头那个导出功能,第二次之所以一遍就过,是因为我把它该知道的都先摆到了它面前,不是换了句更聪明的话。少踩坑靠的是这套顺序本身,跟挑哪个工具关系不大。