:Background Tasks避免慢操作阻塞)
0基礎學會Agent Harness工程13Background Tasks避免慢操作阻塞本篇對應的官方文檔Learn Claude Codes13 Background Tasks支撐慢工具后臺分派、占位結果和完成通知回填的教學結構。OpenAI Function Calling用于核對 Chat Completions 中 assistanttool_calls與roletool/tool_call_id的配對邊界。Python threading用于核對Thread、Lock以及 daemon thread 在進程退出時的資源釋放風險。Python queue用于對比教學代碼的加鎖字典與專用多生產者、多消費者隊列。本篇主要內容第 12 篇已經用Task、blockedBy、owner和 JSON 持久化記住目標但工具 handler 仍在 Agent Loop 內同步運行一條十分鐘的命令會讓前臺一直等待。本篇增加background_tasks、background_results、Lock和 daemon thread先用占位 tool result 完成當前協議配對再把真實結果作為新的task_notification注入messages最后追蹤通知時機、亂序和進程退出等生產邊界。下篇預告慢操作已經能離開前臺運行但仍需要當前交互先發起它。第 14 篇將加入 Cron Scheduler讓未來時間點主動喚醒 Agent。一、任務能跨輪保存為什么慢操作仍會占住循環第 12 篇的代碼主線是創建 Task、寫入.tasks、檢查blockedBy、認領任務并在完成后解鎖下游。它解決了目標的生命周期卻沒有改變工具的執行方式agent_loop()取到一個 tool call 后仍然直接調用 handlerhandler 不返回循環就不能繼續。假設模型決定做兩件事先運行耗時的依賴安裝同時讀取配置文件并檢查參數。若run_bash()使用subprocess.run()同步執行代碼必須先等安裝結束才能回填 tool result模型也不可能在等待期間決定讀文件。Task 文件雖然記得“正在安裝”前臺依然被這次調用占住。這里需要區分三個對象Task 是業務目標記錄“為什么做、依賴誰、當前到哪一步”tool call 是模型在某一輪提出的結構化行動意圖background job 是 Harness 為一次具體慢操作創建的執行實例。一個 Task 可能觸發多個 background job一個 background job 也不能代替 Task 的 owner、依賴和完成標準。從生命周期邊界觀察Task 可以跨進程保留background job 只存在于當前 Python 進程tool call 則屬于當前模型請求的協議上下文。三條生命線只在明確節點交接模型通過 tool call 提出動作Harness 據此創建 background job完成后再由應用決定是否更新 Task。若只因為后臺命令結束就直接把業務 Task 標記為 completed就跳過了結果校驗、副作用確認和下游解鎖條件。第 13 篇提出的解法是把“提交”與“取回結果”拆成兩次交接。前臺只創建線程并立即返回bg_id真實 handler 在后臺執行完成后將輸出寫入結果存儲前臺在后續循環里收集它把新狀態注入給模型。同步與后臺執行的差異不在于命令本身而在于何時把控制權還給 Agent Loop。同步路徑要等真實輸出才能回填后臺路徑先回填“已提交”讓循環繼續處理其他動作。后臺化并沒有讓模型在同一時刻并行思考多輪。Agent Loop 依然是單線程的請求、回填與再請求只有工具執行離開了這條主線。這個邊界能防止把“后臺 I/O”誤解為“多個 Agent 并行推理”。圖中的控制權變化還帶來一個實際判斷適合后臺化的不是“代碼看起來復雜”的工具而是調用方無需立刻拿到最終結果也能繼續推進的操作。讀取即將用于下一步判斷的配置通常應同步完成構建、批量測試和遠程部署則更適合返回句柄。若下一步嚴格依賴真實輸出過早后臺化只會把清晰的順序依賴改造成輪詢和等待。二、后臺執行由哪些狀態組成s13_background_tasks.py保留了第 12 篇的 Task System、Prompt 組裝、Tool Schema、dispatch 和基礎 Agent Loop。本篇只在持久運行層增加四個關鍵對象background_tasks按bg_id記錄原 tool call、命令和running/completed狀態background_results保存已完成 handler 的文本輸出background_lock保護兩個字典的跨線程讀寫daemonThread真正執行 handler結束后寫回狀態和結果。觀察下圖時重點不是記住四個變量名而是看“執行實例、執行結果、互斥規則、執行載體”怎樣分別落位。只有把這四類職責拆開查詢狀態時才不必阻塞真正的工作完成結果也不會與仍在運行的元數據混成一團。兩個字典分別回答“它在做什么”和“它得到了什么”。把狀態和大段輸出分開可以在列表后臺工作時避免每次復制完整結果。但它們都是進程內存與第 12 篇的.tasksJSON 完全不同重啟后bg_0001的狀態和輸出不會恢復。是否轉入后臺由兩層規則決定。run_in_backgroundTrue是模型通過 Tool Schema 顯式提出的請求若沒有該標志is_slow_operation()再用install、build、test、deploy等關鍵字做降級啟發。defis_slow_operation(tool_name:str,tool_input:dict)-bool:用關鍵字識別可能長時間運行的 Bash 命令。iftool_name!bash:returnFalsecommandtool_input.get(command,).lower()slow_keywords[install,build,test,deploy,compile,docker build,pip install,npm install,cargo build,pytest,make,]returnany(keywordincommandforkeywordinslow_keywords)defshould_run_background(tool_name:str,tool_input:dict)-bool:優先采用顯式參數否則回退到啟發式判斷。iftool_input.get(run_in_background):returnTruereturnis_slow_operation(tool_name,tool_input)顯式標志提供可觀察意圖啟發式只是容錯。兩者都不是可靠的資源調度pytest -q可能幾秒結束不含關鍵字的數據遷移卻可能運行數小時。生產系統應讓工具元數據聲明預期耗時、可后臺性和資源限制并由 Harness 最終決定。判斷鏈要觀察優先級顯式True直接進后臺否則只有 Bash 且命中慢關鍵字才進后臺其他工具繼續同步。這保留了快操作的簡單反饋也避免所有 handler 都無條件地增加異步狀態。從決策圖進入代碼時可以把它讀成一項策略函數而不是模型的最終命令。模型只提供偏好Harness 仍應檢查工具是否允許后臺執行、當前容量是否充足、調用是否具有副作用以及調用方是否能夠接受稍后獲得結果。這樣即使 Tool Schema 暴露了run_in_background系統控制權也沒有交給模型。圖中的菱形判斷最終只輸出“采用哪條執行路徑”不會改變原 tool call 的名稱和參數。繼續進入代碼時要檢查后臺分支是否保存了足夠的關聯信息至少包括新的bg_id、原tool_call_id、命令摘要和當前狀態。缺少這些字段之后即使獲得一段結果也無法解釋它來自哪次調用、應該通知哪段會話。因此分派函數的職責到“創建可追蹤執行實例”就結束了線程生命周期、結果寫入和通知交付分別由后續組件承擔。這樣的邊界讓未來把 Thread 換成進程池或外部隊列時Agent Loop 的分支和 tool result 配對仍可保持不變。真正的后臺分派發生在start_background_task()。它保存調用信息創建 worker 閉包啟動 daemon thread然后立即返回bg_iddefstart_background_task(block)-str:在守護線程中執行工具并返回后臺任務 ID。global_bg_counter _bg_counter1bg_idfbg_{_bg_counter:04d}argumentsjson.loads(block.function.arguments)commandarguments.get(command,block.function.name)defworker():resultexecute_tool(block)withbackground_lock:background_tasks[bg_id][status]completedbackground_results[bg_id]resultwithbackground_lock:background_tasks[bg_id]{tool_call_id:block.id,command:command,status:running,}threadthreading.Thread(targetworker,daemonTrue)thread.start()returnbg_idLock保護的是共享字典的復合讀寫不是將整個 handler 鎖住。worker 在鎖外執行耗時工具只在更新狀態和結果時持鎖若把execute_tool()放在with background_lock里其他線程連查狀態都要等慢命令結束異步優勢會被鎖粒度抵消。這段實現還隱含了一個狀態不變量background_tasks[bg_id]必須先以running出現worker 才能把它改成completed結果寫入與狀態切換也應在同一次臨界區完成。否則 collector 可能看見“已完成但沒有結果”或者 worker 極快結束時訪問一個尚未登記的 ID。當前代碼先登記再thread.start()正是為了維持這個順序。Python 文檔明確提醒daemon thread 會在進程關閉時被突然停止打開的文件、事務和其他資源可能沒有正常釋放。因此daemonTrue只是讓教學 CLI 退出時不被后臺線程拖住并不代表任務可靠完成。三、占位結果和完成通知怎樣接回messages后臺化最容易混淆的地方不是線程而是 Chat Completions 消息配對。assistant 已經輸出一個帶 ID 的 tool call后續roletool結果必須使用對應tool_call_id。若 Harness 什么都不回填只想等后臺結束再說當前消息組就不完整模型也無法先繼續處理其他事情。所以第一次回填不是最終輸出而是占位結果“后臺任務bg_0001已啟動結果完成后可用”。這條消息仍用原block.id作為tool_call_id因為它回答的正是“本次工具調用是否已被 Harness 接受”。工具調用 ID 在這個時刻已經消費完畢。若真實命令結束后再發一條相同tool_call_id的 tool message就相當于一個調用返回兩次結果既破壞消息組的一對一關系也會讓歷史裁剪和重放無法判斷哪條是有效結果。真實完成是之后發生的環境事件因此代碼把它組裝為task_notification文本再以新的roleuser消息注入。這是 Harness 內部通知協議不是 OpenAI API 新增的標準 message roleXML 標簽也只是應用選擇的可讀包裝。defcollect_background_results()-list[str]:取出已完成后臺結果并組裝成新的通知。withbackground_lock:ready_ids[bg_idforbg_id,taskinbackground_tasks.items()iftask[status]completed]notifications[]forbg_idinready_ids:withbackground_lock:taskbackground_tasks.pop(bg_id)outputbackground_results.pop(bg_id,)notifications.append(task_notification\nf task_id{bg_id}/task_id\n statuscompleted/status\nf command{task[command]}/command\nf summary{output[:200]}/summary\n/task_notification)returnnotificationspop()使已收集的結果不會在下一輪再次注入這是一個最小的進程內去重。但它沒有持久化 acknowledgement如果已從字典刪除還沒把消息安全寫入會話時進程崩潰通知會丟失。反過來若先寫消息后標記已消費中間崩潰又可能重復注入。兩次交接的完整時序是assistant 提出慢工具調用Harness 創建bg_id立即回填占位 tool result模型繼續處理快操作worker 在后臺完成后寫入結果下一次收集時再以新的 user message 把 observation 送回模型。這條時序暴露了教學實現的一個重要缺口collect_background_results()只在某輪工具處理后執行。若后臺命令在 Agent 已經返回純文本并退出循環后才完成且之后沒有新用戶輸入前臺不會被自動喚醒通知只能留在字典里等下一次交互。第 13 篇實現了“后臺完成后可在后續輪次看見”還沒實現“完成事件立即主動喚醒 Agent”。增量接回主循環的位置只有兩處執行前用should_run_background()選擇同步或后臺一批 tool result 回填后調用collect_background_results()有完成項時追加 user notification。Tool Schema、Task System、Prompt 組裝和chat_completion()都不需要重寫。forblockinmessage.tool_calls:nameblock.function.name argumentsjson.loads(block.function.arguments)ifshould_run_background(name,arguments):bg_idstart_background_task(block)output(f[Background task{bg_id}started] Result will be available when complete.)else:outputexecute_tool(block)messages.append({role:tool,tool_call_id:block.id,content:str(output),})notificationscollect_background_results()ifnotifications:messages.append({role:user,content:\n\n.join(notifications),})在六層代碼地圖中第 13 篇的 worker 和結果存儲屬于持久運行層同步/后臺分支位于工具執行層通知則最終接回循環與狀態層的messages。按這張代碼坐標讀完整文件時可以先定位agent_loop()的分支和回填再追蹤start_background_task()如何寫兩個字典最后看collect_background_results()如何刪除已消費項。這條路徑比從文件第一行開始逐個復習 Task、Memory 和 Prompt 更容易看見本篇增量。代碼地圖也說明了為什么本篇沒有重寫模型交互層后臺機制改變的是工具結果“何時可用”并沒有改變 assistant 如何提出 tool call。穩定的消息協議讓新增能力集中在 Harness 內部如果為了后臺執行發明新的模型 role 或跳過原調用配對局部異步會反過來污染整條對話鏈。四、一組線程和字典為什么還不是可靠任務隊列在不配置 API、不調用模型端點的邊界下仍可以沿本地控制流推演一條正常路徑時刻Harness 動作background_tasksmessages新增內容T0收到慢 Bash tool call無assistant tool callT1創建bg_0001并啟動 workerrunning占位 tool resultT2Agent 處理快工具running其他 tool resultT3worker 寫入輸出completed暫無T4collector 取出結果記錄被popusertask_notificationT5再次調用模型無新 observation 可見這項靜態推演能證明占位結果與完成通知分屬兩個時刻也能證明原 tool call 只配對一次。它不能證明實際模型一定會選擇后臺參數不能證明命令在進程崩潰后會恢復也不能證明多進程下狀態一致。當兩個后臺工作幾乎同時完成時當前代碼按background_tasks的字典遍歷順序收集不一定保留真實完成時間順序。如果業務必須先處理最早完成的事件應保存completed_at或使用 FIFO queuePythonqueue.Queue已經實現了線程間交換所需的鎖語義。當前教學實現還有八類必須公開的缺口。進程退出會中斷工作。daemon thread 不保證清理與結果寫回后臺狀態也沒有持久化。沒有取消和超時管理。run_bash()有單次 subprocess 超時卻沒有對 background job 暴露 cancel、deadline 或進程組終止。沒有容量上限。每個慢調用都創建新線程缺少并發數、隊列長度、CPU、內存和子進程限制。輸出可能丟失。collector 只把前 200 個字符注入通知完整結果被pop后沒有持久可查的 artifact 地址。通知沒有確認機制。結果從存儲移到messages的過程不是事務崩潰可導致丟失或重復。完成不會立即喚醒前臺。collector 依賴后續循環沒有 event loop、消息隊列消費者或獨立喚醒器。缺少冪等與副作用語義。進程無法確認慢命令是未執行、執行中還是已執行但未回報盲目重試可能重復部署或重復寫數據。線程安全不等于多進程安全。threading.Lock只協調當前進程的線程其他進程、機器和重啟后 worker 都看不到這把鎖。閱讀下圖時應把左側每一種失敗都對應到一個缺失的持久事實任務是否被可靠接收、由誰持有租約、是否允許重試、結果是否已經交付、同一副作用是否執行過。只增加更多線程無法補齊這些事實反而會擴大并發窗口。這些風險的共同根因是當前方案只將“等待時間”移出前臺還沒有將任務交給可持久、可重試、可確認的執行系統。生產架構至少需要 durable queue、worker lease、heartbeat、冪等鍵、取消信號、完整 artifact 存儲和 delivery acknowledgement。Task repository 保存業務目標job queue 保存執行實例notification channel 保存可重放事件三者不應只用兩個字典模擬。第 13 篇的完整增量可以壓縮成一條鏈should_run_background()選擇執行策略start_background_task()創建線程并返回占位結果worker 寫入完成狀態collect_background_results()將結果包裝為新 observationagent_loop()再把通知接回messages。原 Tool Schema 與 dispatch map 保持穩定只在 Bash 參數與執行策略上增加一個交點。不過它仍然只能處理“現在已經發起的慢操作”。如果需要每天 09:00 自動檢查構建或在兩小時后重新查詢某個 Task當前 Harness 沒有時間規則、持久計劃和主動喚醒鏈。第 14 篇將在后臺執行之上增加 CronJob、scheduler、queue processor 和一次性/周期性觸發。