AI编程时代,为什么还要手动撸码?
1. 当“AI编程”成为默认选项,我为什么还在手敲每一行函数?
最近在几个技术群和社区里刷到的高频画面是:有人贴出一段用Copilot生成的300行代码,配文“十分钟搞定需求”;有人晒出Cursor里自动补全的整套微服务架构,连Dockerfile和CI脚本都带注释;还有人发截图,显示本地大模型正把PRD文档实时转成TypeScript接口定义——而我坐在工位上,刚删掉第三遍重写的useDebounceEffectHook,手指悬在键盘上方,盯着编辑器里那行手动敲出来的const [value, setValue] = useState(''),突然觉得这行字像一枚生锈的螺丝钉,被拧进了高速旋转的涡轮机里。
这不是怀旧,也不是对抗。我清楚知道,Copilot能在我输入// fetch user profile后,精准补全带错误重试、缓存策略和类型守卫的async函数;我也试过让Claude分析一段崩溃日志,它三秒内就定位到是某个第三方SDK在iOS 16.4下对SharedWorker的非标准调用引发的竞态。这些能力真实、高效、不可逆。但问题在于:当“生成”变成默认动作,谁来定义“生成什么”?谁来判断“生成得对不对”?谁在模型幻觉把Math.random()写成Math.rand()时,第一时间发现并修复?——这些事,恰恰发生在AI无法落笔的缝隙里:需求模糊时的追问、边界条件的穷举、异常路径的预设、性能拐点的直觉、甚至是一段注释里藏着的业务隐喻。
我坚持手动撸码,不是拒绝工具,而是守住“意图锚点”。就像老木匠不会因为有了电动刨就放弃目测木纹走向——AI是更锋利的刨刀,但木纹方向,得人眼来认。过去三个月,我刻意把工作流切成“AI辅助区”和“人脑主控区”:API契约设计、单元测试用例生成、重复性CRUD模板、日志格式标准化,全部交给模型;但状态流转逻辑、副作用触发时机、内存泄漏防护点、以及所有涉及“用户没说但必须做”的隐性需求,一律手写,且强制要求每段核心逻辑旁白式注释(不是// 设置状态,而是// 此处需同步更新localForage缓存,否则离线重进时profile头像丢失——见2023Q4用户投诉#A782)。这种割裂感起初很别扭,现在却成了我的质量防火墙。上周上线的订单状态机,AI生成的初版漏掉了“支付超时自动取消”与“客服人工干预”的并发冲突,正是我在手写状态跃迁表时,用纸笔画出17种时序组合才揪出来的。模型擅长填空,人得负责出题。
提示:不要用“让AI写完再人工审”代替“人在关键节点深度介入”。审阅是滞后防御,介入是前置建模。真正的风险不在AI写错,而在人放弃定义“什么算对”。
2. 手动撸码的现代生存法则:从“写代码”到“编排意图流”
十年前,一个资深开发者的核心竞争力是算法复杂度分析和JVM调优;今天,同等资历的人花在“意图翻译”上的时间可能超过编码本身。我最近重构的搜索推荐模块,典型工作流是这样的:
2.1 需求解构:把模糊描述锻造成可执行契约
产品经理说:“首页搜索要更懂用户”。这不行。我拉他坐下来,用白板拆解:
- “更懂”指什么?是点击率提升?还是长尾词曝光增加?
- “用户”是谁?新客的冷启动需求,和老客的个性化偏好,策略完全不同;
- “首页搜索”包含哪些触点?搜索框输入建议、结果页相关搜索、甚至搜索失败后的智能兜底——每个触点的数据源、延迟容忍、降级方案都不同。
最终产出的不是PRD文档,而是一张意图契约表,包含字段:触点ID、用户分群、核心指标、数据源SLA、兜底策略、可观测性埋点。这张表才是后续所有AI生成任务的唯一输入源。我试过直接把“首页搜索要更懂用户”喂给模型,它生成的代码完美实现了“根据历史点击加权排序”,却完全忽略了新客零行为数据的冷启动问题——因为“新客”这个词根本没出现在原始需求里。手动解构,本质是把自然语言里的歧义、省略、假设,全部显性化、结构化、可验证化。
2.2 工具链重铸:让AI成为“高级胶水”,而非“黑盒产线”
我现在的开发环境像一个精密实验室:
- 上游:用Obsidian管理意图契约库,每个需求卡片关联历史决策记录(比如“为什么不用Elasticsearch而选Meilisearch?因实时增量索引延迟<50ms,见20240315压测报告”);
- 中游:VS Code里配置了自定义Copilot指令集,例如输入
/api-contract user-profile,自动注入当前契约表中该接口的全部约束(字段必填性、枚举值范围、敏感数据脱敏规则); - 下游:所有AI生成的代码,必须通过本地预检流水线:先跑
eslint --fix,再用自定义脚本校验是否包含契约表要求的埋点字段,最后强制插入// AI-GEN: [timestamp] [prompt-hash]水印注释。
这套流程的关键在于:AI不接触原始需求,只处理已被人类提炼、标注、验证过的结构化意图。它像一台高精度CNC机床,但图纸必须由老师傅亲手绘制。上周有同事跳过契约表,直接让AI基于Figma设计稿生成React组件,结果生成的<SearchBar />里,搜索图标用了SVG内联,导致SSR时hydration失败——因为设计稿没标注“需支持服务端渲染”。而我的版本,契约表里明确写了renderMode: 'SSR-compatible',AI生成的组件天然包含dangerouslySetInnerHTML的安全封装。
2.3 质量门禁:用“人肉测试矩阵”对抗模型幻觉
AI生成的代码,最危险的不是语法错误,而是逻辑正确性幻觉。它能写出完美的try...catch,却可能在catch块里静默吞掉网络超时错误。我的应对方式是建立三阶验证矩阵:
- 契约层验证:检查生成代码是否100%覆盖契约表中的所有分支条件(如“用户未登录时,搜索框应显示‘请先登录’提示,并禁用提交按钮”);
- 边界层验证:手动编写极端用例,比如输入
" "(纯空格)、"a".repeat(10000)(超长字符串)、null(故意传空对象),观察是否触发预期降级; - 时序层验证:用
performance.now()在关键路径打点,确认AI生成的防抖逻辑实际延迟是否真在300ms±10ms内,而非它声称的“优化了性能”。
这个过程很慢,但每次都能挖出惊喜。上个月AI生成的WebSocket心跳保活逻辑,契约要求“断连后3秒内重连”,它确实写了setTimeout(reconnect, 3000),但没处理reconnect函数自身失败时的指数退避——这是我在时序验证时,故意拔网线后连续触发5次断连才发现的。模型会计算单次延迟,但不会模拟系统级故障链。
3. 手动撸码的隐性资产:那些AI暂时学不会的“手感”
在键盘上敲击十年以上的人,会形成一种难以言传的“手感”:
- 知道什么时候该拆分函数,不是因为ESLint报错,而是当滚动条需要下拉三次才能看到函数结尾时,直觉告诉你“这里呼吸感窒息了”;
- 能从
console.log输出的毫秒级时间戳里,一眼识别出0.034和34.217之间的本质差异——前者是JS引擎内部调度,后者意味着主线程被阻塞; - 在Git diff里,看到
+ const data = await api.fetch()后面紧跟着+ if (data?.items?.length) {,立刻警觉:data?.items可能为undefined,但?.length在null时返回undefined而非0,这个条件判断实际永远为false。
这些能力,源于数万小时与JavaScript引擎、V8垃圾回收器、浏览器渲染管线的“肉搏”。AI可以学习语法树,但学不会在Chrome DevTools里看到Layout阶段耗时突增时,脊椎发凉的本能反应。我最近维护的一个老项目,核心渲染逻辑里有一段requestIdleCallback包裹的DOM操作,AI生成的优化建议是“改用setTimeout(fn, 0)提升响应速度”。这建议语法完美,逻辑却致命——requestIdleCallback的本意是利用空闲时间避免卡顿,而setTimeout(fn, 0)会抢占下一个事件循环,反而加剧渲染压力。只有亲手调教过几十个FPS掉帧现场的人,才懂idle和immediate之间隔着一条用户体验的生死线。
这种手感还体现在对“技术债气味”的嗅觉上。当AI生成的代码里频繁出现// @ts-ignore、any类型、或document.getElementById('xxx')这类反模式时,我不会立刻修改,而是先查Git Blame:这段代码最初是谁写的?当时为什么选择这个方案?是时间压力,还是技术限制?上个月发现一个@ts-ignore标记,追溯到2019年,当时团队刚迁移到TypeScript,fetch的Response类型定义还不完善。现在@ts-ignore早已多余,但直接删除会导致编译失败——因为下游有三个模块依赖这个any返回值做动态属性访问。真正的解法不是删注释,而是用as unknown as MyResponseType做渐进式迁移,并给每个下游模块发重构通知。AI能删掉@ts-ignore,但不会理解这个注释背后三年的技术演进史。
注意:手感无法速成,但可刻意训练。我的方法是每周选一段AI生成的“优质代码”,用老式调试法(断点、
console.time、Performance面板)逐行验证其实际行为,而不是相信它的注释或类型声明。
4. 手动撸码人的新战场:从代码实现者升级为“AI训练教练”
当编码不再是核心瓶颈,开发者的价值重心必然上移。我现在70%的时间花在三件事上:
Prompt工程:不是写“写一个登录接口”,而是构建包含上下文、约束、示例、拒答规则的完整提示包。比如要求AI生成JWT验证中间件时,我的Prompt包含:
【角色】你是一个有5年Node.js安全审计经验的工程师 【约束】必须使用`jsonwebtoken` v9+,禁用`{ algorithms: ['none'] }`,密钥必须从环境变量读取 【示例】参考`auth.middleware.ts`第12-18行的错误处理风格 【拒答】若请求头无Authorization字段,返回401且不记录日志(防暴力探测)这比单纯写代码难十倍,因为它要求你同时理解框架机制、安全规范、团队约定和模型能力边界。
反馈闭环建设:所有AI生成的代码上线后,我强制要求监控系统捕获两类数据:一是运行时异常(如
TypeError: Cannot read property 'id' of undefined),二是业务指标偏移(如搜索转化率下降0.5%)。这些数据不是丢给运维,而是反向注入到我的Prompt库中,形成“负样本集”。当AI再次生成类似代码时,我会在Prompt里加入:“历史数据显示,此类optional chaining用法在iOS Safari 15.6下有5%概率返回undefined而非null,请改用data && data.items && data.items.length > 0”。知识晶体化:把个人经验转化为AI可消化的结构化知识。我维护一个
dev-knowledge.md文件,里面没有长篇大论,只有原子化条目:## [React] useEffect依赖数组陷阱 - 场景:监听`props.userId`变化重新获取用户数据 - 错误写法:`useEffect(() => { fetchUser(userId) }, [userId])` - 根因:`userId`可能为`null`或`undefined`,导致无限请求 - 正确写法:`useEffect(() => { if (userId) fetchUser(userId) }, [userId])` - 验证方式:在DevTools中修改`userId`为`null`,观察Network标签页是否发起请求这些条目直接作为AI的参考文档,比任何教程都管用——因为它们来自真实的血泪教训。
这种转型不是选择,而是生存必需。当初级开发者能用AI完成90%的CRUD时,“会写代码”就从稀缺技能变成了基础配置。真正稀缺的,是能定义AI该做什么、能判断AI做得好不好、能在AI失效时立刻接管的“意图指挥官”。我最近面试一个候选人,让他用AI生成一个防抖Hook。他很快交出代码,但当我问:“如果用户连续快速点击10次,第5次点击时网络请求超时,第6次点击会触发新请求吗?为什么?”——他愣住了。这个问题不考语法,考的是对异步状态机的肌肉记忆。而这种记忆,只能来自亲手撸过上百个防抖、节流、竞态取消的深夜。
5. 未来已来,但方向盘还在人手里
上个月,我参与了一个内部AI编码大赛:两组人用相同需求文档,一组纯AI生成,一组“人主导+AI辅助”。结果很有趣:AI组代码量多出40%,单元测试覆盖率高15%,但上线后第一周,人主导组的P0故障数为0,AI组有3起——全是“逻辑正确但业务错误”:比如搜索推荐把“儿童玩具”优先推给35岁以上用户,因为模型从历史数据中学到了“35岁用户购买力强”,却忽略了“购买者”和“使用者”的身份分离。
这让我想起机械时代的故事:当第一台自动织布机问世,老师傅们没去砸机器,而是转身成了织机校准师、花色设计师、故障诊断专家。今天的手动撸码人,正在经历同样的职业升维。我们不再比谁敲键盘更快,而比谁定义问题更准、谁校准AI更稳、谁在系统崩塌时重建得更牢。
我书桌抽屉里还放着大学时买的《算法导论》第一版,书页泛黄,边角卷起。最近一次翻开,是在调试一个图遍历算法时,发现AI生成的BFS实现漏掉了环路检测。我指着书上第543页的伪代码对实习生说:“看,这里if not visited[v]的判断,不只是为了性能,更是为了防止无限循环——就像我们做人,有些边界,明知绕过去更快,也必须亲手标出来。”
这大概就是手动撸码人在AI时代的终极价值:不是对抗浪潮,而是成为浪潮里那块礁石——不阻止水流,但让水流学会转弯。
