Skip to content
All notes

8 Agent Native 开发:把工程知识放进环境

一次回答解决不了问题,就让 Agent 在工作区上反复迁移;每次迁移都由 CI 检查,把工程经验写成环境中可执行的约束。

8 Agent Native 开发:把工程知识放进环境

我的理解

  • 让模型先把原型做出来,比如一个 HTML 页面,我才知道自己想要什么、不想要什么。模型自己的视觉品味也是不得不品的一环。
  • Agent Loop 的产物差异,我原来以为来自提示词。其实是同一个提示词在工作区上反复运行:每一轮读到的状态不同,结果才不同。
  • Ralph Loop 很重要。它是 /goal 这类命令的原型:同一个提示词反复运行,每轮重新开始,进展只靠工作区里的文件传给下一轮。
  • Agent 的会话不是一直累积就好。每一轮只该带上真正需要的东西:结论、理由、证据,而不是全部历史。
  • CI 是合并 PR 的最低保障。把 CI 建立得尽可能严格,是减少 AI slop 的一种方式,和尽可能严厉的 verify 是同一件事。
  • 我认可「CI 阻止,Agent 修正」:改代码的 Agent 不能因为过不了检查,就自己调低标准。

原型:让假设变得可观察

前几讲讨论 intent、spec、impl 之间的缝隙,以及复杂性安放在何处。道理讲完,总得开始:做一个符合当前 spec 的原型,让它运行起来。

「Let it fail」不是鼓励随便交差,而是让原型暴露错误。实现可能不满足规格,规格可能没表达需求;两者都对上,关于用户、规模和环境的假设仍可能不成立。纸面上的选课系统,遇到两人同时抢最后一个名额,就会露出另一副面孔。幻灯片的例子是 1901 年莱特兄弟的滑翔机:升力只有预测的三分之一,他们随后自建风洞,重测翼型数据。

讲师认为,AI 给我们最大的能力是随时可以开始。没想清楚也可以先让它「搓一个」,再拿自己的 use case 评审;讨论中随时生成一版,比较变好还是变坏。模块边界清楚时,可以开几个 Agent 并行,人做监工。讲师最多同时开四个,再多大脑就跟不上了。

Agent Loop:同一个提示词,变化的世界

理论上,足够好的模型只需一个 bash 工具。现实是模型还不万能,工程品味尤其难教。讲师还指出训练带来的倾向:强化学习奖励「完成任务」,Agent 找到一条看似可行的路线就一路做下去,任务太难则直接放弃。上下文里装着屎山代码时,它倾向于把代码「扶起来」,而不是推倒重来。导师可以对学生说「这是零分代码」,学生会理解;模型却在遵循指令与改善项目之间选择前者。

应对办法是管理搜索:让模型在 design space 里适当广度优先,再适当深度优先。Geoffrey Huntley 的 Ralph Loop 是最朴素的线性管理方式:

while true; do
    cat PROMPT.md | claude -p
done

每轮重新开始对话,检查当前成果,推进一步并验证。提示词不变,工作区在变:上一轮的实现、测试结果和失败原因留给下一轮。while true 本身不判断完成,验收与停止条件须另行约定。讲师的版本要求每轮只推进明确的步骤,未完成时把下一步写进仓库,确认完成才输出 ralph done。

课上的对照实验:同一个模型做个人名片,直出版本有玻璃动效,却写错邮箱和所属机构;Loop 版本朴素,信息正确。讲师提醒结果有随机性,但投入的算力与提示词不同,产物可以差很多。

讲师也建议每人自己做一个 Agent。课上演示的 ReAct Agent 只有 150 行,工具只有一个 bash。先懂简单的核心,再看成熟产品多做了什么。小原型里硬编码的 timeout,到真实环境就成了问题:Agent 按自己以为的网速设超时,在国内访问 GitHub 时被杀掉。这类设计空间没有公认的最优解;新模型发布时,可以拿掉系统提示词与 skill,用熟悉的任务做背靠背对比。

工作区是状态,Agent 是状态迁移

Everything is a state machine。工作区是状态:需求、文档、部分实现、测试、运行结果、日志、待办。Agent 读入状态,执行一轮,产生下一份状态。

这决定了长程任务(long-horizon task)的做法。任务可以比任何一次会话都长,前提是下一轮能从留下的材料恢复判断:目标是什么,做了什么,哪些结论有证据。上下文压缩后 Agent 常有一段降智期,关键经验不能只留在上一轮的上下文里。「选课功能已完成」是含糊的交接;「普通选课已通过,两个请求竞争最后一个名额的测试失败,记录在某份日志」才能接着做。文档承担的是协作状态,不是工作量展示。

有了 Git、jj,状态还可以分叉,多个 Agent 并行推进。但版本控制允许并行,不保证任务独立:两个各自正确的改动,合在一起仍可能出错。

CI/CD:每次迁移都检查

Ralph Loop 最关键的提示词是:每轮都验证并评估结果。出发点是开发者不可信。讲师在计算机系统基础课上讲过两条公理:机器永远是对的,你写的代码永远是错的。人却总有自信与投机:「只改一句,应该没事」「样例过了,交 OJ 吧」。实习生推一个 PR,生产系统挂掉,并不罕见。

我们希望每个状态都是「好」的,就在每次迁移时检查,把「好」写成可执行的条件:能构建,旧功能没坏,符合项目约定。通过了还能直接发布。祝贺你,重新发明了 CI/CD:

  • 持续集成(CI):频繁集成到主干,每次自动构建、验证。改动越小,出错后越容易定位。Fowler 与 Foemmel 在 2000 年总结了这一实践。
  • 持续交付(Continuous Delivery):始终保持可发布,何时上线由人决定。
  • 持续部署(Continuous Deployment):通过检查的变更自动上线。

机制本身很简单:事件发生,执行指定动作。commit、push、PR、主干更新都是事件节点;之前课上让 Pi 在需要输入时「播放声音」的演示失败,改成 hook 后就好了:动作要挂在明确的事件上,不能指望 Agent 记得做。检查还须放在有约束力的位置:本地 pre-commit 反馈快,但可以用 --no-verify 跳过;远程 required checks 配合分支保护,失败才真正阻止合并。

把发布流程写成 Spec

机制简单,琐碎的是业务规则。两个 PR 单独都绿,合起来却坏了,要在合并前验证最新组合,繁忙的主干可用 merge queue。测试偶发失败,谁负责修,允许重跑几次,何时隔离。线上紧急修复,谁能 override,放行后怎样留痕、补检查。新版本先部署完、旧任务后结束,怎样避免旧版覆盖新版。Pre-LLM 时代光是配置语法就劝退很多人;今天 Agent 可以写配置,但失败怎么办、谁有权例外,仍是需求的一部分。

课程网站的流程可以这样写:

  • 合并:PR 有新提交就重跑 Lint 和测试,合并前验证与最新主干的组合,失败则禁止合并,并落实为分支保护规则。
  • 发布:main 更新后,对该提交运行检查并构建页面;全部成功才部署这次的产物到 github.io,失败保留上一版。

Lint 静态检查格式、可疑写法与项目约定,测试执行程序检查行为,二者不能互相替代。验收时要走失败路径:故意制造一次 Lint 错误、一次测试失败,确认真的合不了、发不出。检验的是门是否关得住,不是配置看起来像不像。

CI 阻止,Agent 修正

Agent 是更不可信的开发者,因为它与人天生不对齐,常做人类不会做的事:一写就是 20 KB 文档;把思考过程逐条写进文档;在细枝末节上过度用力,却忽视设计空间里重要的部分;用截图填满上下文;写出人读不懂、自己却很有信心维护的代码。

讲师特别指出文档与代码的关联。doc.md 与 implementation.py 的对应只在写下的那一刻存在;换一个 Agent 或上下文被压缩,就没人记得要保持一致。两者不一致时听谁的,没人知道。项目一旦成为 slop,人就再也改不动,只能让 Agent 继续改到维护不下去。

只在 AGENTS.md 写「保持简洁」不够:上下文膨胀后 Agent 的注意力会涣散,项目到十万行级别尤其明显。可判定的规则交给 CI,例如文档不超过 1000 字。Agent 能在本地跑 pre-commit,失败后自己想起「文档禁止膨胀」。人积累 slop 的速度远不如 AI,这类规则过去少有人做,今天变得重要。幻灯片把它类比为丰田的 jidoka:发现异常就停线,把质量检查放进生产过程。

常见问题自动检查人工判断
一写就是 20 KB 文档字数预算、重复段落检查内容是否值得保留
堆细节、造多余抽象重复率、未使用代码检查抽象是否必要
小任务变成大重构改动范围白名单、diff 预算超出范围是否批准
代码能跑但读不懂Lint、类型检查、复杂度上限命名与设计审查
改测试让失败消失受保护的回归测试、删测试告警行为变更是否合理
顺手关闭 CI 规则规则文件变更须指定人批准批准者与开发者分离

检查失败时应说明原因与修法,只给红叉,Agent 只能猜规则。还有一条边界:实现功能的 Agent 不能因为过不了检查就单方面调低标准。规则可能确实过时,修改须回到需求和设计理由。==删掉失败断言得到的绿色,与修复程序得到的绿色,颜色相同,含义完全不同。==

Agent Native:把工程知识放进环境

围绕 Agent 的运行环境、工具、约束与反馈机制,常统称为 Harness。Agent Native 开发不只是代码由 AI 写,还要让工程知识成为 Agent 找得到、看得见、能验证的东西:以工作区为状态,每步迁移经过 pre-commit、pre-push、pre-merge 等门控。

OpenAI 的《Harness engineering》从空仓库开始,人不写代码,转而组织环境、表达意图、建设反馈。可拆为四个问题:

  1. 知识是否外部化:「我们一直这样做」对新一轮 Agent 没有意义。AGENTS.md 做目录,设计、计划与决策进仓库,按需查阅。
  2. Agent 能否自己观察程序:每份工作区能独立启动应用,Agent 自己操作页面、查日志、复现 bug,不靠人转述「按钮好像没反应」。
  3. 品味能否变成规则:依赖方向用 linter 检查,报错信息直接包含修复指导。哪些规则值得固定,仍须判断。
  4. 清理是否持续:后台 Agent 定期扫描偏离规则的代码,提交小规模重构,不等坏模式扩散到全仓库。

讲师吐槽「ClosedAI」:文章没有附上可复用的完整门控配置,最重要的一句是「What we're still learning」。仓库里能看到三层:PR 跑快速门禁(格式、拼写、文件过大等),main 跑全量测试,发布时打标签。讲师判断其内部还有更多机制,因为单靠 lint 写不出「有人感」的项目。

其他团队的经验:

  • Stripe Minions:Blueprint 混合两类节点,实现任务、修复 CI 失败交给 Agent,运行 linter、推送分支由普通代码执行。完整 CI 最多两轮,之后交回人审查,因为继续重试的收益递减。
  • Anthropic 的 C 编译器:16 个 Agent 编译 Linux 时撞上同一个 bug,重复修复、互相覆盖。改为大部分文件用 GCC 编译,只把子集交给新编译器,失败后逐步缩小范围。验证方式改变了任务的可分性;增加 Agent 之前,先改善任务表示与反馈。
  • StrongDM:Agent 会用 return true 应付狭窄测试,也会改测试迁就代码。他们把端到端场景放在代码库外,像留出的考题;为 Slack、Okta 等外部服务造行为替身(Digital Twin Universe),与真实服务对照修正后高频重放。替身与真实系统的差异也须验证,否则只是造了一个容易过关的考场。

讲师自己的项目把进展画在架构图上:architecture.md 生成架构图,log book 记录任务,进行中的任务推到 CI,图上显示哪个模块有几个 Agent、进度多少。总架构师看这张图,就能预估一小时后项目的样子。

DeepSeek Harness:把经验写成门禁

课上细读了 DeepSeek Harness。作者用中文写文档,因为母语阅读快一倍;项目要走向世界,就得同时维护中英文两份。文档随手改、随手提交,两份怎样保持一致?

它的做法是「三套件 + 一个门禁」。每篇文档是同一目录下的三个文件:foo.md 正文,foo.zh.md 另一侧译文,foo.i18n.yaml 记录两侧各节内容的哈希。两侧权威对等,谁先写谁是这一次的原文。CI 运行 verify-translation-pairing:三件缺一即红;哈希不匹配时,由 AI 按 patch 只翻译改动的部分,不重新翻译全文。目录因此不干净,可以用 IDE 隐藏另一种语言的文件。讲师由此认为:软件不应只是一棵目录树,而应由关系组成。

其他门禁都在把隐含的一致性要求变成检查:文档字数预算;术语表,可检查两侧对应词的出现次数是否接近;文档里的类型与代码块须逐字出现在源码中;所有链接合法。写作风格样例与翻译规范按需加载,不塞进 AGENTS.md,否则项目长大后上下文装不下。代价是每次提交前要跑大量门禁;人只写 plan 和 log book 时,代码没动,不必跑。

git 历史显示这些机制是逐步长出来的:先有术语约定,再有对等权威与配对契约,很快又意识到约定必须进入自动化流水线。讲师猜测其灵感来自 docstring、JavaDoc、Doxygen 这类让注释与文档保持一致的工具。这些设计一看就觉得「确实应该这样」,却很难凭空想出,来自真正失败过的人。

重构:清理解决不了失效的假设

持续清理技术债,可以比作垃圾回收:消除重复、修正文档、统一接口。但它解决不了失效的设计假设。单写入节点加多个完整副本,用户少时直观;写入增长后,加副本未必能解除瓶颈,因为每个副本都要重放全部更新。代码整理得再漂亮,也不改变负载结构。

Friendster 的工程师 Dathan Pattishall 在 2006 年的复盘中提出按用户分片,方案在 Friendster 没能推进,后来在 Flickr 落地。看出架构该怎么改,和组织能把它改掉,是两件事。讲师给学生的建议是:架构烂掉时开一张白纸,把旧功能逐步迁进来。深度优先走错方向,必要时做模块级甚至项目级重构。

好想法也会在落地中失败。Webvan 的配送中心负载不到设计容量的 40%,订单密度未经验证就重资产扩张,每轮试错都更贵。Quibi 在 2020 年关停,管理层认为构想或时机不足以支撑独立服务。Agent 让实现更便宜,好想法可能更快地「slop 化」:越来越像成品,未经验证的假设却被埋得更深。原型碰到现实,反馈指出问题,重构把认识落实为新结构;失败若没有改变判断,下一轮只会更快地重复上一轮。

Ralph Loop 与 /goal:两种让 Agent 反复尝试的方式

Ralph Loop 是 /goal 这类命令的原型,两者都不信任干活的 Agent 自己说「我完成了」,但实现方式不同。

Ralph Loop:每轮新会话

原始形式只有几行 shell:

while true; do
    cat PROMPT.md | claude -p
done

每轮启动全新会话,聊天记录清空。唯一的记忆是工作区:代码、文档、测试结果、进度文件。外层脚本检查状态文件里有没有 ralph done,有就停,没有就再跑一轮。

名字来自 Geoffrey Huntley 借用的《辛普森一家》角色 Ralph Wiggum:有点笨,但从不放弃。

/goal:同一会话的独立评估

Claude Code 和 Codex 内置的命令:

/goal test/auth 下所有测试通过,且 lint 干净

在同一会话内继续,每轮结束由另一个小模型(默认 Haiku)判断:还没达到、达到了,还是不可能达到。进展靠对话本身传递,不强制写文件,但对话会被压缩。

两者的差异

Ralph Loop/goal
上下文每轮清空,全新会话同一会话累积,会被压缩
靠什么传递进展只有工作区里的文件对话本身,建议也写文件
谁判断完成外部脚本,比如 grep 一个标记另一个小模型读对话记录判断
要自己写循环吗要,三行 shell不要,命令自带
成本每轮不重放历史,通常更省上下文越来越长,越来越贵

共同的核心是把「完成」的判断权交出去:Ralph Loop 交给你写的规则,/goal 交给另一个模型。两者都解决了干活的 Agent 急于宣布完成的问题。

Ralph Loop 的代价是什么都不记得。每一轮必须在文件里写清楚:目标、已完成的内容、走不通的路、下一步和证据位置。写不清楚,下一轮就重新踩坑。/goal 的代价是上下文膨胀,压缩后模型会有一段表现下降。

还有 /loop,按时间间隔触发(比如每 20 分钟检查一次 CI),适合轮询外部状态。/goal 管「做到满足条件」,/loop 管「隔多久看一眼」,两者可以合用。

实践机会:变成判断力的差距

工程判断来自比较:相似需求为什么有人做得简洁,同一个检查为什么这里有用、那里只是噪声,什么时候该改架构。只有亲手遇到,经验才不只是名词。没有 Token,就没有做很多次尝试的机会;资源还须配合学习动机与能力,只让 Agent 不停输出不会积累工程能力。

讲师称之为「斩杀线」:毕业时若不如 Agent 好用,用人单位宁可给 senior 无限 Token,也不培养新人;这条线已从博士降到本科,Agent 还在上涨。实践机会的差距会积累成判断力的差距,在新的教育形态成熟之前,一代学习者可能先被挡在门外。这是讲师的风险判断,不是注定的未来。他的建议是在互联网上找真正优秀的开源项目,从 DeepSeek Harness 读起。