<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>AI 工程实践 on Leon Young</title><link>https://leonyoung.tech/categories/ai-%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/</link><description>Recent content in AI 工程实践 on Leon Young</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Thu, 24 Sep 2026 17:00:00 +0800</lastBuildDate><atom:link href="https://leonyoung.tech/categories/ai-%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/index.xml" rel="self" type="application/rss+xml"/><item><title>人不能扩容，只能被保护 —— 并行Coding Agent的注意力经济学</title><link>https://leonyoung.tech/posts/parallel-coding-agent-attention-economics/</link><pubDate>Thu, 24 Sep 2026 17:00:00 +0800</pubDate><guid>https://leonyoung.tech/posts/parallel-coding-agent-attention-economics/</guid><description>并行跑五个以上 Coding Agent 之后，真正稀缺的不再是模型产能，而是人的注意力。本文复盘我为什么、以及怎样做了 goal_control：把会话状态外置，把中断驱动的交互改成收件箱，只让高风险决策打断人。</description><content:encoded><![CDATA[<blockquote>
<p>一个多月、四十万行代码、五个甚至更多Claude Code会话同时开工之后，我发现真正稀缺的东西已经不是模型的产能，而是我自己的注意力。这篇文章讲我为什么、以及怎么做了goal_control——一个管很多Coding Agent同时干活的薄控制面。它不是产品说明书，是一份带着教训的工程复盘。</p>
</blockquote>
<hr>
<h2 id="一瓶颈换了地方">一、瓶颈换了地方</h2>
<h3 id="五个终端同时闪">五个终端同时闪</h3>
<p>先说一个场景：某天晚上九点多，我面前开着五个Claude Code终端，各跑一个任务。左上角那个在问我&quot;接口返回404还是409&quot;，右边那个刚跑完想让我确认能不能提交，下面一个撞上了权限确认在等我按回车，另外两个我已经不太记得它们跑到哪儿了。</p>
<p>我回答完左上角那个，切到右边，看了两分钟diff，回过头发现左上角那个又停了，这次问的是另一个问题。等我把五个都扫一遍，最早那个已经空等了二十分钟。</p>
<p>这就是过去一个多月的常态：该项目项目40+万行代码、前后端好几个仓（多编程语言），周期短，强度高，我几乎全程用多会话并行开发。刚开始两三个会话并行的时候很爽，产能肉眼可见地翻倍。到了五个以上，情况开始变化：Agent还是那么能干，但我不行了。</p>
<p>具体说，我遇到了四个问题：</p>
<ol>
<li><strong>注意力被持续打断：</strong> 每个会话在任意时刻都可能停下来找我，澄清、审批、权限确认混在一起，随机到达；</li>
<li><strong>多任务状态说不清：</strong> 现在哪些在跑？跑到什么程度？谁做完了？做完了下一个该起谁？这些问题我要靠翻终端和记忆回答。</li>
<li><strong>任务之间靠人衔接：</strong> 一个任务跑完，是我去判断&quot;可以起下一个了&quot;，然后开新会话执行下一个任务。Agent能自主完成&quot;一个任务&quot;，但&quot;很多任务&quot;的调度全在我手里。</li>
<li><strong>决策不可回溯：</strong>一个任务跑几个小时，中间我做了七八次决定。事后想知道&quot;它为什么最后长成这样&quot;，得在几十万token的对话里翻。</li>
</ol>
<p>说实话，头两周我以为这是我自己不够专注。后来我意识到不对：<strong>人脑本来就不适合当抢占式调度器</strong>。</p>
<h3 id="说白了是两类问题不是四个">说白了是两类问题，不是四个</h3>
<p>我把这四个问题放在一起看了很久，最后的结论是：它们不是四件事，是两件事，而且有明确的因果链。</p>
<p><strong>问题1、2、3的根子是同一个：状态没有外置。</strong> Coding Agent会话的&quot;状态&quot;是隐式的，散落在对话历史里。会话自己到后期都要花力气回忆&quot;我是谁、我在做哪一片&quot;，你当然无法观测一个从未被写下来的东西。<strong>状态不外置，就没法知道谁跑到哪儿；不知道谁跑到哪儿，就没法自动触发下一个；于是一切都得人来衔接</strong>。</p>
<p>有意思的是，&ldquo;进度&quot;这件事我其实早就定义好了。我的开发方法是先把需求拆成任务切片，每片有验收标准，再把切片聚成Goal编排成依赖图。进度就是&quot;第几片完成了&rdquo;，粒度天然存在。缺的只是让会话把这个状态持续写到上下文之外的某个地方。</p>
<p><strong>问题4才是真正的硬问题：交互模型是中断驱动的。</strong> 每个Agent都可以在任意时刻以最高优先级抢占我，而人的上下文切换成本是分钟级的。N个Agent 的打断随机到达，我被迫在脑子里做抢占式调度。这件事跟&quot;状态透不透明&quot;无关，就算每个会话都把状态写得清清楚楚，它们还是会随时打断我。</p>
<p>所以，解决问题的顺序也就清楚了：<strong>先做状态外置，再改交互模型</strong>。前者是后者的前置条件。</p>
<h3 id="为什么原生coding-agent不管这件事">为什么原生Coding Agent不管这件事</h3>
<p>有人会问，Claude Code、Codex这么强，为什么不直接管这件事？</p>
<p>我的看法是：它们的一等公民是&quot;会话&quot;，而我需要的一等公民是&quot;目标&quot;。一个会话就是一个Agent加一段上下文，它天生擅长把一个任务做完。<strong>但&quot;很多任务之间的依赖、调度、状态汇总、决策排队&quot;这些东西不属于任何一个会话，它们属于会话之上的那一层。</strong>原生工具没有这一层，不是能力不够，而是这不在它的抽象范围内。</p>
<p>好在原生工具把这一层需要的原语都给了。Claude Code有headless模式（<code>claude -p</code>），可以像跑一个脚本一样跑一个Agent；有<code>--resume</code>可以续回一个会话；有<code>stream-json</code>输出格式可以逐行拿到会话的每一步。采集面和控制面的原语都在，缺的只是我这一侧薄薄的一层约定和聚合。</p>
<h3 id="我先否掉的方案k8s式编排层">我先否掉的方案：K8s式编排层</h3>
<p>动手之前我评估过一个很自然的想法：既然有很多&quot;工作负载&quot;要调度，能不能做一个K8s式的会话编排层？声明式期望状态、控制循环、健康探针、自愈重启。</p>
<p><strong>结论是：思想能借，形态不能抄。</strong></p>
<p>能借的是三样：<strong>期望状态与实际状态分离</strong>（Goal编排文档是期望状态，外置的运行状态是实际状态）；<strong>控制循环</strong>（持续比对&quot;该跑而没跑&quot;并触发）；<strong>统一的状态上报协议</strong>。</p>
<p>不能抄的原因在于负载性质完全相反。K8s管的是同质、无状态、可廉价重启的进程，核心价值是自愈、扩缩容、资源装箱。<strong><em>Agent任务恰恰相反：有状态、重启昂贵、不可抢占、完成判定模糊。</em></strong>你不需要调度器决定任务跑在哪，不需要副本数，更不需要健康探针——Agent的&quot;健康&quot;是语义层面的，一个进程活着但在原地打转，探针探不出来。</p>
<p>更贴切的心智模型其实是<strong>CI/CD流水线 + 带人工审批门禁的收件箱</strong>：Goal约等于一个CI job，依赖约等于<code>needs:</code>，人工决策点约等于<code>when: manual</code>。任务是一次性的，有明确产出物（一个提交），失败了重跑而不是迁移。</p>
<p>于是goal_control的形态就定下来了：<strong>不是一个平台，是&quot;文件 + 薄脚本 + 一个人机协作控制台&quot;</strong>。</p>
<h2 id="二我的解决思路">二、我的解决思路</h2>
<p>这一部分讲设计思想，不讲功能。功能在后面。</p>
<h3 id="先说清两个词任务切片与goal">先说清两个词：任务切片与Goal</h3>
<p>我简单介绍一下我的部分开发模式，先建立一个基本认知就够了。</p>
<p><img alt="图2" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/fig-2-granularity.png" title="图2">
<strong>任务切片</strong>是可以独立实现、独立测试、独立合并的最小单元，自带验收标准。它是开发方法层面的粒度：拆解需求的时候产出的就是它。一片切片通常对应几个文件的改动和一组测试。</p>
<p><strong>Goal</strong>是拓扑序上连续的2到4个切片。它是执行层面的粒度：一个Goal对应一段完整的 Prompt，交给一个headless会话一口气跑完，组尾做一次验证。为什么不能只用一种粒度？因为两种粒度服务的对象不同。切片太细，如果每片起一个会话，会话之间要反复重建上下文，启动开销和上下文丢失都吃不消；Goal太粗，如果只以Goal为单位汇报，一个跑三小时的Goal中间就是黑盒，你不知道它卡在第一片还是第三片。所以调度按Goal，汇报按切片。</p>
<p><strong>任务组</strong>则是一份清单，也就是一张Goal之间的依赖图，goal_control调度的对象是Goal和Goal之间的边，切片只是它眼里的进度刻度。</p>
<h3 id="1-会话是易耗品goal才是一等公民">1. 会话是易耗品，Goal才是一等公民</h3>
<p>这是整个设计最底层的一条：<strong>Session不是一等公民，它只是Goal某一次运行的执行载体，可以死、可以被</strong><code>--resume</code><strong>、可以换一个新会话接着跑</strong>。Goal的状态必须活在会话之外，才谈得上一切：<strong>谁在跑、卡在哪、下一个起谁、事后复盘</strong>。</p>
<p>对应到实现上，<code>claude -p</code>起一个headless进程，输出逐行落盘，退出即被收割。Goal状态文件里只记一个<code>session_id</code>，作为&quot;续回去&quot;的句柄。人的每一次答复、每一次介入，本质上都是&quot;结束当前进程，带着新信息把同一个会话续起来&quot;。</p>
<h3 id="2-状态必须活在会话之外">2. 状态必须活在会话之外</h3>
<p>我给执行会话定了一条协议：<strong>编排层不读你的对话记录，goalctl命令是你向外汇报的唯一通道。</strong> 每完成一片跑<code>slice-done</code>，阶段性进展跑<code>note</code>，要人拍板跑<code>ask</code>，做完了跑 <code>done</code>。这段协议头会被拼在每个Goal的Prompt前面，逐字送进<code>claude -p</code>。</p>
<p>有人会说，靠Agent自觉汇报靠得住吗？长上下文后期它会不会忘？会。所以除了它自报的语义状态，还有一层机械状态：<em>runner持有子进程句柄，进程退出了它一定知道；日志是逐行落盘的，Agent每一步做了什么都在盘上</em>。语义状态告诉你&quot;它在做第几片&quot;，机械状态告诉你&quot;它还活着吗、在等人吗&quot;。两层互补。</p>
<p>我曾经想过让一个&quot;监督Agent&quot;去读其他Agent的完整transcript来汇总状态。很快放弃了：<strong>让LLM转述LLM的状态是一条不可靠的信道，token成本极高，而且摘要会失真</strong>——我踩过&quot;收尾摘要跟事实相反&quot;的坑，结构化状态文件才可靠。</p>
<h3 id="3-注意力调度让agent少打断人而不是让人更快响应">3. 注意力调度：让Agent少打断人，而不是让人更快响应</h3>
<p>这是goal_control最重要的一个设计方向。我把它压缩成一句话：</p>
<blockquote>
<p><strong>不是让人更快地响应Agent，而是让Agent更少地打断人。</strong>
<img alt="图1" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/fig-1-attention-model.png" title="图1">
怎么做到？四步：</p>
</blockquote>
<p><strong>第一步，给&quot;打断&quot;建立分类学。</strong> 过去所有打断在我眼里是同一种东西，这就是疲劳的来源，实际上至少有四类：</p>
<table>
  <thead>
      <tr>
          <th><strong>类型</strong></th>
          <th><strong>例子</strong></th>
          <th><strong>应有的处理方式</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>阻断性决策</strong></td>
          <td>需求歧义、方案二选一、范围裁决</td>
          <td>进队列，Agent停下等答复</td>
      </tr>
      <tr>
          <td><strong>危险操作审批</strong></td>
          <td>删数据、推送、改共享配置、跨阶段门禁</td>
          <td>高风险实时推送；低风险应预授权放行</td>
      </tr>
      <tr>
          <td>阶段性验收</td>
          <td>一个Goal收尾、验收标准全绿待确认</td>
          <td>天然的批处理点，攒着</td>
      </tr>
      <tr>
          <td>FYI / 进度通知</td>
          <td>&ldquo;第3片完成了&rdquo;</td>
          <td>只写状态文件，永不打断</td>
      </tr>
  </tbody>
</table>
<p>只有前两行的高风险子集配得上实时打断你。经验上，这部分不到全部打断的十分之一。</p>
<p><strong>第二步，把决策收件箱做成一等公民。</strong> Agent遇到需要人的事项，不是在终端里干等，而是写一个结构化的决策请求：背景摘要（不需要读对话记录就能懂）、选项、它自己的推荐和理由、风险等级。写完它就结束回合，把进程让出来。人在自己选的时间集中清空收件箱，答案写回文件，runner用<code>--resume</code> 把答案送回原会话。</p>
<p>这里的收益不只是打断次数变少，<strong>同类决策放在一起做，决策质量更高，</strong>上下文相近的裁决放在一起，比随机到达的裁决做得好，这是我实际体会到的。</p>
<p><strong>第三步，从源头减少决策请求的产生。</strong> 这一步比前两步都便宜。权限确认这种机械打断，靠允许清单和Goal Prompt里的预授权条款就能消掉大半。对低风险歧义，协议里明确要求 Agent按自己的推荐默认值继续执行、把假设记下来，人在验收时批量复核，有异议再翻案。把&quot;事前阻断&quot;变成&quot;事后可否决&quot;，是消灭队列长度的最强手段。还有决策点前移：<strong>很多中途打断其实是切片定义不清的下游症状，在拆解阶段多花十分钟封口语义，比运行期处理五个澄清请求便宜得多。</strong></p>
<p><strong>第四步，只保留一条高优先级打断通道。</strong> 真正的高风险事项（不可逆操作、整组阻塞）走桌面通知实时触达，而且这条通道必须保持低频。一旦它变成常规通道，你就退回原点了。</p>
<h3 id="4-管一个goal的一生而不只是跑没跑">4. 管一个Goal的一生，而不只是&quot;跑没跑&quot;</h3>
<p>我要管的不是&quot;进程在不在&quot;，是一个Goal从进入系统到沉淀下来的完整过程：什么时候能开工、开工了跑到哪、跟别的Goal什么关系、中间谁介入过、说了什么、什么时候算完、产出了哪些提交、事后怎么复盘。</p>
<p>这决定了状态模型必须显式：<strong>一个Goal有八个真实状态（待启动、等门禁、执行中、被按停、等额度、等人裁决、失败、完成）</strong>，每一次跃迁都记进只追加的事件流水，每一次会话输出都完整落盘。复盘不是事后&quot;总结&quot;出来的，是这些事实原样拼出来的。</p>
<h3 id="5-尽量并行但安全的边界是仓">5. 尽量并行，但&quot;安全&quot;的边界是仓</h3>
<p>Goal之间的依赖是显式的DAG。没有前置依赖的Goal可以并发，前置全DONE的Goal自动解锁，一批完成自动进入下一批。这部分是纯机械动作，难点在于&quot;完成&quot;这个事件得能被机器检测到，而这正是状态外置解决的。</p>
<p>但有一条边界我坚持得很死：<strong>同一个代码仓同一时刻只跑一个Goal（worktree除外）。</strong> 原因很实际。多个会话共用一个工作区时，收口那一步的<code>git add</code>会把别人的半成品一起卷进来，这是真实发生过的事故。依赖图上能并行，不代表在同一个工作区里能安全并行。goal_control的并行度上限是&quot;仓的数量&quot;，不是&quot;依赖图的宽度&quot;。</p>
<h3 id="6-放弃虚假的eta">6. 放弃虚假的ETA</h3>
<p>复杂Coding Agent任务的执行时间不确定性很大。同一个Goal，代码侦察可能十分钟也可能四十分钟，一次全量验证可能一个半小时。给一个看起来精确的&quot;预计14:32完成&quot;，价值很低，而且会让人产生错误的预期。</p>
<p><strong>所以goal_control明确不做ETA。它只呈现现在是什么状态、进度到第几片、当前在做什么、是不是阻塞、要不要人。</strong><strong>切片计数就是最好的进度条，它粗糙，但它是真的。</strong></p>
<h3 id="7-不信任agent的自述">7. 不信任Agent的自述</h3>
<p>Agent说&quot;我做完了&quot;，只是它觉得它做完了。是否真的完成由机械复核裁决：跑一遍该仓的验证命令，检查其他仓有没有被越界改动，两条都过才允许提交。这不是对模型的不信任，是对任何&quot;自述&quot;的不信任。人也一样。这条原则后来抓到过一次真实的&quot;只说不做&quot;，后面会讲。</p>
<h3 id="8-控制面要薄">8. 控制面要薄</h3>
<p>在&quot;零基础设施&quot;和&quot;完整Supervisor Agent&quot;之间，我选了中间偏轻的位置：<strong>文件承载状态，脚本承载调度，人只通过收件箱和一个控制台介入。没有数据库，没有消息队列，没有常驻的监督Agent。</strong></p>
<p>为什么不上Supervisor Agent？因为它引入了一个新的不可靠层。监督者自己也会错，而你没法再雇一个监督者去监督它，薄的那一层已经覆盖了九成收益。</p>
<h2 id="三goal_control是怎么设计的">三、goal_control是怎么设计的</h2>
<p>这一部分讲机制，目标是让你理解：一个Goal是怎么进来、怎么被调度、怎么跑、什么时候找人、最后怎么完成和沉淀的。</p>
<h3 id="一张图看全貌">一张图看全貌</h3>
<p><img alt="图3" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/fig-3-architecture.png" title="图3">
图3里有四个角色：</p>
<ul>
<li><strong>清单</strong>是期望状态：一份JSON，列出每个Goal的依赖、所属仓、Prompt文件、开工前门禁、超时、模型与推理强度，以及每个仓的验证命令，一份清单就是一个任务组。</li>
<li><strong>runner</strong>是控制循环：它每20秒reconcile 一轮，读清单和状态文件，按固定顺序做九件事：<strong>落实人点下的暂停、收割已退出的进程、处理超时、续跑人工介入过的会话、续跑人答复过的会话、恢复额度限流的会话、处理门禁、启动就绪的Goal、全部完成后跑一次任务组级全量验证。</strong></li>
<li><strong>执行会话</strong>是易耗执行体：<code>claude -p</code>起的headless进程，Prompt是协议头加Goal正文，输出以<code>stream-json</code> 逐行落盘。它通过goalctl向外汇报，通过两个锚点行告诉runner本轮的结局。</li>
<li><strong>状态文件</strong>是实际状态，也是整个系统唯一的事实来源：每个Goal一个JSON，每条决策一个JSON，一份只追加的事件流水，人工介入档案，以及每次运行的完整日志。控制台读这些文件，人的答复写回这些文件，runner下一轮读到，闭环。</li>
</ul>
<p>有一个细节值得单独说：<strong>控制台没有任何内存状态。</strong> 它是标准库起的HTTP服务，每次请求都重读文件出JSON。这意味着服务重启不丢任何决策，页面刷新后跟文件完全一致，命令行和页面可以同时操作同一条消息（靠文件锁串行化）。这不是懒，是刻意的：<strong>状态只在一个地方，就不会有两份真相</strong>。</p>
<h3 id="一个goal是怎么进来怎么被调度的">一个Goal是怎么进来、怎么被调度的</h3>
<p><img alt="图4" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/fig-4-goal-lifecycle.png" title="图4">
图4是Goal的状态机，三种颜色对应三个写者：<strong>深色是Agent自报，青色是runner推进，琥珀色是人裁决。</strong></p>
<ul>
<li>Goal从PENDING开始。配了开工前门禁的，要等前置依赖全DONE才开审批单，转GATED等人放行——依赖没跑完就开单，等于让人核对一个还不存在的成果。</li>
<li>依赖全DONE、门禁放行、该仓没被占、工作区干净，四条齐了runner就起<code>claude -p</code>，转RUNNING。控制台把&quot;下一轮就起&quot;派生为READY显示，判据跟runner一字不差，否则就会&quot;显示READY但什么都没发生&quot;。</li>
<li>RUNNING期间有一条硬规矩：<strong>状态文件只有执行会话一个写者，runner不写。</strong> 超时收割和人工暂停是例外，也得先终止进程、等它退出才落笔——原子写只防半截文件，不防互相覆盖，这条不变量只能靠流程。</li>
</ul>
<p>从RUNNING出去有五条路：<strong>Agent问人（BLOCKED_ON_HUMAN）、人按停（PAUSED）、额度限流（QUOTA_WAIT，到点自动续跑）、自述完成（进机械复核，派生VERIFYING）、超时或异常退出（FAILED）</strong>。前三条都回RUNNING，靠同一个动作：<code>--resume</code>带着新信息续回原会话。</p>
<p><img alt="图6" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/fig-6-dag-waves.png" title="图6">
图6是一次真实任务组的依赖图：六个Goal、十五个切片、三道门禁。它说明了两件事。一是波次是自动推进的，一个Goal DONE之后下游在下一轮自动解锁，我不需要&quot;点下一个&quot;。二是G-02和G-03在依赖图上可以并行。</p>
<h3 id="什么时候找人两条通道故意不合并">什么时候找人：两条通道，故意不合并</h3>
<p>goal_control里有两条人机通道，方向相反，我刻意没有合并它们。</p>
<p><img alt="图5" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/fig-5-decision-loop.png" title="图5">
<strong>第一条是收件箱：Agent找人。</strong> 图5是一次完整的决策闭环。</p>
<p>Agent要拍板就执行<code>goalctl ask</code>，<strong>带上背景、选项和风险等级</strong>：写一条OPEN消息、把Goal置为BLOCKED_ON_HUMAN，然后进程退出——<strong>它不会在那儿空等你</strong>。人答完，消息变 ANSWERED；runner下一轮用<code>--resume</code> 把答复注入原会话，消息变APPLIED，Goal回到RUNNING，上下文没丢。门禁审批走同一条通道。</p>
<p>写者分工是硬的：OPEN到ANSWERED只有人能做，ANSWERED到APPLIED只有runner 会做。</p>
<p><strong>第二条是介入：人找Agent，</strong>看实时流发现方向偏了，就按停、补一段指导、接着跑。为什么不塞进收件箱？收件箱的语义是&quot;Agent在等你&quot;，它驱动热泳道告警。人自己发起的动作混进去，这句提醒就不可信了。</p>
<p><code>claude -p</code>是一次性headless进程，stdin空着，没有控制通道，<strong>&ldquo;不杀进程的暂停&quot;不存在</strong>——暂停就是&quot;终止进程，再<code>--resume</code> 续跑&rdquo;。我拿真实会话验证过，续回去上下文不丢、session_id不变。而持有子进程句柄的只有runner，所以页面上点&quot;暂停&quot;只是写一张请求单，下一轮才落实。</p>
<h3 id="怎么完成怎么沉淀">怎么完成、怎么沉淀</h3>
<p>Agent跑完全部切片，执行<code>goalctl done</code>留下完成自述，然后退出。runner收割到自述后，进入图7的收口流水线。</p>
<p><img alt="图7" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/fig-7-finisher.png" title="图7"></p>
<ul>
<li>第一步是机械复核：逐仓跑验证命令，查有没有越界改别的仓。越界按&quot;比启动时多出来的&quot;算，不按&quot;其他仓必须干净&quot;算——工作区常年压着无关改动，后一种口径每个Goal都假失败。复核不过就判FAILED，不提交任何改动；人修好后<code>goalctl finish</code> 只重跑复核。</li>
<li>复核过了，逐文件<code>git add</code>（绝不用<code>-A</code>），提交信息带story前缀和自述，推送后回填评审地址。推送有三种拿不准的情形：本仓已领先远端、待补推的sha对不上HEAD、push失败。一律停下交给人，本地提交不丢——宁可让人多点一次，也不替人推别人的提交。</li>
<li>沉淀是自然发生的：状态跃迁、决策正文、逐行日志各自落盘，复盘页原样拼出来。</li>
</ul>
<h2 id="四现在能做什么">四、现在能做什么</h2>
<p><img alt="整体示意图-1" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/overview-1.png" title="整体示意图-1">
<img alt="整体示意图-3" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/overview-3.png" title="整体示意图-3">
<img alt="整体示意图-2" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/overview-2.png" title="整体示意图-2">
<img alt="整体示意图-4" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/overview-4.png" title="整体示意图-4"></p>
<h3 id="1-接管一个项目">1. 接管一个项目</h3>
<h4 id="配置工作区">配置工作区</h4>
<ul>
<li>goal_control是独立仓，被它编排的工作区在另一个地方。在清单里配一个<code>workroot</code> 指向工作区根，再列出各子仓的验证命令，这个项目就被接管了。</li>
<li>每个Goal声明自己的仓、依赖、Prompt文件；跨前后端的Goal用<code>repos</code>声明允许改动的全部仓，启动前置、并发占用、越界判据、逐仓复核和逐仓提交均按整批仓算。</li>
</ul>
<h4 id="按goal绑定模型">按Goal绑定模型</h4>
<ul>
<li>每个Goal可单独绑定执行模型与推理强度。简单的片用快模型，难的片上更强的模型。未配置则一字不传，由Claude Code默认决定——升级不会悄悄改动任何存量Goal的执行。</li>
</ul>
<h4 id="任务组切换">任务组切换</h4>
<ul>
<li>一份清单对应一个任务组，换任务组就换一份清单，状态和日志按Goal ID共存，互不干扰。</li>
</ul>
<p><img loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-1.jpg"></p>
<h3 id="2-goal总览">2. Goal总览</h3>
<ul>
<li>顶栏四个数字回答四个问题：<strong>等你决策几条、执行中几个、阻塞几个、已完成几个</strong>。这里的&quot;阻塞&quot;口径是刻意放宽的：等人裁决、等门禁、等额度、失败，四种都算，因为它们都是&quot;不动它就永远不会动&quot;，漏掉任何一种顶栏数字就骗人。</li>
<li>左侧导轨按story分组列出全部Goal，每行带状态、切片进度、所属仓、模型短标。被卡住的Goal打呼吸的&quot;等你裁决&quot;角标。</li>
</ul>
<p><img loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-3.jpg">
<img loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-2.jpg"></p>
<h3 id="3-决策队列重要">3. 决策队列（重要）</h3>
<p>这是最重要的一页，消息分三组：</p>
<ul>
<li><strong>REALTIME 实时通道</strong>：高风险且未推迟的OPEN，唯一允许打断你的。</li>
<li><strong>BLOCKING阻断性决策</strong>：真的停住了某个Goal的。</li>
<li><strong>DEFERRABLE可后置</strong>：其余的，Agent已按默认值继续，你复核或翻案。</li>
</ul>
<p>只要有决策真卡着 Goal，控制台就不许它安静：自动弹窗、顶栏横幅、导轨角标、标签页标题变成&quot;(2) 等你裁决&quot;。决策能数字键加回车快速过，审批不行——不可逆的动作只能明确点下去。</p>
<p>&ldquo;稍后处理&quot;只压住弹窗，横幅、角标、标题一个不少。藏起来的只有打扰，阻塞藏不掉。</p>
<p><img alt="决策列表 &amp; 决策历史" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-4-1.jpg" title="决策列表 &amp; 决策历史"></p>
<ul>
<li>Agent写的决策正文常常上千字。<strong>控制台把它按角色分层：决定、候选、事实、时机、后果。只还原不改写，规则没命中就退回原样显示</strong>。跳过说明文字的审批等于没有审批，这一层就是为了让人不跳过。</li>
<li>答复能捎一句自由指令，随答案注入会话。也可以&quot;下一轮再看&rdquo;——只影响排序，Goal该卡还是卡着。开了提醒开关，这两组会用桌面通知加提示音叫你。</li>
</ul>
<p><strong>没做的</strong>：低风险预授权直接放行。开关摆在页面上但禁用着——谁有权替人放行、怎么留审计，还没想清楚。</p>
<p><img alt="审批弹窗" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-8.png" title="审批弹窗"></p>
<h3 id="4-调度dag">4. 调度DAG</h3>
<p>依赖图按拓扑层序展开，每层一行。头部四个指标：</p>
<ul>
<li>关键路径长度</li>
<li>理论并行度</li>
<li>当前前沿</li>
<li>依赖漂移（前置未DONE却已开跑的数量）。</li>
</ul>
<p>READY节点有一个&quot;启动&quot;按钮，runner常驻时禁用并提示&quot;会自动调度&quot;，runner不在时可以手工推一轮。</p>
<p><img alt="Goal依赖 &amp; 执行情况" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gm-5.jpg" title="Goal依赖 &amp; 执行情况"></p>
<h3 id="5-执行态">5. 执行态</h3>
<p>每个在飞的Goal一张卡片：</p>
<ul>
<li>状态</li>
<li>已运行时长</li>
<li>切片进度条</li>
<li>最近三条事件</li>
<li>进程号</li>
<li>本轮实际用的模型和推理强度</li>
<li>累计成本</li>
</ul>
<p>进度条的分母来自切片声明，切片标题从Goal正文里解析出来。</p>
<p><img alt="Goal执行情况" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-7.png" title="Goal执行情况"></p>
<p><img alt="在飞的Goal卡片" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-6.png" title="在飞的Goal卡片"></p>
<h3 id="6-执行过程与人工介入">6. 执行过程与人工介入</h3>
<ul>
<li>这一页是会话的实时执行流展示（这个板块目的是为了提高”心理安全感“😂）：说明文字、思考块、工具调用与返回、心跳、答复接缝、收尾汇总。日志是stream-json 逐行落盘的，所以边跑边看，进程被杀也保住已发生的；轨迹按字节游标增量读。</li>
<li>&ldquo;介入这次执行&quot;就是那条通道：停下会话，写下判断，它带着完整上下文接着做；留空提交就是&quot;看了一眼，没问题&rdquo;。每次介入都归档，此前介入过几次、说了什么都在。</li>
<li>它必要，但不该成为常规操作。频繁介入多半是切片或Goal定义出了问题，该回拆解阶段修。</li>
</ul>
<p><img alt="Goal实时执行流 &amp; 第二处人工介入口子" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-9.jpg" title="Goal实时执行流 &amp; 第二处人工介入口子"></p>
<h3 id="7-复盘">7. 复盘</h3>
<ul>
<li>Goal完成以后，复盘页回答&quot;这个任务为什么变成了现在这样&quot;：会话次数、墙上时钟、累计成本、对话轮次、token；</li>
<li>状态机走过的路和每态停留时长；每段会话的起因、退出码和成本；每条决策的提问、答复、时间；逐仓提交和评审地址；以及完整事件流水。</li>
</ul>
<p><img alt="Goal执行复盘首页" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-10.jpg" title="Goal执行复盘首页">
<img alt="本次Goal执行过程中人工接入点回顾" loading="lazy" src="/posts/parallel-coding-agent-attention-economics/gn-11.jpg" title="本次Goal执行过程中人工接入点回顾"></p>
<h3 id="8-其它顺手的能力">8. 其它顺手的能力</h3>
<ul>
<li><strong>查看完整Prompt</strong>：这Goal真正被送进<code>claude -p</code>的那一整段，协议头加正文，逐字一致，一个<code>&lt;pre&gt;</code>原样渲染，不切段不高亮，所见即所执行。</li>
<li><strong>命令行双入口</strong>：<code>goalctl status</code>、<code>goalctl inbox</code>、<code>goalctl answer</code>，跟页面走同一条写路径。控制台挂了不影响编排。</li>
<li><strong>离线只读看板</strong>：runner每轮自动重生成一个静态HTML，控制台起不来时的兜底。</li>
<li><strong>额度限流自动重试</strong>：撞上用量限制自动等到恢复时间续跑，不需要人。</li>
</ul>
<h2 id="五真实跑了一个任务组之后">五、真实跑了一个任务组之后</h2>
<p>数据来自一次真实的任务组执行：六个Goal、十五个切片、三道门禁，落在一个后端仓和一个前端仓上。</p>
<h3 id="一昼夜的账">一昼夜的账</h3>
<table>
  <thead>
      <tr>
          <th><strong>Goal</strong></th>
          <th><strong>切片</strong></th>
          <th><strong>Agent提问</strong></th>
          <th><strong>门禁</strong></th>
          <th><strong>会话段</strong></th>
          <th><strong>成本（美元）</strong></th>
          <th><strong>备注</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>G-01</td>
          <td>3</td>
          <td>2</td>
          <td>0</td>
          <td>3</td>
          <td>12.1</td>
          <td>首轮3小时14分超时被收割，人工续回</td>
      </tr>
      <tr>
          <td>G-02</td>
          <td>3</td>
          <td>1</td>
          <td>0</td>
          <td>2</td>
          <td>50.3</td>
          <td>提问四选一，人两小时后答复</td>
      </tr>
      <tr>
          <td>G-03</td>
          <td>1</td>
          <td>2</td>
          <td>1</td>
          <td>3</td>
          <td>18.3</td>
          <td></td>
      </tr>
      <tr>
          <td>G-04</td>
          <td>3</td>
          <td>1</td>
          <td>1</td>
          <td>2</td>
          <td>21.2</td>
          <td>机械复核拦下一次</td>
      </tr>
      <tr>
          <td>G-05</td>
          <td>2</td>
          <td>1</td>
          <td>0</td>
          <td>2</td>
          <td>25.3</td>
          <td>切片里的&quot;证据&quot;与代码不符，Agent停下来问</td>
      </tr>
      <tr>
          <td>G-06</td>
          <td>3</td>
          <td>0</td>
          <td>1</td>
          <td>1</td>
          <td>22.6</td>
          <td>零提问，两处保留意见记在note里</td>
      </tr>
  </tbody>
</table>
<p>十二次人工答复，约一百五十美元，从G-01续跑到G-06收口墙上时钟约一昼夜。其中十三个多小时是G-06在等一个被我弄脏的前端工作区——这个后面单说。</p>
<h3 id="打断次数变少了更重要的是变可选了">打断次数：变少了，更重要的是变&quot;可选&quot;了</h3>
<p>先说诚实的部分：这次实跑里，&ldquo;批量清空收件箱&quot;只真正发生了一次。那天下午两点零六分，我一口气答了两条，一条门禁、一条四选一的方案裁决，它们从上午十一点五十几分起就在队列里等着，而我那两个小时在做别的事。其余的答复大多在一到四分钟内完成，因为新工具上线第一周我其实是盯着的。</p>
<p>所以&quot;打断次数明显减少&quot;目前是一个观察，不是一个测量。但有几类打断确实消失了，这是确定的：<strong>权限确认没有了（</strong><code>claude -p</code><strong>用自动模式加协议约束）；&ldquo;跑完了吗&quot;这类进度询问没有了（看导轨就行）；&ldquo;下一个起谁&quot;没有了（runner自己起）。</strong>剩下的打断只有两种：<strong>Agent真的拿不准的裁决，和门禁。</strong></p>
<p>更重要的变化是打断变成了&quot;可选的&rdquo;。<strong>Agent停下来等我的时候，它是安静地停在文件里，不是在终端里闪。什么时候去处理，是我决定的。</strong></p>
<h3 id="可控具体指什么">&ldquo;可控&quot;具体指什么</h3>
<p>过去同时跑五个会话，我最怕的不是某个会话出问题，是我不知道哪个出了问题。现在&quot;可控&quot;的意思很具体：任何时刻，下面四个问题我都能在三秒内回答，而且答案来自文件，不来自记忆。</p>
<ul>
<li>哪些Goal在跑，跑到第几片？</li>
<li>哪些Goal停了，停在什么状态，等的是什么？</li>
<li>有几条事在等我，哪一条真的卡着执行？</li>
<li>下一个会起谁，为什么现在还没起？</li>
</ul>
<p>第四个问题过去是最难回答的，现在DAG页的&quot;当前前沿&quot;和dry-run直接给出来。</p>
<h3 id="意外收获">意外收获</h3>
<p><strong>决策被文档化了：</strong>G-02那条四选一的裁决，Agent写了两千多字的背景：切片定义的文件域漏掉了装配点，切片要求&quot;不产生迁移&quot;但数据库的 CHECK 约束让这条不可能成立，四个出口各自的代价是什么，它推荐哪个、为什么。<strong>我读完就能拍板，不用翻它的对话记录。事后想知道&quot;为什么这个Goal动了迁移文件&rdquo;，那条消息就是答案。</strong></p>
<p><strong>切片的&quot;漂移守卫&quot;真的救了场：</strong>G-05的切片里有一条标为&quot;证据等级A&quot;的代码现状描述，跟库内原文不符。按原文件域实施会写一个全仓没有读者的计数，验收静默失败。Agent按守卫条款停下来问，而不是硬做。这说明&quot;决策点前移&quot;和&quot;运行期收件箱&quot;是配合工作的：<strong>拆解阶段封口越严，运行期的提问质量越高。</strong></p>
<p><strong>default-with-veto 在工作：</strong>G-06全程零提问，但留了两处&quot;须知&quot;在note里：一处接线不在文件域内所以没接，一处接缝暴露了但没有调用方。这正是协议要求的行为：低风险歧义按推荐默认值继续，把假设记下来。</p>
<p><strong>成本可见了：</strong>过去我不知道一个Goal花多少钱（订阅制）。现在每段会话的成本、轮次、token都在复盘页上，累计一个任务组约一百五十美元，这个数字第一次有了。</p>
<h2 id="六下一步">六、下一步</h2>
<h3 id="支持更多coding-agent">支持更多Coding Agent</h3>
<p>goal_control对Claude Code的依赖其实很薄：一条启动命令、一条续跑命令、两个锚点行、一段协议头、一种日志格式。这些都集中在启动器和轨迹解析两个模块里。Codex也有headless和goal特性，接进来主要是适配层的工作。真正需要抽象的是&quot;一个执行体应该提供什么能力&rdquo;：可启动、可续回、可逐行观测、可被终止。</p>
<h3 id="从goal模型再抽象一层">从Goal模型再抽象一层</h3>
<p>目前整个系统建立在我自己的&quot;切片 → Goal → DAG&quot;方法之上。清单的字段、切片轨道的解析规则、组尾验证的约定，都带着这套方法的形状。</p>
<p>我想把它抽象成四个更通用的概念：<strong>工作单元</strong>（一段Prompt加一个工作区加一组完成判据）、<strong>依赖</strong>、<strong>完成判据</strong>（机械可验证的那部分）、<strong>决策协议</strong>（工作单元怎么向外汇报、怎么提问）。切片和 Goal只是这四个概念的一种实例化。做到这一步，用别的任务组织方式的人也能用它。</p>
<h3 id="其它值得做的方向">其它值得做的方向</h3>
<ul>
<li><strong>低风险预授权。</strong> 现在所有决策都要人点一下，default-with-veto的假设记录目前只落在note里，不是一等对象，所以&quot;可后置&quot;那一组在真实数据里基本是空的。把假设做成可复核、可翻案的对象，是让队列长度真正下降的下一步。</li>
<li><strong>hooks兜底采集。</strong> 现在的机械状态靠runner持有的进程句柄，Claude Code的hooks可以在会话不知情的情况下把&quot;停下来了、在等输入&quot;这类事件写进中央日志，是更优雅的采集点。</li>
<li><strong>会话恢复的一等入口。</strong> 超时被收割的会话，其transcript还在，session_id就是文件名。现在恢复靠一套手工步骤，应该做成一个命令。</li>
<li><strong>进度感知的超时。</strong> 硬超时会误杀一个还在正常推进的长Goal。既然有逐行轨迹，&ldquo;多久没有新进展&quot;比&quot;跑了多久&quot;更适合当收割判据。</li>
<li><strong>多人协作与鉴权。</strong> 控制台只绑本机、不做鉴权，因为它现在只服务一个人。团队用需要这一层。</li>
</ul>
<h2 id="最后">最后</h2>
<blockquote>
<p>中断这个机制是为CPU发明的，它成立的前提只有一条：CPU的上下文切换便宜到可以忽略。可就算是CPU，负载一高也扛不住。Linux的网卡驱动里有个叫NAPI的机制，包来得太密的时候，它做的第一件事是<strong>把中断关掉</strong>，改成内核自己按节奏去轮询，队列空了再把中断打开。内核工程师早就想明白了：<strong>事件多到一定程度，被动响应不如主动去取。</strong></p>
</blockquote>
<table>
  <thead>
      <tr>
          <th><strong>CPU / 网卡</strong></th>
          <th><strong>人 / Coding Agent</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>上下文切换接近零成本</td>
          <td>上下文切换是分钟级的，贵得离谱</td>
      </tr>
      <tr>
          <td>包密到一定程度，NAPI关中断转轮询</td>
          <td>Agent多到五个以上，你就是那块高负载的网卡</td>
      </tr>
      <tr>
          <td>NMI等高优先级中断谁也拦不住</td>
          <td>只有高风险事项配得上实时打断你</td>
      </tr>
  </tbody>
</table>
<p>人的上下文切换不是便宜，是贵得离谱——开头那个晚上，我切走两分钟，回来最早那个已经空等了二十分钟。所以&quot;从中断到收件箱&quot;不是我的偏好，是负载决定的：<strong>五个以上Agent同时跑，你就是那块高负载的网卡，该关中断了。</strong></p>
<p>关中断不是全关。CPU有屏蔽位、有优先级，NMI谁也拦不住，对应到人，就是这篇文章我最想留下的一句：</p>
<p><strong>只有高风险配得上打断你：</strong>其余的进队列，你按自己的节奏取。</p>
<p>goal_control是这套想法的一个粗糙实现。它迟早可能会被哪个产品内置掉，这不重要。重要的是我花了两周才想明白、而你读到这里只花了二十分钟的那件事：<strong>今天的Coding Agent默认你是一块CPU，随时可中断，切换不要钱，而你不是。</strong></p>
]]></content:encoded></item></channel></rss>