loops-l1.html 9.64 KB
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>L1 · Unit Loop · LemonBoss PRD</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="assets/shell.css" />
</head>
<body data-page="loops-l1.html">
<div class="shell">
  <aside id="sidebar"></aside>
  <main class="page">
    <header class="page-head">
      <div class="crumb"><span class="pill">02 · LOOPS</span><span class="sep">/</span><span>L1 · Unit Loop</span></div>
      <h1>L1 · Unit Loop</h1>
      <p class="deck">最内层。在<b>单一 Unit</b> 范围内跑「写代码 → 跑单测 → 失败重写」直到全绿或耗尽预算。L1 是 L2 的子例程,几乎所有「自动化软件开发」体感来自这里。</p>
    </header>
    <article class="body">

      <section>
        <h2>状态机</h2>
        <svg viewBox="0 0 1100 240" width="100%" role="img">
          <defs><marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#0E0E10"/></marker></defs>
          <g font-family="JetBrains Mono" font-size="12" fill="#0E0E10">
            <rect x="20"  y="90" width="120" height="60" rx="4" fill="#fff" stroke="#0E0E10"/><text x="80" y="125" text-anchor="middle">pending</text>
            <rect x="200" y="90" width="120" height="60" rx="4" fill="#E2F2EA" stroke="#0E8C5A"/><text x="260" y="120" text-anchor="middle">coding</text><text x="260" y="138" text-anchor="middle" font-size="10" fill="#3B3B40">CoderAgent</text>
            <rect x="380" y="90" width="120" height="60" rx="4" fill="#E2F2EA" stroke="#0E8C5A"/><text x="440" y="120" text-anchor="middle">testing</text><text x="440" y="138" text-anchor="middle" font-size="10" fill="#3B3B40">TesterAgent</text>
            <rect x="560" y="30" width="120" height="60" rx="4" fill="#FCE7E2" stroke="#C7341A"/><text x="620" y="60" text-anchor="middle">failed</text><text x="620" y="78" text-anchor="middle" font-size="10" fill="#3B3B40">retry ≤ N</text>
            <rect x="560" y="150" width="120" height="60" rx="4" fill="#fff" stroke="#0E8C5A" stroke-width="2"/><text x="620" y="185" text-anchor="middle">passed</text>
            <rect x="740" y="90" width="120" height="60" rx="4" fill="#FFF6CC" stroke="#8a6b00"/><text x="800" y="120" text-anchor="middle">blocked</text><text x="800" y="138" text-anchor="middle" font-size="10" fill="#3B3B40">超出预算</text>
            <rect x="920" y="150" width="120" height="60" rx="4" fill="#ECE4FB" stroke="#6E48C3"/><text x="980" y="185" text-anchor="middle">→ L2</text>
            <line x1="140" y1="120" x2="195" y2="120" stroke="#0E0E10" marker-end="url(#arr)"/>
            <line x1="320" y1="120" x2="375" y2="120" stroke="#0E0E10" marker-end="url(#arr)"/>
            <line x1="500" y1="120" x2="555" y2="180" stroke="#0E0E10" marker-end="url(#arr)"/>
            <line x1="500" y1="110" x2="555" y2="60" stroke="#C7341A" marker-end="url(#arr)"/>
            <path d="M620,30 Q420,-10 260,90" fill="none" stroke="#C7341A" stroke-dasharray="4 3" marker-end="url(#arr)"/>
            <line x1="680" y1="60" x2="735" y2="115" stroke="#8a6b00" stroke-dasharray="3 2" marker-end="url(#arr)"/>
            <line x1="680" y1="180" x2="915" y2="180" stroke="#6E48C3" marker-end="url(#arr)"/>
          </g>
        </svg>
      </section>

      <section>
        <h2>状态字典</h2>
        <table class="dense">
          <thead><tr><th>state</th><th>谁写入</th><th>含义</th><th>退出条件</th></tr></thead>
          <tbody>
            <tr><td><code>pending</code></td><td>Architect 创建 unit 时</td><td>spec 已就绪,等待 LoopController 调度</td><td>有空闲 Coder Agent → coding</td></tr>
            <tr><td><code>coding</code></td><td>LoopController</td><td>Coder 正在写 <code>code/</code> 下文件</td><td>Coder 返回 success → testing</td></tr>
            <tr><td><code>testing</code></td><td>LoopController</td><td>Tester 正在 vitest run</td><td>全绿 → passed · 红 → failed</td></tr>
            <tr><td><code>failed</code></td><td>Tester</td><td>本轮单测失败,retries++</td><td>retries &lt; budget → coding · 否则 → blocked</td></tr>
            <tr><td><code>passed</code></td><td>Tester</td><td>单测全绿,等所有 sibling Unit 跟进</td><td>所有 Unit passed → L2 gate</td></tr>
            <tr><td><code>blocked</code></td><td>LoopController</td><td>耗尽 retry,无法自愈,需 L3 介入</td><td>人类反馈 → 重置回 pending</td></tr>
          </tbody>
        </table>
      </section>

      <section>
        <h2>调度与并行</h2>
        <p>LoopController 复用 <code>server/src/core/scheduler.js</code> 的 <code>getExecutionGroups()</code>:根据 <code>depends_on</code> 把所有 pending Unit 分成一波一波。同一波之间并行,跨波串行。</p>
        <pre class="code"><span class="c">// units.json 节选</span>
<span class="p">{</span>
  <span class="n">"units"</span>: <span class="p">[</span>
    <span class="p">{</span> <span class="n">"id"</span>: <span class="s">"auth-be"</span>,    <span class="n">"depends_on"</span>: <span class="p">[]</span> <span class="p">}</span>,
    <span class="p">{</span> <span class="n">"id"</span>: <span class="s">"profile-be"</span>, <span class="n">"depends_on"</span>: <span class="p">[]</span> <span class="p">}</span>,
    <span class="p">{</span> <span class="n">"id"</span>: <span class="s">"auth-fe"</span>,    <span class="n">"depends_on"</span>: <span class="p">[</span><span class="s">"auth-be"</span><span class="p">]</span> <span class="p">}</span>,
    <span class="p">{</span> <span class="n">"id"</span>: <span class="s">"profile-fe"</span>, <span class="n">"depends_on"</span>: <span class="p">[</span><span class="s">"profile-be"</span><span class="p">]</span> <span class="p">}</span>
  <span class="p">]</span>
<span class="p">}</span>
<span class="c">// scheduler.getExecutionGroups() →</span>
<span class="c">// wave 1: [auth-be, profile-be]    (parallel)</span>
<span class="c">// wave 2: [auth-fe, profile-fe]    (parallel)</span></pre>
      </section>

      <section>
        <h2>Coder → Tester 握手协议</h2>
        <p>两个 Agent 通过文件系统握手,不直接通信。这让任意一个 Agent 死掉都不会丢上下文。</p>
        <div class="io-grid">
          <div class="io-box">
            <h4>Coder 写入</h4>
            <ul>
              <li><code>units/{id}/code/**</code> — 源码</li>
              <li><code>units/{id}/contract.json</code> — 暴露给下游的契约</li>
              <li><code>units/{id}/coder.log</code> — 决策日志(供 retry 时回顾)</li>
            </ul>
          </div>
          <div class="io-box">
            <h4>Tester 读取 + 写入</h4>
            <ul>
              <li>读 <code>units/{id}/code/**</code></li>
              <li>读 <code>units/{id}/spec.json</code> 推导测试用例</li>
              <li>写 <code>units/{id}/tests/**</code></li>
              <li>写 <code>units/{id}/runs/{runId}/result.json</code></li>
            </ul>
          </div>
        </div>
      </section>

      <section>
        <h2>重试策略 — retryWithReflection</h2>
        <p>从 <code>evolution-loop.js</code> 抽出共用函数。每次 Tester 失败,把失败原因、上次 Coder 的决策日志、原 spec 一起喂回 Coder,让模型「反思」上次错在哪。</p>
        <pre class="code"><span class="k">async function</span> <span class="n">retryWithReflection</span>(<span class="v">unit</span>, <span class="v">budget</span>) <span class="p">{</span>
  <span class="k">let</span> failures = <span class="p">[]</span>;
  <span class="k">for</span> (<span class="k">let</span> i = <span class="v">0</span>; i &lt; budget; i++) <span class="p">{</span>
    <span class="k">const</span> coderInput = <span class="p">{</span> spec: unit.spec, history: failures <span class="p">}</span>;
    <span class="k">await</span> <span class="n">agentManager</span>.<span class="n">callAgent</span>(<span class="s">'coder'</span>, coderInput);

    <span class="k">const</span> r = <span class="k">await</span> <span class="n">agentManager</span>.<span class="n">callAgent</span>(<span class="s">'tester'</span>, <span class="p">{</span> unit <span class="p">}</span>);
    <span class="k">if</span> (r.<span class="n">ok</span>) <span class="k">return</span> <span class="p">{</span> ok: <span class="v">true</span>, retries: i <span class="p">}</span>;
    failures.<span class="n">push</span>(r.<span class="n">failures</span>);
  <span class="p">}</span>
  <span class="k">return</span> <span class="p">{</span> ok: <span class="v">false</span>, reason: <span class="s">"budget_exceeded"</span> <span class="p">}</span>;
<span class="p">}</span></pre>
      </section>

      <section>
        <h2>预算与降级</h2>
        <div class="callout warn">
          <b>R1 风险联动:</b>每个 unit 全局 retry 预算 = 5(可在 <code>units.json</code> 用 <code>retry_budget</code> 覆盖)。耗尽后置为 <code>blocked</code>,LoopController 不会自动进 L2,而是发 SSE 事件等待人类介入(L3 接管)。
        </div>
      </section>

    </article>
    <nav class="prev-next"></nav>
    <footer class="site"><div><b>Section</b>02 · Loops</div><div><b>Page</b>loops-l1.html</div></footer>
  </main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>