1. 一百亿Token烧完,我才看懂“Code is cheap”这句话坑在哪
先亮身份。我是一个写了十年代码、最近两年把主力工作流全部搬到 AI 编程工具上的后端工程师。过去一年多,我在各种 AI 编程工具上累计消耗的 Token 量,按后台账单折算大概在一百亿这个量级。这个数字听起来很唬人,但拆到每天、每个项目、每次对话里,其实就是一个重度用户正常烧出来的量。
也正是这个量级的消耗,让我彻底想明白一件事:“Code is cheap”是这两年 AI 编程圈里流传最广、也最害人的一句口号。
这句话的原始语境大概是这样的:大模型写代码几乎不要钱,生成一段 CRUD、一个工具函数、一个组件,几秒钟就出来了,所以代码本身不再稀缺,稀缺的是想法和判断。听起来很对,对吧?但你真把它当成工作信条用上三个月,就会发现账单、返工、线上事故会一起教你做人。
我写这篇东西,不是要唱衰 AI 编程。恰恰相反,我现在每天的工作流里,AI 编程工具占了七成以上的编码时间。我想说的是:代码的“生成成本”确实变便宜了,但代码的“总拥有成本”一点都没便宜,甚至因为生成太快而变得更贵了。这个认知差,就是一百亿 Token 给我上的最贵的一课。
这篇文章适合三类人看:一是刚上手 AI 编程工具、还在兴奋期的开发者;二是团队里负责推 AI 编程规范、被 Token 账单和代码质量两头夹击的技术负责人;三是已经用了一段时间、感觉“好像快但好像又没快多少”的中级工程师。我会把 Token 到底花在哪、SDD 这类规范驱动开发为什么重要、CLAUDE.md 这种上下文文件怎么用、Lint 为什么是 AI 时代的最后一道防线,全部拆开讲清楚。
核心关键词我先摆出来:Token、Code、SDD、CLAUDE.md、Lint。这五个词基本构成了 AI 编程工作流的完整闭环,缺一个都会出问题。下面我按“为什么贵、贵在哪、怎么省、怎么防”这条线,一层层往下拆。
2. 拆解“Code is cheap”:生成便宜,但拥有昂贵
2.1 生成成本和拥有成本,是两笔完全不同的账
很多人算 AI 编程的账,只算了第一笔:生成成本。一段代码让模型写出来,消耗多少 Token,折算多少钱。这笔账确实便宜,便宜到可以忽略不计。但代码这东西不是写完就完事了,它要进仓库、要被人读、要被测试、要上线、要维护、要在半年后被人改。这些加起来才是拥有成本。
我举个特别具体的例子。有一次我让模型帮我写一个“根据用户 ID 批量查询订单状态”的接口。模型十秒钟给了我八十行代码,逻辑看着没问题,Token 花了不到两千。我当时觉得真香。结果这段代码上线两周后出了个问题:批量查询没有做分页保护,某个大客户一次性传了五万个 ID 进来,直接把数据库连接池打满了。
修复这个问题花了我一个下午,加上线上告警、回滚、写复盘文档,综合成本远超我自己手写这段代码的时间。模型生成那八十行只花了不到两千 Token,但这段代码的拥有成本是几千块人民币加一个下午的焦虑。这就是“Code is cheap”最大的认知陷阱:它只看到了生成那一瞬间的便宜,完全忽略了代码进入系统之后的全部代价。
2.2 Token 账单的真实结构:输入远比输出贵
再来说 Token 这件事本身。很多人以为 Token 消耗主要发生在模型“写代码”的时候,也就是输出 Token。实际用下来你会发现,真正烧钱的是输入 Token,也就是你每次喂给模型的上下文。
我拉过自己一个月的账单做拆解,结构大概是这样:
| 消耗类型 | 占比 | 说明 |
|---|---|---|
| 系统提示与工具定义 | 15% | 每次请求都要带的固定开销 |
| 项目上下文文件 | 30% | CLAUDE.md、目录结构、相关文件 |
| 历史对话累积 | 25% | 多轮对话里不断重复的旧内容 |
| 实际代码输出 | 20% | 模型真正写出来的代码 |
| 工具调用与返回 | 10% | 读写文件、执行命令的结果 |
看这张表就明白了:模型真正“写代码”只占两成,剩下八成全是上下文。你每让模型改一行代码,它可能要先读进去几千行相关文件。这就是为什么“Code is cheap”在账单层面根本不成立——代码输出是便宜的,但让模型理解你的代码库是昂贵的。
我踩过最典型的一个坑:早期我习惯把整个文件甚至整个模块贴给模型,让它“看着改”。一个两千行的文件,每次对话都贴一遍,改五轮就是一万行输入。后来我学会只贴相关函数加必要的类型定义,同样的任务 Token 消耗直接降到原来的三分之一。这个优化不涉及任何工具技巧,纯粹是认知问题。
2.3 返工成本才是真正的黑洞
比 Token 账单更可怕的,是返工。模型生成的代码有个特点:第一版看起来永远是对的。语法正确、命名规范、注释齐全,甚至还会贴心地给你加错误处理。但你真跑起来,边界条件、并发场景、异常路径,总有几个地方是错的。
我统计过自己经手的一个中型项目,模型生成的代码里,大约有 30% 需要至少一次返工,10% 需要两次以上。返工一次意味着什么?意味着你要重新组织上下文、重新描述问题、重新等模型输出、重新 review。这一轮下来,Token 消耗是首次生成的两到三倍,时间成本更是没法算。
更隐蔽的是“看起来对但实际错”的代码。这类代码不会报错,测试也可能过,但在特定输入下会出问题。我遇到过一个模型写的日期处理函数,正常日期都对,但遇到闰年二月二十九号就崩了。这种 bug 靠 review 很难发现,往往要等线上出问题才暴露。返工成本加上这种隐蔽 bug 的排查成本,才是 AI 编程真正的成本大头。
3. SDD 规范驱动开发:把“便宜”变成“可控”的关键
3.1 SDD 到底是什么,为什么 AI 时代突然重要了
SDD,Specification-Driven Development,规范驱动开发。这个概念其实不新,传统软件工程里一直有“先写规格再写代码”的说法。但在 AI 编程时代,它从一个“最佳实践”变成了“生存必需”。
原因很简单:模型不会读心,你脑子里的隐性需求它一概不知道。你让它写一个“用户登录接口”,它默认给你写最朴素的用户名密码校验。但你的系统可能需要短信验证码、可能需要设备指纹、可能需要风控拦截、可能需要登录失败次数限制。这些你没说,它就不写。等你想起来,又是一轮返工。
SDD 的核心思路是:在让模型写代码之前,先把“要什么”用结构化的方式写清楚。这个“写清楚”不是写一段自然语言描述,而是写一份模型能直接消费的规格。规格里要包含:输入输出定义、边界条件、异常处理、依赖关系、验收标准。
我现在的习惯是,任何超过五十行的功能,先花十分钟写一份简版 SDD,再让模型动手。这十分钟看起来是额外开销,但它能把返工率从 30% 压到 10% 以下。省下来的返工 Token 和时间,远超写规格的成本。
3.2 一份能直接喂给模型的 SDD 长什么样
空谈概念没意义,我直接给你看我实际用的一份 SDD 模板。这是一个“订单状态批量查询”功能的规格,你可以照着改:
## 功能:订单状态批量查询 ### 输入 - userId: string, 必填, 用户唯一标识 - orderIds: string[], 必填, 订单 ID 列表, 长度 1-100 - 请求方必须携带有效鉴权信息 ### 输出 - 成功: { orderId, status, updatedAt }[] - 部分失败: 返回成功列表 + 失败订单 ID 及原因 - 全部失败: 返回错误码和错误信息 ### 边界条件 - orderIds 为空数组: 返回空列表, 不报错 - orderIds 超过 100 个: 返回参数错误 - 订单不属于该用户: 该订单标记为无权限, 不泄露存在性 - 数据库查询超时: 返回服务繁忙, 不暴露内部错误 ### 异常处理 - 数据库连接失败: 重试一次, 仍失败则返回 503 - 单个订单查询失败: 不影响其他订单, 记录日志 ### 验收标准 - 100 个订单查询响应时间 < 500ms - 无权限订单不返回任何状态信息 - 所有异常路径都有对应测试用例这份规格不到四十行,但它把模型最容易漏掉的东西全写清楚了。边界条件、异常处理、验收标准,这三块是 AI 生成代码最容易翻车的地方,也是 SDD 价值最高的地方。
3.3 SDD 和 Token 消耗的关系:先贵后便宜
有人会问:写 SDD 本身也要消耗 Token 啊,这不是更贵了吗?短期看是的,长期看完全相反。
我做过对比实验。同一个功能,A 组直接让模型写,B 组先写 SDD 再让模型写。结果 A 组首次生成 Token 消耗 3000,但返工三轮后总消耗 12000。B 组写 SDD 消耗 2000,生成消耗 3500,返工一轮消耗 2000,总消耗 7500。B 组总 Token 消耗比 A 组低 37%,而且交付质量明显更高。
更关键的是,SDD 是可以复用的。同一个模块的后续功能,可以基于已有 SDD 扩展,边际成本越来越低。而直接让模型写代码,每次都是从零开始描述需求,边际成本几乎不变。SDD 是把一次性的规格投入,摊薄到整个项目生命周期里的做法。
4. CLAUDE.md 与上下文工程:让模型真正懂你的项目
4.1 CLAUDE.md 不是配置文件,是项目的“入职手册”
CLAUDE.md 这类文件,本质上是给 AI 编程工具看的项目说明书。你可以把它理解成“新员工入职手册”——一个新人进项目,你需要告诉他项目结构、技术栈、代码规范、常用命令、注意事项。模型也一样,你不告诉它,它就只能靠猜。
我见过太多人把 CLAUDE.md 写成一句“这是一个 React 项目”,然后抱怨模型生成的代码不符合项目风格。模型不是不聪明,是你没给它足够的信息。一份好的 CLAUDE.md,应该让模型在读完之后的第一次输出,就有七成以上的概率符合你的预期。
我自己的 CLAUDE.md 大概长这样,你可以参考结构:
# 项目说明 ## 技术栈 - 后端: Node.js 20 + TypeScript 5.3 + Fastify - 数据库: PostgreSQL 15 + Prisma - 测试: Vitest + Supertest - 代码规范: ESLint + Prettier, 提交前必须通过 ## 目录结构 - src/routes: 路由定义, 每个文件对应一个资源 - src/services: 业务逻辑, 不直接操作数据库 - src/repositories: 数据访问层, 唯一允许写 SQL 的地方 - src/types: 共享类型定义 - tests: 测试文件, 与 src 目录结构镜像 ## 编码约定 - 所有函数必须有显式返回类型 - 错误统一用 AppError 类抛出, 不直接 throw new Error - 数据库操作必须在 repository 层, service 层不出现 Prisma 调用 - 所有对外接口必须有输入校验, 用 zod schema ## 常用命令 - 开发: pnpm dev - 测试: pnpm test - 类型检查: pnpm typecheck - 提交前: pnpm lint && pnpm typecheck && pnpm test ## 注意事项 - 不要修改 prisma/schema.prisma 除非明确要求 - 新增依赖前先确认是否已有同类库 - 所有时间字段统一用 UTC 存储这份文件不到五十行,但它能极大降低模型“跑偏”的概率。上下文工程的核心,就是把项目里那些“老员工都知道但新人不知道”的隐性知识,显性化地写出来。
4.2 上下文分层:不是所有信息都值得塞进去
CLAUDE.md 有个常见误区:写得越长越好。我早期也这么干,把项目里所有能想到的东西都塞进去,结果 Token 消耗暴涨,模型反而抓不住重点。
正确的做法是分层。我把上下文分成三层:
| 层级 | 内容 | 加载时机 | 典型长度 |
|---|---|---|---|
| 全局层 | 技术栈、目录结构、编码约定 | 每次请求 | 500-1000 Token |
| 模块层 | 当前模块的接口定义、依赖关系 | 进入模块时 | 300-800 Token |
| 任务层 | 当前任务的具体规格、相关文件 | 每次任务 | 按需, 通常 1000-3000 Token |
全局层放在 CLAUDE.md 里,模块层放在各模块目录下的说明文件里,任务层在对话里临时提供。这样模型每次只加载必要信息,Token 消耗可控,理解精度也更高。
我实测下来,分层之后同样的任务 Token 消耗降低约 40%,而且模型输出的一次通过率反而提升了。原因很简单:信息少了,噪音就少了,模型更容易抓住重点。
4.3 上下文失效的典型场景和应对
上下文不是写完就一劳永逸的。项目在变,上下文也会失效。我遇到过几种典型情况:
第一种是目录结构变了但 CLAUDE.md 没更新。模型按旧结构找文件,找不到就瞎猜,生成的代码 import 路径全是错的。应对方法是把 CLAUDE.md 的更新纳入代码 review 流程,目录结构变更必须同步更新。
第二种是依赖版本升级后约定没改。比如项目从 Prisma 4 升到 5,API 有变化,但 CLAUDE.md 里还写着旧用法。模型按旧用法写,跑起来就报错。应对方法是在 CLAUDE.md 里标注版本号,升级依赖时检查相关约定。
第三种是多人协作时上下文冲突。不同的人往 CLAUDE.md 里加不同约定,最后文件又长又矛盾。应对方法是给 CLAUDE.md 设一个 owner,所有修改走 PR review,保持文件精简一致。
提示:CLAUDE.md 的维护成本经常被低估。我建议每周花十分钟过一遍,删掉过时内容,补充新约定。这十分钟能省下后面无数次的返工。
5. Lint 与自动化防线:AI 时代的最后一道闸
5.1 为什么 Lint 在 AI 编程里变得更重要了
传统开发里,Lint 是个“锦上添花”的东西。代码风格不统一,忍忍就过去了。但在 AI 编程里,Lint 从锦上添花变成了生死线。
原因在于:模型生成的代码,风格漂移是常态。同一个项目里,模型这次给你用const,下次可能用let;这次用箭头函数,下次用function;这次错误处理用 try-catch,下次用 Promise.catch。你不管,代码库三个月就变成风格大杂烩。
更严重的是,模型会生成一些“语法正确但逻辑可疑”的代码。比如未使用的变量、永远为真的条件、被吞掉的异常。这些东西人 review 容易漏,但 Lint 规则能精准抓住。
我现在的工作流里,Lint 是模型输出后的第一道自动检查。模型写完代码,先跑 Lint,不通过直接打回重写,根本不进入人工 review 环节。这一道闸能过滤掉大概四成的低级问题,极大减轻 review 负担。
5.2 一套针对 AI 生成代码的 Lint 配置
普通的 Lint 配置不够用,因为它是为人类写的代码设计的。针对 AI 生成代码,我加了几条额外规则。以 ESLint 为例:
// .eslintrc.js 关键规则摘录 module.exports = { rules: { // 禁止未使用变量, AI 经常生成用不上的中间变量 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // 禁止 console.log, AI 调试代码经常忘记删 'no-console': ['error', { allow: ['warn', 'error'] }], // 强制显式返回类型, AI 经常省略 '@typescript-eslint/explicit-function-return-type': 'error', // 禁止 any, AI 遇到类型问题就 any '@typescript-eslint/no-explicit-any': 'error', // 强制 await, 防止 AI 忘记 await 异步调用 '@typescript-eslint/no-floating-promises': 'error', // 禁止空 catch 块, AI 经常吞异常 'no-empty': ['error', { allowEmptyCatch: false }], // 强制 === 而非 == 'eqeqeq': ['error', 'always'], }, };这几条规则看起来普通,但每一条都对应一类 AI 生成代码的高频问题。no-floating-promises尤其重要,模型写异步代码时忘记 await 是家常便饭,这条规则能直接抓住。
5.3 从 Lint 到测试:构建多层自动防线
Lint 只是第一层。完整的自动防线应该是这样的:
| 防线 | 工具 | 拦截问题类型 | 执行时机 |
|---|---|---|---|
| 第一层 | ESLint + Prettier | 风格、语法、明显逻辑问题 | 模型输出后立即 |
| 第二层 | TypeScript 类型检查 | 类型不匹配、接口不一致 | Lint 通过后 |
| 第三层 | 单元测试 | 逻辑错误、边界条件 | 类型检查通过后 |
| 第四层 | 集成测试 | 模块间交互问题 | 提交前 |
| 第五层 | 人工 review | 架构、设计、业务逻辑 | 自动检查全通过后 |
这套防线的核心思路是:让机器先过滤掉机器能发现的问题,人只看机器看不出来的问题。我实测下来,这套流程能把人工 review 的时间压缩到原来的三分之一,而且漏网之鱼明显减少。
注意:不要指望模型自己跑测试。模型会“假装”测试通过,或者写一个永远为真的测试。测试必须由你或 CI 系统独立执行,模型写的测试只能作为参考。
6. 一百亿 Token 换来的实操心得与避坑清单
6.1 Token 消耗的优化技巧
先说几个我实测有效的 Token 优化手段,都是真金白银换来的:
第一,控制对话轮次。一个任务尽量在三轮对话内完成。超过三轮,历史上下文累积的 Token 消耗会指数级上升。如果三轮还没搞定,说明规格没写清楚,应该停下来重写 SDD,而不是继续对话。
第二,善用文件引用而非粘贴。大多数 AI 编程工具支持直接引用文件路径,让工具自己去读。这比你把文件内容粘贴到对话里省 Token,因为工具可以只读相关部分,而粘贴是全文进上下文。
第三,定期清理历史。长对话的历史是 Token 黑洞。完成一个任务后,开新对话,把必要的上下文通过 CLAUDE.md 和 SDD 重新提供,比拖着长历史继续聊省得多。
第四,输出要求具体化。不要说“帮我优化这段代码”,要说“把这段代码的时间复杂度从 O(n²) 降到 O(n log n),保持接口不变”。具体的要求能让模型一次到位,减少返工。
6.2 常见问题速查表
下面这张表是我踩坑踩出来的,覆盖了 AI 编程工作流里最高频的问题:
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 模型生成的代码 import 路径错误 | 上下文里目录结构过时 | 检查 CLAUDE.md 目录部分 | 更新目录结构说明 |
| 生成的代码风格与项目不符 | 缺少编码约定 | 检查 CLAUDE.md 约定部分 | 补充具体约定和示例 |
| 同一功能反复返工 | SDD 不完整 | 检查边界条件和异常处理 | 补全 SDD 后重新生成 |
| Token 消耗异常高 | 上下文过长或对话轮次过多 | 查看账单明细 | 分层上下文, 控制轮次 |
| 模型“假装”测试通过 | 让模型自己跑测试 | 检查测试执行方式 | 测试由 CI 独立执行 |
| 生成的代码有隐蔽逻辑错误 | 边界条件未覆盖 | 检查 SDD 验收标准 | 补充边界测试用例 |
| 多人协作时输出不一致 | CLAUDE.md 冲突 | 检查文件修改历史 | 设 owner, 走 PR review |
6.3 几条血泪教训
最后分享几条我用一百亿 Token 换来的教训,每一条都对应一次真实的翻车:
教训一:不要相信“看起来对”的代码。模型生成的代码第一眼永远是对的。我现在养成的习惯是,任何模型生成的代码,先跑测试,再看逻辑,最后才看风格。顺序不能反。
教训二:SDD 的投入永远值得。我早期为了省时间跳过 SDD,结果返工花的时间是写 SDD 的十倍。现在我宁可多花十分钟写规格,也不愿意多花一小时返工。
教训三:Lint 规则要跟着模型一起进化。模型会“学会”绕过你的 Lint 规则。比如你禁了any,它就用unknown然后强转。所以 Lint 规则需要定期 review 和补充,不能一劳永逸。
教训四:Token 账单要定期看。我见过太多团队月底看到账单才傻眼。建议每周看一次消耗趋势,发现异常及时排查。Token 消耗突然暴涨,往往意味着某个环节出了问题。
教训五:AI 编程工具是放大器,不是替代品。它放大你的判断力,也放大你的疏忽。你对项目理解越深,它帮你越多;你对项目越模糊,它坑你越狠。Code is cheap 是谎言,但“判断力 is expensive”是真话。
这套工作流我用了大半年,从最初的兴奋期,到中间的踩坑期,再到现在的稳定期,最大的体会是:AI 编程不是让你少思考,而是让你把思考花在更值钱的地方。写代码这件事确实变便宜了,但写出对的代码、可维护的代码、不出事的代码,从来没有便宜过。