上个月我接了个内部数据可视化平台的前端改造要在现有基础上补一套轻量级2D图形库覆盖十几类统计图形的绘制、动画和导出。坐在编辑器前我做了个以前绝不敢想的决定让AI来写。结果挺有意思——AI真把大约90%的代码写完了又快又规整可剩下那10%坐标变换方向、圆角边界、批量渲染的性能回收、动画收尾的跳变一个比一个阴差点把我整个项目坑成线上事故。这篇文章就把整个过程摊开讲AI在图形库这类项目里到底能扛到哪一步哪些环节必须人工接管以及我是怎么用测试和审查把最后那10%的地雷一颗颗排掉的。1. 为什么我会让AI去写图形库1.1 先交代清楚这套图形库要干什么我说的图形库不是那种通用渲染引擎而是针对数据可视化场景的轻量封装基础形状矩形、圆、椭圆、扇形、圆角矩形、路径折线、三次贝塞尔曲线、坐标变换平移、旋转、缩放、渐变色填充、粒子动效、以及把画布内容导出成图片或SVG。选原生Canvas而不是echarts、d3或者p5.js原因很实际目标项目的定制化程度特别高交互细节和视觉风格都跟业务强绑定通用图表库反而要绕路去覆盖它的默认行为自己维护一套小库体积可控行为完全可控出问题能直接定位到画布的某次draw调用。这个选择也天然决定了工作量不涉及复杂的底层引擎大部分代码就是文档里查API 教科书里抄几何公式 典型渲染流程的拼接。这种代码结构稳定、边界清晰、在开源世界里样本极多正是当前大模型输出质量最高的那类内容。所以从一开始我就不是心血来潮而是觉得这个项目非常适合做AI写代码的试点。1.2 把任务拆到AI能驾驭的粒度直接甩给AI一句帮我写一个图形库大概率得到一堆能跑但完全没法用的脚手架。真正有效的做法是先拆任务把整个库切成模块级的小单元数学工具库向量运算、颜色解析、插值函数、基础形状绘制器、变换系统、动画循环、批量渲染器、导出模块。每个模块单独给AI提出明确需求并约定好统一的坐标系和接口风格。比如我会在提示词里写死这些约束所有角度参数使用弧度制Canvas坐标系为y轴向下禁止使用任何外部依赖所有绘制函数必须返回当前绘图状态以便链式调用。这些约束不是在限制AI而是在帮它避开最常见的坐标系理解偏差。拆得越细AI每次需要思考和生成的代码量就越少出错的概率也随之下降——这一点在后面我会反复强调。1.3 对AI的长处和短板提前有数让AI干活之前我心里其实有个底这种库不是新东西GitHub上开源实现一大堆大模型训练时看过海量的Canvas绘制、贝塞尔计算、缓动函数代码所以常规模块它生成得又快又像样。但它有两个明显的短板第一它背下来的代码往往来自不同坐标系、不同API版本和不同编码习惯拼在一起容易出现不自洽第二它对当前项目的性能约束、生命周期和边界条件没有真实感知习惯写功能正确的代码不太会主动考虑极端输入下稳不稳。所以我在项目开始时就定了调AI负责产出人负责验收验收标准不是能跑而是在所有边界情况下都能跑。这套机制最终保了我一命因为坏代码往往不是在常规路径上爆雷的。2. AI确实漂亮地交出了90%2.1 基础形状与交互一次通过的快乐第一块让AI写的是基础形状模块。我给了它一份接口清单Rect需要支持位置、宽高、圆角半径、填充和描边Circle需要支持圆心、半径、起始角和终止角所有形状都要有点选和拖拽的事件回调。AI返回的代码比我想象中成熟很多——它不仅把绘制函数写全了还自动处理了beginPath()、closePath()的调用顺序甚至内部封装了一个简单的命中检测用ctx.isPointInPath判断点是否落在形状内部。这个模块我几乎没改就直接合入了。AI之所以表现好是因为画一个圆角矩形这类需求在训练语料里太常见了正确写法已经被反复强化过。我当时的感受是这种可复用的、标准化的API封装确实是AI的舒适区给它一个清晰的接口签名它能还你一个教科书级的实现。2.2 数学工具集看起来稳边界还得自己补数学工具模块也是AI的强项向量归一化、线性插值、颜色值在hex/rgb/hsl之间的转换这些函数它写得飞快而且大部分逻辑正确。举个例子它给的hex转rgba函数能正确解析#RGB、#RRGGBB、#RRGGBBAA三种格式还把透明度、浮点精度的处理都考虑到了这段代码比我自己手写的还干净。但我也发现了一个典型问题AI写的函数对合法输入的处理很好但对非法输入几乎不设防。归一化零向量会产生NaN负半径会被原样往下传非法颜色字符串会返回一个看似正常实则错误的黑色。所以在合并这个模块前我花了一个下午补齐了边界测试空值、负数、超范围角度、超大数值、undefined参数一个case一个case地喂。测试通过后才敢把图形库的数学地基交到它手里。这是我的第一条经验AI写工具函数可以放心让它产出主逻辑但边界保护一定要人肉补齐否则后面所有依赖它的模块都会跟着坏。2.3 动画与渲染脚手架能跑但先别急着信动画循环部分AI给的方案是标准做法requestAnimationFrame驱动的主循环加上一个基于时间戳的帧间隔计算把渲染频率控制在60fps。它还自动处理了页面切后台时动画暂停、切回来后时间补偿的问题这部分代码的质量超出了我的预期。批量渲染的第一版也顺利跑起来了——一个简单的粒子系统demo300个粒子在画布上飘动帧率稳定在60fps。我当时还挺满意以为性能这块也稳了。现在回看这种demo场景跑得通恰恰是最容易麻痹人的阶段因为性能问题的爆发条件往往藏在数量级和调用频率里当初demo里300个粒子根本触及不到后面那个雷区。这个问题后面会专门展开讲。2.4 导出功能几乎不用改的惊喜导出模块包括Canvas转PNG、转SVG以及把内部图形数据序列化成JSON。这类功能本质上是标准API的薄封装canvas.toDataURL()、canvas.toBlob()、SVG字符串拼接流程固定、变化极少。AI写出来几乎没有需要修改的地方尤其是SVG导出那段它把矩形、圆形、贝塞尔路径全映射成了对应的SVG标签连fill-rule和stroke-linecap都处理对了。到这一步项目进度推进得异常顺利我当时甚至开始盘算后面是不是可以批量用AI做其他模块。幸好这种乐观情绪没持续太久因为接下来要处理的就是整个项目里最难的10%——坐标变换、极端边界和性能瓶颈。AI在这些地方的表现把一个AI很行的好故事瞬间拉回现实。3. 剩下10%的翻车现场3.1 先把事故清单亮出来等真正做起复杂图形和动效时问题开始集中爆发。我把遇到的主要事故列了个清单每个都是会直接导致视觉错误或卡顿的硬伤旋转45度的方块在画布上往左下角跑了旋转方向肉眼可见是反的先缩放再平移的变换序列图形直接飞出画布可见区域圆角矩形的圆角半径设置超过盒子尺寸一半后出现一段诡异的反向圆弧贝塞尔曲线的等分点标注重叠用弧长参数化修复后才正常粒子数量一上5000帧率从60fps直线掉到20fps动画长跑后结束瞬间总是会出现一次明显的视觉跳动。这些bug有一个共同特点它们都不是跑不起来级别的错误而是看起来一切都正常但特定条件下结果错了的隐蔽问题。这类问题恰恰是AI生成的代码最危险的形态因为常规测试根本覆盖不到。3.2 旋转矩阵坐标系把AI骗了先说那个最要命的旋转bug。图形库需要一个旋转矩形的功能AI给出的实现是标准的二维旋转矩阵// AI初稿数学系标准旋转矩阵 function rotatePoint(x, y, angle) { const c Math.cos(angle); const s Math.sin(angle); return { x: x * c - y * s, y: x * s y * c }; }单看这段函数数学系同学挑不出毛病。但在Canvas坐标系里y轴是向下的这个矩阵意味着正角度在视觉上做顺时针旋转而我们在业务上的约定是正角度逆时针旋转。结果就是想让方块逆时针转45度它偏偏顺时针跑了所有图形的朝向关系全部镜像。排查的时候我一度怀疑是调用方把角度传错了直到用Math.atan2打印了几个点的实际位置才发现是旋转矩阵的符号约定和Canvas坐标系的y轴方向打架。修复方案是把sin项的符号反转按Canvas坐标系重写// 修复后适配Canvas y轴向下坐标系 function rotatePoint(x, y, angle) { const c Math.cos(angle); const s Math.sin(angle); return { x: x * c y * s, y: -x * s y * c }; }这事的教训很直接AI的所有几何知识基于标准数学坐标系但Canvas、SVG这类图形环境用的是y轴向下的屏幕坐标系。正方向的定义一旦不同旋转、角度、atan2的符号全都会错。以后我交给AI的图形任务无一例外都会在提示词里强调坐标系方向并且加一条硬性要求——旋转方向测试用例必须包含正角度和负角度两种情况。3.3 变换组合scale和translate的顺序之争第二个事故发生在组合变换上。需求是做一个先缩放2倍再向右平移10像素的图形变换。AI按字面意思直接写了// AI初稿字面顺序 ctx.scale(2, 2); ctx.translate(10, 20);但Canvas的变换系统里后调用的变换会先作用于图元。所以这段代码实际执行的是先向右平移10像素再整体缩放2倍位移量也被翻倍成了20像素。在平移量小、缩放倍数接近1时这种偏差很难肉眼发现可真做地图缩放那种大比例变换时图形会直接飞到画布外面去。解决这个问题我当时在代码里加了组防守性的注释和顺序约定同时改用显式矩阵组合来彻底绕开API顺序心智负担// 修复方案显式组合矩阵先缩放再平移 // 注意Canvas的transform参数顺序a, b, c, d, e, f function composeScaleThenTranslate(sx, sy, tx, ty) { ctx.setTransform(sx, 0, 0, sy, tx * sx, ty * sy); }这里的核心原因是矩阵乘法不满足交换律而Canvas的transform方法又是按调用顺序后应用的语义AI从训练数据里学到的是字面顺序即应用顺序于是踩进了坑。我最终的结论是涉及多个变换叠加时不要在代码里连续调用scale/translate/rotate而是手动算好最终的矩阵值再用setTransform一次性设置。这样既清晰又不会因为调用顺序的迷惑性产生运行时意外。3.4 圆角矩形arcTo的隐藏规则圆角矩形这个功能前面基础模块AI写得很顺利但用到极端参数时就出问题了。现象是这样的一个宽100、高40的盒子圆角半径设成35渲染结果里居然出现了一段反向弯曲的弧线整个图形看起来像被什么力量掰弯了。问题出在ctx.arcTo的隐藏约束上。Canvas的arcTo方法要求圆心角半径不能超过两条连接线中较短那条的一半一旦超过它会按照自己的规则生成一条大半径弧方向可能跟预期完全相反。AI的代码直接用了传入的半径完全没有做钳制// AI初稿没做半径钳制 function drawRoundedRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x r, y); ctx.arcTo(x w, y, x w, y h, r); // ... }修复起来倒不难关键就是加一道钳制// 修复方案半径钳制在半个短边以内 function drawRoundedRect(ctx, x, y, w, h, r) { const half Math.min(w, h) / 2; const radius Math.max(0, Math.min(r, half)); // 后续用radius绘制 }这条是我送给所有做Canvas图形开发的人的一句话但凡涉及arcTo、arc、quadraticCurveTo这类几何API边界条件一定躲不掉AI不会主动提醒你传入半径超过盒子尺寸一半会出事。它只会按照通用写法输出看起来正确的代码真正解决问题的是人肉补上的clamp逻辑。3.5 贝塞尔等分点均匀t不等于均匀距离这是整个项目里让我熬夜最久的一个问题。需求是给一条三次贝塞尔曲线生成等间距的标注点比如在曲线上均匀放10个文字标签。AI的初始实现特别符合直觉把参数t从0到1均匀取10个值每个值用de Casteljau算法算出对应坐标// AI初稿均匀采t再算点 function sampleCubic(p0, p1, p2, p3, n) { const points []; for (let i 0; i n; i) { const t i / n; points.push(deCasteljau(p0, p1, p2, p3, t)); } return points; }这段代码的问题在于贝塞尔曲线的参数t和曲线实际长度之间不是线性关系。控制点分布不均匀时t变化相同的增量在曲线上的距离差可能非常大。我把一个控制点拉到远离端点的地方跑了一下结果前30%的标注点挤成一团中段稀疏后段又挤成一团视觉上完全没法看。要修就得做弧长参数化先把曲线按足够小的步长切成大量小段用梯形法或辛普森法算出每一小段的弧长并累加得到一条t对应累计弧长的查找表然后要找给定弧长对应的点就在这个表上二分查找对应的t。修复后的代码大概是这样的// 修复方案基于弧长表的等分采样 function buildArcLengthTable(p0, p1, p2, p3, steps 200) { const table []; let prev deCasteljau(p0, p1, p2, p3, 0); let length 0; table.push({ t: 0, length: 0 }); for (let i 1; i steps; i) { const t i / steps; const cur deCasteljau(p0, p1, p2, p3, t); length Math.hypot(cur.x - prev.x, cur.y - prev.y); table.push({ t, length }); prev cur; } return table; } // 给定目标弧长二分查找t function findTByLength(table, target) { let lo 0, hi table.length - 1; while (lo hi) { const mid (lo hi) 1; if (table[mid].length target) lo mid 1; else hi mid; } return table[lo].t; }这个修复实现起来并不复杂但如果没有认识到均匀t不等于均匀距离这个本质怎么调都是白费。AI在这个问题上的失误不是写错代码而是对想当然的简洁算法缺乏警惕。这也让我总结出一条原则凡是公式型、映射型的算法AI产出的第一版往往是最直观但不够精确的方案必须针对精度写专门的测试。3.6 五千粒子卡成PPTGC比逻辑更致命性能事故是另一个坑。功能层面一切正常可粒子数量从300增加到5000后帧率瞬间从60fps掉到20fpsCPU占用率飙升到90%。我一开始怀疑是绘制逻辑太复杂逐行排查之后才发现问题出在资源创建上。AI写的渲染循环里每一帧都会创建新的渐变对象和新的Path2D对象// AI初稿每帧new对象导致GC风暴 function render(particles) { particles.forEach((p) { const grad ctx.createLinearGradient(0, 0, p.x, p.y); const path new Path2D(); path.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle grad; ctx.fill(path); }); }createLinearGradient和new Path2D都不是免费的午餐它们会触发生成原生对象、分配内存、注册到Canvas上下文等一连串昂贵操作。5000个粒子每帧都这样玩等于每秒钟创建30万个临时对象GC线程忙到飞起渲染主线程被反复打断。修复方向很明确对象复用。渐变只在粒子属性变化时创建一次Path2D按粒子半径分组缓存到对象池里每帧只更新位置相关的变换// 修复方案对象池 复用Path2D const pathPool new Map(); // key: 半径, value: Path2D function getOrCreatePath(radius) { if (!pathPool.has(radius)) { const path new Path2D(); path.arc(0, 0, radius, 0, Math.PI * 2); pathPool.set(radius, path); } return pathPool.get(radius); } function render(particles) { particles.forEach((p) { // 只用translate改变位置不重建Path2D ctx.save(); ctx.translate(p.x, p.y); ctx.fill(pathPool.get(p.r) || getOrCreatePath(p.r)); ctx.restore(); }); }修完以后5000粒子的帧率回到了55fps上下。这件事给我的冲击比语法错误大得多AI的代码在功能上是完全正确的它甚至能帮你把粒子位置计算得头头是道但它不考虑这个对象每帧都被重建这种性能层面的反模式。性能优化必须靠人肉压测去发现指望AI自动规避还早得很。3.7 动画最后一帧的跳变最后一个视觉问题是动画收尾时的跳变。我们做了一组缓动动画物体从A点滑向B点核心逻辑是AI用速度衰减实现的每帧根据剩余距离调整速度当剩余距离小于某个阈值时直接把位置设为目标值。// AI初稿剩余距离小于阈值直接归位 if (Math.abs(target - current) 0.001) { current target; return; }这个阈值在多数情况下是感知不到的但有一个特殊场景会露馅页面切后台又切回来或者动画跑得特别久物体逼近目标但距离阈值还有一点点差距时视觉上会突然加速滑完最后一小段。特别是配合阻尼系数调得比较大的时候这个跳变非常刺眼。我把这套数值趋近逻辑换成了基于时间线的插值方案动画的总时长和进度是确定的渲染时直接用easeOutCubic这类缓动函数根据进度算位置不再用每帧逼近目标的方式。这样无论动画跑多久、在哪一帧结束最后的位置都是精确落在目标上的天然不会有跳变。经验分享动画系统这种状态持续累积的代码用每帧逼近目标的写法看似直观但几乎没法精确控制终态。时间线插值才是正确的打开方式。AI在这个问题上给出的方案属于功能可用、手感粗糙的类型最终还得靠人来定策略。4. 给AI代码装一套安检门4.1 AI写的测试只测到了它自己知道的正确前面几个事故暴露了一个共同问题AI生成的单元测试根本抓不住这些bug。原因在于AI写测试时习惯直接对实现写断言——它知道自己实现了什么于是测试就验证它自己知道的东西。比如旋转函数AI写的测试会断言旋转45度后坐标符合它自己实现的矩阵结果可这份断言恰恰把错误的符号也一并验证成了正确。正确的做法是让测试锚定规格而不是锚定实现。动手写代码之前我先写一份行为规格正角度在Canvas坐标系里应该是逆时针旋转组合变换先缩放后平移的最终位移量等于平移量乘以缩放系数圆角半径必须被钳制在盒子尺寸的一半以内。然后让AI根据这份规格去补测试用例。这种先规格、后测试、再实现的顺序看起来慢实际能省掉后面不知道多少排查时间。4.2 四个必须人工盯防的重灾区经过这一轮实战我总结出四类AI生成代码的重灾区只要涉及图形、坐标、性能相关项目这几处我都不会再交给AI自由发挥第一是坐标系与方向定义。Canvas/SVG的y轴向下、角度方向、Math.atan2返回值的象限约定每个都可能和AI从数学教科书里背来的知识打架。第二是矩阵与变换组合。矩阵乘法不满足交换律API的调用顺序和应用顺序相反这类顺序敏感性代码必须人工核对最终效果。第三是数值稳定性与边界条件。除零、负半径、非法颜色、超大角度AI通常不会主动防御。第四是性能与生命周期。临时对象创建、事件监听器绑定与解绑、Canvas状态的save/restore配对AI很少主动考虑资源复用和内存泄漏。这四个领域我现在的策略是让AI写出初版代码然后我逐行review重点盯边界分支和资源生命周期。不是不信任AI而是这些地方一旦出错排查成本远高于编写成本。4.3 视觉回归用像素级diff兜底单元测试能抓逻辑错误可像旋转方向反了、圆角出现反向弧这类视觉问题断言写起来很别扭。我额外加了一道保险视觉回归测试。做法是把每个图形模块的示例渲染成固定尺寸的Canvas再导出成PNG作为基准图每次代码变更后重新渲染一次然后用像素对比工具把两张图的差异统计出来。只要差异超过预设的阈值比如0.1%的像素点就说明这次改动引入了视觉变化。这个机制特别适合对付AI代码的无征兆回归。有一次AI改了个看似无关的排序逻辑结果影响到一个图形的绘制顺序渲染结果整体变了像素diff立刻报警。这种问题靠人工肉眼对比几十个示例页面不盯到崩溃是发现不了的。注意像素对比方案的阈值要分场景调。抗锯齿导致的边缘像素差异很常见阈值设太严会天天误报建议先跑一遍基线生成差异统计再按项目实际容忍度设定阈值。4.4 让AI分步干活而不是一口气甩五百行另外一条重要经验是控制单次生成代码的规模。我试过把一个功能复杂的模块一次性丢给AI结果它输出的代码内部自相矛盾函数签名前后不一致、一个常量被两个不同地方以不同值定义、局部变量名遮蔽全局变量。后来我改成把任务拆成10到20个小文件每次只让AI生成一个独立的纯函数或一个小类生成后立刻跑一遍测试和视觉示例确认没问题再继续下一个。这种分步式迭代看起来效率低实际反而整体更快。因为AI在单文件范围内的正确率远高于大模块而模块间的耦合问题会在第一步就暴露出来而不是等到代码全部生成完才发现一个绕不开的接口错误。跟AI协作写代码本质上是把它当成一个需要频繁反馈的协作者而不是丢一个需求就等着验收的工具。5. 一张速查表与我的AI编程工作流5.1 图形库常见AI失误速查表把这次的坑整理成一张表方便以后做排查参考症状根因处理方式旋转方向与预期相反Canvas坐标系y轴向下AI用了标准数学旋转矩阵对sin项符号取反补充正负角度测试组合变换后图形飞出画布scale/translate调用顺序与应用顺序不一致改用setTransform显式传矩阵值圆角矩形出现反向弧arcTo的半径超过短边一半未做钳制clamp半径到min(w, h) / 2贝塞尔等分点间距不均匀均匀采样参数t误当成按弧长均匀采样建立弧长表二分反查t粒子数量增加后帧率暴跌每帧创建Path2D/渐变对象引发GC风暴对象池复用、减少每帧临时对象动画收尾视觉跳动用距离小于阈值直接归位的数值逼近法改用时间线插值精确控制终态5.2 AI能扛和不能扛的边界这轮项目下来我心里对AI写图形库这件事有了非常清晰的分界线。AI真正能打的部分是接口定义相对固定、逻辑路径明确、有海量开源参考的标准代码——基础形状绘制、颜色解析、坐标转换数学工具、Canvas/SVG标准API封装、常规动画循环框架这些交给AI效率和正确率都很高。AI还远不能放心扛的部分是三类问题第一类是与运行环境强相关的细节知识比如Canvas坐标系方向、API的隐藏边界条件第二类是序列敏感的算法比如变换组合、弧长参数化这种看着简单、顺序和精度决定成败的地方第三类是性能与资源管理AI的代码写出来是逻辑正确的但它不会主动考虑每帧创建30万个临时对象会不会把GC压垮。这90%和10%的划分现在已经成为我评估所有AI编程项目的标准模板。5.3 我现在实际在用的协作流程这套工作流现在基本固定下来了。每接到一个模块我第一件事是写接口规格和行为规格明确输入输出、坐标系约定、边界行为然后让AI根据规格生成初版代码重点是主流程和标准算法代码回来后我不急着合并先补边界测试、跑视觉示例、做一轮针对矩阵和生命周期的人工review确认都通过了再进入下一个模块。这样做的代价是前期准备工作变多了但收益非常明显AI生成代码的速度优势被完整保留而它最容易出错的边界和性能环节又被人工防线兜住了。最重要的是整个项目里几乎没有出现过上线后才发现视觉错误的情况这是以前纯手工开发时都不一定能保证的。我个人的体会是AI写图形库回答是能而且90%都能。但那10%的坑每一个都足够让项目回滚、让用户截图吐槽、让开发熬到凌晨。别因为前90%太顺就放松警惕——图形编程的水永远藏在坐标系、边界条件和运行时的细节里。现在团队里已经有一条不成文的规定凡是涉及transform、arcTo、性能循环和坐标系方向的代码合并前必须过一遍人肉review不管它是人写的还是AI写的。这条规矩就是这次项目拿命换来的。