Files
analysis_claude_code/s13_background_tasks/README.zh.md
2026-08-11 15:13:13 +08:00

8.6 KiB
Raw Blame History

s13: Background Tasks — 慢操作放后台

English · 中文 · 日本語

s01 → ... → s11 → s12 → s13s14 → s15 → ... → s18 → s19

"慢操作丢后台, 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

本章把慢操作放入后台线程Agent 继续运行循环;任务完成后,结果以通知形式注入对话。

同步 vs 后台:

同步 (s12) 后台 (s13)
慢操作 Agent 干等 后台线程执行
Agent 空闲 否,继续处理
结果 立即返回 下轮注入通知
判断标准 bash 的 run_in_background 参数,启发式兜底

工作原理

should_run_background: 显式请求优先,启发式兜底

模型通过 bash 工具的 run_in_background 参数显式请求后台执行。如果模型没有指定,则使用关键词启发式判断。只有 bash 会进入这条路径,其他工具仍按原来的参数规则校验和执行。

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_name != "bash":
        return False
    if tool_input.get("run_in_background") is True:
        return True
    return is_slow_operation(tool_name, tool_input)

start_background_task: 后台执行与生命周期

把工具调用包装成 worker 函数,扔到 daemon 线程里执行。每个后台任务有唯一 ID状态存在 background_tasks 字典里:

_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():
        try:
            output, exit_code = _run_bash_process(block.input["command"])
            status = "completed" if exit_code == 0 else "failed"
            result = _format_bash_result(output, exit_code)
        except Exception as exc:
            status, result = "failed", f"Error: {exc}"
        with background_lock:
            background_tasks[bg_id]["status"] = status
            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。命令以非零状态退出或 worker 抛出异常时,任务会进入 failed不会再被写成成功完成。Shell 会在独立的进程组中启动;命令完成、超时,或 Agent 经正常路径、SIGTERM 退出时,运行时会停止原进程组。这只是生命周期清理,并不是沙箱;另建 session 的进程仍可能离开该进程组。

collect_background_results: 通知收集

后台任务完成后,收集结果并格式化为 <task_notification> 通知:

def collect_background_results() -> list[str]:
    """Collect terminal results as task_notification messages."""
    with background_lock:
        ready_ids = [bid for bid, task in background_tasks.items()
                     if task["status"] in ("completed", "failed")]
    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>{task['status']}</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 消息:

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不变执行策略变了

试一下

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 装一个闹钟。