让活自己跑,agent 不阻塞
「Fire and forget — the agent doesn't block while the command runs.」
阻塞式调用的痛
S02 里的 bash 工具是同步的:subprocess.run(..., timeout=120),跑 npm install 这种要 90 秒的命令,整个 agent loop 卡住 90 秒。用户盯着终端,不知道是挂了还是在干活。
s08 的解法:给 agent 一个 background_run 工具。它立刻返回一个 task_id,命令在另一个线程跑。agent 继续循环、做别的事;bg 任务完成时往通知队列里塞结果。
def run(self, command: str) -> str: task_id = str(uuid.uuid4())[:8] self.tasks[task_id] = {"status":"running", ...} thread = threading.Thread(target=self._execute, args=(task_id, command), daemon=True) thread.start() return f"Background task {task_id} started" # 立即返回
结果怎么回到 agent?
关键是一个线程安全的队列:bg 线程完成时往队列 append;主线程每次 LLM 调用前把队列 drain 空,把完成通知塞进 messages 作为 user 消息。
def agent_loop(messages): while True: # Drain bg notifications before each LLM call notifs = BG.drain_notifications() if notifs: messages.append({ "role": "user", "content": f"<background-results>{notif_text}</background-results>", }) response = client.messages.create(...) ...
这样 agent 在第 N 轮 spawn 了一个 bg 任务,第 N+3 轮该任务完成时,下一次 LLM 调用会自动带上结果——模型看到 <background-results> 块就知道:「哦那个任务跑完了,我继续」。
时间线演示
下面 widget 让你模拟:主线程每秒 tick 一下(模拟 agent 循环节拍);你可以随时 spawn bg 任务。看两条线索如何在「drain 点」汇合。
哪些命令应该放后台?
并不是所有命令都该扔后台。判据两个:
- 耗时:几秒以内的同步跑更简单,省得维护队列。
- 结果重要程度:如果下一步紧接着要用这个结果(比如
cat file.txt之后立刻grep),后台没意义——你还是得等它。