
Agent Harness 的系统架构
从一个 change 出发,看懂运行循环、职责分层、信息流与迁移边界
§ 本节目录
C01 解释了为什么要造马具:模型是外因,harness 是可被工程团队改造的内因。那张地图回答的是为什么这么走,却还没有展开:一套能持续交付的 harness,作为整体运转时究竟长什么样。
这一节补上工程坐标。先跟随一个 change 穿过系统,再分别从组织、信息和能力三个角度看它;随后再回到这套架构的形成史与迁移条件。读到具体数字、文件路径和脚本时,请把它们当作我个人项目在中等规模下自然收敛出的落点,不是标准答案。项目结构、规模和检验方式不同,最后长出的部件会不同;但需要回答的问题大体相同。
真正需要解释的不是 Agent,而是 change 怎样走到交付
先不谈技术,先看一项具体改动怎样走完它的路。
一个 change,也就是一次意图明确、边界清楚的改动,可能是新功能、接口调整或 bug 修复。从它进入系统到成为可交付候选物,至少要回答七个问题:
- 一句话意图,怎样变成 agent 能读的可执行输入?
- 每一步做什么,由谁决定?
- 哪些步骤必须确定性执行,不能交给模型猜?
- 开发出的东西,谁来判断“对不对”?
- 判断失败后,材料怎样回到下一轮?
- 什么情况下应当停下来交给人?
- 一件事结束后,留下的是聊天记录,还是可审计的证据?
这不是 harness 独有的问题。任何要交付候选物、又必须接受检验的系统都会遇到它们。过去,流程中的工程师兼任了一条“隐性总线”:写代码时记得评审意见,跑测试时知道本轮改了什么。让 agent 无人值守地接手之后,这条总线不能再靠某个人的记忆维持,必须被写进系统。
C01 的“造马具”是一句思想;这一节要把它落实成工程画面。先依次建立四个视角:一个 change 怎样穿过系统,一次协作内部怎样分层,一条约束怎样跨边界传递,以及一套成熟系统最终长成怎样的能力剖面。这四张图不是搭建教程,而是帮你辨认系统的地图。地图立住后,再换一个问题:这些能力为什么会按这个顺序长出来,又该怎样带到别的项目里。
一个 change 怎样穿过系统
一次 change 不是“叫 agent 干一下”,而是一条可以重入的状态链。
四个相位各回答一件事:
- prepare:把意图变成可执行输入。它检查环境,为本次改动建立独立工作现场,并确认变更提案、任务清单和评估计划等材料已经就绪。做不到,就不进入开发。
- develop:开发 agent 在准备好的现场推进实现,再运行一组确定性检查。我们项目把这组检查放在
agents/gates/run.sh,也就是每次开发后都会执行的门禁脚本;它依次运行类型检查、静态规则、测试、构建和禁区扫描。agent 说“完成”时,交出的仍只是候选物。 - review:独立的评估 agent 按变更契约审查候选物,写出可审计的评估报告。结果可能是
passed,即进入收口;needs_revision,即带着具体证据回到开发;或blocked,即退出循环并交给人处理。 - finalize:把通过的候选物收口。它汇总每轮开发与评估记录,推送分支,并归档契约与证据;它不自动合入主分支,最终决定仍在人手里。
develop 与 review 可以来回多轮。你的项目可以是两轮,也可以是五轮,但必须设一个硬上限。上限不是为了逼 agent 成功,而是为了在预算耗尽时把问题交回人:判据错了、任务范围错了,还是模型能力不足,不能由脚本替你判断。
四个相位的入口集中在 agents/loop/flow.sh,这是我们项目中按相位分派工作的 bash 脚本。比如 flow.sh develop --change <change> --round 1 只推进当前相位,结束时输出一句 next: 提示,不会擅自进入下一相位。脚本的克制很重要:它负责记录状态和提供下一步,不负责替人作推进决定。
如果这一节只能留下一个直觉,那就是:一个 change 不是一次 agent 调用,而是一条可重入的状态链;失败不靠重开会话,靠上一轮证据进入下一轮。 后面谈计划、门禁与循环时,你会不断回到这条链上。
确定性的事交给流程,判断性的事交给 Agent
从任何一个相位往里放大,看到的都不该只是“一个 agent 在干活”,而是职责被分开的几个位置。
这些位置各自承担不同责任:
- 确定性调度层:由 bash 脚本组成,不消耗模型 token。它读取当前状态、拼装本轮输入、运行门禁、维护状态目录。变量替换、文件存在性和状态分支交给程序更稳定,也更便宜。
- 主力双席:开发 agent 负责实现意图,评估 agent 负责按判据审查。二者拥有不同的任务输入和运行上下文,共享的是同一份契约,而不是“我写完后自己确认没问题”的自我背书。
- 按需委派的 SubAgent:它们适合处理过程繁琐、结论相对简洁的任务,例如探索仓库、执行浏览器端到端测试或收集视觉证据。SubAgent 只回传结论和必要证据路径,原始日志、大段代码和二进制材料留在它自己的现场。我们把嵌套深度限制为三层;更通用的原则是,限制必须以“结论还能否被人复核”为依据。
- 人:人不在调用栈里,却必须在关键节点在场。定方向、改判据、处理
blocked,以及决定是否合入,都是不能被自动挤出的职责。
看一次开发与评估出现分歧的场景。开发 agent 完成本轮修改并跑完门禁,留下 dev-r<N>.md,也就是按轮次编号保存的开发报告;它认为任务已完成。评估 agent 对照同一份契约执行检查,必要时委派端到端测试,发现有条款未兑现,于是在 eval-r<N>.md 中写明失败条款和证据路径。调度层读到 needs_revision 后不解释、不站队,只把这份报告装配进下一轮开发输入,并更新状态。分歧若超出项目允许的轮次,就转为 blocked,由人判断下一步。
这里没有谁“更聪明”。可靠性来自清晰边界:调度层不替人判断,评估不替开发背书,SubAgent 不把原材料淹没主上下文,人也不放弃决策权。后文在把 Agent 排成一支队伍里,会从这里继续展开具体的编制与协作规则。
跨边界传递的不是聊天记录,而是契约与证据
运行路径和职责分层都已经出现了边界:相位之间、开发与评估之间、主力与 SubAgent 之间,以及相邻轮次之间。接下来要问的是:穿过这些边界的到底是什么?
不必逐项背三条信息流。沿着一条约束走一遍,就能看清机制。
假设变更契约里有一句:“付款接口的响应字段名不许改。”它在系统里怎样旅行?
它诞生在项目真源。 这条约束可能写在 OpenSpec 的规范文件 openspec/specs/<capability>/spec.md,也可能写在项目入口文档 AGENTS.md 的红线段落。OpenSpec 是把一次改动的提案、任务和验证要求写成文件的机制;<capability> 是具体能力名称的占位符。关键不在文件名,而在同一件事只保留一个可更新的位置。后文契约的地图与真源会专门讨论怎样建立这种真源优先级。
它被收进本次 change 的契约。 openspec/changes/<change-id>/ 是当前改动的档案目录,里面的 proposal、tasks 和评估计划引用这条约束的来源,而不是复制一份原文。引用而非复制,减少了两份规矩各自演化的风险。
它进入本轮开发输入。 调度脚本触发开发时,会把任务卡、契约引用、上一轮评估报告和门禁摘要组合成运行时输入。我们把拼装逻辑放在 agents/loop/flow-lib.sh,即供主入口脚本调用的公共函数库。对上一轮反馈有一条硬约束:只改,不扩范围。评估报告说了什么,就让开发 agent 回应什么,不能借修复之名扩大任务。
它约束代码,也接受多条检验路径的撞击。 如果开发 agent 改了付款接口字段名,敏感改动扫描、自动化测试、评估 agent 的逐条比对,以及端到端测试都有机会发现偏差。每一条路径都可能漏,但多条相互独立的路径共同对齐同一份契约,才能让“符合要求”不是一句自述。
它留下证据并进入下一轮。 门禁输出被保存到 .gate-logs/,也就是工作现场内按次存放检查日志的目录;评估报告引用具体条款与截图路径。SubAgent 的大日志和 JSON 原材料不被塞回主窗口,主力 agent 只拿结论与路径。后文的先认识上下文,再动手与上下文怎么管会解释这种“先卸载、再按需读取”的纪律。若本轮需要修订,下一轮直接引用 eval-r<N>.md 中失败的条款;文件系统于是成为跨上下文交接的总线。
它回到人的最终判断。 收口阶段保留改动本体、评估报告、门禁日志与端到端证据。人决定是否合入前,可以沿着证据链逐项翻看。
一条约束从真源出发,穿过契约、运行时输入、代码、证据与反馈,最终回到人面前。每一次跨边界,系统都要回答三个问题:保留什么,卸载什么,怎样防止失真。
如果只能带走一句话:上下文工程不是往窗口塞更多信息,而是让正确材料在正确时间穿过正确边界,并留下可核验证据。
把运行画面投影为能力栈
前面三幅图讲的是一项工作怎样运动;现在换成静态剖面,看支撑这条运动链的能力都有哪些。
自下而上,六层各用一句话辨认即可:
- 底座层:可编程的平台与按角色分配的模型。后文会在Agent平台与任务底座模型怎么选中讨论怎样根据可改造性和角色差异选择它们。
- 阵地层:每个 change 独占的工作现场、进场体检和运行时预览环境。后文的给 agent 建三段阵地会解释为什么工作隔离是可信执行的前提。
- 装备层:按需要配置的工具与分时效保存的记忆。它们分别解决“够不够得着”和“还记不记得住”,具体取舍留给Agent的手与脑。
- 上下文层:输入材料的优先级、装载时机和跨边界防火墙。它防的不是“信息少”,而是错误、冗余和过时的信息占住窗口。
- 契约层:项目地基、变更提案、验收判据和 agent 间输出协议。没有契约,验证就没有可对照的判据;从计划走到契约,是把 Plan 产物变成Agent可读的契约的核心任务。
- 验证层:确定性检查、独立评估与人的最终判断。门禁解决“有没有明显坏掉”,评估和人继续回答“是否真正做对”。Agent 交付之前的门禁与Agent的评估系统会分别拆开这两层。
不必逐层朗读,先记住四条依赖关系:
- 没有契约,验证没有判据。 门禁全绿不等于 change 做对了;“对”只能对着明确的要求核。
- 没有阵地,执行拿不到可信现场。 在被其他任务污染的工作区里跑出的通过,没有说服力。
- 没有证据回流,循环只是重复运行。 没有上一轮报告,下一轮只是从同一处再撞一次。
- 没有人的目标判断,系统只能沿既有判据优化。 判据本身对不对、这项 change 值不值得做,仍要由人回答。
能力层是成熟系统的空间剖面,不是搭建顺序。从零开始,不是先花两周把底座堆满,再去补契约;而是先让一条最小 change 跑通,让底座、阵地、契约和验证各有一小部分,再在真实问题中补齐。C01 所说的“每个阶段是一颗曳光弹”,在工程上就是这个意思。
到这里,四张地图已经闭合:一条 change 的时间路径、一次协作的职责边界、一条约束的信息旅程,以及成熟系统的能力剖面。接下来切换视角:不再问“它由什么组成”,而是问“它为什么会按这个顺序长出来”,以及“什么条件下能迁到别处”。
架构为什么按这个顺序长出来
能力层不是搭建顺序。真实的搭建过程也不是先画好蓝图再施工,而是主要矛盾不断转移,迫使系统补上新的能力。
| 阶段 | 上一阶段解决了什么 | 随之暴露的新矛盾 | 因而长出的能力 |
|---|---|---|---|
| 一:想清楚,定契约 | 起点 | 意图与实现总对不上 | 项目地基、变更契约、判据先于实现 |
| 二:备齐弹药,开始动手 | 契约立起来了 | agent 能力有限,任务规模膨胀 | 上下文分账、职责分层、平台选型、阵地隔离 |
| 三:撞墙之后,补成门禁 | 能动起来了 | agent 自评完成,实际并未完成 | 确定性检查、自修回路、独立评估、判据审计 |
| 四:让它持续转起来 | 单轮不再自说自话 | 一次跑成不等于长期能跑 | 生命周期、反馈引用、容错与人介入 |
| 五:推广到协作与生产 | 单人场景跑得转 | 上生产、换项目、拉团队后失效 | 生产回归、真源治理、团队对齐、迁移判据 |
这张表与 C01 的五阶段总图分工不同。C01 给出的是每一阶段的主要矛盾;这里展示的是每种能力如何被上一阶段的失败逼出来。规则不是凭空设计的,而是被失败教出来的。
三个坐标也不能混为一谈:五阶段是形成史,能力栈是成熟系统的空间结构,change 生命周期是单项任务的时间顺序。照着别人的能力栈从下往上盖,或照着别人的生命周期机械排步骤,都是在借别人的形状塞自己的问题。
这不是孤例,但也不是行业标准答案
我的实践不是凭空长出来的。同一时期,几家公开写过类似问题的团队,在结构上出现了相似选择,但具体配置明显不同。
长任务需要结构化交接物。 Anthropic 在 Effective harnesses for long-running agents 中让进度文件承担跨上下文交接,并用初始化脚本和功能清单维持每轮的干净状态。相似处在于,交接依赖制品而不是“记住上文”;不同处在于,他们使用单个结构化进度文件,我的项目以变更档案与门禁日志分开归档,因为批次任务需要按 change 独立追溯。
仓库知识必须对 agent 可读,并按需披露。 OpenAI 在 Harness engineering: leveraging Codex in an agent-first world 中将环境、上下文、约束与反馈视为工程投资。相似处在于,仓库不是“AI 自己读读就会懂”;不同处在于,OpenAI 的场景允许 agent 直接发起合入,我的项目保留了人工合入闸门,因为这笔决策成本仍然值得支付。
确定性节点与 agent 节点需要混合编排。 Stripe 的 Minions 用工作流连接确定性节点与 agent 节点,也提供隔离开发环境和集中工具设施。相似处在于,判断交给 agent,可确定执行交给程序;不同处在于,Stripe 面对的是数千工程师与大量服务,我这里用的是 worktree、脚本和少量按需工具。结构相似,不意味着配置相同。
还有一条来自本地多批次记录的观察:固定模型,只调整 harness 中的判据、反馈写法或评估规则,通过率就可能相差 30 到 50 个百分点。这是这个项目的经验量级,不是可外推的通则;它至少提示我们,模型能力不是系统表现的唯一解释。
把这些相似与差异放在一起,得到的不是“谁抄了谁”,而是一条更有用的判断:能不能迁移,不看长得像不像,而看它们的检验方式是否同构。
迁移的不是部件,而是检验闭环
同一套做法在一个项目有效,换一个项目却失效,往往不是技术栈变了,而是目标产物接受检验的方式变了。下面这一正一反两个案例,使用的是同一套 harness、相近的 agent 分工,却得到了不同结果。
正例:多个既有后端项目的骨架复用。 我在部门内推广时,最容易复用的是一批已有基础、但历史包袱不轻的后端服务。它们能启动进程,有健康检查,有基础回归,也使用近似的 CI/CD。第二个项目之后,通常只需调整契约和判据;调度层、门禁结构、评审分工和收口方式很少改动。
一次响应序列化库升级中,开发 agent 跑完门禁,编译和单元测试都通过了。但真实回归发现,新库处理某类空字段的格式与旧库不同:客户端原本接收 null 的位置拿到了空字符串,页面呈现为空白输入框。评估报告把这一现象和证据路径带回下一轮,开发 agent 修复后才通过。这里有效的不是“后端脚本”本身,而是回归路径把真实运行拉进了判据。
反例:把后端 harness 直接搬到 SDK 项目。 语言、构建工具和 CI/CD 看起来都相似,目标产物却不同。后端是长驻进程,可以单独启动、探测、跑回归;SDK 是嵌入宿主的库,不能靠自身独立运行,它的正确性要在宿主项目的集成测试里才看得见。直接搬用后端脚本时,门禁依旧可以全绿,但这个“绿”只表示编译通过,不表示在宿主的真实边界里正确。一次方法在宿主边界场景中抛异常,harness 从头到尾都没有机会发现它。
因此,SDK 项目真正需要补的不是更多脚本,而是一条能启动宿主集成测试、把宿主失败带回当前 change 的检验路径。复制脚本不能复制有效性;搬骨架容易,接通检验方式更难。
能否迁移的关键不在语言和框架,而在目标产物怎样回到实践接受检验。
判据本身很短,用起来才有效。把它拆成四步,每一步不是必须照抄的 SOP,而是开始迁移前值得逐一经过的判断关口:
- 认识项目:真源在哪,启动方式是什么,“跑通”究竟意味着什么?
- 问自己:能否向没接触过项目的人解释验证方式?能否独立启动它,或用现有脚本重现一次“跑通”?
- 通过条件:能解释,能启动,或能稳定重现已有验证。
- 建立最短反馈:最明显的错误能否被机器稳定拒绝?
- 问自己:故意写入一处典型错误,门禁能否抓住?跑一次真实回归,能否复现确定性的失败信号?
- 通过条件:失败可复现,通过可信。做不到这一点,后面的自动化都建在浮沙上。
- 接通任务闭环:失败能否成为下一轮的明确输入?
- 问自己:评估报告能否被下一轮开发引用?工作现场能否恢复,而不是每次重新开始?
- 通过条件:证据可引用,状态可恢复。
- 扩展协作与生产边界:新机制真的解决了当前瓶颈吗?
- 问自己:想加入多 agent 编排、长期知识库或自动分发时,能否指出一个已发生的失败在推动它?还是只因为“看起来更完整”?
- 通过条件:有真实失败案例,也有可观察的收益证据。
四步的价值不在于填满流程,而在于允许你在某一关停下。一个声称“agent 可以自主运行”的项目,如果连第二关都没过,最明显的错误仍然靠人肉发现,再多复杂机制也救不了它。C01 交给你的六问在这里有了实际用法:每搬一套做法,都问它解决什么矛盾、如何回到实践受检验、适用条件是否仍然成立。
把地图交回读者
这一节的收束不是“你已经学会了”,而是把一张可以回到自己项目使用的地图交到你手里。
不提供可直接抄走的模板。下面是一张诊断表的样式,请回到自己的项目里填:
| 栏 | 我在自己项目里的答案(示例栏,不填) |
|---|---|
| 我的项目卡在哪一阶段? | 例如:意图和实现总对不上,对应阶段一 |
| 我的产物怎样受实践检验? | 例如:启动服务、健康检查与一组回归 |
| 已经有的能力 | 例如:一份 AGENTS.md 与三道 CI 门禁 |
| 当前最短缺口 | 例如:没有独立评估,开发者自己宣布完成 |
| 我借过什么做法,是否验证过适用条件? | 例如:借了大厂多 agent 编排,但项目只有二十个脚本 |
| 下一步只补哪一处? | 例如:先补一个 evaluator,其他暂不动 |
诊断表的意义不在“填全”,而在于逼你一次只挑一处补。先从这套 harness 概括出通则,再回到自己的项目寻找主要矛盾,这才是 C01 所说的“由特殊到一般,再由一般到特殊”。
看完这张工程地图,第一步不是选平台、装工具或搭多 agent 编制。第一步是把意图变成边界清楚、可执行、可检验的 change。 下一节从 Plan 出发,讲怎样把问题理清,再变成这样的 change。