为什么你的 Claude Code 还在转圈等,别人的已经干完三件事了?

为什么你的 Claude Code 还在转圈等,别人的已经干完三件事了?

同样是让 Claude Code 跑全量测试,有人盯着终端转圈等 8 分钟,什么也干不了;有人的 Claude Code 拿到一个任务 ID 就转头去改别的模块、查日志、写单测,等测试跑完,一条通知插进对话,它再回来接着修失败用例。

差别不在模型,在于用没用对它的异步工具。

我一开始也以为这只是"后台跑个命令"。直到把 Claude Code 2.1.303 和 2.1.304 的请求体抓下来对比,才发现不是:tools 从 20 个变成 12 个,system prompt 5,768 个字符,正文一个字没改。

Claude Code 正在把一部分工具调用从"函数调用"改成"提交任务"。用前端的话说,就像把同步阻塞调用改成了 Promise:tool_use 先返回一个任务 ID,等跑完了再由运行时把结果通知推回来。比 Promise 更进一步的是:这些任务还能持续推送多条事件(Monitor),也能随时按 ID 取消(TaskStop)。

这篇不聊虚的概念,直接把这 5 种后台玩法拆开,顺便聊聊什么时候千万别用------因为用错了,省下的等待时间会从 token 账单里双倍还回去。

本文基于 Claude Code 2.1.303--2.1.305 的行为;版本差异会在文中标出。抓包还带出另一层异步(ToolSearch 的默认值变化),放在文末附录;不关心版本细节可以跳过。
省流版

  1. 异步工具不是"后台跑命令",是"工具返回任务 ID,完成时再叫你"。
  2. 5 种形态:后台 Bash、后台 Agent、Monitor、Cron / ScheduleWakeup、Workflow。
  3. 大多数场景只需要前两种;Workflow 是最后一步,不是第一步。
  4. 两个最容易被低估的成本:prompt cache(模型侧上下文缓存)和权限边界。

先分清:两种异步,解决的不是一回事

Claude Code 里有两个东西都被叫"异步",但它们解决的不是同一个问题。

维度 定义异步(ToolSearch,懒加载) 执行异步(后台任务)
异步的对象 工具的 JSONSchema 工具的执行与结果
触发方式 模型调用 ToolSearch run_in_background 或任务型工具
返回什么 完整 schema 任务 ID / 句柄
完成信号 同一次调用内返回 新的一轮事件
省什么 上下文 token 墙钟时间(实际耗时)
主要代价 多一次搜索往返 状态、权限、成本、生命周期

一句话:前者是懒加载,后者是任务调度。

混着聊,就会问出"ToolSearch 能不能让工具并行"这种问题。不能。它自己就是同步调用,只是把 schema 的获取从"启动时全量注入"改成了"用到时一次往返"。

为什么非要拆成两步?

同步工具调用是一次阻塞 RPC:模型发起 Bash,harness(模型外面的运行时/调度层)执行,结果回到上下文,模型才能继续。这期间模型不能做别的事,用户也只能等。

Claude Code 给前台 Bash 的默认超时是 120 秒,最大 600 秒。但更有意思的是超时之后的行为:命令不会直接失败,而是被移到后台 ------除非命令以 sleep 开头(sleep 本身就是在等,转后台没有意义),或者后台任务被禁用。

这个设计说明了一件事:长任务的正确归宿不是更长的 timeout,而是任务队列。

于是就有了下面 5 种形态。它们不是 5 个功能,是同一套任务模型的 5 个入口。

5 种形态,一张表先看完

形态 代表工具 立即返回 唤醒信号 取消
后台命令(Bash) Bash + run_in_background 任务 ID 进程退出,重新唤起模型 TaskStop
后台子代理(Agent) Agent 代理 ID 完成通知 TaskStop / 名称 / agent ID
事件监控(Monitor) Monitor 监控句柄 每行输出 / 每条消息 TaskStop
定时任务(Cron / ScheduleWakeup) CronCreate / ScheduleWakeup 任务 ID 到点触发一轮 CronDelete / stop: true
脚本编排(Workflow) Workflow 任务 ID task-notification TaskStop

表里没有 Agent Teams:它是多个会话的协作,不是单任务的异步形态;选型表里会作为对照项出现。

下面按"从轻到重"排,每个给一个最小调用和适用边界。

1. 后台 Bash:把进程交给任务表

模型侧的一次调用长这样:

json 复制代码
{
  "name": "Bash",
  "input": {
    "command": "npm test -- --runInBand",
    "description": "Run the full test suite",
    "run_in_background": true
  }
}

从调用到结果,整条链路是这样的:

图 1:后台 Bash 的完整链路。调用立即返回任务 ID + 输出路径;进程退出后由 harness 注入通知,模型再用 Read 取输出。

三个细节:

  • 不要加 &。 工具描述写得很明确:命令会 detached 运行,跨 turn(轮次)存活,进程退出时重新唤起模型。& 是 shell 层的后台,和 harness 的任务表不是一回事。
  • timeout 的含义变了。 前台是"最多等多久",后台是"最多活多久":默认 600 秒,最大 7200 秒。
  • 输出落盘。 任务返回里带输出路径,模型用 Read 去取,不要把几万行日志灌回上下文。2.1.277 已经移除了 TaskOutput,别再找它。

后台任务还有一整套生命周期约束:

约束 值 说明
输出上限 5GB 单个任务超过会被终止
会话退出 自动清理 Claude Code 退出时任务结束
内存压力 空闲 ≥ 30 分钟且没有 turn 在跑 macOS / Linux 会停止这类任务
无人值守时限 默认 30 分钟,最大 2 小时 -p(非交互模式)、CI、云会话
-p + 文本 prompt 默认 10 分钟 一次性任务,没有后续交互
本地交互式 会话存续期间无时限 但退出 Claude Code 仍会清理

2. 后台 Agent:默认就不等你

Agent 工具负责创建子代理(subagent)。它的 run_in_background 描述很能说明设计取向:

Subagents run in the background by default; you'll be notified when one completes.

只有在"下一步动作完全依赖这个结果,且期间没有别的事可做"时,才建议传 false。换句话说,前台子代理是例外,后台才是默认。

它和后台命令的区别在返回物 :命令的产物是输出文件,子代理的产物是一份最终报告。工具描述特意提醒:子代理的最终报告不会展示给用户,主对话需要转述关键结论;已完成的子代理可以用 SendMessage 按名称继续,上下文还在,新开一个 Agent 则是从零开始。

隔离也做进了参数:

  • isolation: "worktree":给子代理一个临时 git worktree(同一仓库的独立工作目录),没改动就自动清理;
  • isolation: "remote":扔到远程云环境,始终后台,是否可用由网关决定。

后台子代理最大的坑不是并发数,是结果的所有权。工具描述里有一句硬约束:

Never fabricate or predict a pending agent's results --- the notification is never something you write yourself.

模型不能"猜"一个还没回来的结果,也不能自己编一条完成通知。通知必须由 harness 注入。这条约束把异步系统里最容易出现的"幻觉完成"堵在了协议层。

3. Monitor:等的不是结束,是变化

后台命令等的是进程退出,Monitor(事件监控)等的是每一行输出。工具描述把它定义为"在后台跑一个命令,把每一行输出喂回 Claude",也可以直接连 WebSocket,把每条消息当成一个事件。

这解决的是另一类问题:不是"这个任务要跑多久",而是"这件事什么时候发生变化"。tail 日志、轮询 CI、盯目录、跟踪长脚本,都属于这一类。

三个容易被忽略的限制:

  • 有截止时间。 默认 5 分钟,最长 30 分钟;-p 单 prompt 场景最长 10 分钟。到点结束,只给模型一条通知,让它决定要不要重新开。
  • 不是所有环境都有。 Amazon Bedrock、Google Cloud Vertex(Agent Platform)、Microsoft Foundry 不可用;设置了 DISABLE_TELEMETRY 或 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 也会消失。
  • 走 Bash 同一套权限。 auto 模式下由分类器审查命令。

我同时设了 DISABLE_TELEMETRY=1 和 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1,2.1.303 和 2.1.304 的工具列表里都没有 Monitor;文档和实测一致。企业环境关了遥测,Monitor 就用不了。

4. Cron / ScheduleWakeup:把"未来"也变成任务

CronCreate(定时任务)接受标准 5 段 cron,按本地时区解释:

json 复制代码
{
  "name": "CronCreate",
  "input": {
    "cron": "7 * * * *",
    "prompt": "check the deploy status and report failures",
    "recurring": true,
    "durable": false
  }
}

几个很"工程"的约束:

  • 只在 REPL(交互式会话)空闲时触发,不会打断正在进行的 turn;-p / CI 没有 REPL,cron 不会触发,别把它当 CI 调度器;
  • 每个会话最多 50 个 cron 任务;
  • 循环任务 7 天后自动过期,最后触发一次再删除;
  • 默认只存在内存里,durable: true 才写入 .claude/scheduled_tasks.json;durable 只负责跨重启存活,7 天过期是循环任务本身的规则,不会因为 durable 而延长;
  • 有 jitter:循环任务最多晚半个周期(上限 30 分钟),整点/半点的单次任务最多早 90 秒。

jitter 和"别把任务都排在 :00"是同一个考虑:全世界的 Claude 同时打 API,对谁都没好处;工具描述甚至建议模型主动挑一个非 0/30 的分钟。

另一条路径是 ScheduleWakeup,服务于 /loop 的动态节奏:模型每次醒来后自己决定下一次什么时候再检查,延迟被限制在 60--3600 秒。CronCreate 用于用户给定的时间点;ScheduleWakeup 用于"盯着这个 PR"这类由模型决定节奏的场景。

5. Workflow:把编排本身写成脚本

Workflow 是这 5 种里最重的一种。它不在对话里逐轮决定下一步,而是执行一段 JavaScript 脚本,脚本里用 agent()、parallel()、pipeline()、phase() 编排子代理。工具描述里的官方示例(节选,省略了 DIMENSIONS 和几个 schema 的定义):

ts 复制代码
const results = await pipeline(
  DIMENSIONS,
  d => agent(d.prompt, { phase: 'Review', schema: FINDINGS_SCHEMA }),
  review => parallel(review.findings.map(f => () =>
    agent(`Verify: ${f.title}`, { phase: 'Verify', schema: VERDICT_SCHEMA })
  ))
)

这段脚本的关键不是语法,是中间结果放在脚本变量里,不放在模型上下文里。对话只拿到最终报告。工具描述写得很直白:

Workflows run in the background --- this tool returns immediately with a task ID, and a <task-notification> arrives when the workflow completes.

它还有三个门槛:

  • 必须由用户显式 opt-in。 关键词 ultracode、会话级 ultracode、明确说"用 workflow"等;模型不能自己觉得"这任务适合并行"就开。
  • 默认规模指南是 medium。 少于 10 个 agent,用户可以在 /config 里改。
  • 支持 resumeFromRunId 断点续跑。 未改动的 agent() 调用直接命中缓存,只有新增或修改的才重跑。

Agent Teams 默认关闭,需要 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1;它解决的是多个会话互相讨论,不是单任务异步。

调度器视角:异步工具到底难在哪

把 5 种形态放在一起,从外部行为看,Claude Code 的主循环就不再是"模型调工具、工具返回结果",而是一个带任务表的事件循环:

图 2:把 5 种形态抽象成任务表。同步调用走 tool_result;异步调用进任务注册表,完成后经事件队列以 system-reminder / task-notification 的形式回来。图里的 Esc 用于打断当前 turn,不取消后台任务;取消后台任务用 TaskStop。

从这个视角看,异步工具的设计可以收成四件事:工具结果返回句柄而不是 payload;完成事件由 harness 注入,模型不能自造;取消和 deadline 是一等公民;输出落盘、权限留在前台。每条的细节在前面的形态里都出现过;如果你自己写 Harness,文末有一份落地清单。

最值得抄的一段:把缓存写进调度

ScheduleWakeup 的工具描述里有一大段在讲"该等多久",核心不是 cron 语义,是成本:

This session's requests use the default 5-minute Anthropic prompt-cache TTL. Sleeping past 300 seconds means the next wake-up reads your full conversation context uncached --- slower and more expensive.

然后它给了三段建议,外加一个要避开的点:

建议等待 缓存状态 典型场景
60--270 秒 命中 主动轮询外部状态:CI、部署、远程队列
300 秒 最差 不要选:既付了缓存 miss,又没有摊薄
300--3600 秒 未命中 没有更早检查的必要,或作为长时间兜底
1200--1800 秒 一次未命中 300--3600 区间里的推荐心跳,20--30 分钟检查一次

1200--1800 秒是 300--3600 秒区间内心跳的推荐值;300 秒单独避开。

这段描述值得单独拿出来,因为它暴露了一个大多数 Agent 框架没有处理的维度:调度器不只要知道任务什么时候该跑,还要知道模型侧的一次唤醒值多少钱。

5 分钟缓存 TTL 把等待切成两个成本区间;心跳用 1200--1800 秒,更长的等待(最多 3600 秒)留给兜底。

这也是为什么工具描述里反复强调"不要轮询":

Do NOT schedule a short-interval wakeup to poll for background work you started --- when harness-tracked work finishes, you are re-invoked automatically, so polling is wasted.

轮询在普通后端里只是浪费请求;在 Agent 里,每次轮询都是一次完整的模型调用,会把上下文重新读一遍。异步工具的第一使命,是把"等待"从模型调用里拿出去。

最容易踩的坑:通知不一定来

异步系统最脆弱的就是通知链路。2.1.299 修了 ScheduleWakeup 在 SDK、IDE、remote-control 中,以及后台任务运行时不触发的问题;2.1.290 修了 Mac 睡眠唤醒后后台 agent 报 "Agent stalled"、Workflow 子代理从头重启;2.1.300 修了云容器替换后已恢复的 agent 被判 lost。

结论:长任务要有兜底。ScheduleWakeup 自己的描述就写着,对 harness 能跟踪的工作不要轮询,但要留一个 1200 秒以上的 fallback,防止通知丢了。

另外两个容易被忽略的边界,前面已经提过,这里只留一句:后台任务不会一直在(无人值守默认 30 分钟、最大 2 小时,本地会话退出也会清理);后台也不能替用户同意(权限提示回到主会话,跨会话权限洗白被明确禁止)。

怎么选:默认同步,按"等待的性质"分流

需求 选择 判断依据
几秒内返回 同步工具 异步的任务管理成本高于等待成本
一条命令要跑几分钟 Bash + run_in_background 不需要推理,只需要时间
一个需要读文件、做判断的旁支任务 Agent(默认后台) 结果重要,过程不该污染主上下文
持续发生的事件(日志、CI、WebSocket) Monitor 等的不是结束,是变化
固定时间/周期触发 CronCreate;自定节奏用 ScheduleWakeup 触发条件是时间,不是任务完成
几十个 agent、需要交叉验证 Workflow 编排本身需要被固化、复跑、断点续跑
多个会话要互相讨论 Agent Teams(实验性) 需要 peer 协作,不是单纯并发

表里的第 2--6 行是 5 种异步形态;同步工具和 Agent Teams 是对照项。Agent Teams 是会话协作,不是单任务异步,所以不计入 5 种。

三个容易选错的场景:

  • 短任务用异步:一条 10 秒的命令开后台,额外付出任务 ID、通知、输出文件、清理的成本,不如直接等。
  • 同文件并行:子代理可以用 worktree 隔离,但 Agent Teams 的 teammate 不做 worktree 隔离,必须按文件边界分活,否则同文件冲突很难避免。
  • 把 Workflow 当"更多子代理" :Workflow 的价值是脚本可读、可复跑、可 resume,不是单纯把并发数从 5 提到 50。没有验证环节、没有中间结果复用的 fan-out,用普通 Agent 更便宜。

这三类错误的共同点,是把"并发"当成了免费资源。实际上,异步的每一层都要付钱:任务管理、缓存 miss、子代理的独立上下文。选错形态,省下来的等待时间,最终会从 token 账单里还回去。

常用命令,直接抄:

想看什么 用什么 在哪用
当前会话的后台任务 /tasks 会话内 slash command
动态 workflow 的进度 /workflows 会话内 slash command
所有后台会话 claude agents 终端 CLI
把当前 Bash 转后台 Ctrl+B 会话内快捷键
停掉所有后台子代理 Ctrl+X Ctrl+K(连按两次确认) 会话内快捷键

Ctrl+X Ctrl+K 停的是后台子代理;要停某条后台 Bash,用 TaskStop 按任务 ID 取消。

如果你自己写 Harness:把设计原则落成协议

如果你不自己写 Harness,这节可以跳过;前面的选型表已经够用。

这些原则落到代码里,主要是四件事:

  1. 协议要支持句柄。 同步返回 result,异步返回 {task_id, output_path, deadline};不要让长任务伪装成同步调用。
  2. 每个任务都要有 ID、输出落点、取消入口、截止时间。 四者缺一,任务就会在某个边界变成孤儿。
  3. 把模型侧的缓存窗口写进调度策略。 等待不是免费的;如果一次唤醒要重读整个上下文,调度粒度就应该按缓存 TTL 设计。
  4. 权限不能外包给后台。 后台任务不能替用户同意,也不能把被拒绝的操作转交给另一个会话执行。

至于"完成事件必须由运行时注入""输出落盘、上下文只拿摘要",它们是协议层的硬约束,不是实现细节------做不到,前面四件事也守不住。

最后说点得罪人的

Claude Code 的异步工具,真正变化的不是"能后台跑命令"这件事------shell 早就能 &。变化的是工具调用的语义:从"函数返回结果"变成"提交任务、等待事件、按 ID 取消"。

这个语义一旦成立,Agent 的瓶颈就从"模型有多快"变成了"调度器有多可靠"。后台 Bash 能不能在会话退出时收干净、子代理的结果会不会丢、定时任务在机器休眠后还记不记得、一次唤醒要不要重读上下文------这些才是异步系统的分水岭。

所以我的判断是:大多数团队不需要先上 Workflow。先把后台 Bash、TaskStop、输出落盘和截止时间做对,就已经拿到了异步工具最实用的一层收益。 多代理编排是下一步,不是第一步。

好的 Harness 不消灭等待,它把等待变成可调度的事件。

附:2.1.304 的默认值变化

这一节是开头抓包的后续,不影响主线的 5 种形态;不关心版本细节可以跳过。

回到开头那份请求体。2.1.304 默认的 tools 数组里有 12 个工具,其中两个在 2.1.303 默认里没有:ToolSearch 和 DeferredToolPlaceholder。但这两个不是 2.1.304 新增的工具------2.1.303 设了 ENABLE_TOOL_SEARCH=true 之后,工具列表与 2.1.304 默认完全一致。变的只是默认值。

算一下数就清楚了:2.1.303 默认的 20 个工具里,10 个常驻、10 个被延迟;2.1.304 把这 10 个延迟掉,再加上两个新工具,正好 12 个。被延迟的名单里,CronCreate、SendMessage、TaskStop 本身就是异步任务的控制面------在这组自定义 base URL 的抓包里,它们也要先经过 ToolSearch 才能调用。四组对照的完整数据放在下面的"数据说明"里。

结论:2.1.304 没有改机制,只把自定义 ANTHROPIC_BASE_URL 下的 tool search 默认打开了。此前走网关的用户必须手动设 ENABLE_TOOL_SEARCH,现在不用了;两组交叉对照的结果完全一致(见"数据说明")。

这次默认值切换还带出一个回归:如果 Vertex(Agent Platform)、Bedrock 或网关拒绝 tool search,2.1.304 会让该配置下的每个请求都失败;2.1.305 才改成"自动去掉 tool search 重试"。走网关的人,2.1.304 不是可以停留的版本。

但无论默认值怎么变,ToolSearch 都不是"后台执行":它仍然发生在当前 turn 内,模型发起搜索、拿到 schema、继续调用。它省的是上下文,不是墙钟时间。

数据说明

文中 2.1.303 / 2.1.304 的请求体数据来自本地抓包:下载对应版本的 darwin-arm64 原生包,用 -p、假 API key、空配置目录,本地起一个假的 /v1/messages 服务记录请求,模型统一为 claude-sonnet-5-5。工具数量与字符数为四次运行的实测值,未做 token 估算。

运行 tools 数量 schema 字符数 整个请求体
2.1.303 默认(自定义 base URL) 20 48,860 64,980
2.1.304 默认(自定义 base URL) 12 28,567 45,065
2.1.303 + ENABLE_TOOL_SEARCH=true 12 28,567 45,068
2.1.304 + ENABLE_TOOL_SEARCH=false 20 48,860 64,979

整个请求体的 1--3 字节差异来自版本号/配置路径;tools JSON 本身一致。

被延迟的 10 个工具是:CronCreate、CronDelete、CronList、EnterWorktree、ExitWorktree、NotebookEdit、SendMessage、TaskStop、WebFetch、WebSearch。

参考资料

相关推荐
喵个咪1 小时前
GoWind CMS 拆解:给 NL2SQL 上四把锁——智能问数的安全边界
后端
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - HTML5 SVG
前端·html·html5
飞飞鱼在摸鱼1 小时前
前端转Agent开发 | JavaScript 从 0 到 1 完全指南
前端·javascript
喵个咪1 小时前
GoWind CMS 拆解:在 Go 里给 TipTap 搭一套 Yjs 协同后端
后端·go·cms
开开心心就好1 小时前
课堂随机点名工具,绿色版免安装双击即用
java·服务器·前端·ffmpeg·音视频·perl·symfony
不像程序员的程序媛1 小时前
springboot支持的web容器
前端·spring boot·后端
喵个咪1 小时前
GoWind Admin 拆解:一个 Sender 接口接 39 个通知渠道
后端·架构·go
xiaobaoyu1 小时前
目录合集
前端