Personal Website

因纽特·冬

从design到上线,一次完全交给agent的开发经验

发布于 # AI

背景

年初换到了新的team,接到了一个新的任务,我们产品的audit log本来是放到我们服务里面,存储在rds mysql保留一年,并有一个audit app来查询,现在支持更久的存储和查询,我们需要把audit log从我们这边转移到到xdl(一个data lake)中,所以整个任务分为3个部分。

从design到实现

这里不写详细的design和实现,只是记录和agent(claude code)协作中我做了哪些动作和取舍。

  1. 不惧怕从0到1写一个完整的service
    这次任务中,其中ingstor data 部分和audit log app从xdl api查询数据属于要在原来的代码中修改,使用js编写。迁移旧数据,需要一个新的服务来做。此时,我有几个选择:
    (a) 新建一个代码仓库,用自己擅长的语言
    (b) 在原来的js仓库中,新建一个migration服务,用和原仓库相同的语言(js)
    (c) 在原来的js仓库中,新建一个migration服务,用自己擅长的语言(go)
    最终我的选择是c,我认为这也是正确的选择。其实考虑的出发点很简单,既然要和agent协作完成,我不光要考虑自己还要考虑agent。毫无疑问,monorepo的结构天然给了agent需要的上下文,虽然迁移服务是新的,但是audit log原本的那套代码可以让agent很好的学习到从数据库的连接到audit schema到数据的流转。当我告诉他我现在要做数据的迁移了,它自然get到了来龙去脉。其次在选择语言上,一个直觉是使用js,好处是可以复用之前的一些代码,然而我认为代码的复用不再是一个值得优先考虑的选项。之前讲的代码复用还是为了方便人来理解和写代码,但是现在agent写代码了。更何况,很多代码没有好到可以一行不改的复用。
    最终我还是选择了使用go语言,你可能会说你看你还是站在人的角度了,ai写什么都一样呀。确实,我之所以选择我熟悉的语言最大的原因是,为了我可以更好的控制agent和review它的产出(当然go更节省资源也是真的)。因为我对这个语言更熟悉,我便可以从一开始的代码结构上就开始“挑刺”,我便可以在它写goroutine和channel时直接指出,你要小心它们有哪些容易写出bug的陷阱等,我便可以在它无脑使用interface时告诉他interface应该在什么时候才该用等。因为对这门语言的熟悉,让我在和agent协作上更顺畅,能彼此知道对方在说什么,而不是一味的accept它的建议。归根结底,控制权还在我手中。最后的结果是,我很快的写出了迄今为止,我认为最好的go项目代码,而不只是能跑就行。
    之所以之前会惧怕一个新的service,因为有很多除了代码之外的不熟悉的工作需要,比如ci/cd的工作,编写新的部署脚本等。但是有了ai,这些不熟悉但又知道大概的东西也没什么好怕的了。

  2. 不惧怕重构之前的代码,哪怕屎山
    任务中还要修改之前的代码,在实现过程中,发现有些废弃或者写的不好的地方。放到以前,我和大家的做法可能是一致的,因为时间和精力的缘故,我们只会关注自己那部分功能实现的怎么样,其他的能不动就不动。但这次不一样,agent让我有了时间和精力开始重新考虑遇到屎山代码时,是继续屎上雕花变成更大的屎山还是重构。最终我还是选择了后者,当然我不是重构整个屎山项目,而是从一个很小的切入点开始,该删减删减,该重构重构。这里重要的是,我选择了跟我需求最相关的代码重构。至今为止,我还没有完整重构过一整个屎山项目,我也认为这不是好的出发点。
    我认为正确的重构姿势是按phase来,切出可重构的功能,逐步击破,而不是一股脑让agent来做。
    之所以说不惧怕,因为在agent的帮助下,有了更多时间和精力去验证结果。甚至这一步也可以让agent来完成。所以,敢重构的底气是因为在agent的加持下我速度够快。重构快,验证快,试错快,迭代快。

  3. 关键的design还是要和人沟通 在整个任务的开发流程中,几个关键设计还是和leader不断讨论才最后决定的。他也给了我很多启发。之所以要讨论,我认为有以下几个原因:

  1. 及时commit,有的时候最初的就是最好的
    这来源于一次教训,当我周五在激烈的和ai讨论了好几轮实现后,仍没找到最优的。这个过程也没及时的commit。等我周一回来,经过一段思考,我发觉最初的一个版本是最好的,但是此时我发现我退出了那次session,代码的样子是最后一个版本的样子。此时我再次打开对话也无法回去,我只能默默的开始重复周五跟他讨论的对话,这看起来挺傻的。像是时光倒流。所以,再你不确定有没有更好的方案的时候,及时commit可能会成为你的后悔药。

  2. 如何review代码
    当agent哐哐哐写好代码时,要怎么审查呢。我有个屡试不爽的方法,就是我会告诉它,你帮我review下你这次所有的改动,并且打分。这个时候它会发现一些bug和可以改进的地方。此时,你可能会问为啥它当时写的时候怎么没意识到这些问题呢?最后的一句prompt就能让它知道?其实背后是有科学道理的,那就是现在ai的底层都是基于一个叫注意力机制的东西,也就说说它是有重点的看。你在让它写代码的时候,往往是一个个需求来问它来写,基本上是好几轮对话。此时它的注意是在你每次的问题上,当最后,你说全盘review并打分,无疑是强行让ai把注意力收敛到这次所有的改动上,而且打分这个方式,也让ai把注意集中到分析代码bug等方向上。所以,适当让ai收敛自己的注意力而不是持续发散是关键。
    上面讲到的是agent怎么review自己的代码,那人怎么review呢。我认为不需要review全部,你只需要告诉它,我的精力有限,只能看1000行代码,但我还要保证代码的功能和质量,帮我找出哪些是我需要review的代码。这是合理的,因为其实一个服务有大量的object转来转去的代码,这些是无聊的且不核心。我们需要找到核心的代码去审查,而那些增删改查就交给e2e的测试吧。

  3. 能验证才能迭代,才能上线。快速试错就是快速成功

  4. agent+cli几乎可以完成任何事情
    cli就是全部。在我使用的过程中,没有什么花里胡哨的skills,有的只是gh(git cli),kubectl cli,aws cli等。有了这些cli,agent就可以做许多事情。比如,看某个pod的log或者查看k8s某个设置,因为有kubectl,这些都是动动嘴的事,再也不用记命令。有一个比较印象深刻的事情,是如果我想查看数据库表,但是数据库是托管在aws上,且本地无法直接连接。我通常的做法时先通过k8s环境中的一个pod(这个pod可以连接mysql),然后使用port-forward+ssh tunnel的方式在本地访问。agent在做到某件事情的时候想去数据库里看一下,但是当时我没开启这个隧道了,agent看到代码中有连接数据库的代码就试图去连接本地的,发现连接失败,它看了配置文件,发现数据库可能部署在本地环境访问不到的地方,于是它使用k8s cli自己探索到现在某个eks环境中,然后借助其中随便一个pod,进入这个pod,写了连接数据库的代码,自己就连上了,然后验证了它的改动。
    这里有个很深的洞察是,也许你就只需要创建cli,然后agent自然会用单个或者组合的方式去穷尽探索。如果说API和sdk是面向程序员,那么cli就是面向agent。

  5. 用简单的方式使用claude code
    是的,这次实践中,我没有用skills或者mcp,也没有为此写agent.md,也没有使用什么memory,就是对话和让它自由的探索我的工作目录和我环境中可用的cli。但这已经足够了。唯一我认为值得提的是,要在终端使用cc,不要在任何ide中。在终端,我可以同时对一个/多个项目快速的开始提问探索。

  6. 不要写design文档
    代码才是最新的准确的唯一信息源,有问题直接通过agent来探索代码。任何文档都是过期产物,都是某个时刻的快照。要在真实的环境中找答案,而不是历史中。

  7. 适当收敛你的完美主义 我为迁移服务不光做了后端,为了观测性好,我还做了dashboard。直接html embed到go中,也不需要前端框架之类的。这个时候,一切都完美闭环了,但是我还是会对页面更好看有执念,因为agent改的又很快,就上瘾似的一遍遍迭代,只为满足视觉效果。其实这就有点没必要了。