Files
analysis_claude_code/s13_background_tasks/README.zh.md
2026-07-31 03:54:12 +08:00

217 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# s13: Background Tasks — 慢操作放后台
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → s11 → s12 → `s13` → [s14](../s14_cron_scheduler/) → s15 → ... → s20 → s21
> *"慢操作丢后台, agent 继续处理"* — 后台线程跑命令, 完成后注入通知。
>
> **Harness 层**: 后台 — 异步执行, 不阻塞主循环。
---
## 问题
你用过洗衣机吗把衣服扔进去按下启动然后去做饭、回消息或看论文。30 分钟后洗衣机"滴滴滴"提醒你:好了。你不会站在洗衣机前面干等 30 分钟。
Agent 的 bash 工具也一样。`pip install torch` 要 10 分钟,`npm run build` 要 3 分钟。这些命令一跑Agent 就在等 bash 工具返回,没法利用这段时间处理别的任务。
读文件是毫秒级,不等。`git status` 一秒内返回,不等。但 `npm install`分钟级。Agent 等 10 分钟什么都不做,而 LLM 按 token 计费,空转就是浪费。
---
## 解决方案
![Background Tasks Overview](images/background-tasks-overview.svg)
本章把慢操作放入后台线程Agent 继续运行循环;任务完成后,结果以通知形式注入对话。
同步 vs 后台:
| | 同步 (s12) | 后台 (s13) |
|---|---|---|
| 慢操作 | Agent 干等 | 后台线程执行 |
| Agent 空闲 | 是 | 否,继续处理 |
| 结果 | 立即返回 | 下轮注入通知 |
| 判断标准 | — | `run_in_background` 参数(模型显式请求),启发式兜底 |
---
## 工作原理
### should_run_background: 显式请求优先,启发式兜底
模型通过 bash 工具的 `run_in_background` 参数显式请求后台执行。如果模型没有指定,则使用关键词启发式判断:
```python
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
"""Fallback heuristic: commands likely to take > 30s."""
if tool_name != "bash":
return False
cmd = tool_input.get("command", "").lower()
slow_keywords = ["install", "build", "test", "deploy", "compile",
"docker build", "pip install", "npm install",
"cargo build", "pytest", "make"]
return any(kw in cmd for kw in slow_keywords)
def should_run_background(tool_name: str, tool_input: dict) -> bool:
"""Model explicit request takes priority; fallback to heuristic."""
if tool_input.get("run_in_background"):
return True
return is_slow_operation(tool_name, tool_input)
```
### start_background_task: 后台执行与生命周期
把工具调用包装成 worker 函数,扔到 daemon 线程里执行。每个后台任务有唯一 ID状态存在 `background_tasks` 字典里:
```python
_bg_counter = 0
background_tasks: dict[str, dict] = {} # bg_id → {tool_use_id, command, status}
background_results: dict[str, str] = {} # bg_id → output
background_lock = threading.Lock()
def start_background_task(block) -> str:
"""Run tool in a daemon thread. Returns background task ID."""
global _bg_counter
_bg_counter += 1
bg_id = f"bg_{_bg_counter:04d}"
def worker():
result = execute_tool(block)
with background_lock:
background_tasks[bg_id]["status"] = "completed"
background_results[bg_id] = result
with background_lock:
background_tasks[bg_id] = {
"tool_use_id": block.id,
"command": block.input.get("command", ""),
"status": "running",
}
thread = threading.Thread(target=worker, daemon=True)
thread.start()
return bg_id
```
`start_background_task()` 返回 `bg_id``daemon=True` 确保 Agent 进程退出时线程一起退出。
### collect_background_results: 通知收集
后台任务完成后,收集结果并格式化为 `<task_notification>` 通知:
```python
def collect_background_results() -> list[str]:
"""Collect completed results as task_notification messages."""
with background_lock:
ready_ids = [bid for bid, task in background_tasks.items()
if task["status"] == "completed"]
notifications = []
for bg_id in ready_ids:
with background_lock:
task = background_tasks.pop(bg_id)
output = background_results.pop(bg_id, "")
notifications.append(
f"<task_notification>\n"
f" <task_id>{bg_id}</task_id>\n"
f" <status>completed</status>\n"
f" <command>{task['command']}</command>\n"
f" <summary>{output[:200]}</summary>\n"
f"</task_notification>")
return notifications
```
通知不复用原始 `tool_use_id`。原始 tool call 已经用占位 `tool_result` 回复了,后台完成是独立事件,用 `task_notification` 格式注入。这符合 Messages API 的工具配对语义:一个 `tool_use` 只对应一个 `tool_result`
### 循环中的集成
agent_loop 里,工具执行分两条路,通知和结果合并为一条 user 消息:
```python
results = []
for block in response.content:
if block.type != "tool_use":
continue
if should_run_background(block.name, block.input):
bg_id = start_background_task(block)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": f"[Background task {bg_id} started] "
f"Result will be available when complete."})
else:
output = execute_tool(block)
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
# 通知和工具结果合入同一条 user 消息
user_content = []
bg_notifications = collect_background_results()
if bg_notifications:
for notif in bg_notifications:
user_content.append({"type": "text", "text": notif})
user_content.extend(results)
messages.append({"role": "user", "content": user_content})
```
慢操作先回一个带 `bg_id` 的占位 tool_resultLLM 知道这个命令还在跑,可以先做别的事。后台完成后,通知作为独立 text block 和当前轮的 tool_result 一起组成 user 消息。
### 合起来跑
```
Turn 1:
LLM → bash "npm install" (run_in_background=true)
→ start_background_task → bg_0001
→ tool_result: "[Background task bg_0001 started]..."
→ LLM: "OK, I'll check later. Let me also read the config."
Turn 2:
LLM → read_file "package.json" (fast, sync)
→ tool_result: file content
→ collect: bg_0001 done! inject <task_notification>
→ LLM sees: config file + install notification in one message
```
Agent 没干等npm install 跑后台的时候,它去读了配置文件。
---
## 相对 s12 的变更
| 组件 | 之前 (s12) | 之后 (s13) |
|------|-----------|-----------|
| 执行模型 | 全部同步 | 慢操作后台线程 + 通知注入 |
| bash schema | `command` | `command` + `run_in_background` |
| 新函数 | — | `should_run_background`, `is_slow_operation`, `start_background_task`, `collect_background_results` |
| 新类型 | — | `background_tasks: dict`, `background_results: dict`, `background_lock: Lock` |
| 通知格式 | — | `<task_notification>`(不复用 tool_use_id |
| 循环行为 | 工具串行执行 | 慢操作异步,快操作同步,通知每轮收集 |
| 工具 | 8 (s12) | 8不变执行策略变了 |
---
## 试一下
```sh
cd learn-claude-code
python s13_background_tasks/code.py
```
试试这些 prompt
1. `Run pip list in the background and find all Python files in this directory`
2. `Run npm install (use run_in_background) and while waiting, read package.json`
3. `Create a task to setup the project, then run pip list in the background`
观察重点:慢操作有没有被送到后台?`bg_id` 是否返回?后台通知有没有以 `<task_notification>` 格式注入?
---
## 接下来
后台任务解决了"慢操作不阻塞"。但如果想定时做某件事呢?比如"每天早上 9 点跑测试"、"每 5 分钟检查一次服务器状态"。
s14 Cron Scheduler → 给 Agent 装一个闹钟。
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->