前几讲一直在讨论软件工程里的几道缝隙:人的意图(Intent)怎样变成规格说明(Spec),规格又怎样变成实现(Implementation)。版本管理帮助我们保存、比较和协调变化;需求分析与架构设计帮助我们理解问题,把复杂性放在合适的位置。问题本身带来的本质复杂性(essential complexity)不能凭空消失,工具、表示和实现方式额外引入的偶然复杂性(accidental complexity),则值得我们不断消减。
这些道理都重要,但不能让我们永远停留在讨论道理的阶段。在某个时刻,我们总得开始:做出一个符合当前 Spec 的原型,让它运行起来,看看究竟造出了什么。
“Let it fail”并不是鼓励随便交差,而是允许原型暴露我们的错误。实现可能没有满足规格,规格也可能没有表达真正的需求;即使两者都对上了,需求里关于用户、规模和运行环境的假设仍然可能不成立。纸面上说得通的选课系统,遇到两个人同时抢最后一个名额,就可能出现另一副面孔。先把想法变成可以观察的东西,才有机会知道下一步该改实现、改规格,还是改架构。
当越来越多实现工作交给 AI,问题也就从“怎样让它写出这段代码”,扩展成“怎样组织一个能够持续开发、验证和修正的软件生产过程”。这就是本讲讨论 Agent Native 开发的起点:从一开始,就把 Agent 当作实际参与工作的开发者,为它设计工作环境。
从聊天式问答,到能够读写文件、执行命令和使用工具的 Agent,AI 越来越自主。把这种能力推到极端,可以作一个思想实验:如果模型足够聪明,拥有足够的工具、资源和行动权限,也许给它一个 Bash 入口,它就能组织完成极其复杂的工作。造芯片、证明黎曼猜想,是对这种想象的夸张表达,并不是说今天的模型已经具备这些能力。
现实的问题恰恰在于,模型还没有万能到可以放心托付一切。尤其是工程品味:什么值得抽象,哪里应该保持简单,什么改动正在让项目变得难以维护,并不容易只靠一句提示词教会。既然一次回答不能解决全部问题,能不能让它不断尝试?
Geoffrey Huntley 提出的 Ralph Loop 给出了一种非常朴素的形式。名字借用了《辛普森一家》中的 Ralph Wiggum,带着一点笨拙却执着的意味。下面这段代码表达了循环的骨架:
while true; do
cat PROMPT.md | claude -p
done
每轮都把同一个任务交给 Agent,让它检查当前成果,选择下一项工作,修改并验证。一次会话结束,下一轮重新开始。提示词没变,工作区却变了:上一轮实现了功能、留下了测试结果、记录了失败原因,下一轮就能够接着这些状态继续。这是 Huntley 对 Ralph 的介绍中值得把握的机制。
循环本身并没有魔法。while true 不会自动识别“已经完成”或“无法改进”,也不能保证下一轮一定比上一轮好。实际使用时,必须另外定义验收条件、预算或停止条件;反复遇到相同失败时,还要判断是不是任务表述、工具或架构出了问题。不断运行是产生进展的机会,能识别进展的反馈才让这些机会有价值。
理解这种开发方式,可以借用一个熟悉的视角:Everything is a state machine。
工作区就是状态。它不只包含当前代码,也包含需求、设计文档、测试用例、运行结果、日志和待办事项。Agent 读入这些状态,执行一轮工作,把它们变成下一份状态。于是,一次开发可以理解为一次状态迁移:从“这个功能尚未实现”,到“已有实现但测试失败”,再到“失败原因已定位,修复通过了检查”。
这也说明,长期任务不能只靠一段越来越长的聊天记录维持。所谓 long-horizon task,是需要跨越许多步骤、许多次会话才能完成的任务。新一轮 Agent 必须能够从留下来的材料中判断:目标是什么,已经做了什么,哪些结论有证据,哪里还没有完成。如果关键经验只存在于上一轮的上下文里,下一轮就得重新踩坑。
例如,“选课功能已完成”是一句含糊的交接;“普通选课已通过,两个请求竞争最后一个名额的测试失败,失败记录位于某份日志”则给出了可以继续工作的起点。文档在这里承担的是协作状态,而不是工作量展示。写得越多未必越有用,能让接手者恢复判断才有用。
有了 Git、Jujutsu 这样的版本管理工具,状态还可以分叉。不同 Agent 在不同工作区中推进不同任务,再把结果集成起来。这不就是现代软件开发的基本方式吗?不过,版本控制允许并行,并不自动保证任务彼此独立。两个改动各自正确,合在一起仍可能出错;大家也可能同时扑向同一个问题。要让并行有收益,任务划分、依赖关系和集成检查都得跟上。
Ralph Loop 中一个关键要求是:每轮都验证并评估结果。 其出发点可以说得很不客气:开发者是不可信的。这里的人类开发者也一样——“我只改了一点点,不会有事”,往往正是事故的开场白。
既然开发会不断改变工作区,而我们希望进入主干的状态都是“好”的,就应该在集成这些改变时执行检查。把“好”变成具体条件:能构建、测试通过、没有破坏已有功能、满足项目的重要约束。如果这些条件成立,再把结果发布出去。祝贺你,重新发明了 CI/CD。
持续集成(Continuous Integration,CI)强调频繁把小规模改动集成到共同主干,并自动构建和验证。它应对的是多人长期各写各的,最后才发现互相冲突的“集成地狱”。Fowler 和 Foemmel 在 2000 年的 CI 文章里,就讨论了每天多次运行可重复的自动构建与测试。改动越小、反馈越及时,定位问题时需要怀疑的范围通常也越小。
到了发布环节,CD 有两个常见含义。持续交付(Continuous Delivery)要求软件始终处于可发布状态,什么时候真正上线可以由人决定;持续部署(Continuous Deployment)则进一步把通过检查的变更自动发布给用户。2010 年的《Continuous Delivery》及其作者对两种 CD 的说明,把自动化的关注点从构建测试延伸到了整个交付过程。
这些名字听起来很大,机制却可以从一个很小的想法理解:事件发生时,执行指定动作。提交、推送、发起合并请求、主干更新,都是可以挂接动作的节点。之前尝试让 Agent 在合适时机“播放声音”提醒人的需求,也可以从这里重新理解:需要把动作落实到明确的事件,而不能只期待它记得做。把动作换成运行检查、构建页面、部署网站,就是开发流程中的 Hook。
不过,有检查和检查有约束力,是两回事。如果测试失败后仍然能照常合并,检查只是在旁边亮了一盏红灯。以 GitHub 为例,可以把特定状态检查设为合并必须满足的条件,并通过分支保护约束直接推送与绕过权限。合并队列还可以检查待合并改动与主干、队列中其他改动的组合。GitHub 分支保护文档。
CI/CD 真正琐碎的部分,往往是业务规则。两个合并请求单独都通过,合起来却失败,怎么办?测试偶尔失败,是重跑还是隔离,谁负责修?线上急需修复时,谁有权例外放行,之后怎样补检查?两个发布任务同时运行,新版本先完成,旧任务随后才结束,会不会把网站覆盖回旧版?这些问题都不是写出一份配置文件就自然消失的。
在大模型出现之前,光是熟悉配置语法和平台细节,就足以劝退不少人。今天可以把配置交给 Agent,但我们仍然要表达清楚希望系统怎样工作。例如,一个课程网站的流程可以这样约定:
main 更新后,对这次提交运行检查并构建页面;全部成功才部署这次构建的产物到 github.io,失败则保留上一版。Lint 是对代码格式、可疑写法或项目约定的静态检查,不必先运行完整业务就能发现一部分问题。测试则通过执行程序检查具体行为。二者承担不同职责,不能因为其中一项通过,就省掉另一项。
验收这套流程时,也应该观察失败路径。故意引入一次 Lint 错误,确认它真的阻止合并;让测试失败,确认没有发布新页面;再修好错误,确认能正常更新。这样检验的是“门是否真的关得住”,而不只是“配置文件看起来像那么回事”。
在 Agentic AI 时代,CI 的价值还会增加。Agent 能以很快的速度生成代码,也能以很快的速度积累维护负担。课堂中可以见到一些很有代表性的偏执:一句话能交代的事情写成 20 KB 文档,把每个细节在多个地方重复一遍,在不重要的小问题上反复加固,或者写出功能勉强能跑、但人类很难读懂的代码。这些是实际协作中的观察,不是对所有模型的统一排名。
如果项目仍然需要人共同维护,这些倾向就必须受到实在的约束。只在规范里写“请保持简洁”,效果未必稳定;把可以机械判断的部分交给检查,反馈就能进入每一次开发循环。
例如,文档不断膨胀,可以为特定入口文件设置合理的大小限制,把详细材料放到有组织的位置;模块边界不断被绕过,可以检查禁止的依赖方向;代码难以阅读,可以用格式化、静态检查和复杂度规则拦下一部分明显问题;同一项规则到处复制,可以检查重复内容,并要求共同维护者判断应当保留哪一处事实来源。这些约束都是针对具体项目设计的,不能把“文件短”简单等同于“设计好”。
自动检查也应说明为什么失败、怎样修复。只返回一个红叉,会让 Agent 花时间猜规则;指出违规依赖的位置与允许的访问方式,才能让下一轮有明确方向。于是形成一个有用的闭环:CI 阻止,Agent 修正,再由同一套标准验证。
这里还有一个边界:负责实现功能的 Agent,不能因为过不了检查,就单方面把标准调低。测试确实可能过时,架构规则也可能需要改变,但修改这些规则需要回到需求和设计理由。删掉失败断言得到的绿色,与修复程序得到的绿色,颜色相同,含义完全不同。
围绕 Agent 建立的运行环境、工具、约束与反馈机制,常被统称为 Harness。这个词可以理解为让模型的能力能够稳定用于具体工作的整套配套设施。Agent Native 开发因此不仅是“代码由 AI 写”,还包括让工程知识成为 Agent 找得到、用得上、能够验证的东西。
OpenAI 的 Harness engineering 文章描述了从空仓库开始的实验:代码交给 Agent,人的工作转向组织环境、表达意图和建设反馈机制。文中把仓库知识组织为可查阅的结构,让 Agent 直接启动和观察应用,用 Lint 与结构测试检查架构边界,并持续提交小规模清理改动。
沿着这些实践,可以把自己的项目拆成四个具体问题。首先,人脑中的知识有没有外部化?“我们一直这样做”对刚开始一轮工作的 Agent 没有意义。重要决定应该进入版本管理,说明背景、约束和理由;入口文档负责引导查阅,不必把所有知识一次塞进去。
其次,Agent 能不能自己看到程序行为?开发选课页面时,如果每次都等人转述“按钮好像没反应”,反馈既慢又容易丢信息。若它能启动独立实例、操作页面、查看日志,就能直接检查点击后是否发出请求、服务端怎样处理、界面最终显示什么。
第三,架构品味能否转化为可检查的规则?比如,页面组件是否绕过业务层直接访问数据,某类日志是否缺少必要字段,错误处理是否沿着约定接口传播。规则写进检查之后,团队就不必每次从头解释相同要求。当然,哪些规则值得固定下来,本身仍然需要判断。
第四,清理工作是否持续发生?如果生成代码不断复制已有的不良模式,等到全仓库都长成同一种坏样子再修,成本就高了。小规模、经常性的修复可以抑制扩散,让下一轮 Agent 看到更好的例子。
读到这里,自然会想:“好,那把完整的 CI 门控拿来看看吧!”课堂里的“ClosedAI”吐槽,指的是文章没有附上该内部项目可直接复用的完整配置;不能由此说它没有 CI,文中确实介绍了相关约束。这个落差也提醒我们:看懂经验总结,还不等于拥有一套能在自己项目里工作的工程系统。
并不是每一步都需要模型作决定。Stripe 在 Minions 的工程介绍中,用 Blueprint 组织混合工作流:实现任务、修复失败等步骤交给 Agent,运行 Lint 等确定步骤则由普通代码保证执行。它还限制 CI 重试轮数,而不让一次任务无限循环。
这个例子提供了一种直接的设计思路:如果某一步每次都必须执行,且执行方式已经明确,就把它写进流程。模型适合处理需要理解和选择的问题,固定工序可以由程序可靠地完成。重试也有成本;连续失败时,应该判断反馈是否带来了新信息,而不是把“再试一次”当作永远正确的回答。
第二个例子来自 Anthropic 的 C 编译器实验。团队让 16 个 Agent 协作,编译 Linux 时却发现大家撞上同一个错误,重复修复、互相覆盖。后来,他们用 GCC 编译大部分文件,只让新编译器负责一部分,再通过切换文件的编译器缩小失败范围。不同 Agent 因而能处理不同子集暴露的问题;单独通过、组合失败的文件仍需进一步排查。
值得学习的是验证方式怎样改变了任务的可分性。原来只知道“整个内核编译失败”,大家只能涌向同一个入口;有了可信的参照和可缩小的失败集合,就能把大问题拆成可以分别推进的工作。增加 Agent 数量之前,先改善任务表示和反馈,可能更有效。
第三个例子来自 StrongDM 的工程自述。他们发现,Agent 可能用过于简单的实现应付狭窄测试,也可能修改测试去迁就实现。为此,他们把端到端用户故事组织为场景,常放在代码库之外,作为相对独立的验收依据。对于包含 Agent 的产品,还会观察多次执行轨迹中满足用户意图的情况,而不只看一次测试是否全绿。
他们还为外部服务建立行为替身,模拟 API 与边界情况,使大量场景能够反复运行。Digital Twin Universe描述了这种做法。把它迁移到选课系统,可以设计“最后一个名额刚被别人占走”“请求已成功提交,但响应丢失”这样的练习环境。开发 Agent 不必等真实学生踩到这些问题,才知道失败是什么样子。替身与真实系统之间的差异也必须验证,否则我们只是造出了一个特别容易过关的考场。
从 Agent 周围的工具、约束和验证机制,也能理解本讲“Deepseek Harness”这个题目的着眼点:模型之外,还有一整套让工作得以推进的装备。那些看似理所当然的设计,常常来自真的做过、失败过、修正过。仅凭空想象,很难一下子想到所有有效的边界和反馈方式。
但持续清理技术债,也有它解决不了的问题。我们可以把日常清理类比为垃圾回收(garbage collection):消除重复、修正文档、统一接口、防止坏模式扩散。这里说的是工程维护的类比,并非编程语言运行时的内存回收。即使这些工作全部做好,系统仍可能建立在一个已经失效的设计假设上。
例如,假设所有用户的数据都可以集中在一个写入节点,其他节点保留完整副本以分担读请求。用户少时它很直观;写入和数据量持续增长后,增加读副本未必能解除瓶颈,因为每个副本仍要承担全部更新。把代码整理得再漂亮,也不会自动改变这种负载结构。失败在此提供的价值,是帮助我们看清应当改变哪一层设计。
Friendster 的亲历工程师 Dathan Pattishall 在 2006 年的复盘中,讨论了主库加完整副本所面临的写入复制、I/O 与复制延迟问题。他提出按用户拆分数据,使不同服务器只承担部分数据与相应负载;但据他的叙述,这个方案没有在 Friendster 顺利推进,后来在 Flickr 得到了应用。
这份复盘不能单独解释一家公司的全部兴衰,却很清楚地暴露出一个工程问题:看出架构应该怎样改,和组织真的能够把它改掉,是两件事。及时重构既要求技术判断,也要求团队有能力调整分工、迁移数据、安排上线,并承担变化过程中的成本。一直修补局部症状,可能只是在更认真地维护一个已经不合适的前提。
这样的风险不局限于代码。互联网时代已经有许多诱人的想法,在架构、运营或需求验证环节遭遇现实。
Webvan 的网上买菜设想并不难理解,但仓储配送设施能容纳多少订单,与实际能获得多少订单,是不同的问题。其 2000 年年报披露,配送中心当时的业务量低于原设计容量的 40%,订单量也低于实现盈利所需的水平。课堂从中提炼的教训是:如果需求密度没有得到验证,庞大的仓配系统会把一次判断错误变得昂贵。这个结论是对案例的工程分析,不能简化成“某一种技术架构导致了全部失败”。
Quibi 则尝试为移动设备提供精品短视频。它在 2020 年的告别信中宣布结束业务,并提出两种可能原因:想法本身不足以支撑独立流媒体服务,或者推出时机不合适。这是管理层的回顾和解释,并不是对失败原因的实验鉴定。但它很适合提醒我们:有内容、有技术、有产品形态,并不等于用户需要这样一个独立服务。
Agent 让实现更便宜,却不会使这些问题消失。一个好想法可以迅速生成许多页面、文档和功能,也可能逐步“slop 化”:看起来越来越像成品,重复、冗余和未经验证的假设却越来越多。局部测试不断通过,真实用户仍然可能不愿意用;代码越写越多,原先需要检验的关键假设反而被埋住了。
因此,开始做原型与及时重构并不矛盾。原型让我们碰到现实,反馈让我们知道哪里需要改变,重构则把这种认识落实为新的结构。如果失败没有改变我们的判断,下一次循环可能只会更快地重复上一次。
这里还有一个社会性的威胁:不做很多次、不见过很多个真实系统,就很难做好。工程判断来自比较:相似需求为什么有人做得简洁,有人越做越乱?同一个检查为什么在这里有用,在那里只制造噪声?什么时候应该继续局部修补,什么时候必须改变架构?只有亲手遇到这些问题,经验才不只是几个名词。
在 Agent 开发中,Token 是模型处理和生成内容所消耗的基本计量单位,也代表了一部分实际试错成本。没有足够的 Token 预算,就很难获得同等数量、同等规模的自主开发尝试。“没有 Token,就没有做很多次尝试的机会”,强调的是这种实践资源的门槛;它不是说花钱越多就一定学得越好。
资源还必须和学习动机、学习能力结合。愿不愿意追问一次失败,能不能读懂别人的设计,是否会把观察变成下一轮可验证的改进,都影响着尝试最终产生什么。只是让 Agent 不停输出,并不会自动积累工程能力。相反,能理解反馈的人,会把一次尝试留下的经验组织起来,使后面的尝试更有价值。
“这不就斩杀了吗……”背后,是对教育过渡期的担忧:新的软件生产方式已经出现,让更多人获得高质量实践机会的教育组织却未必及时跟上。实践机会的差距可能积累成判断力的差距,在新的大学形态成熟之前,一代学习者可能先被挡在门外。这是本讲提出的风险判断,不是已经注定的未来。
它也把课程的任务推到了更具体的位置:除了讲方法,还要让学生有机会运行系统、观察失败、理解好的设计,并真正完成几轮有反馈的修改。我们想培养的,是知道怎样让下一次尝试比上一次更有价值的人。