Background Tasks
Task RuntimeBackground Execution Lanes|287 LOC|6 tools
Background execution is a runtime lane, not a second main loop.
s00 > s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > [ s13 ] > s14 > s15 > s16 > s17 > s18 > s19
慢命令可以在旁邊等,主迴圈不必陪著發呆。
這一章要解決什麼問題
前面幾章裡,工具呼叫基本都是:
模型發起
->
立刻執行
->
立刻返回結果
這對短命令沒有問題。
但一旦遇到這些慢操作,就會卡住:
npm installpytestdocker build- 大型程式碼生成或檢查任務
如果主迴圈一直同步等待,會出現兩個壞處:
- 模型在等待期間什麼都做不了
- 使用者明明還想繼續別的工作,卻被整輪流程堵住
所以這一章要解決的是:
把“慢執行”移到後臺,讓主迴圈繼續推進別的事情。
建議聯讀
- 如果你還沒有徹底穩住“任務目標”和“執行槽位”是兩層物件,先看
s13a-runtime-task-model.md。 - 如果你開始分不清哪些狀態該落在
RuntimeTaskRecord、哪些還應留在任務板,回看data-structures.md。 - 如果你開始把後臺執行理解成“另一條主迴圈”,先看
s02b-tool-execution-runtime.md,重新校正“並行的是執行與等待,不是主迴圈本身”。
先把幾個詞講明白
什麼叫前臺
前臺指的是:
主迴圈這輪發起以後,必須立刻等待結果的執行路徑。
什麼叫後臺
後臺不是神秘系統。
後臺只是說:
命令先在另一條執行線上跑,主迴圈先去做別的事。
什麼叫通知佇列
通知佇列就是一條“稍後再告訴主迴圈”的收件箱。
後臺任務完成以後,不是直接把全文硬塞回模型,
而是先寫一條摘要通知,等下一輪再統一帶回去。
最小心智模型
這一章最關鍵的句子是:
主迴圈仍然只有一條,並行的是等待,不是主迴圈本身。
可以把結構畫成這樣:
主迴圈
|
+-- background_run("pytest")
| -> 立刻返回 task_id
|
+-- 繼續別的工作
|
+-- 下一輪模型呼叫前
-> drain_notifications()
-> 把摘要注入 messages
後臺執行線
|
+-- 真正執行 pytest
+-- 完成後寫入通知佇列
如果讀者能牢牢記住這張圖,後面擴充套件成更復雜的非同步系統也不會亂。
關鍵資料結構
1. RuntimeTaskRecord
task = {
"id": "a1b2c3d4",
"command": "pytest",
"status": "running",
"started_at": 1710000000.0,
"result_preview": "",
"output_file": "",
}
這些欄位分別表示:
id:唯一標識command:正在跑什麼命令status:執行中、完成、失敗、超時started_at:什麼時候開始result_preview:先給模型看的簡短摘要output_file:完整輸出寫到了哪裡
教學版再往前走一步時,建議把它直接落成兩份檔案:
.runtime-tasks/
a1b2c3d4.json # RuntimeTaskRecord
a1b2c3d4.log # 完整輸出
這樣讀者會更容易理解:
json記錄的是執行狀態log儲存的是完整產物- 通知只負責把
preview帶回主迴圈
2. Notification
notification = {
"type": "background_completed",
"task_id": "a1b2c3d4",
"status": "completed",
"preview": "tests passed",
}
通知只負責做一件事:
告訴主迴圈“有結果回來了,你要不要看”。
它不是完整日誌本體。
最小實現
第一步:登記後臺任務
class BackgroundManager:
def __init__(self):
self.tasks = {}
self.notifications = []
self.lock = threading.Lock()
這裡最少要有兩塊狀態:
tasks:當前有哪些後臺任務notifications:哪些結果已經回來,等待主迴圈領取
第二步:啟動後臺執行線
“執行緒”這個詞第一次見可能會有點緊張。
你可以先把它理解成:
同一個程式裡,另一條可以獨立往前跑的執行線。
def run(self, command: str) -> str:
task_id = new_id()
self.tasks[task_id] = {
"id": task_id,
"command": command,
"status": "running",
}
thread = threading.Thread(
target=self._execute,
args=(task_id, command),
daemon=True,
)
thread.start()
return task_id
這一步最重要的不是執行緒本身,而是:
主迴圈拿到 task_id 後就可以先繼續往前走。
第三步:完成後寫通知
def _execute(self, task_id: str, command: str):
try:
result = subprocess.run(..., timeout=300)
status = "completed"
preview = (result.stdout + result.stderr)[:500]
except subprocess.TimeoutExpired:
status = "timeout"
preview = "command timed out"
with self.lock:
self.tasks[task_id]["status"] = status
self.notifications.append({
"type": "background_completed",
"task_id": task_id,
"status": status,
"preview": preview,
})
這裡體現的思想很重要:
後臺執行負責產出結果,通知佇列負責把結果送回主迴圈。
第四步:下一輪前排空通知
def before_model_call(messages: list):
notifications = bg.drain_notifications()
if not notifications:
return
text = "\n".join(
f"[bg:{n['task_id']}] {n['status']} - {n['preview']}"
for n in notifications
)
messages.append({"role": "user", "content": text})
這樣模型在下一輪就會知道:
- 哪個後臺任務完成了
- 是成功、失敗還是超時
- 如果要看全文,該再去讀檔案
為什麼完整輸出不要直接塞回 prompt
這是本章必須講透的點。
如果後臺任務輸出幾萬行日誌,你不能每次都把全文塞回上下文。
更穩的做法是:
- 完整輸出寫磁碟
- 通知裡只放簡短摘要
- 模型真的要看全文時,再呼叫
read_file
這背後的心智很重要:
通知負責提醒,檔案負責存原文。
如何接到主迴圈裡
從 s13 開始,主迴圈多出一個標準前置步驟:
1. 先排空通知佇列
2. 再呼叫模型
3. 普通工具照常同步執行
4. 如果模型呼叫 background_run,就登記後臺任務並立刻返回 task_id
5. 下一輪再把後臺結果帶回模型
教學版最小工具建議先做兩個:
background_runbackground_check
這樣已經足夠支撐最小非同步執行閉環。
這一章和任務系統的邊界
這是本章最容易和 s12 混掉的地方。
s12 的 task 是什麼
s12 裡的 task 是:
工作目標
它關心的是:
- 要做什麼
- 誰依賴誰
- 現在總體進度如何
s13 的 background task 是什麼
本章裡的後臺任務是:
正在執行的執行單元
它關心的是:
- 哪個命令正在跑
- 跑到什麼狀態
- 結果什麼時候回來
所以最穩的記法是:
task更像工作板background task更像執行中的作業
兩者相關,但不是同一個東西。
初學者最容易犯的錯
1. 以為“後臺”就是更復雜的主迴圈
不是。
主迴圈仍然儘量保持單主線。
2. 只開執行緒,不登記狀態
這樣任務一多,你根本不知道:
- 誰還在跑
- 誰已經完成
- 誰失敗了
3. 把長日誌全文塞進上下文
上下文很快就會被撐爆。
4. 把 s12 的工作目標和本章的執行任務混為一談
這會讓後面多 agent 和排程章節全部打結。
教學邊界
這一章只需要先把一個最小執行時模式講清楚:
- 慢工作在後臺跑
- 主迴圈繼續保持單主線
- 結果透過通知路徑在後面回到模型
只要這條模式穩了,執行緒池、更多 worker 型別、更復雜的事件系統都可以後補。
這章真正要讓讀者守住的是:
並行的是等待與執行槽位,不是主迴圈本身。
學完這一章,你應該真正掌握什麼
學完以後,你應該能獨立複述下面幾句話:
- 主迴圈只有一條,並行的是等待,不是主迴圈本身。
- 後臺任務至少需要“任務表 + 通知佇列”兩塊狀態。
background_run應該立刻返回task_id,而不是同步卡住。- 通知只放摘要,完整輸出放檔案。
如果這 4 句話都已經非常清楚,說明你已經掌握了後臺任務系統的核心。
下一章學什麼
這一章解決的是:
慢命令如何在後臺執行。
下一章 s14 要解決的是:
如果連“啟動後臺任務”這件事都不一定由當前使用者觸發,而是由時間觸發,該怎麼做。
也就是從“非同步執行”繼續走向“定時觸發”。