<?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-Agent on Tarragon</title><link>https://tarrragon.github.io/blog/tags/ai-agent/</link><description>Recent content in Ai-Agent on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 25 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/ai-agent/index.xml" rel="self" type="application/rss+xml"/><item><title>並行 AI Agent 修改同一檔案的衝突模式與協調策略</title><link>https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>多人（或多 agent）並行開發時，如果修改集中在同一個檔案，協調成本可能抵消並行的收益。以下是一個具體案例。&lt;/p>
&lt;p>v0.3.0 的 JS SDK 開發中，五張 ticket 被並行派發給五個 AI agent：flush 邏輯、離線容錯、自動攔截、頁面生命週期、rate limiting。前四個都需要修改同一個檔案 &lt;code>monitor.ts&lt;/code>。&lt;/p>
&lt;p>結果：&lt;/p>
&lt;ul>
&lt;li>三個 agent 回報 branch protection hook 阻擋 src 編輯&lt;/li>
&lt;li>兩個 agent 回報 &lt;code>file modified since read&lt;/code> 拒絕 Edit（另一個 agent 正在寫同一檔案）&lt;/li>
&lt;li>PM 花了多個回合協調 commit 策略：「你先 commit」「你等他完成」「你只 git add 你的檔案」&lt;/li>
&lt;li>最終 PM 手動合併所有 agent 的變更，做了一個統一 commit&lt;/li>
&lt;/ul>
&lt;p>並行派發的目標是縮短總工時。但五個 agent 改同一檔案時，協調成本抵消了並行的收益。&lt;/p>
&lt;h2 id="根因派發粒度錯在-ticket-層而非檔案層">根因：派發粒度錯在 ticket 層而非檔案層&lt;/h2>
&lt;p>派發決策看的是 ticket 的獨立性——五張 ticket 描述的功能確實獨立（flush、離線、攔截、生命週期各自有清楚的邊界）。但獨立的功能不等於獨立的檔案。五個功能的修改都集中在 &lt;code>monitor.ts&lt;/code> 這一個檔案上。&lt;/p>
&lt;p>ticket 獨立 =/= 檔案獨立。並行安全的判斷基準應該是後者。&lt;/p>
&lt;h2 id="教訓">教訓&lt;/h2>
&lt;p>&lt;strong>派發前掃描 &lt;code>where.files&lt;/code>&lt;/strong>：如果多張 ticket 的目標檔案有交集，序列化派發。前一張完成並 commit 後，再派下一張。&lt;/p>
&lt;p>&lt;strong>序列的代價比衝突的代價低&lt;/strong>：五個 agent 序列執行可能需要 5 倍時間，但每個 agent 在乾淨的工作區上操作，不需要協調。五個 agent 並行但衝突，PM 的協調時間加上 agent 的等待和重試，總成本可能更高。&lt;/p>
&lt;p>&lt;strong>Worktree 隔離不是萬靈丹&lt;/strong>：git worktree 讓每個 agent 有獨立的工作目錄，避免 working tree 衝突。但如果兩個 agent 修改同一檔案的不同區段，merge 時仍需人工判斷。Worktree 解決的是「同時寫同一個 working tree」的問題，不解決「同時改同一個檔案的語意衝突」。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>這個 pattern 不限於 AI agent。人類開發者在同一個 Sprint 中被分配修改同一個檔案的不同功能時，也會遇到 merge conflict。差異在於人類可以口頭協調（「我先改完你再改」），agent 目前缺乏這個即時溝通管道。派發者（PM 或 CI 系統）需要在派發時就做好檔案衝突預判。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p>多人（或多 agent）並行開發時，如果修改集中在同一個檔案，協調成本可能抵消並行的收益。以下是一個具體案例。</p>
<p>v0.3.0 的 JS SDK 開發中，五張 ticket 被並行派發給五個 AI agent：flush 邏輯、離線容錯、自動攔截、頁面生命週期、rate limiting。前四個都需要修改同一個檔案 <code>monitor.ts</code>。</p>
<p>結果：</p>
<ul>
<li>三個 agent 回報 branch protection hook 阻擋 src 編輯</li>
<li>兩個 agent 回報 <code>file modified since read</code> 拒絕 Edit（另一個 agent 正在寫同一檔案）</li>
<li>PM 花了多個回合協調 commit 策略：「你先 commit」「你等他完成」「你只 git add 你的檔案」</li>
<li>最終 PM 手動合併所有 agent 的變更，做了一個統一 commit</li>
</ul>
<p>並行派發的目標是縮短總工時。但五個 agent 改同一檔案時，協調成本抵消了並行的收益。</p>
<h2 id="根因派發粒度錯在-ticket-層而非檔案層">根因：派發粒度錯在 ticket 層而非檔案層</h2>
<p>派發決策看的是 ticket 的獨立性——五張 ticket 描述的功能確實獨立（flush、離線、攔截、生命週期各自有清楚的邊界）。但獨立的功能不等於獨立的檔案。五個功能的修改都集中在 <code>monitor.ts</code> 這一個檔案上。</p>
<p>ticket 獨立 =/= 檔案獨立。並行安全的判斷基準應該是後者。</p>
<h2 id="教訓">教訓</h2>
<p><strong>派發前掃描 <code>where.files</code></strong>：如果多張 ticket 的目標檔案有交集，序列化派發。前一張完成並 commit 後，再派下一張。</p>
<p><strong>序列的代價比衝突的代價低</strong>：五個 agent 序列執行可能需要 5 倍時間，但每個 agent 在乾淨的工作區上操作，不需要協調。五個 agent 並行但衝突，PM 的協調時間加上 agent 的等待和重試，總成本可能更高。</p>
<p><strong>Worktree 隔離不是萬靈丹</strong>：git worktree 讓每個 agent 有獨立的工作目錄，避免 working tree 衝突。但如果兩個 agent 修改同一檔案的不同區段，merge 時仍需人工判斷。Worktree 解決的是「同時寫同一個 working tree」的問題，不解決「同時改同一個檔案的語意衝突」。</p>
<h2 id="適用場景">適用場景</h2>
<p>這個 pattern 不限於 AI agent。人類開發者在同一個 Sprint 中被分配修改同一個檔案的不同功能時，也會遇到 merge conflict。差異在於人類可以口頭協調（「我先改完你再改」），agent 目前缺乏這個即時溝通管道。派發者（PM 或 CI 系統）需要在派發時就做好檔案衝突預判。</p>
]]></content:encoded></item><item><title>新增欄位忘記同步 reset — 跨測試狀態洩漏的系統性根因</title><link>https://tarrragon.github.io/blog/work-log/%E6%96%B0%E5%A2%9E%E6%AC%84%E4%BD%8D%E5%BF%98%E8%A8%98%E5%90%8C%E6%AD%A5-reset-%E8%B7%A8%E6%B8%AC%E8%A9%A6%E7%8B%80%E6%85%8B%E6%B4%A9%E6%BC%8F%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%A0%B9%E5%9B%A0/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E6%96%B0%E5%A2%9E%E6%AC%84%E4%BD%8D%E5%BF%98%E8%A8%98%E5%90%8C%E6%AD%A5-reset-%E8%B7%A8%E6%B8%AC%E8%A9%A6%E7%8B%80%E6%85%8B%E6%B4%A9%E6%BC%8F%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%A0%B9%E5%9B%A0/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>JS SDK 的 Monitor class 在一輪並行開發中，三個開發者各自新增了 private 欄位：&lt;code>flushing&lt;/code>（flush 併發 guard）、&lt;code>retryCount&lt;/code>（重試計數）、&lt;code>lastHeartbeat&lt;/code>（心跳時間戳）。三個欄位各自在功能邏輯中被正確使用，但都沒有加進 &lt;code>__reset()&lt;/code> 方法。&lt;/p>
&lt;p>測試框架在每個 test case 之間呼叫 &lt;code>__reset()&lt;/code> 清理狀態。因為 &lt;code>retryCount&lt;/code> 沒被重置，第一個 test case 把 retryCount 遞增到 1，第二個 test case 繼承了這個值，retry 邏輯提前觸發，測試失敗。&lt;/p>
&lt;p>失敗的測試看起來像是 retry 邏輯有 bug，但實際上 retry 邏輯完全正確——問題出在測試隔離。&lt;/p>
&lt;h2 id="根因隱含契約沒有顯性化">根因：隱含契約沒有顯性化&lt;/h2>
&lt;p>Class 的每個 private 欄位都有一個隱含契約：「所有生命週期路徑都知道你的存在。」這包括初始化（constructor / init）、重置（reset / dispose）、序列化（toJSON，如適用）。&lt;/p>
&lt;p>新增欄位時，開發者通常會先在功能邏輯中使用這個欄位——因為那是他加欄位的目的。但「同步到 reset」不是功能邏輯的一部分，它是一個跨切面的維護動作。遺漏的機率隨欄位數和開發者數增加而上升。&lt;/p>
&lt;p>多人（或多 AI agent）並行開發時問題更嚴重——每個人只看自己加的欄位，沒有人有動機去檢查 reset 的完整性。並行修改同一檔案的協調問題見 &lt;a href="https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/" data-link-title="並行 AI Agent 修改同一檔案的衝突模式與協調策略" data-link-desc="並行派多個開發者或 AI agent 同一批 ticket，反覆修改同一個檔案、卡在 branch protection 與 file-modified-since-read。問題在派發策略沒考慮檔案層級的衝突。">parallel_agent_same_file_conflict&lt;/a>。&lt;/p>
&lt;h2 id="防護state-registry-pattern">防護：State Registry Pattern&lt;/h2>
&lt;p>將所有 private 欄位的初始值集中宣告一次：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">initialState() {&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">config&lt;/span>: &lt;span class="kt">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">buffer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">flushing&lt;/span>: &lt;span class="kt">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">retryCount&lt;/span>: &lt;span class="kt">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">lastHeartbeat&lt;/span>: &lt;span class="kt">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 新增欄位加在這裡——init 和 reset 自動包含
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>reset 改用 &lt;code>Object.assign(this, initialState())&lt;/code>。新增欄位只改一處，init 和 reset 自動同步。&lt;/p>
&lt;p>配合一個 reset 完整性測試：reset 後 snapshot 比對 initialState 的所有 key——新增欄位但忘記加到 initialState 會因型別或 key 不一致而紅燈。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>任何有「重置到初始狀態」需求的 class：測試框架的 setUp/tearDown、物件池的回收、singleton 的 reinit。問題在「新增欄位」和「同步 reset」是兩個分開的動作（TypeScript、Go、Dart 都會遇到）——只要是分開的，就有遺漏的可能。State Registry 把兩者合併成一個動作。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p>JS SDK 的 Monitor class 在一輪並行開發中，三個開發者各自新增了 private 欄位：<code>flushing</code>（flush 併發 guard）、<code>retryCount</code>（重試計數）、<code>lastHeartbeat</code>（心跳時間戳）。三個欄位各自在功能邏輯中被正確使用，但都沒有加進 <code>__reset()</code> 方法。</p>
<p>測試框架在每個 test case 之間呼叫 <code>__reset()</code> 清理狀態。因為 <code>retryCount</code> 沒被重置，第一個 test case 把 retryCount 遞增到 1，第二個 test case 繼承了這個值，retry 邏輯提前觸發，測試失敗。</p>
<p>失敗的測試看起來像是 retry 邏輯有 bug，但實際上 retry 邏輯完全正確——問題出在測試隔離。</p>
<h2 id="根因隱含契約沒有顯性化">根因：隱含契約沒有顯性化</h2>
<p>Class 的每個 private 欄位都有一個隱含契約：「所有生命週期路徑都知道你的存在。」這包括初始化（constructor / init）、重置（reset / dispose）、序列化（toJSON，如適用）。</p>
<p>新增欄位時，開發者通常會先在功能邏輯中使用這個欄位——因為那是他加欄位的目的。但「同步到 reset」不是功能邏輯的一部分，它是一個跨切面的維護動作。遺漏的機率隨欄位數和開發者數增加而上升。</p>
<p>多人（或多 AI agent）並行開發時問題更嚴重——每個人只看自己加的欄位，沒有人有動機去檢查 reset 的完整性。並行修改同一檔案的協調問題見 <a href="/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/" data-link-title="並行 AI Agent 修改同一檔案的衝突模式與協調策略" data-link-desc="並行派多個開發者或 AI agent 同一批 ticket，反覆修改同一個檔案、卡在 branch protection 與 file-modified-since-read。問題在派發策略沒考慮檔案層級的衝突。">parallel_agent_same_file_conflict</a>。</p>
<h2 id="防護state-registry-pattern">防護：State Registry Pattern</h2>
<p>將所有 private 欄位的初始值集中宣告一次：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">initialState() {</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">config</span>: <span class="kt">null</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">buffer</span><span class="o">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">flushing</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">retryCount</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">lastHeartbeat</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1">// 新增欄位加在這裡——init 和 reset 自動包含
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="p">};</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>reset 改用 <code>Object.assign(this, initialState())</code>。新增欄位只改一處，init 和 reset 自動同步。</p>
<p>配合一個 reset 完整性測試：reset 後 snapshot 比對 initialState 的所有 key——新增欄位但忘記加到 initialState 會因型別或 key 不一致而紅燈。</p>
<h2 id="適用場景">適用場景</h2>
<p>任何有「重置到初始狀態」需求的 class：測試框架的 setUp/tearDown、物件池的回收、singleton 的 reinit。問題在「新增欄位」和「同步 reset」是兩個分開的動作（TypeScript、Go、Dart 都會遇到）——只要是分開的，就有遺漏的可能。State Registry 把兩者合併成一個動作。</p>
]]></content:encoded></item></channel></rss>