一个多月、四十万行代码、五个甚至更多Claude Code会话同时开工之后,我发现真正稀缺的东西已经不是模型的产能,而是我自己的注意力。这篇文章讲我为什么、以及怎么做了goal_control——一个管很多Coding Agent同时干活的薄控制面。它不是产品说明书,是一份带着教训的工程复盘。
一、瓶颈换了地方
五个终端同时闪
先说一个场景:某天晚上九点多,我面前开着五个Claude Code终端,各跑一个任务。左上角那个在问我"接口返回404还是409",右边那个刚跑完想让我确认能不能提交,下面一个撞上了权限确认在等我按回车,另外两个我已经不太记得它们跑到哪儿了。
我回答完左上角那个,切到右边,看了两分钟diff,回过头发现左上角那个又停了,这次问的是另一个问题。等我把五个都扫一遍,最早那个已经空等了二十分钟。
这就是过去一个多月的常态:该项目项目40+万行代码、前后端好几个仓(多编程语言),周期短,强度高,我几乎全程用多会话并行开发。刚开始两三个会话并行的时候很爽,产能肉眼可见地翻倍。到了五个以上,情况开始变化:Agent还是那么能干,但我不行了。
具体说,我遇到了四个问题:
- 注意力被持续打断: 每个会话在任意时刻都可能停下来找我,澄清、审批、权限确认混在一起,随机到达;
- 多任务状态说不清: 现在哪些在跑?跑到什么程度?谁做完了?做完了下一个该起谁?这些问题我要靠翻终端和记忆回答。
- 任务之间靠人衔接: 一个任务跑完,是我去判断"可以起下一个了",然后开新会话执行下一个任务。Agent能自主完成"一个任务",但"很多任务"的调度全在我手里。
- 决策不可回溯:一个任务跑几个小时,中间我做了七八次决定。事后想知道"它为什么最后长成这样",得在几十万token的对话里翻。
说实话,头两周我以为这是我自己不够专注。后来我意识到不对:人脑本来就不适合当抢占式调度器。
说白了是两类问题,不是四个
我把这四个问题放在一起看了很久,最后的结论是:它们不是四件事,是两件事,而且有明确的因果链。
问题1、2、3的根子是同一个:状态没有外置。 Coding Agent会话的"状态"是隐式的,散落在对话历史里。会话自己到后期都要花力气回忆"我是谁、我在做哪一片",你当然无法观测一个从未被写下来的东西。状态不外置,就没法知道谁跑到哪儿;不知道谁跑到哪儿,就没法自动触发下一个;于是一切都得人来衔接。
有意思的是,“进度"这件事我其实早就定义好了。我的开发方法是先把需求拆成任务切片,每片有验收标准,再把切片聚成Goal编排成依赖图。进度就是"第几片完成了”,粒度天然存在。缺的只是让会话把这个状态持续写到上下文之外的某个地方。
问题4才是真正的硬问题:交互模型是中断驱动的。 每个Agent都可以在任意时刻以最高优先级抢占我,而人的上下文切换成本是分钟级的。N个Agent 的打断随机到达,我被迫在脑子里做抢占式调度。这件事跟"状态透不透明"无关,就算每个会话都把状态写得清清楚楚,它们还是会随时打断我。
所以,解决问题的顺序也就清楚了:先做状态外置,再改交互模型。前者是后者的前置条件。
为什么原生Coding Agent不管这件事
有人会问,Claude Code、Codex这么强,为什么不直接管这件事?
我的看法是:它们的一等公民是"会话",而我需要的一等公民是"目标"。一个会话就是一个Agent加一段上下文,它天生擅长把一个任务做完。但"很多任务之间的依赖、调度、状态汇总、决策排队"这些东西不属于任何一个会话,它们属于会话之上的那一层。原生工具没有这一层,不是能力不够,而是这不在它的抽象范围内。
好在原生工具把这一层需要的原语都给了。Claude Code有headless模式(claude -p),可以像跑一个脚本一样跑一个Agent;有--resume可以续回一个会话;有stream-json输出格式可以逐行拿到会话的每一步。采集面和控制面的原语都在,缺的只是我这一侧薄薄的一层约定和聚合。
我先否掉的方案:K8s式编排层
动手之前我评估过一个很自然的想法:既然有很多"工作负载"要调度,能不能做一个K8s式的会话编排层?声明式期望状态、控制循环、健康探针、自愈重启。
结论是:思想能借,形态不能抄。
能借的是三样:期望状态与实际状态分离(Goal编排文档是期望状态,外置的运行状态是实际状态);控制循环(持续比对"该跑而没跑"并触发);统一的状态上报协议。
不能抄的原因在于负载性质完全相反。K8s管的是同质、无状态、可廉价重启的进程,核心价值是自愈、扩缩容、资源装箱。Agent任务恰恰相反:有状态、重启昂贵、不可抢占、完成判定模糊。你不需要调度器决定任务跑在哪,不需要副本数,更不需要健康探针——Agent的"健康"是语义层面的,一个进程活着但在原地打转,探针探不出来。
更贴切的心智模型其实是CI/CD流水线 + 带人工审批门禁的收件箱:Goal约等于一个CI job,依赖约等于needs:,人工决策点约等于when: manual。任务是一次性的,有明确产出物(一个提交),失败了重跑而不是迁移。
于是goal_control的形态就定下来了:不是一个平台,是"文件 + 薄脚本 + 一个人机协作控制台"。
二、我的解决思路
这一部分讲设计思想,不讲功能。功能在后面。
先说清两个词:任务切片与Goal
我简单介绍一下我的部分开发模式,先建立一个基本认知就够了。
任务切片是可以独立实现、独立测试、独立合并的最小单元,自带验收标准。它是开发方法层面的粒度:拆解需求的时候产出的就是它。一片切片通常对应几个文件的改动和一组测试。
Goal是拓扑序上连续的2到4个切片。它是执行层面的粒度:一个Goal对应一段完整的 Prompt,交给一个headless会话一口气跑完,组尾做一次验证。为什么不能只用一种粒度?因为两种粒度服务的对象不同。切片太细,如果每片起一个会话,会话之间要反复重建上下文,启动开销和上下文丢失都吃不消;Goal太粗,如果只以Goal为单位汇报,一个跑三小时的Goal中间就是黑盒,你不知道它卡在第一片还是第三片。所以调度按Goal,汇报按切片。
任务组则是一份清单,也就是一张Goal之间的依赖图,goal_control调度的对象是Goal和Goal之间的边,切片只是它眼里的进度刻度。
1. 会话是易耗品,Goal才是一等公民
这是整个设计最底层的一条:Session不是一等公民,它只是Goal某一次运行的执行载体,可以死、可以被--resume、可以换一个新会话接着跑。Goal的状态必须活在会话之外,才谈得上一切:谁在跑、卡在哪、下一个起谁、事后复盘。
对应到实现上,claude -p起一个headless进程,输出逐行落盘,退出即被收割。Goal状态文件里只记一个session_id,作为"续回去"的句柄。人的每一次答复、每一次介入,本质上都是"结束当前进程,带着新信息把同一个会话续起来"。
2. 状态必须活在会话之外
我给执行会话定了一条协议:编排层不读你的对话记录,goalctl命令是你向外汇报的唯一通道。 每完成一片跑slice-done,阶段性进展跑note,要人拍板跑ask,做完了跑 done。这段协议头会被拼在每个Goal的Prompt前面,逐字送进claude -p。
有人会说,靠Agent自觉汇报靠得住吗?长上下文后期它会不会忘?会。所以除了它自报的语义状态,还有一层机械状态:runner持有子进程句柄,进程退出了它一定知道;日志是逐行落盘的,Agent每一步做了什么都在盘上。语义状态告诉你"它在做第几片",机械状态告诉你"它还活着吗、在等人吗"。两层互补。
我曾经想过让一个"监督Agent"去读其他Agent的完整transcript来汇总状态。很快放弃了:让LLM转述LLM的状态是一条不可靠的信道,token成本极高,而且摘要会失真——我踩过"收尾摘要跟事实相反"的坑,结构化状态文件才可靠。
3. 注意力调度:让Agent少打断人,而不是让人更快响应
这是goal_control最重要的一个设计方向。我把它压缩成一句话:
不是让人更快地响应Agent,而是让Agent更少地打断人。
怎么做到?四步:
第一步,给"打断"建立分类学。 过去所有打断在我眼里是同一种东西,这就是疲劳的来源,实际上至少有四类:
| 类型 | 例子 | 应有的处理方式 |
|---|---|---|
| 阻断性决策 | 需求歧义、方案二选一、范围裁决 | 进队列,Agent停下等答复 |
| 危险操作审批 | 删数据、推送、改共享配置、跨阶段门禁 | 高风险实时推送;低风险应预授权放行 |
| 阶段性验收 | 一个Goal收尾、验收标准全绿待确认 | 天然的批处理点,攒着 |
| FYI / 进度通知 | “第3片完成了” | 只写状态文件,永不打断 |
只有前两行的高风险子集配得上实时打断你。经验上,这部分不到全部打断的十分之一。
第二步,把决策收件箱做成一等公民。 Agent遇到需要人的事项,不是在终端里干等,而是写一个结构化的决策请求:背景摘要(不需要读对话记录就能懂)、选项、它自己的推荐和理由、风险等级。写完它就结束回合,把进程让出来。人在自己选的时间集中清空收件箱,答案写回文件,runner用--resume 把答案送回原会话。
这里的收益不只是打断次数变少,同类决策放在一起做,决策质量更高,上下文相近的裁决放在一起,比随机到达的裁决做得好,这是我实际体会到的。
第三步,从源头减少决策请求的产生。 这一步比前两步都便宜。权限确认这种机械打断,靠允许清单和Goal Prompt里的预授权条款就能消掉大半。对低风险歧义,协议里明确要求 Agent按自己的推荐默认值继续执行、把假设记下来,人在验收时批量复核,有异议再翻案。把"事前阻断"变成"事后可否决",是消灭队列长度的最强手段。还有决策点前移:很多中途打断其实是切片定义不清的下游症状,在拆解阶段多花十分钟封口语义,比运行期处理五个澄清请求便宜得多。
第四步,只保留一条高优先级打断通道。 真正的高风险事项(不可逆操作、整组阻塞)走桌面通知实时触达,而且这条通道必须保持低频。一旦它变成常规通道,你就退回原点了。
4. 管一个Goal的一生,而不只是"跑没跑"
我要管的不是"进程在不在",是一个Goal从进入系统到沉淀下来的完整过程:什么时候能开工、开工了跑到哪、跟别的Goal什么关系、中间谁介入过、说了什么、什么时候算完、产出了哪些提交、事后怎么复盘。
这决定了状态模型必须显式:一个Goal有八个真实状态(待启动、等门禁、执行中、被按停、等额度、等人裁决、失败、完成),每一次跃迁都记进只追加的事件流水,每一次会话输出都完整落盘。复盘不是事后"总结"出来的,是这些事实原样拼出来的。
5. 尽量并行,但"安全"的边界是仓
Goal之间的依赖是显式的DAG。没有前置依赖的Goal可以并发,前置全DONE的Goal自动解锁,一批完成自动进入下一批。这部分是纯机械动作,难点在于"完成"这个事件得能被机器检测到,而这正是状态外置解决的。
但有一条边界我坚持得很死:同一个代码仓同一时刻只跑一个Goal(worktree除外)。 原因很实际。多个会话共用一个工作区时,收口那一步的git add会把别人的半成品一起卷进来,这是真实发生过的事故。依赖图上能并行,不代表在同一个工作区里能安全并行。goal_control的并行度上限是"仓的数量",不是"依赖图的宽度"。
6. 放弃虚假的ETA
复杂Coding Agent任务的执行时间不确定性很大。同一个Goal,代码侦察可能十分钟也可能四十分钟,一次全量验证可能一个半小时。给一个看起来精确的"预计14:32完成",价值很低,而且会让人产生错误的预期。
所以goal_control明确不做ETA。它只呈现现在是什么状态、进度到第几片、当前在做什么、是不是阻塞、要不要人。切片计数就是最好的进度条,它粗糙,但它是真的。
7. 不信任Agent的自述
Agent说"我做完了",只是它觉得它做完了。是否真的完成由机械复核裁决:跑一遍该仓的验证命令,检查其他仓有没有被越界改动,两条都过才允许提交。这不是对模型的不信任,是对任何"自述"的不信任。人也一样。这条原则后来抓到过一次真实的"只说不做",后面会讲。
8. 控制面要薄
在"零基础设施"和"完整Supervisor Agent"之间,我选了中间偏轻的位置:文件承载状态,脚本承载调度,人只通过收件箱和一个控制台介入。没有数据库,没有消息队列,没有常驻的监督Agent。
为什么不上Supervisor Agent?因为它引入了一个新的不可靠层。监督者自己也会错,而你没法再雇一个监督者去监督它,薄的那一层已经覆盖了九成收益。
三、goal_control是怎么设计的
这一部分讲机制,目标是让你理解:一个Goal是怎么进来、怎么被调度、怎么跑、什么时候找人、最后怎么完成和沉淀的。
一张图看全貌
图3里有四个角色:
- 清单是期望状态:一份JSON,列出每个Goal的依赖、所属仓、Prompt文件、开工前门禁、超时、模型与推理强度,以及每个仓的验证命令,一份清单就是一个任务组。
- runner是控制循环:它每20秒reconcile 一轮,读清单和状态文件,按固定顺序做九件事:落实人点下的暂停、收割已退出的进程、处理超时、续跑人工介入过的会话、续跑人答复过的会话、恢复额度限流的会话、处理门禁、启动就绪的Goal、全部完成后跑一次任务组级全量验证。
- 执行会话是易耗执行体:
claude -p起的headless进程,Prompt是协议头加Goal正文,输出以stream-json逐行落盘。它通过goalctl向外汇报,通过两个锚点行告诉runner本轮的结局。 - 状态文件是实际状态,也是整个系统唯一的事实来源:每个Goal一个JSON,每条决策一个JSON,一份只追加的事件流水,人工介入档案,以及每次运行的完整日志。控制台读这些文件,人的答复写回这些文件,runner下一轮读到,闭环。
有一个细节值得单独说:控制台没有任何内存状态。 它是标准库起的HTTP服务,每次请求都重读文件出JSON。这意味着服务重启不丢任何决策,页面刷新后跟文件完全一致,命令行和页面可以同时操作同一条消息(靠文件锁串行化)。这不是懒,是刻意的:状态只在一个地方,就不会有两份真相。
一个Goal是怎么进来、怎么被调度的
图4是Goal的状态机,三种颜色对应三个写者:深色是Agent自报,青色是runner推进,琥珀色是人裁决。
- Goal从PENDING开始。配了开工前门禁的,要等前置依赖全DONE才开审批单,转GATED等人放行——依赖没跑完就开单,等于让人核对一个还不存在的成果。
- 依赖全DONE、门禁放行、该仓没被占、工作区干净,四条齐了runner就起
claude -p,转RUNNING。控制台把"下一轮就起"派生为READY显示,判据跟runner一字不差,否则就会"显示READY但什么都没发生"。 - RUNNING期间有一条硬规矩:状态文件只有执行会话一个写者,runner不写。 超时收割和人工暂停是例外,也得先终止进程、等它退出才落笔——原子写只防半截文件,不防互相覆盖,这条不变量只能靠流程。
从RUNNING出去有五条路:Agent问人(BLOCKED_ON_HUMAN)、人按停(PAUSED)、额度限流(QUOTA_WAIT,到点自动续跑)、自述完成(进机械复核,派生VERIFYING)、超时或异常退出(FAILED)。前三条都回RUNNING,靠同一个动作:--resume带着新信息续回原会话。
图6是一次真实任务组的依赖图:六个Goal、十五个切片、三道门禁。它说明了两件事。一是波次是自动推进的,一个Goal DONE之后下游在下一轮自动解锁,我不需要"点下一个"。二是G-02和G-03在依赖图上可以并行。
什么时候找人:两条通道,故意不合并
goal_control里有两条人机通道,方向相反,我刻意没有合并它们。
第一条是收件箱:Agent找人。 图5是一次完整的决策闭环。
Agent要拍板就执行goalctl ask,带上背景、选项和风险等级:写一条OPEN消息、把Goal置为BLOCKED_ON_HUMAN,然后进程退出——它不会在那儿空等你。人答完,消息变 ANSWERED;runner下一轮用--resume 把答复注入原会话,消息变APPLIED,Goal回到RUNNING,上下文没丢。门禁审批走同一条通道。
写者分工是硬的:OPEN到ANSWERED只有人能做,ANSWERED到APPLIED只有runner 会做。
第二条是介入:人找Agent,看实时流发现方向偏了,就按停、补一段指导、接着跑。为什么不塞进收件箱?收件箱的语义是"Agent在等你",它驱动热泳道告警。人自己发起的动作混进去,这句提醒就不可信了。
claude -p是一次性headless进程,stdin空着,没有控制通道,“不杀进程的暂停"不存在——暂停就是"终止进程,再--resume 续跑”。我拿真实会话验证过,续回去上下文不丢、session_id不变。而持有子进程句柄的只有runner,所以页面上点"暂停"只是写一张请求单,下一轮才落实。
怎么完成、怎么沉淀
Agent跑完全部切片,执行goalctl done留下完成自述,然后退出。runner收割到自述后,进入图7的收口流水线。

- 第一步是机械复核:逐仓跑验证命令,查有没有越界改别的仓。越界按"比启动时多出来的"算,不按"其他仓必须干净"算——工作区常年压着无关改动,后一种口径每个Goal都假失败。复核不过就判FAILED,不提交任何改动;人修好后
goalctl finish只重跑复核。 - 复核过了,逐文件
git add(绝不用-A),提交信息带story前缀和自述,推送后回填评审地址。推送有三种拿不准的情形:本仓已领先远端、待补推的sha对不上HEAD、push失败。一律停下交给人,本地提交不丢——宁可让人多点一次,也不替人推别人的提交。 - 沉淀是自然发生的:状态跃迁、决策正文、逐行日志各自落盘,复盘页原样拼出来。
四、现在能做什么

1. 接管一个项目
配置工作区
- goal_control是独立仓,被它编排的工作区在另一个地方。在清单里配一个
workroot指向工作区根,再列出各子仓的验证命令,这个项目就被接管了。 - 每个Goal声明自己的仓、依赖、Prompt文件;跨前后端的Goal用
repos声明允许改动的全部仓,启动前置、并发占用、越界判据、逐仓复核和逐仓提交均按整批仓算。
按Goal绑定模型
- 每个Goal可单独绑定执行模型与推理强度。简单的片用快模型,难的片上更强的模型。未配置则一字不传,由Claude Code默认决定——升级不会悄悄改动任何存量Goal的执行。
任务组切换
- 一份清单对应一个任务组,换任务组就换一份清单,状态和日志按Goal ID共存,互不干扰。

2. Goal总览
- 顶栏四个数字回答四个问题:等你决策几条、执行中几个、阻塞几个、已完成几个。这里的"阻塞"口径是刻意放宽的:等人裁决、等门禁、等额度、失败,四种都算,因为它们都是"不动它就永远不会动",漏掉任何一种顶栏数字就骗人。
- 左侧导轨按story分组列出全部Goal,每行带状态、切片进度、所属仓、模型短标。被卡住的Goal打呼吸的"等你裁决"角标。

3. 决策队列(重要)
这是最重要的一页,消息分三组:
- REALTIME 实时通道:高风险且未推迟的OPEN,唯一允许打断你的。
- BLOCKING阻断性决策:真的停住了某个Goal的。
- DEFERRABLE可后置:其余的,Agent已按默认值继续,你复核或翻案。
只要有决策真卡着 Goal,控制台就不许它安静:自动弹窗、顶栏横幅、导轨角标、标签页标题变成"(2) 等你裁决"。决策能数字键加回车快速过,审批不行——不可逆的动作只能明确点下去。
“稍后处理"只压住弹窗,横幅、角标、标题一个不少。藏起来的只有打扰,阻塞藏不掉。

- Agent写的决策正文常常上千字。控制台把它按角色分层:决定、候选、事实、时机、后果。只还原不改写,规则没命中就退回原样显示。跳过说明文字的审批等于没有审批,这一层就是为了让人不跳过。
- 答复能捎一句自由指令,随答案注入会话。也可以"下一轮再看”——只影响排序,Goal该卡还是卡着。开了提醒开关,这两组会用桌面通知加提示音叫你。
没做的:低风险预授权直接放行。开关摆在页面上但禁用着——谁有权替人放行、怎么留审计,还没想清楚。

4. 调度DAG
依赖图按拓扑层序展开,每层一行。头部四个指标:
- 关键路径长度
- 理论并行度
- 当前前沿
- 依赖漂移(前置未DONE却已开跑的数量)。
READY节点有一个"启动"按钮,runner常驻时禁用并提示"会自动调度",runner不在时可以手工推一轮。

5. 执行态
每个在飞的Goal一张卡片:
- 状态
- 已运行时长
- 切片进度条
- 最近三条事件
- 进程号
- 本轮实际用的模型和推理强度
- 累计成本
进度条的分母来自切片声明,切片标题从Goal正文里解析出来。


6. 执行过程与人工介入
- 这一页是会话的实时执行流展示(这个板块目的是为了提高”心理安全感“😂):说明文字、思考块、工具调用与返回、心跳、答复接缝、收尾汇总。日志是stream-json 逐行落盘的,所以边跑边看,进程被杀也保住已发生的;轨迹按字节游标增量读。
- “介入这次执行"就是那条通道:停下会话,写下判断,它带着完整上下文接着做;留空提交就是"看了一眼,没问题”。每次介入都归档,此前介入过几次、说了什么都在。
- 它必要,但不该成为常规操作。频繁介入多半是切片或Goal定义出了问题,该回拆解阶段修。

7. 复盘
- Goal完成以后,复盘页回答"这个任务为什么变成了现在这样":会话次数、墙上时钟、累计成本、对话轮次、token;
- 状态机走过的路和每态停留时长;每段会话的起因、退出码和成本;每条决策的提问、答复、时间;逐仓提交和评审地址;以及完整事件流水。

8. 其它顺手的能力
- 查看完整Prompt:这Goal真正被送进
claude -p的那一整段,协议头加正文,逐字一致,一个<pre>原样渲染,不切段不高亮,所见即所执行。 - 命令行双入口:
goalctl status、goalctl inbox、goalctl answer,跟页面走同一条写路径。控制台挂了不影响编排。 - 离线只读看板:runner每轮自动重生成一个静态HTML,控制台起不来时的兜底。
- 额度限流自动重试:撞上用量限制自动等到恢复时间续跑,不需要人。
五、真实跑了一个任务组之后
数据来自一次真实的任务组执行:六个Goal、十五个切片、三道门禁,落在一个后端仓和一个前端仓上。
一昼夜的账
| Goal | 切片 | Agent提问 | 门禁 | 会话段 | 成本(美元) | 备注 |
|---|---|---|---|---|---|---|
| G-01 | 3 | 2 | 0 | 3 | 12.1 | 首轮3小时14分超时被收割,人工续回 |
| G-02 | 3 | 1 | 0 | 2 | 50.3 | 提问四选一,人两小时后答复 |
| G-03 | 1 | 2 | 1 | 3 | 18.3 | |
| G-04 | 3 | 1 | 1 | 2 | 21.2 | 机械复核拦下一次 |
| G-05 | 2 | 1 | 0 | 2 | 25.3 | 切片里的"证据"与代码不符,Agent停下来问 |
| G-06 | 3 | 0 | 1 | 1 | 22.6 | 零提问,两处保留意见记在note里 |
十二次人工答复,约一百五十美元,从G-01续跑到G-06收口墙上时钟约一昼夜。其中十三个多小时是G-06在等一个被我弄脏的前端工作区——这个后面单说。
打断次数:变少了,更重要的是变"可选"了
先说诚实的部分:这次实跑里,“批量清空收件箱"只真正发生了一次。那天下午两点零六分,我一口气答了两条,一条门禁、一条四选一的方案裁决,它们从上午十一点五十几分起就在队列里等着,而我那两个小时在做别的事。其余的答复大多在一到四分钟内完成,因为新工具上线第一周我其实是盯着的。
所以"打断次数明显减少"目前是一个观察,不是一个测量。但有几类打断确实消失了,这是确定的:权限确认没有了(claude -p用自动模式加协议约束);“跑完了吗"这类进度询问没有了(看导轨就行);“下一个起谁"没有了(runner自己起)。剩下的打断只有两种:Agent真的拿不准的裁决,和门禁。
更重要的变化是打断变成了"可选的”。Agent停下来等我的时候,它是安静地停在文件里,不是在终端里闪。什么时候去处理,是我决定的。
“可控"具体指什么
过去同时跑五个会话,我最怕的不是某个会话出问题,是我不知道哪个出了问题。现在"可控"的意思很具体:任何时刻,下面四个问题我都能在三秒内回答,而且答案来自文件,不来自记忆。
- 哪些Goal在跑,跑到第几片?
- 哪些Goal停了,停在什么状态,等的是什么?
- 有几条事在等我,哪一条真的卡着执行?
- 下一个会起谁,为什么现在还没起?
第四个问题过去是最难回答的,现在DAG页的"当前前沿"和dry-run直接给出来。
意外收获
决策被文档化了:G-02那条四选一的裁决,Agent写了两千多字的背景:切片定义的文件域漏掉了装配点,切片要求"不产生迁移"但数据库的 CHECK 约束让这条不可能成立,四个出口各自的代价是什么,它推荐哪个、为什么。我读完就能拍板,不用翻它的对话记录。事后想知道"为什么这个Goal动了迁移文件”,那条消息就是答案。
切片的"漂移守卫"真的救了场:G-05的切片里有一条标为"证据等级A"的代码现状描述,跟库内原文不符。按原文件域实施会写一个全仓没有读者的计数,验收静默失败。Agent按守卫条款停下来问,而不是硬做。这说明"决策点前移"和"运行期收件箱"是配合工作的:拆解阶段封口越严,运行期的提问质量越高。
default-with-veto 在工作:G-06全程零提问,但留了两处"须知"在note里:一处接线不在文件域内所以没接,一处接缝暴露了但没有调用方。这正是协议要求的行为:低风险歧义按推荐默认值继续,把假设记下来。
成本可见了:过去我不知道一个Goal花多少钱(订阅制)。现在每段会话的成本、轮次、token都在复盘页上,累计一个任务组约一百五十美元,这个数字第一次有了。
六、下一步
支持更多Coding Agent
goal_control对Claude Code的依赖其实很薄:一条启动命令、一条续跑命令、两个锚点行、一段协议头、一种日志格式。这些都集中在启动器和轨迹解析两个模块里。Codex也有headless和goal特性,接进来主要是适配层的工作。真正需要抽象的是"一个执行体应该提供什么能力”:可启动、可续回、可逐行观测、可被终止。
从Goal模型再抽象一层
目前整个系统建立在我自己的"切片 → Goal → DAG"方法之上。清单的字段、切片轨道的解析规则、组尾验证的约定,都带着这套方法的形状。
我想把它抽象成四个更通用的概念:工作单元(一段Prompt加一个工作区加一组完成判据)、依赖、完成判据(机械可验证的那部分)、决策协议(工作单元怎么向外汇报、怎么提问)。切片和 Goal只是这四个概念的一种实例化。做到这一步,用别的任务组织方式的人也能用它。
其它值得做的方向
- 低风险预授权。 现在所有决策都要人点一下,default-with-veto的假设记录目前只落在note里,不是一等对象,所以"可后置"那一组在真实数据里基本是空的。把假设做成可复核、可翻案的对象,是让队列长度真正下降的下一步。
- hooks兜底采集。 现在的机械状态靠runner持有的进程句柄,Claude Code的hooks可以在会话不知情的情况下把"停下来了、在等输入"这类事件写进中央日志,是更优雅的采集点。
- 会话恢复的一等入口。 超时被收割的会话,其transcript还在,session_id就是文件名。现在恢复靠一套手工步骤,应该做成一个命令。
- 进度感知的超时。 硬超时会误杀一个还在正常推进的长Goal。既然有逐行轨迹,“多久没有新进展"比"跑了多久"更适合当收割判据。
- 多人协作与鉴权。 控制台只绑本机、不做鉴权,因为它现在只服务一个人。团队用需要这一层。
最后
中断这个机制是为CPU发明的,它成立的前提只有一条:CPU的上下文切换便宜到可以忽略。可就算是CPU,负载一高也扛不住。Linux的网卡驱动里有个叫NAPI的机制,包来得太密的时候,它做的第一件事是把中断关掉,改成内核自己按节奏去轮询,队列空了再把中断打开。内核工程师早就想明白了:事件多到一定程度,被动响应不如主动去取。
| CPU / 网卡 | 人 / Coding Agent |
|---|---|
| 上下文切换接近零成本 | 上下文切换是分钟级的,贵得离谱 |
| 包密到一定程度,NAPI关中断转轮询 | Agent多到五个以上,你就是那块高负载的网卡 |
| NMI等高优先级中断谁也拦不住 | 只有高风险事项配得上实时打断你 |
人的上下文切换不是便宜,是贵得离谱——开头那个晚上,我切走两分钟,回来最早那个已经空等了二十分钟。所以"从中断到收件箱"不是我的偏好,是负载决定的:五个以上Agent同时跑,你就是那块高负载的网卡,该关中断了。
关中断不是全关。CPU有屏蔽位、有优先级,NMI谁也拦不住,对应到人,就是这篇文章我最想留下的一句:
只有高风险配得上打断你:其余的进队列,你按自己的节奏取。
goal_control是这套想法的一个粗糙实现。它迟早可能会被哪个产品内置掉,这不重要。重要的是我花了两周才想明白、而你读到这里只花了二十分钟的那件事:今天的Coding Agent默认你是一块CPU,随时可中断,切换不要钱,而你不是。
怎么做到?四步: