# 我让AI从0做了一个网站，最后结果怎么样？

URL: https://caijiao.org/posts/177
Source: docs/posts/177.md
Description: 从空目录到本地跑通用了不到一天，后面又补了整整两周：金额算出了浮点尾数、13MB 的图把主线程锁死、静态部署时子路由 404。AI 省下的是写第一版的时间，省不掉的是这三处收尾。

动手之前我先定死了验收标准，免得最后变成"看着挺好"这种没法讨论的结论。需求是一个图片格式转换的小工具站：文件在浏览器里处理、不上传服务器，页面上要有三档套餐，月付年付可切换，年付要显示折后的月均价。

加钱的计算和加图片的处理，是因为我想看它在两类地方翻不翻车——**涉及钱的计算**和**占 CPU 的计算**。这两处恰好是 demo 能过、真实用户一进来就露馅的地方。

## 第一版确实比我预想的快

不到一天，本地 `npm run dev` 就有了一个能点的东西：路由分好了、每个工具一个独立 URL、列表页有分页、Tailwind 类名铺到了每个角落。

超出预期的具体是一件事——它自己把工具拆成了独立路由，而不是一个 SPA 里的 Tab 切换。这个选择很差劲的地方在于写起来更麻烦，但对一个靠搜索活着的站来说是最关键的：**每个工具必须是能单独被抓取、单独被打开的页面**。我没提这个要求，它照着自己的理解做了。

## 金额那行，它写错了两次

年付折后月均价的第一版是 `(plan.monthly * 12 * 0.8) / 12`。逻辑对，结果在页面上显示成 `15.920000000000002`。

问题不出在逻辑，出在它用了浮点做金额链式运算。JavaScript 里 `19.9 * 12 * 0.8` 就已经带着尾数了，再除回来也没救。第二版它改成 `toFixed(2)`，这是更糟的一步：`toFixed` 返回字符串，我没注意到它后面又把结果乘了一次，字符串被隐式转回数字，尾数原样回来了。

## 比写错更麻烦的：它会把不确定写成确定

生成期间我反复遇到一个现象：问它某个环节是不是该用 A 方案，它会给出一段非常完整的解释，然后落到 B 上。这不是答错，是它默认要为每一个问题给出结论。

具体到这个站：它选了浏览器原生的解码能力，没有说明任何约束，也没提兼容性边界。这些本该由人来提的部分，被一句自信的描述盖过去了。

后来我把所有"你不确定"的地方改成让它列假设而不是下结论——**把决策点显式暴露出来，把实现交出去**。这一条改完，后面几轮的返工次数明显下降。

最后我把规则写进上下文让它重写：**金额全程用整数分参与运算，只在渲染那一层做格式化**；入参用 schema 校验，不接受浮点数进入计算。这段逻辑现在是全站唯一一处我要求它必须写单元测试的地方。我后来给它定了一条硬规矩：**涉及钱的表达式里不允许小数直接参与乘除**，必须先把最小单位——也就是"分"——提出来参与左侧的所有运算。`toFixed` 只允许出现在最后渲染的那一句里，不允许出现在任何结果会被再次参与计算的表达式中。

补的三组测试是这么选的：含税的、跨月折算的、以及折扣后正好出现半分的那种金额。最后这个用例它第一版直接四舍五入掉了，对订阅产品来说，这是会被用户指着账单来问的。

这类精度问题的处理方式在[金额与浮点精度](/decimaljs/)里讲得更细，值得先看完再回来改 AI 生成的代码。

## 13MB 图片进来时，页面完全不动了

单张 13MB 的 JPEG 走 canvas 缩放再重新编码，主线程从点下按钮到出结果之间没有任何响应——不是慢，是彻底的假死，连 hover 都不动。

更要命的是它写的进度条是假的：整个处理是同步串下来的，浏览器根本没有机会重排，进度值从 0 直接跳 100。

正确的做法是把解码和编码搬进 Worker，主线程只负责把文件读成 ArrayBuffer 传过去。这里有个 AI 常踩的具体坑：**Worker 里没有 `document`**，它第一版搬过去的代码在 Worker 里用了 `OffscreenCanvas` 之外的 DOM API，控制台直接报错。图片重编码这种活也可以直接换用预编译好的编解码器，[WebAssembly](/webassembly/) 那条路省事得多，代价只是要多加载一个二进制。

连带的问题在内存。一张 13MB 的 JPEG 解码成位图之后体积会放大一个量级，而它生成的实现同时握着原始 ArrayBuffer、解码后的位图和编码结果三份数据不放，处理两三张之后标签页就涨到几百 MB。正确的做法是每一段处理完立刻释放，并且限制同时处理的张数——这个限制它没加。

## 部署这道坎，它完全不知道存在

本地跑得好好的，推到静态目录用 nginx 托管，刷新一个子路由就 404。原因是单页应用的 history 路由需要服务端兜底，`try_files` 没配；另外 wasm 文件的 `Content-Type` 必须是 `application/wasm`，配错的话 `instantiateStreaming` 会失败，而它生成的兜底分支没写，于是浏览器就静默拿了坏结果。静态服务的这几项配置在[nginx 教程](/nginx/)里是一节就能讲完的事，但在 AI 生成的项目里从来没人提。

## 一天上线，和十二个月有人来，是两笔账

terrific.tools 的作者老老实实做了 12 个月，2025 年 11 月迎来第一个完整变现月：展示广告 174.41 美元，加上卖桌面版应用的 125 美元，合计不到 300 美元，同期每千次会话广告收入 6.89 美元。

![terrific.tools 官网首页截图](/images/2026-09/terrific.tools.png)

*terrific.tools 首页在显著位置推 macOS 和 Windows 桌面应用，文件转换可以离线跑。*

Omni Calculator 那头是另一个量级：2026 年 6 月的估算月访问约 1429 万，其中 `/other/test-grade` 这一个页面的主词月搜索量 5900，带来的月流量是 61086。一个页面。

把这两组数字放在一起看，AI 一天交付的是"上线"这个动作本身，而 12 个月和 1429 万描述的是另一件事——有人从外面走进来。

下次再开新项目，我会在第一段提示词里就把三条约束钉死：金额一律走整数分、任何超过一帧的处理必须离开主线程、告诉我你的静态服务器是什么。它是一个能把明显的事做对的合作者，只是它不知道你跑在什么上面。

---

*terrific.tools 数据来自作者在 Indie Hackers 的 2025 年 12 月更新；Omni Calculator 访问量为 2026 年 6 月第三方估算。*
