Background Tasks
之前 Agent 执行命令时只能傻等——npm install 跑 3 分钟,Agent 干坐 3 分钟。
s08 让 Agent 会说:"你先跑着,我做点别的,你跑完了告诉我。"
问题:有些命令要跑很久,Agent 只能干等
s01-s07 的所有工具都是同步阻塞的——Agent 调 bash "npm install",然后什么也不做,等 npm 跑完(可能 3 分钟),拿到输出,继续下一步。这 3 分钟里 Agent 完全闲置。
三个浪费场景
场景一:用户说"装依赖,顺便创建配置文件"。Agent 先 npm install(3 分钟),等完了再创建文件(5 秒)。明明可以同时做——npm 在后台跑,Agent 同时创建文件。
场景二:Agent 想跑测试套件(2 分钟)。它只能在测试跑完后再阅读输出决定下一步。但测试跑的时候 Agent 的脑子是完全空的——它在等 subprocess 返回。
场景三:Agent 同时需要装前端依赖和后端依赖。s07 的 Agent 只能一个一个来——先 cd frontend && npm install,等完再 cd backend && pip install。明明两个互不依赖,完全可以同时跑。
🍳 生活类比
s01-s07 的 Agent 就像一个厨师,每次只能做一件事:烧水的时候站在灶台前等着,水开了才能去切菜。
s08 的 Agent 像一个真正的厨师:把水烧上(后台任务),同时去切菜(继续工作)。水开了,水壶叫一声(通知),厨师回来看一眼,继续做菜。
解决方案:Fire and Forget(发射后不管)
s08 让 Agent 能把慢命令丢到后台线程去跑,自己立刻继续做别的事。命令跑完后,结果通过一条通知消息"塞回"对话。
① 发射:background_run 启动线程,立即返回一个任务编号
② 不管:Agent 继续工作,不等待,不轮询
③ 通知:后台线程完成后,把结果放入通知队列。Agent 下一次调 LLM 前,"排空"通知队列——所有完成的后台任务结果被注入对话
案例对比:有后台 vs 没后台,同一个任务差多少?
任务:"安装 npm 依赖,同时创建 src/index.js 和 README.md,然后跑一下测试"
❌ 没有后台任务(s07 及之前)
bash "npm install" → 等 3 分钟 → 完成 → write_file "src/index.js" → write_file "README.md" → bash "npm test" → 等 1 分钟 → 完成。
总耗时:约 4 分钟。Agent 闲置了约 4 分钟。
✅ 有后台任务(s08)
background_run "npm install" → 立即返回 → write_file "src/index.js"(同时 npm 在后台跑)→ write_file "README.md"(npm 可能还在跑)→ npm 完成通知到达 → background_run "npm test" → Agent 继续分析...→ 测试完成通知到达。
总耗时:约 3 分钟(两个安装命令的时间重叠了 Agent 的文件创建操作)。Agent 没有闲置——它在后台任务运行期间做了有价值的文件创建和分析工作。
BackgroundManager:线程 + 队列 + 锁
BackgroundManager 是 s08 的核心类,只有约 50 行代码,但它引入了 harness 工程中三个新概念:线程、通知队列、线程锁。一个一个看。
初始化:三样东西
为什么需要线程锁? 因为两个线程会同时访问通知队列——后台线程往里面加数据(完成通知),主线程从里面取数据(排空)。如果同时读写,数据可能损坏。锁保证同一时刻只有一个线程在操作队列。
run():启动线程,立即返回
守护线程(daemon=True):如果主程序退出(用户按了 Ctrl+C),后台线程自动被杀死。不会出现"主程序退出了但后台线程还在跑"的诡异情况。
_execute():在线程里跑命令,完成后推送通知
通知管道:后台结果怎么"回到"对话?
这是 s08 最巧妙的设计。后台任务完成后,结果不是"弹窗"或"系统提醒"——而是作为一条 user 消息被悄悄塞进对话。和 s03 的 nag reminder、s05 的 skill 内容一样的注入方式。
drain_notifications():取走所有通知,清空队列
为什么叫 "drain"(排空)? 因为这个方法把队列里所有积压的通知一次性取走。如果 Agent 连续工作了 3 轮没有检查通知,这期间可能有 2 个后台任务完成了——drain 会把两个通知都拿走,让 LLM 在下一轮调用前看到所有新信息。
注入到对话:每次 LLM 调用前
通知不是弹窗,不是系统警告。它是作为一条 user 消息,以 <background-results> 标签包裹,悄悄地出现在 messages 末尾:
LLM 看到的是一条普通对话消息。它自然地响应:"好的,npm install 完成了,安装了 142 个包。现在我来继续..."——Agent 不需要"轮询"后台状态。它被被动通知。
两个新工具:background_run + check_background
background_run:发射后台任务
Agent 调用 background_run(command="npm install") → 线程启动,立即返回任务编号。Agent 不需要等——它可以接着做别的事。
check_background:查看后台任务状态
Agent 可以随时问"我派出去的那个任务跑完了吗?"——调 check_background(task_id="a1b2c3d4") 看一个任务,或调 check_background()(不传参数)看所有任务。
大多数时候 Agent 不需要主动 check——通知会自动到达。但 check 给了 Agent 一种主动性:"我等不及了,让我看看跑到哪了"。
dispatch map:又是两行
和 s02 学的模式一模一样——加工具 = 加 handler + 加 schema + 字典加一行。从 s02 到 s08,这个模式一次都没变过。
Agent Loop 集成:通知注入的位置很关键
agent_loop 中通知注入的时机
通知在每次 LLM 调用之前被 drain。这保证了 LLM 在思考下一步之前,能看到所有已完成的后台任务结果。
为什么在 LLM 调用前而不是调用后? 如果在调用后 drain,LLM 在那一轮就看不到已完成的结果——它要多等一轮才能对结果做出反应。在调用前 drain = LLM 第一时间知道结果。
check_background 的特殊处理:它不在通知管道中
check_background 走的是普通工具 dispatch——LLM 调用它,handler 返回结果,作为 tool_result 注入。这和 background_run 的通知管道是两条不同的信息路径:
通知管道(被动):后台完成 → 通知队列 → drain → user 消息 → LLM 看到
主动查询(主动):LLM → check_background → handler → tool_result → LLM 看到
两条路径互不干扰,服务于不同的使用场景。
从 s08 带走的三个核心认识
"不等"比"等得快"更好
不是让命令跑得更快,而是让 Agent 在命令跑的时候不闲着。后台执行把"等待时间"变成了"生产时间"。
通知优于轮询
Agent 不需要反复问"完了吗?"。后台完成后自动推送通知。注入方式和 tool_result 完全一样——不搞特殊通道。
线程是杠杆,锁是安全带
几行 threading 代码换来真正的并发执行。但共享数据必须加锁——多线程编程的第一条规则。s08 用最少的锁(只在队列操作时)换取最大的安全。
落地案例:后台任务在真实 AI 产品中的应用
📦 Claude Code 的后台执行
Claude Code 在执行 npm install、pip install、docker build 等耗时命令时,内部就是用的后台线程模式。你会看到它"同时"做多件事——其实不是真正的并行 LLM 调用,而是命令在后台跑,LLM 继续思考其他任务。
🏗️ CI/CD Pipeline 的并行 Stage
Jenkins/GitHub Actions 中,不同 job 在不同 runner 上并行执行。s08 的后台线程就是这个模式的单机版——同一个 Agent 进程内,不同线程并行执行不同命令。
🤖 多 Agent 系统的"任务执行器"
s09 的多 Agent 团队中,每个 Agent 派发的"执行任务"其实就是 background_run。Agent A 说"帮我跑这个测试"→ 后台线程启动 → Agent A 继续做别的事 → 测试完成通知到达。
s08 给了单个 Agent 并行执行的能力。
s09 将引入多个 Agent 通过邮箱通信协作——
每个 Agent 可以把自己的重活丢到后台,继续和其他 Agent 协调。