如何把慢速 bash 命令丢进后台线程而不阻塞主循环:learn-claude-code s11 的 run_in_background 与结果收集
【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code
在 Agent Loop 里,一条慢速 bash 命令会卡住整个 harness:安装依赖、跑完整测试套件、构建项目动辄几分钟,在命令返回之前,harness 既无法处理当前响应里的下一个工具调用,也无法开始下一轮模型调用。learn-claude-code 的 s11 章节(s11_background_tasks)解决这个问题:模型在 bash 调用上显式声明run_in_background: true,命令交给后台线程执行,当前工具调用立刻返回一个带bg_id的占位结果,主循环继续处理其他工具调用;在后续某一轮开始时,已完成的结果被收集为<task_notification>消息注入对话。本文带你跑通 s11,讲清这条机制的完整路径,并给出验证派发与收集是否生效的方法。
运行 s11 并发出触发后台的提示词
准备环境
s11 的入口是 s11_background_tasks/code.py,它是可独立运行的章节实现,依赖一个 Anthropic 兼容的模型 API。按根 README.md 的 Quick Start 准备:
pip install -r requirements.txt cp .env.example .envrequirements.txt 里有三个依赖:anthropic>=0.25.0、python-dotenv>=1.0.0、pyyaml>=6.0。然后按 .env.example 修改.env:
# API Key (required) ANTHROPIC_API_KEY=sk-ant-xxx # Model ID (required) MODEL_ID=claude-sonnet-4-6 # Base URL (optional, for Anthropic-compatible providers) # ANTHROPIC_BASE_URL=https://api.anthropic.comcode.py 中有两个细节:它调用load_dotenv(override=True)加载环境变量,然后MODEL = os.environ["MODEL_ID"]——MODEL_ID缺失会直接抛KeyError无法启动;如果设置了ANTHROPIC_BASE_URL,代码会清掉ANTHROPIC_AUTH_TOKEN避免干扰。.env.example还列出了 MiniMax、GLM、Kimi、DeepSeek 等兼容供应商的 Base URL 与模型名,可按需替换。
启动 REPL 并发送测试提示词
python s11_background_tasks/code.py在s11 >>提示符下输入问题回车发送,输入q退出。发送 章节 README “Try It” 给出的三条提示词:
Run pip list in the background and find all Python files in this directoryRun npm install (use run_in_background) and while waiting, read package.jsonRun a short sleep in the background, then list all Markdown files
这三条提示词都是“一条慢命令 + 一条快命令”的结构,可以观察同一个会话里主循环能否在慢命令运行期间继续干活。
run_in_background 如何把命令派进后台线程
s11 构建在前一章的同步内核之上(README 对比表中称 s04 Kernel,code.py 里工具实现、hooks 与权限检查也都标注“From s04”),工具池不变,仍是bash、read_file、write_file、edit_file、glob五个工具,只在 bash schema 上加了一个参数,并新增后台执行部分。按四步看。
1. schema 声明与显式判定
bash 工具的 schema 增加了run_in_background(code.py 第 170-175 行):
{"name": "bash", "description": "Run a shell command.", "input_schema": {"type": "object", "properties": { "command": {"type": "string"}, "run_in_background": {"type": "boolean"}}, "required": ["command"]}}是否走后台完全由这个参数决定:
def should_run_background(tool_name: str, tool_input: dict) -> bool: return ( tool_name == "bash" and tool_input.get("run_in_background") is True )只有 bash 调用且参数显式为True才进入后台路径,其余调用照旧同步执行。README 强调 harness 不再根据install、build、test这类关键词猜测——由工具调用显式选择执行模式。系统提示词同时约束模型:“Set run_in_background to true only for independent Bash commands.”
2. BackgroundManager:立即返回 bg_id,守护线程执行命令
BackgroundManager.start()先登记任务并立即返回任务 ID(bg_0001、bg_0002……四位自增),再启动一个守护线程执行命令(code.py 第 314-342 行节选):
with self._lock: self._counter += 1 task_id = f"bg_{self._counter:04d}" self.tasks[task_id] = { "tool_use_id": block.id, "command": command, "status": "running", } thread = threading.Thread( target=self._run, args=(task_id, command), daemon=True, )start()对非 bash 块抛ValueError("Only Bash commands can run in the background"),空命令也会抛ValueError;线程启动失败时会回滚刚登记的任务。
后台线程里的_run()执行完命令后:退出码为 0 标记completed,非零退出码或执行异常标记failed,然后把任务 ID 放入就绪队列。命令由_run_bash_process()运行:subprocess.Popen(..., shell=True, start_new_session=True),等待超时 120 秒,超时返回Error: Timeout (120s);输出本身截断到 50000 字符。README 说明 shell 在自己的进程组里启动,命令结束、超时或 Agent 通过正常退出 /SIGTERM路径退出时,runtime 会停止那个原始进程组——这是生命周期清理而不是沙箱:创建了另一个会话的进程可以留在组外。
3. 占位结果让主循环继续
s11 版的execute_tool()先在主线程跑PreToolUsehooks(含权限检查),检查通过后才决定同步还是后台(code.py 第 425-443 行):
def execute_tool(block) -> str: blocked = trigger_hooks("PreToolUse", block) if blocked is not None: return str(blocked) if should_run_background(block.name, block.input): try: task_id = start_background_task(block) output = ( f"[Background task {task_id} started] " "The result will be collected on a later turn." ) except Exception as error: output = f"Error: {error}" else: output = call_tool(block) trigger_hooks("PostToolUse", block, output) return output所以后台命令返回给模型的tool_result是这句占位话,而不是命令输出。模型拿到它就能立刻发起下一个工具调用,主循环不会被慢命令阻塞。
4. 结果收集:下一轮开始时注入 task_notification
Agent loop 在每次 LLM 调用之前收集已完成的结果(code.py 第 448-457 行):
def agent_loop(messages: list): while True: inject_background_results(messages) response = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000, )collect_background_results()(即BackgroundManager.collect())从就绪队列取出已完成任务,组装成固定格式的通知(第 371-380 行):
notifications.append( f"<task_notification>\n" f" <task_id>{task_id}</task_id>\n" f" <status>{task['status']}</status>\n" f" <command>{task['command']}</command>\n" f" <summary>{result[:500]}</summary>\n" f"</task_notification>" )注意通知不沿用原调用的tool_use_id:原工具调用已经被占位tool_result应答过了,完成结果作为独立事件注入,从而保持“一个tool_use恰好对应一个tool_result”。inject_background_results()把通知追加到最后一条 user 消息,若最后一条不是 user 消息则新建一条 user 消息。
已完成的任务不会自行唤醒 Agent,它只是待在队列里等下一轮循环来收。README 给过一个三轮示例(文档示例):
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 Turn 3: → collect bg_0001 as <task_notification> → LLM sees: config file + install notification in one messagenpm install在后台跑的同时,循环被用来读了package.json,第 3 轮时安装结果以通知形式出现。
汇总一下 s11 相对同步内核的全部改动(README “What s11 Adds” 表):
| 项 | 变化 |
|---|---|
| bash schema | command+run_in_background |
| 新增函数 | should_run_background、start_background_task、collect_background_results、inject_background_results |
| 新增类型 | BackgroundManager |
| 通知格式 | <task_notification>(不沿用tool_use_id) |
| 循环行为 | 显式后台执行,已完成结果在后续轮次收集 |
| 工具数量 | 仍是 5 个,不变 |
验证后台派发与结果收集
README 的 “What to observe” 给出了判断标准。用上面的提示词运行时,依次确认三点:
- 派发:模型显式设置
run_in_background后,命令是否被派进后台?终端会打印[background] started bg_0001: <命令前 60 字符>,工具结果里是否有bg_id? - 主循环继续:派发后模型是否在同一会话里发起了另一个工具调用(例如派完
pip list后立刻去globPython 文件)? - 收集:后续某轮 LLM 调用开始时,终端打印
[background] collected bg_0001: completed(非零退出则是failed),模型收到的消息中是否出现对应的<task_notification>?
不想消耗 API 额度时,可以用仓库自带的测试在代码层面核对。tests/test_background_tasks.py mock 掉了anthropic模块并注入伪造响应,不需要 API Key:
pytest tests/test_background_tasks.py注意pytest不在 requirements.txt 里,需要先自行安装。测试的断言正好对应机制的几个关键点:
test_background_execution_requires_an_explicit_bash_flag:bash 不带run_in_background参数不走后台;带参数的非 bash 工具(如write_file)同样不走后台;test_background_bash_passes_permission_before_dispatch:命令命中 deny list(如含rm -rf)时,权限 hook 在派发前拦截,不产生任何后台任务,tool_result为Permission denied;test_completed_result_is_collected_once_before_a_later_llm_call:已完成的后台命令在下一次 LLM 调用的第一条消息中注入,消息包含<task_notification>、<task_id>bg_XXXX</task_id>(XXXX为四位自增编号)、<status>completed</status>和命令输出;收集后再调collect_background_results()返回空列表——结果只注入一次。
使用这套机制要知道的边界
- 只有 bash 能后台:
start()拒绝非 bash 块和空命令,抛ValueError; - 收集时机:结果只在下一轮循环开始(LLM 调用前)被收集,循环不继续时结果就一直留在队列里;
- 失败处理:非零退出码与后台线程异常都标记
failed,但结果仍会被收集并以通知注入;120 秒等待超时返回Error: Timeout (120s),同样标记failed; - 长度限制:命令输出截断到 50000 字符,通知里的
<summary>只取结果前 500 字符; - 进程清理不是沙箱:runtime 只停止命令的原始进程组,新建了会话的进程可以留在组外;
- 是否后台由模型决定:harness 不做关键词判断,系统提示词要求模型只把
run_in_background用于相互独立的命令。
下一步:从后台执行到定时触发
README 的 “What's Next” 指出了后续章节:后台任务解决了“慢操作不阻塞”的问题,但如果想要“每天早上 9 点跑测试”“每 5 分钟检查一次服务器状态”这类按时间表触发的任务,由 s12 Cron Scheduler 一章处理。
【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考