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》从空仓库开始,人不写代码,转而组织环境、表达意图、建设反馈。可拆为四个问题:
- 知识是否外部化:「我们一直这样做」对新一轮 Agent 没有意义。AGENTS.md 做目录,设计、计划与决策进仓库,按需查阅。
- Agent 能否自己观察程序:每份工作区能独立启动应用,Agent 自己操作页面、查日志、复现 bug,不靠人转述「按钮好像没反应」。
- 品味能否变成规则:依赖方向用 linter 检查,报错信息直接包含修复指导。哪些规则值得固定,仍须判断。
- 清理是否持续:后台 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 读起。