盲盒还是工具?不同视角下的 AI 编程

151 阅读9分钟

image.png

AI 编程的进化历程

众所周知,AI 编程工具在短短几年内经历了一场颠覆性的进化。我作为一名前端开发者,很荣幸见证了 AI 从一开始的 “一本正经的胡说八道” 到后面一步一步成长为 “直接接管项目的资深架构师”。在这个进化的过程中,相信大家都有相同的感受,有看见 AI 写代码能力变得越来越强的兴奋,也有随时被 AI 替代的焦虑。

在近几年内,相信大家都换了很多种的编程工具。不可否认的是 AI 的能力变强的同时,随之变强的还有我们的适应能力和接受能力。从早期的 “对话生成式” 到今天的 “全自动 Agent 辅助”,我简单的说下我深度使用过的几款 AI 编程工具的感受吧!

  1. 文心一言: 作为当时第一批文心一言的使用者,再回头看,不得不说百度真的是 起来个大早,赶了个晚集
  2. GPT: 少数的“业界灯塔”,也是身边人用的最多的一款,稳如老狗。
  3. DeepSeek: 给我的感受是最强的时刻停留在了2025年的春节前后,属于国内少有**“高光神话”**。
  4. 元宝: 回答简洁,在 gemini 没有出现之前是国内少有的不错选择。
  5. 豆包: 问代码相关的问题比较少,主要用于日常答疑,情商高,接地气。
  6. gemini: 能力全面的优等生,也是少有的目前还在使用的 AI工具。
  7. Cursor: 我可称之为颠覆工作流的“生产力怪兽”,属于直接重构了我的编码习惯,让我感受到巨大的危机感。
  8. Trae: Cursor 太贵,退而求其次的选择,对国内开发者极度友好,但后期免费变成越来越慢,问问题需要等待很久,最后放弃。
  9. codex: 公司付费,Token 不限制,用起来很爽,虽有弊端,但不可否认是目前最强辅助。

不同的人使用 AI 工具的区别

这里以我最近几个月在使用 Codex 举例,对于专业的程序员来说,它更像是一个机智的效能工具,你可以享受它思考的结果,但是最终的代码审查必须由你主导。它可以用来消除重复劳动、提高生产力,你尽管把那些脏活累活扔给它,它不知疲倦,大多数时候可以完美的完成工作,甚至有时候写出来的代码比你自己写的更好,也有时候写的代码你会看不明白,还有时候写的代码会重复啰嗦,需要你不断的的纠正他。但长时间相处过后你却越来越依赖它,总之它更像是亦师亦友的存在。

代码分析

如果你没有使用过,可能很难有那种复杂的感受。这里为了刚好的说明问题,举个🌰

为了更好的理解需求变更:可以参考图片的描述,大致知道页面的功能。

3c4aa8f41c2b271fdfa7879d40645163.png

假设有这样一个需求变更,我会这样告诉 codex 的。如图:需要在绩效考核明细的每一行的评分人后面都添加一个填充的图标,当点击填充图标的时候,就复制当前行的评分人自动填充到后面的评分人中,注意此功能只限制在当前供应商对应的评分项中,不可以夸供应商进行复制填充,同时将填充的逻辑放在 utils.ts 文件中。

于是等待 3m52s 你大致会得到这样的一串代码:

function collectScoreLineRecords(
  records: DataSetRecord[] = [],
): DataSetRecord[] {
  return records.reduce<DataSetRecord[]>((result, record) => {
    if (!isSupplierRecord(record)) {
      result.push(record);
    }
    const children = (record.children || []) as DataSetRecord[];
    if (children.length > 0) {
      result.push(...collectScoreLineRecords(children));
    }
    return result;
  }, []);
}

function findSupplierRecord(record?: DataSetRecord | null) {
  let current = record;
  while (current && !isSupplierRecord(current)) {
    current = current.parent as DataSetRecord | undefined;
  }
  return isSupplierRecord(current) ? current : undefined;
}

function getSameSupplierScoreLineRecords(record?: DataSetRecord | null) {
  const supplierRecord = findSupplierRecord(record);
  if (!supplierRecord) {
    return [];
  }
  return collectScoreLineRecords(
    (supplierRecord.children || []) as DataSetRecord[],
  );
}

export function fillScoreUserDown(record?: DataSetRecord | null) {
  if (!record) {
    return 0;
  }
  const scoreUserId = record.get('scoreUserId');
  if (!hasValue(scoreUserId)) {
    return 0;
  }
  const scoreLineRecords = getSameSupplierScoreLineRecords(record);
  const currentIndex = scoreLineRecords.indexOf(record);
  if (currentIndex < 0) {
    return 0;
  }
  const targetRecords = scoreLineRecords.slice(currentIndex + 1);
  targetRecords.forEach(item => {
    item.set('scoreUserId', scoreUserId);
  });
  return targetRecords.length;
}

看到这串代码,你一开始是不是也有点懵,为什么一个小的功能,它写了4个方法?

fillScoreUserDown(record)
collectScoreLineRecords(records)
findSupplierRecord(record)
getSameSupplierScoreLineRecords(record)

让我们一起看下这些方法分别做了那些事情?

一. fillScoreUserDown 方法

export function fillScoreUserDown(record?: DataSetRecord | null) {
  // 1.判断当前行是否存在?如果没有传入当前行,直接结束,并返回 0。
  if (!record) {
    return 0;
  }

  // 2.获取当前行的评分人,并判断是否有评分人,如果当前行没有评分人,就不继续填充。这一步是为了避免把空值向下填充,把后面的评分人都清掉。
  const scoreUserId = record.get('scoreUserId');
  if (!hasValue(scoreUserId)) {
    return 0;
  }

  // 3.获取当前供应商下的所有评分项,根据当前行找到它所属的供应商,然后拿到这个供应商下面的评分项列表。
  const scoreLineRecords = getSameSupplierScoreLineRecords(record);

  // 4. 找到当前行在评分项列表中的位置,并判断当前行的位置是否小于0
  const currentIndex = scoreLineRecords.indexOf(record);
  if (currentIndex < 0) {
    return 0;
  }

  // 5.取当前行后面的评分项并填充评分人: 假设有4个评分项点击的是 评分项2,那它会填充:[评分项3,评分项4,]
  const targetRecords = scoreLineRecords.slice(currentIndex + 1);
  targetRecords.forEach(item => {
    item.set('scoreUserId', scoreUserId);
  });

  // 6.返回填充了多少行
  return targetRecords.length;
}

对于 fillScoreUserDown 方法的点评

  1. 第一点:判断行是否存在属于重复判断,因为在调用这个方法之前已经判断过了。如果当前行不存在则不会调用 fillScoreUserDown 方法。
  2. 第二点:如果没有评分人则不进行向下填充的判断也是非必要的,我并没有描述这种场景但是它考虑进去了,我认为即使向下填充也是可以的,有些场景就想批量删除评分人呢?
  3. 第三点:当前评分项的位置在评分列表中不存在,其实这种场景也是不会发生的。
  4. 第四点:在每一个判断之后它都返回了一个数字,我想这个目的是为了告诉外面填了多少行。这个也是我没有要求,它自己加的冗余代码。

二. getSameSupplierScoreLineRecords 方法

这段代码的作用是:根据当前评分项行 record,找到它所属供应商下面的所有评分项行。

function getSameSupplierScoreLineRecords(record?: DataSetRecord | null) {
  // 1. 找当前行所属的供应商行
  const supplierRecord = findSupplierRecord(record);

  // 2. 如果没找到供应商,返回空数组(这种数据也不存在)
  if (!supplierRecord) {
    return [];
  }
  
  // 3. 收集这个供应商下面的评分项
  return collectScoreLineRecords(
    (supplierRecord.children || []) as DataSetRecord[],
  );
}

三. findSupplierRecord 方法

这个方法的作用是:从当前行 record 开始,沿着父级一路往上找,直到找到“供应商行”。

function findSupplierRecord(record?: DataSetRecord | null) {
  // 1. 从当前行开始找
  let current = record;

  // 2. 只要当前行存在,并且不是供应商行,就继续往父级找
  while (current && !isSupplierRecord(current)) {
    current = current.parent as DataSetRecord | undefined;
  }

  // 3. 找到供应商行就返回,否则返回 undefined
  return isSupplierRecord(current) ? current : undefined;
}

四. collectScoreLineRecords 方法

这个方法的作用是:从一组树形记录里,递归收集所有“评分项行”,并排除“供应商行”。

function collectScoreLineRecords(
  records: DataSetRecord[] = [],
): DataSetRecord[] {
  // 1. 用 reduce 累加结果
  return records.reduce<DataSetRecord[]>((result, record) => {
    // 3. 如果当前行不是供应商行,就加入结果
    if (!isSupplierRecord(record)) {
      result.push(record);
    }
    // 4. 继续处理子级:递归调用自己,把子级里的评分项也收集进来
    const children = (record.children || []) as DataSetRecord[];

    if (children.length > 0) {
      result.push(...collectScoreLineRecords(children));
    }
    return result;
  }, []);
}

其实这个方法是最没有必要的,这里考虑了多层级的树形嵌套结构,实际上只有供应商(父级)和评分项(子级)。

代码简化

看完了4个方法,我不得不说codex的防御性编程做的真好,但是有很多地方属实是过度设计了,简化后的代码如下:

export function fillScoreUserDown(record: DataSetRecord) {
  const supplierRecord = record.parent;
  const scoreUserId = record.get('scoreUserId');
  const scoreLineRecords = (supplierRecord.children || []) as DataSetRecord[];
  const currentIndex = scoreLineRecords.indexOf(record);
  const targetRecords = scoreLineRecords.slice(currentIndex + 1);
  targetRecords.forEach(item => {
    item.set('scoreUserId', scoreUserId);
  });
}
  1. 找到当前评分项所属的供应商行
  2. 获取当前行评分人
  3. 获取当前供应商下所有评分项
  4. 找到当前行的位置
  5. 取出当前行后面的评分项
  6. 给后续评分项设置评分人

总结

举上面的例子我其实想说的是,懂代码的人和不懂代码的人的区别究竟在哪里呢?如果只是考虑能不能做出来,功能正不正常两者没有本质区别,真正的区别在于做出来以后是否稳定、是否贴合项目、后续是否好维护等问题。

不懂代码的人用 Codex 加这个功能,通常可以把需求描述清楚,让 Codex 找位置、改代码、跑检查。优点是速度快,门槛低,像这次“评分人后面加图标,点击后向下填充”,Codex 基本能帮着做出来。但风险是:一开始可能不容易判断边界,往往容易写出过度复杂的代码,而这些代码又没有检查过,后期维护可能出现第一版好的地方第二版突然就坏了,而维护也只能依赖 Codex 自己判断。

专业前端用 Codex,更像是把 Codex 当成“高效率执行搭档”,对于代码中出现的问题可以及时纠偏,让 Codex 输出更接近可维护代码。例如:

  1. 这个功能应该加在列里,还是塞进 Select 里面。
  2. 不能破坏 DataSet 字段绑定。
  3. 供应商汇总行和评分项行的表格结构不同,要处理 colSpan
  4. 向下填充必须限制在当前供应商范围。
  5. 工具函数是否过度封装,是否应该简化。
  6. 类型断言是否多余。
  7. 改完后要跑哪些校验。

这次其实就是一个很典型的例子:Codex 可以快速实现,专业的前端可以站在代码审查和设计的视角判断实现是否符合真实业务边界。同时这也给前端开发者带来一个新的问题,那就是掌握一定的后端和运维知识是非常必要的。AI 不一定会立马淘汰程序员,但是 AI 降低了程序员的学习门槛,这就会导致原本就内卷的市场会变得更加内卷。所以前端继续深入前端知识是活下去的基础,而学习后端和运维是则是放大器。

最后:保持空杯,砥砺前行,希望你始终保持对技术的敬畏之心

感谢阅读,我们下期再见!