代码上道:智能体编程时代的质量守则
“AI 可以替你做一切,唯独不能替你坐牢。”
这个名梗表明,即使越来越多的生产代码已经不再由人类工程师手写完成,而是由智能体(Agent)根据指令大规模生成,但是智能体从社会属性上并不能对它自己生成的代码负责,一旦代码在生产环境造成问题,或者需要对代码做审计,仍然需要一个对应的 Code Owner 来负责。
早在二十年前,经典著作《程序员修炼之道》开篇就讲了一个有关“我的源码被猫吃了”的寓言故事:最好的软件项目也不可避免地会遇到推迟交付,或出现未曾预料的技术问题。一旦问题出现,Code Owner 就需要承担责任,依靠自己的专业去解决问题。书中写到:
责任意味着你对某事积极认同。你保证事情能够搞定,并为之做出承诺,但你不必直接掌握事情的每个方面。除了个人尽力做好,你必须分析超出你控制范围的风险情况。……不要把问题归咎于别人或其他什么事情上,也不要寻找借口。不要把所有问题都归咎于供应商、编程语言、管理或是同事。这些因素都可能是问题的一部分。它们的确会解决方案造成影响,但不是给你的接口。
在智能体编程(Agentic Coding)时代,把“智能体”插入到上面“供应商、编程语言、管理或是同事”当中毫无违和感。所谓“我的源码被猫吃了”,书中指的是如果项目源码只有一份,而你丢失了且没有备份,这就是你的责任,推卸说“我的源码被猫吃了”无助于事。对应到智能体编程场景,也就是一旦线上出现问题,推脱说这是 AI 写出来的 BUG 并不能撇清你作为 Code Owner 应该承担的责任。
本文以“代码上道(Code Soundness)”为题,首先简述智能体编程的优势,然后说明人在智能体编程的循环中如何发挥自身价值,从而可靠地交付智能体编程所产生的代码。
Soundness 通常翻译为“可靠”或“稳固”。这里根据其发音和出自《是,大臣》的梗,将其翻译为“上道”,既是指代码的可靠性,也意指代码如行车上道一样,部署到生产环境稳定运行。
智能体编程的长处
根据大半年来高强度智能体编程的经验,我认为智能体在编程领域大致有三大长处:
- 智能体有更长的上下文
- 智能体比人更有耐心
- 智能体可以异步工作
这几点可以用一个例子一起说明。
比如要在现有软件上实现一个新功能,落地方式可能有很多种。从前,Code Owner 通常是自己上网搜索、翻阅博文和参考实现,再结合自己的经验做出初步判断,然后本地实验做出原型再来比较。这样一个完整流程下来,往往一周时间甚至一个月时间就过去了。现在有了智能体支持,通常的做法变成了,调研的事情交给智能体完成,智能体拥有的知识库远比人类要广,联网搜索的速度也比人类要快,而且不会疲劳;智能体出具调研报告以后,人类选择其中可能的数个方向,驱动智能体并发地进行实验,然后再对智能体的产出做反复地拷打和迭代,形成两三个较为完善的原型进一步选择,最终选定一个方案开始上线测试。这个过程很可能一天乃至一个下午就可以完成。
这个过程里通常还会发生这样的事情:(1)只要能说清楚的需求,智能体基本都能按照字面意思尽可能的拟合,于是此前实验阶段由于编码的时间成本限制而做不了的事情,或者被迫欠下技术债先糊弄过去的部分,现在可以全面按照实际需要实施再做取舍。(2)除去需要上线的功能,配套的行为测试和性能测试用例,此前往往也是没精力写的。但是智能体非常擅长编写测试用例,并且不厌其烦地做完整覆盖,于是你可以不消耗额外精力的获得一套基础的测试套件。(3)整个设计和实验的过程,基本都是在智能体可触及的上下文范围内完成,因此过后需要重新翻找当时的决策,智能体也能快速回忆起来。
关于最后一点,或许你经常听到智能体没有长期记忆,聊到后面忘了前面的说法。但是这主要是人类对智能体完美的期待,希望它能记住所有事情并做出最好的决策。实际上,人的短期记忆要差劲得多。
高强度智能体编程的同行应该都经历过,在多轮对话或长时间等待智能体完成工作之后,自己忘记了当前会话(Session)一开始是要干嘛,或者接下来要做什么。这个时候,模糊地让智能体回顾一下对应的历史,并重新生成一份计划,往往是可行的。其实这些上下文信息基本就在本地,要想追溯某个改动的历史的,对应的 Git History 和 GitHub Issue/PR 也都是可以找到的。人一般很难耐心地翻找这些材料,但这对于智能体来说都不是事儿:直接读文件,或者调用命令筛选内容,加载到上下文里一起分析就行。相较之下,人光是记住各种命令的名字、参数和含义,就已经脑袋爆炸了。
人的价值
简述过智能体在编程领域的长处,接下来就要说说它力有未逮的地方了。这也是人在智能体编程的循环中发挥自己价值的切入点。
归根结底,AI 永远不需要承担责任,人类才能承担责任。为了使自己能够有信心承担代码交付后产生的责任,人必须理解智能体生产的代码。
同样,这不是一个智能体编程时代新出现的问题。如果你把智能体生产的代码看作是你从某位刚离职的同事那里接手过来的代码,你会发现情况非常相似:
- 同事写的天书代码基本看不懂,甚至不知道原本是要干啥的;
- 一句话交接,代码意图需要逐行、逐函数地进行逆向工程。
这样看来,理解智能体生产的代码,情况甚至还要更好一些:
- 这些代码是根据你的意图生成的,所以至少从最高层级的抽象上,你知道这些代码应该要解决什么问题;
- 智能体不是已经离职的同事,它还在你身边,而且会不厌其烦事无巨细地回答你的问题,并代劳完成代码的调整。
经验丰富的程序员接手他人代码的第一件事,通常就是走读代码梳理逻辑,在开发测试流程畅通的前提下,大刀阔斧的重构代码。所谓的代码重构,也就是在外部行为不变的情况下,把代码调整成自己好理解的形态。
同样地,要想可靠地交付智能体生产的代码,也要对其进行重构,使得自己能够流畅地说明程序实现了什么样的功能,在给定条件下会表现出什么行为,限界上下文划分的标准和领域概念的设计思路,以及如果要继续迭代或扩展,应该从哪里入手。
我讲几个例子来说明这个过程具体如何实施。
第一个例子是 ScopeDB SaaS 平台近期的部署运维工作都被智能体接管了。虽然还不是无人值守,我会保持清醒监督过程和结果,以防止出现故障的情况做最终兜底,但是我已经充分信任智能体到了能够在发出部署上线指令以后,切到别的窗口忙别的事情的程度了。
我能做到这一点,主要是提前完成了三个工作。
首先,整个部署运维的技术栈是我逐行确认设计实现好的,我非常清楚 ScopeDB SaaS 平台一共包含哪些资源,每个资源被那个服务拥有,又是被哪个框架管理。因此,对于一个有 GPT 5.5 及以上指令遵循水平的模型来说,即使它的执行出了偏差,我也知道如何指挥智能体排查问题,以及紧急情况下该执行什么快速回滚策略。
其次,智能体授权范围和标准动作也是经过设计的。曾经我放手让智能体接管 GitOps 的时候,出现过 Force Push 导致 Terraform 死锁的情况,针对这些典型的智能体会犯的错误,我都有对应的规则指导它按照安全的执行路径来完成上线。对于典型的滚动发版,元数据升级,节点扩缩容等等动作,我也会总结出一套不断迭代的 SOP 让智能体照章办事。
最后,其实完全授权智能体合并 GitOps 的变更和做线上操作,都是在我自己手动跑过或全程监督看着智能体跑过,且基本确定 AI 执行的就是同一套流程的时候,才会授权。也就是说,近期 ScopeDB SaaS 的部署运维工作,都是之前已经做的方案,只不过版本号变成了下一个版本,或者特定参数有简单的数值变化而已。这种情况下,甚至有时候智能体的计划或者执行有了偏差,我只要让它回归到上次上线的经验,智能体就能自动恢复了。
这个例子总结下来,在部署运维的部分,我对实际执行动作的理解具体化成了:
- Infrastructure as Code (IaC) 的一整套方案
- AGENTS.md 和 SOP 对智能体行为的约束和指引
- 过往案例(Operation Journals)的直接参考
这样我基本就可以相信 GPT 照章办事不会整蛊我,或者说出了事我也可以兜底。但是如果换一个模型,对应的信任可能就要重新建立,需要的约束和提示信息可能也不尽相同。
第二个例子是 ScopeDB 的前端。
由于前端所见即所得,部署无状态,我最近在没有精力钻研前端框架选型和具体组件和功能设计的情况下,采取的方式是:
- 后端定义前端,首先确定后端数据和业务逻辑是正确的,前端以目前前沿模型的智力,基本也就能对个七七八八了;
- 要求智能体说明前端逻辑的整体设计,只要它能说得清楚,我觉得大体也就还差不多,实际上前端重要的也就是用户旅程体验;
- GPT 6 Astra 熟练掌握 Computer Use 以后,我现在会让 GPT 直接录屏演示我要上线的前端功能全流程是怎么样的。
总而言之,由于前端所见即所得,所以目前我对前端编程最大的投入,就是选型了 SvelteKit 作为底层框架,并且仔细设计了鉴权的流程,其他的基本都是看起来差不多 OK 就行了。
但我认为这不是最合理的办法,而是我个人的前端经验不足,且目前精力不足,从而不得不采取的折衷方式。如果有足够的时间做调研,或者开始了解前端开发的典型的取舍点有哪些,我应该也会把新的经验都具体化成的代码当中的模块划分和命名准则,从而让我对 ScopeDB 的前端有更深的掌控。
最后一个例子就是开源基础软件/库的开发了。这部分的经验跟开发 ScopeDB 的数据库内核有很多异曲同工之处。
基础软件/库的开发,代码本身就是交付物,几乎必须深入到代码的设计和实现,才能确定交付到下游用户手里的是什么样的接口,每个接口是什么用的语义,Edge Cases 的行为和整体的性能表现。
我会从如何设计项目结构、如何 Review PR 以及如何最终交付三个方面出发,讨论智能体编程带给基础软件/库开发的改变。
关于项目结构,由于我近期基本开发的都是 Rust 的基础库,我就直接快进到结论,搬出在 Apache Asyncband (Incubating) 项目开发中迭代出来的结构:

相同的结构已经被我用在下面一系列仓库中,且工作效果良好:
- https://github.com/apache/datasketches-rust
- https://github.com/fast/hashcrew
- https://github.com/scopedb/cache2
这个结构有几个值得说道的点。
首先,核心开发区是项目同名目录 asyncband 和对应的集成测试(tests-integration)、性能测试(benchmarks)以及示例程序(examples)目录。
asyncband包含项目核心功能实现代码和随代码的文档,不必多说。examples包含相对复杂从而不好放在文档注释里的示例程序,给它分配顶层目录的核心原因是让跳转到项目仓库主页的用户更容易发现。tests-integration和benchmarks都是测试,但是行为测试和性能测试关注点不同,分开比较好管理。各自分配顶层目录,主要是为了跟主 crateasyncband分开,作为 local-only 的 crates 存在,这样可以解决一些潜在的发版循环依赖问题,以及最终发布的 crate 上可以尽可能少的沾染仅测试用的依赖。
其次,大部分开发时常用的动作,我都封装在 xtask 目录下,使其可以通过类似 cargo x lint --fix 这样的命令来调用。随后,我只要在 AGENTS.md 文件里加上一句:
Usecargo xas the source of truth for repository workflows. Readcargo x --helpand the relevant subcommand’s--helpbefore running build, test, lint, or formatting commands.
基本上整个项目机械格式化和测试/文档回归的工作,智能体就能够像调用 Hook 一样每次都执行了。
此外,由于智能体可以不厌其烦的对齐项目当中的信息,因此,我把项目文档的信息结构设计成了:
- README.md 面向跳转到项目仓库首页的用户,只讲项目定位和典型用法,并附上跳转到其他内容的链接。完全不讲内部细节或开发指南。
- AGENTS.md 面向开发本项目的智能体的指令,基本包含使用
cargo x命令的指南,基础风格指南和文档指南。如果项目特化的内容过多,可能会把具体的指南分散到 CONTRIBUTING.md 或 STYLES.md 等文件中再做引用。 - CHANGELOG.md 面向用户升级的变更查阅手册,有智能体帮忙转写以后,维护成本骤然降低。唯一需要注意的就是在 AGENTS.md 里说明白 CHANGELOG.md 是面向下游用户的,并且同一个开发周期内的变更应该合并成版本之间实际看到的变更。
最后,每个项目都可能安装项目特定的技能(SKILL),比如 Asyncband 项目就针对 ASF 项目发版时需要做的合规检查和发布流程,创建了 license-audit 和 release 两个技能;而 ScopeDB 内核仓库则针对数据库开发的基本守则开发了 review 和 checklist 两个把关代码质量的技能。
迭代出这样一套标准的项目结构以后,基本上我想做什么事情,指挥智能体在这个仓库上下文当中产生的 diff 都是高度可预期的。每个目录下出现什么样的改动意味着做了什么事情,我心里有数。同时,我也能通过折叠对应目录,保证 Review 时先关注想要关注的部分。要想指挥智能体生成对应内容的时候,智能体也更容易在相同目录下参考过往做法按图索骥生产出风格基本一致的代码。实际上,在迭代这个项目结构的过程中,我经常到了一个新仓库发现结构不一样,于是让智能体参考已经实施了相应结构的项目,结合当前项目的特点做一轮落地,然后给一个详细的取舍报告。基本看完报告,我就能知道智能体做了什么事情,为什么,再做一些微调,结果就能收敛了。
关于 Review PR 的问题,我有以下几个观点。
第一个是现在必须默认所有 PR 都有 AI 辅助的成分。
有了新的趁手工具自然要用,用了也没啥问题。只是从 Maintainer/Reviewer 的角度,不要误会几千行的 diff 都是 PR Author 慢慢写的,从而产生不必要的情感亏欠。
第二个是现在不得不假设很多 PR 都是纯 AI 生成的,甚至提出问题、分析问题和创造实现都是 AI 自闭环。
如果 AI 搞得不错,那也就罢了,正常合并感谢就行。但是如果生成的 PR 有问题,或者分析问题分析错了,那么 Maintainer/Reviewer 不必努力跟 PR Author 沟通,尤其是第一次试图沟通以后,对方回复明显是自主智能体的情况。这种情况下,如果自己知道问题怎么解决,也有时间解决,那就自己带着 AI 上手改了合并就行。否则是可以把发现的问题或阶段性总结回复上去,等自己有时间,或者别人接手再基于当前上下文继续搞就行。
我知道有些 Maintainer 不习惯直接上手改别人 PR 的内容,但是这跟先合并完贡献的 PR 然后再自己 follow-up 是没有区别的。智能体编码时代,写代码是 Cheap 的,交流是昂贵的。除非对方表现出交流的能力和意愿,否则假定对方是自主智能体,对双方的效率都是更高的。
最近在 Apache DataSketches 项目里,就来了一个自主智能体扫描问题并自动提交修复。该账号今年六月第一次活跃,然后全网扫描 ASF 项目,通过 fuzzing 和白盒分析的方式,给复数个项目提交了 PR 修复问题。DataSketches 的一位项目管理委员会(PMC)成员问我,说是不是该提名这个人做 Committer 了。我说但凡这个账号表现出一点人性,我肯定一早就提名了。但是现在看,这个账号基本就是一个自主智能体,而且很多 PR 到我手上都需要我再 follow-up 才能合并,并且我试图让他修改,他的回复也很人机,而且改不对,恐怕还是不太合适。最终我建议这位 PMC 成员,如果你觉得对方还不错,可以试试跟智能体背后操作的人取得联系,看看他是真的有什么理解,还是完全放手让智能体决策。
这里同样是一个负责的问题。因为如果提交 PR 的是自主智能体,实际上负责的人是最终合并代码的我。在这种模式下,我不需要赋予这个智能体 Committer 的权限,因为无论如何我都需要复核,而智能体并没有表现出能自主决策的能力。我从多年前在若干个 Apache 项目里担任 PMC 成员的时候开始,就一直秉承着同一个选拔新人的标准:
The candidate should be a real human spending time on AsyncBand, taking actions bravely with caution.
这里的重点在于敢于做出决定,同时谨慎小心,知道什么时候寻求帮助。目前哪怕是最前沿的智能体都无法表现出这种特质,而只是听从中之人的指令行动,对于编程当中最麻烦的模棱两可的决策,几乎无法做出可靠的判断,而是你说什么它都“你说得对”。
其实真人开发者也有这样的。找到一个 Good First Issue 以后问 Maintainer 要怎么做,你说一句他就做一句,遇到任何抉择都摆烂求助。这种人别说 Committer 了,我可能都懒得去搭理他的“贡献”。有那个时间指导清楚,我自己都把 Issue 解完了。
至于在 Pull Request Template 或者仓库上下文里给智能体投毒,我觉得大可不必。只要假定所有贡献都是智能体提交的,其实也就差不多了。
关于最终交付前的把关问题,我近期在发布开源基础库的时候发现了一个效果不错的提示词:
你从最新的 main 分支切出一个 draft PR 想一想,作为 Asyncband 这样的库呢,你发出去对吧,一个基础库肯定会有人骂你。那他会觉得说哪个地方不好用呢?哪些地方觉得说你可能写的有点 AI Slop 呢?哪些地方有太多余的测试,有错误的接口,有人体工学不合适的地方,文档有缺失,各种各样的问题。欸,怎么对我的场景不能用,对吧?这东西你全部考虑一下,然后做一个 PR 来优化。那既然你知道说你一个大 commit 我 review 不了,那么你每个小的动作都单独提交,每个 commit message 都要写清楚为什么做,这样就可以了。这里还有一个问题,就是我曾经这样提示你的时候,你会过度优化,而且有很多 false positive 啊。我希望你好好地给我检查一下,绝对不要再整出 false positive 的 commits 了!
如果是现在的我重新组织,我会强调两个点:
- 站在用户的角度出发,也就是第一性原理重新自顶向下审视和设计结构,评判行为;
- 消除多余的测试和 AI Slop 的代码,也就是消融实验。
这里的第一性原理和消融实验是我在长时间 Prompt 之后发现的魔术字。只要提起第一性原理,几乎所有前沿模型都会激活格局打开相关的参数,自顶向下审视设计,避免过度陷入局部细节;而消融实验能驱使智能体并发地执行多轮消除中间概念的实验,有效减少不必要的中间层。
如果要做再仔细一些的质量保证,我可能会带着智能体做一系列的用户旅程测试。
举个具体的例子,Apache DataSketches 的 BloomFilter 原本有个 invert 方法,行为是反转 BloomFilter 每个 Bit 位。我跟智能体 Battle 过几轮这个接口实际的使用场景,再对比其他语言生态,还有这个接口一开始加进来的历史溯源之后,发现实际没有单独使用的需要,唯一可能有就价值的是作为 BloomFilter 做差集的辅助函数。于是我就把 invert 方法删掉,加上实际有用的 difference 方法。
再举个直观点的例子,ScopeDB 用户注册和登录的界面,我让智能体做录屏报告,发现用户登录界面也有跟注册界面一样的“注册动机”可选框,这显然是不对的。于是我就指挥智能体避免这个问题,结合友商常规的做法,重新给出用户注册和登录的流程设计,我再挑选一个实现。
总而言之,这些手段的最终目的是把智能体生成的似是而非(Plausible)的代码变成可信(Convincing)的代码,人在其中的价值,就是确保最终交付的代码可靠。