一边等程序,一边把另一件事做完
“后台运行 main.py,等待时先读 expected.txt,最后报告结果。”
如果工具一直等到 main.py 退出才返回,主循环就卡在这一步。明明只需要读一个预期文件,也得排在程序后面。把工具移到后台,想解决的就是这段先后关系:启动和完成分开交接,循环可以先做不依赖结果的事。
但读到 expected.txt 里的 42,并不表示程序也输出了 42。后台化让两件事可以交错发生,没有取消最后核对真实结果的必要。
请求是:后台运行main.py,等待时先读expected.txt。读取预期不依赖程序输出,可以先做;最终核对仍要等本次结果。
- 用户要等程序结果,主循环却不用一直等:请求是:后台运行main.py,等待时先读expected.txt。读取预期不依赖程序输出,可以先做;最终核对仍要等本次结果。
- 获准后启动,先交一张收据:主循环收到bg-1 / started,保存为原工具调用的结果。此时程序可能还在运行,不能回答已经得到了42。
- 继续独立工作,先读预期文件:主循环拿到expected.txt里的42。图中这次流转表示控制权回到循环后继续工作,后台程序没有调用读取工具。
- 下一轮开始前,检查后台是否结束:程序完成后,宿主把退出码与输出固定为bg-1的结果。若仍未结束就跳过,不等待,也不为轮询而消耗模型调用。
- 把最终结果交给下一轮模型:收据已经配对完毕,完成信息另作普通文本通知。模型现在才能把真实输出与预期核对;如果模型早已停下,新结果先交给宿主,不能假装模型见过。
前两篇任务系统里的 run,是持久工作记录;这里的 bg-1,是当前会话里的进程登记。前者回答“这件工作由谁做、依赖谁”,后者回答“这个程序还在运行吗、退出结果是什么”。后台进程结束,也不会替你把任务图里的 run 标成 completed。
收据到底证明了什么?
原来的一次工具调用可以直接交回退出码和输出。现在,工具调用先返回一份 started 收据,告诉循环程序已启动,以及以后用哪个后台 ID 对照结果。
| 交接 | 传递的内容 | 现在能据此判断什么 |
|---|---|---|
| 模型 → Harness | run_background,参数为空 | 模型提出了启动请求,尚未执行 |
| Harness → 原循环 | bg-1,started | 启动接口已返回,尚不知道最终输出 |
| 程序 → 宿主登记 | 退出码、输出或超时 | 这次运行已结束,但可能失败 |
| 宿主 → 下一轮模型 | 带 bg-1 的文本通知 | 模型这次才拿到可供核对的结果 |
启动本身也可能失败:文件目录不对、系统无法创建进程,都不应该被包装成“后台已经开始”。没有许可的请求更应在启动前停下。后台只是执行方式,不能成为权限检查的旁门。
结果什么时候插进来?
最容易出错的写法,是后台工作者一结束就直接往 messages 里加一项。此时主循环可能正在保存另一组工具结果,也可能已经发出模型请求。读者很难再说清:模型这一轮到底看见了哪些内容?
本课让后台进程对象只提供状态,历史仍由主循环在下一轮开始前提交。检查时,未结束就立即返回;已经结束就生成通知,连同当前历史作为候选交回原循环。模型请求读到的是一个完整快照。
这也意味着,程序在模型请求发出之后才结束,当前请求看不到它。等下一个边界再接入即可,没有必要为了抢这几毫秒去修改已经发出的请求。
原工具调用已经收到 started 收据,完成信息不能再伪装成同一 ID 的第二份 tool_result。本例使用普通 user 文本通知,内容是宿主编码的 JSON,标注后台 ID、原调用 ID、退出状态与输出;它是一种教学约定,不是新的 API 消息类型。程序输出仍是待核对的数据,不能因为带了宿主标记就升级成指令。
如果模型先说完了呢?
程序需要三秒,模型却很快读完 expected.txt,回答“程序还在运行”。这时旧循环已经以 answered 停下。后台程序稍后结束,不会让已经返回的模型响应自动长出一个结论。
本课把新结果交给宿主展示,同时说明它尚未交给模型。宿主可以在新的交互中继续处理,或者另行设计受预算约束的恢复入口;当前示例不会暗中重启模型。等待期间也不会不断问模型“现在好了吗”。单纯等待增加的是耗时;额外轮询模型才增加请求,不能把二者混为一谈。
退出时还要回收本次启动的程序,不能只丢掉 Python 变量。本课只运行可信、没有后代进程的固定 main.py,使用超时和显式 kill/wait 管理直接子进程。重启恢复、进程树控制和分布式队列都超出了这个最小例子。
先把三个边界写清楚
实践篇把机制拆成 start_background、collect_background、prepare_background 三个函数:第一个回答“有没有真的启动”,第二个回答“有没有已经结束的结果”,第三个回答“这份结果有没有进入历史”。三个问题不能用一个 running 布尔值替代。
参考课程的 Background Tasks 使用后台线程登记与完成通知。本课独立实现固定程序的非阻塞检查,保留相同的交接问题,便于逐行观察。Claude Code 的官方交互文档也描述了后台运行与输出取回,但本文代码不是其内部实现的源码还原。