开头碎碎念
好几个月没更新了,这段时间一直在实习,自己又比较懒就没有去做分享。对于现在AI的快速发展也是如此,感觉自己知道很多,但是实践过少,所以对于agent的使用也只是比较普通的对话+skill的使用,harness什么的听说过,但没实践,所以这篇文章也是比较浅薄,提出的很多问题应该都有解决方法了。本文没有太多建设性建议,可以当作一个普通的吐槽文章。好了说一下我在AI编程中遇到的问题吧
背景
本次在实习中参与了一个新项目的开发(属于老项目中的,但是又是独立出来的一个工具),AI给我的感觉就是,拿到需求文档和接口文档后就开始框框写代码,但是写出来的代码又很混乱且就算拿到了html原型图也难以一步到位还原设计稿。对于分组件的编写也需要人为干预才行。所以如果前期没有约定好规范的话后面的代码就是一坨屎山,出了bug也是在屎山上拉屎。所以这几天虽然没有什么工作,但是我一直在去做cr去重构一些逻辑,真的很累啊。(文中示例代码均做过处理或为废稿,不存在泄露问题)
主要问题
1.不太喜欢分组件编写
场景是该项目需要在一个页面里完成五个阶段的AI自动化美工操作,不同阶段显示的内容和数据状态不一样。按我个人的想法是分成五个不同的页面组件,放在一个总的组件里,我们先叫这个组件为<Main/>吧。由<Main/>来统一管理这五个阶段,但是AI他最开始就会把所有内容都放到这个Main中,这会让组件非常的臃肿。但是在hook的编写上AI又会自发性的去分文件编写,这让我挺不理解的。包括一些弹窗组件AI也喜欢全部放Main中,唯一的好处可能只有查bug的时候可以不用切文件了吧。
2.防御性代码过多
这部分也是很头疼,某些场景下确实需要防御性代码,但是AI给我一种过度防御的感觉,什么事情都喜欢做兜底。当然兜底是好的,但是在开发的过程中过多的兜底我感觉会影响我对bug的判断,而且对于一些非常明确的东西AI还是会自己去脑补很多可能的问题,去写非常多的冗余代码。而且因为前期的防御性编程导致后续修改代码会感觉像在屎上拉屎。下面举几个例子
例一.请求字段兜底过多
首先来看一段AI生成的一个展开相应内容获取对应字段数据的函数。在有明确的接口文档的情况下他依旧为了防止字段识别错误多写了很多兜底代码。这让代码看起来非常的凌乱,实际上真正需要的代码只有短短的一句return{返回对应字段数据}。问他为什么要这样做,他说了半天意思就是怕前后端没对齐,后端返回的字段有误。我认为的就是后端没有按规定返回字段的情况下,如果我前端还做了这么多兜底的话那就相对于是掩盖了后端的错误,这样对后面项目的维护会有不好的影响。倒不如接口文档写的是什么我就对应的去接收,有错误让出问题的人按规范编写就是了。
function parseImageItem(im: Record<string, unknown>): ImageItem | null { const imageUrl = String(im.imageUrl ?? im.image_url ?? im.url ?? im.imgUrl ?? '').trim() if (!imageUrl) return null const imgSrcRaw = im.imageSource ?? im.image_source ?? im.imgSource ?? im.img_source const itemStageRaw = im.itemStageAction ?? im.item_stage_action ?? im.subStageAction ?? im.sub_stage_action ?? im.bizStage return { historyId: im.historyId != null ? String(im.historyId) : im.history_id != null ? String(im.history_id) : undefined, imageUrl, imageName: String(im.imageName ?? im.image_name ?? im.name ?? '图片').trim() || '图片', sourceType: String(im.sourceType ?? im.source_type ?? ''), sourceId: String(im.sourceId ?? im.source_id ?? ''), imageId: im.imageId != null ? String(im.imageId) : im.image_id != null ? String(im.image_id) : undefined, imageSource: imgSrcRaw != null && String(imgSrcRaw).trim() ? String(imgSrcRaw).trim() : undefined, itemStageAction: itemStageRaw != null && String(itemStageRaw).trim() ? String(itemStageRaw).trim() : undefined, historyStage: toNumber(im) } }例二.屎上拉屎
这个方法解决的问题是重复请求的问题。举个例子,当我打开一个图片列表的时候,前端会向后端发送图片列表的请求,正常情况其实发送一次如果返回200了那就应该不用发送了,现在的情况是无论是否成功,都至少重复发送了两次请求。排查问题发现是AI为了同步当前任务的状态,会在Main中进行一次请求(请求来干嘛我也不知道,他说为了同步),然后在列表打开的时候也会请求一次,这就导致了重复请求,为了解决这个问题我告诉他我们只是单纯的展示数据而已,不涉及到状态同步。于是乎,他给我整出了以下代码。
const currentRuquest = new Map<string, Promise<unknown>>() /** 并发合并:已有在途请求则直接返回其 Promise,避免重复 HTTP */ function dedupeConcurrentSlotsRead<T>(taskId: string, execute: () => Promise<T>): Promise<T> { const existing = currentRuquest.get(taskId) as Promise<T> | undefined if (existing) return existing const pending = execute().finally(() => { if (currentRuquest.get(taskId) === pending) currentRuquest.delete(taskId) }) currentRuquest.set(taskId, pending) return pending }给大家解释一下这段代码,里面的execute就是我们发送的请求,调用他会执行一个promise。existing指向的是currentRequest中存放的当前任务中已经发送出去且还未返回响应的请求。如果existing存在(请求发送中),就直接返回改请求,如果不存在说明还未发送,就会执行execute()并执行currentRuquest.set(taskId, pending),请求发送完后会执行delete操作。串行就不用说了,如果是并发的请求因为发送的时间几乎是同时的,所以在第二个请求返回200前就会执行delete,这就避免了重复发送。但是回到最开始的问题,其实我们只需要在打开列表的时候按需请求一次就好了,不需要非常麻烦。
3.不喜欢用外部库
这一块的问题就是很多时候原生的工具虽然可能运行效率高,但是在代码的可读性以及可维护性上会差不少,增加开发负担。不过把代码编译成原生的形式来提高性能我倒是觉得是一个很有意思的优化点,实际上vue团队也已经在尝试了(抛弃vdom,直接编译成js直接操控真实dom),这个以后单独开文章讲。
本次遇到的最大的问题是在状态管理方面,像vue有vuex、pinia这两种主流方案,react也有Redux等方案。但是AI独爱props传递,搭配子组件回调,这样一来组件深的时候层层传递,代码就会变的非常难看。给我最大的感触就是在项目中有一个颜色切换的功能。有那么多方案他偏偏选择了用props去一个一个传递theme,真的受不了了。
4.变量名简略
这个不知道是不是跟我的提示词有关系。我让他用最少最简洁的代码去解决问题,他就直接开始用const a这种变量名来写代码了,我自己cr的时候都没法一眼明白他在干嘛。下面举个例子
const did = props.dialogId.trim() if (!did || typeof sessionStorage === 'undefined') { clear() return } try { const raw = sessionStorage.getItem(ExportSessionKey(did)) if (!raw) { clear() return } const o = JSON.parse(raw) as { ids?: unknown[]; urls?: unknown[] } ExportImageIds.value = new Set( (o.ids || []).map((x) => String(x).trim()).filter(Boolean) ) extraExportImageUrls.value = new Set( (o.urls || []).map((x) => String(x).trim()).filter(Boolean) ) } catch { clear() }总之就是很难受。
4.过度设计
这个是老生常谈了就不过多赘述了。
总结
暂时先写这么多,还有数据结构设计等问题也是让人感觉无效代码过多。以上问题其实都是项目最开始没设计好规范导致的问题,现在其实都有很多skill和牛马工程来进行约束了,比如前段时间的claude.md,里面就做了很多不错的约束。这方面我觉得可以仔细研究一下claude源码,里面的设计模式还是很有意思的,有时间我研究完出一篇文章讲讲这个。归根结底就是磨刀不误砍柴工,要从架构师的视角去完成需求,而不是普通的程序员。
好了,要去优化屎山了,祝好。