Python 复刻:agents/s08_background_tasks.py
跑长面板回归、Bootstrap 置换、Placebo 检验这种要等几十秒到几分钟的命令,Claude Code 可以丢到后台跑,自己继续干别的。后台命令跑完时它会收到一条通知,回头看结果。
8.1 慢工具的麻烦
有些工具单次执行要等很久。一次 reghdfe 高维固定效应回归在 26441 obs × 3608 firms 的面板上约 0.5 秒。Bootstrap 500 次中介效应抽样约 90 秒。Placebo 500 次随机置换 PC 再跑 reghdfe 约 6–8 分钟。整套 IV1+IV2 联合 Hansen J 加 K-P Wald F(ivreg2 含 partial out 与 first-stage 诊断)约两分钟。这些工具的共同点是:发起命令几乎不花时间,但等结果要几十秒到几分钟。结果是重要的,但不需要立刻看。
如果按普通方式同步等结果,Claude 在等命令结束的几分钟里完全闲着。这段时间它本来可以做其它事——整理上一组回归的三线表、读下一段 docs/01_实证研究设计 的机制章节、规划下一步。同步等待相当于浪费了模型的"思考时间"。
后台任务把这种同步等待替换为异步通知。Claude 发起命令时不等结果,立刻得到一个任务编号,然后继续干别的。等命令真的跑完,结果通过通知机制送回到 Claude 的对话历史里,它在下一轮看到通知就回头看结果。
8.2 一次真实运行的后台任务串
下面是 do10 收尾那次会话里 Placebo 500 次置换从启动到回收的过程。
轮 1:你说"do7_iv_psm 已经跑完了 IV1/IV2/IV3 + PSM 1:1 + Heckman + Change 四个 panel,最后一步 Placebo 500 次置换还没跑,启动后接着把 do9 异质性表汇总成 table9_hetero.csv"。
轮 2:Claude 调用 bg_run 工具,传入命令 stata-mcp do .statamcp/stata-mcp-dofile/do7b_finalize.do(do7b 是 do7 的尾部 Placebo 块,单独抽出便于后台跑)。代码侧立即返回一个任务编号 bg-7a3f,状态 running。这一步在毫秒级完成。
轮 3:Claude 看到任务编号已发出,按你的指令切到下一件事:读 do9_hetero.do 与 06_结果输出/logs/do9_hetero.log,确认 do9 已经把规模 / Lev / 国有 / 上市年限 4 维度的 8 个子样本回归都跑完落 estout,准备做汇总。
轮 4:Claude 调用 mcp__stata-mcp__stata_do 跑一个新的小 do-file 把 8 列 b/se 导出为 table9_hetero.csv。这次工具调用同步等约 3 秒。
轮 5:导出完成。Claude 读 06_结果输出/tables_tsv/table9_hetero.csv 核对一遍,看到规模高 -0.0002、规模低 0.0007、Lev 高 0.0003、Lev 低 0.0006、国有 -0.0001、非国有 0.0009**、上市长 0.0007、上市短 -0.0002。其中只有"非国有"那一列 PC 系数 0.0009** 显著。
轮 6:Claude 跑组间差异检验。又起一次 stata-mcp_do,跑 suest + test 命令做 4 组组间系数 Z 检验。结果落 06_结果输出/tables_tsv/table9_diff.csv,写明 4 维度 p 值。规模 p=0.27、Lev p=0.31、国有 p=0.16、上市年限 p=0.34,组间差异都不显著。
轮 7:差异检验跑完,距离启动 bg-7a3f 已 5 分钟。系统在下一次模型调用之前检查通知队列——队列是空的,Placebo 还在跑。Claude 看到没通知,继续干下一件事:把 table9_hetero.csv 与 table9_diff.csv 合并成异质性章节素材,写一段叙事——"4 维度子样本中仅非国有显著、其它三维度系数方向不一致且组间差异未通过显著性检验"。
轮 11:距离启动 bg-7a3f 已 7 分钟。这次模型调用之前系统检查通知队列:bg-7a3f 状态 done,输出含"Placebo 500 次置换:真实 β=0.0004,|β_perm| ≥ |真实 β| 的次数 0,p_perm=0.000"。系统把这条通知作为一条 system message 插进当前对话历史。
轮 12:Claude 在这一轮的对话历史里看见通知。回头看输出——Placebo 真实 β 大于全部 500 次置换的绝对值,证实主效应不是样本结构性偏差。Claude 调用 Write 落 06_结果输出/tables_tsv/table7_psm_placebo_summary.csv,把 PSM ATE 0.0032***(t=3.976)和 Placebo p_perm=0.000 合并写一份摘要。
轮 13:摘要落盘,stop_reason=end_turn。
Placebo 后台跑的 7 分钟里,主会话推进了 do9 异质性的 8 列汇总、组间差异检验、4 维度叙事撰写——三件事各自完成、各自落盘。如果按同步等待 Placebo 跑完再做这些事,整套时间是 7 + 5 = 12 分钟;用后台异步通知,重叠之后约 7 分钟。
8.3 后台任务的运作
Claude 调用 bg_run 工具时,传入要在后台跑的命令。代码侧的执行函数做这几件事:生成一个 8 位的任务编号、启动一个独立的命令进程跑这个命令、把进程信息记到一份内存表里、立刻把"任务编号 + running"作为结果返给 Claude。这一步在毫秒之内完成,主对话立即获得回应进入下一轮。
代码侧另起一个后台监工——一个轻量的循环,每 5 秒检查一遍那张表里所有 running 进程的状态。某个进程跑完了,监工把它的输出打包成一条通知塞进通知队列。输出可能是上面那次 Placebo 真实 β 与 500 次置换分布的比较,也可能是失败的错误信息——比如 stata-mcp 内存不足崩了、PC 字段名拼错了。
下次 Claude 主对话要发起新一轮模型调用之前,系统先看通知队列里有没有未处理的通知。有就把通知作为一条系统消息加进当前对话历史。Claude 在这一轮看到通知就知道某后台任务完成了,可以调一次 bg_check 工具读详细 log,或者直接根据通知里的概要决定下一步。
8.4 为什么这样设计
主对话必须按顺序运行,这是不能让步的——模型每一轮的判断依赖前一轮的对话历史。让主对话并行会撕裂对话上下文,模型分不清自己在哪条任务线上。
但工具调用本身可以异步。工具的执行与主对话是松耦合的,只要工具完成后能把结果送回对话历史,工具执行过程不必让主对话停下来等。改变的只是工具发起与结果回到对话之间的时间间隔。
异步通知还能让 Claude 同时跑多个慢任务。多个 bg_run 同时发起后总等待时间约等于最慢的那一段,不会累加。实证场景里 Placebo 500 次置换、Bootstrap 中介效应、IV1+IV2 联合 Hansen J 三个任务可以同时丢后台,主对话在最慢那一段结束前就已经开始处理已完成任务的结果。
8.5 容易踩的坑
通知注入只发生在下次模型调用之前。如果主对话在所有后台任务跑完之前就空闲了——你不再输入指令,通知会等到下次你说话时才注入。这通常不是问题,但若你期望通知立即触发某动作(例如 Placebo 跑完自动发送邮件提醒),需要额外的轮询机制。
后台任务的输出有时会很大。一次 Placebo 500 次置换 Stata log 可能有 3000–5000 行(每轮 reghdfe 都打一遍 iteration)。监工把完整输出存在那条通知对象上,长时间累积多个大输出任务会占内存。Claude 读完通知后通常只摘要保留几行——比如那次 Placebo 只在对话历史里留下"500 次置换 p_perm=0.000"这一行,3000 行 log 丢掉。
后台任务无法被主对话中途中断。要终止某个后台任务需要显式调用 bg_kill 工具,或者退出 Claude Code 让监工销毁所有后台进程。模型本身不会主动终止后台任务,除非用户明确指示。
8.6 知识地图
| 关键词 | 含义 | 容易误会的点 |
|---|---|---|
| bg_run | 发起后台命令的工具,立即返回任务编号 | 后台跑的是子命令,不是另一个 Claude |
| 后台监工 | 每 5 秒检查所有后台任务状态的循环 | 是简单的轮询模式,不需要复杂的异步框架 |
| 通知队列 | 任务完成时结果塞队列,下次 LLM 调用前取出 | 是拉取模式;Claude 主动检查时才看到通知,不会被推送唤醒 |
| 主对话并行 | 后台跑的同时主对话继续推进 | 主对话依然顺序,只是工具调用本身异步 |