Lesson 08 · 并发

让活自己跑,agent 不阻塞

「Fire and forget — the agent doesn't block while the command runs.」

⏱ 약 10 분 · 📝 3 개 인터랙티브 컴포넌트 · 🧑‍💻 기반 shareAI-lab · s08_background_tasks.py

阻塞式调用的痛

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 点」汇合。

哪些命令应该放后台?

并不是所有命令都该扔后台。判据两个:

  1. 耗时:几秒以内的同步跑更简单,省得维护队列。
  2. 结果重要程度:如果下一步紧接着要用这个结果(比如 cat file.txt 之后立刻 grep),后台没意义——你还是得等它。
Interactive

Widget 1 · Timeline · 主线程 + 2 条 bg 线程

点 Spawn 给 bg 线程派活。主线程每 tick 会检查 notification queue。看三条 swim lane 如何交织。

🧠 Main (agent loop)
⚙ Background thread A
(idle)
⚙ Background thread B
(idle)
queue: []
Interactive

Widget 2 · Fire & Forget 判断 · 8 个命令,哪些值得后台?

每个命令选 foreground(同步等)或 background(异步 spawn)。注意思考「要不要等结果」+「耗时」这两个维度。

答对 0 / 8
Interactive

Widget 3 · Drain Timing · 通知在哪一轮被看到

关键规则:主线程在每次 LLM 调用前 drain 队列。给你 5 个场景,答 bg 任务完成后,结果会在第几轮进入 agent 视野。

答对 0 / 5