自研 Agent(学习 Claude Code 实现)过程的一个高频困惑:模型一次调用多个工具是"并行",把工具丢后台是"后台",这两个是一回事吗?CC 两个都有吗?谁来决定?这篇文章一次讲透。
开头:一个常见的直觉误区
刚开始学的时候,我下意识把"并行"和"后台"当成一回事------都是"同时做多件事"嘛。其实它们是两个完全正交的维度,解决的是不同的问题:
- 并行(parallel_tool_calls) :解决"这一轮怎么跑 "------模型一次返回 N 个工具调用,它们同时执行 ,结果一起在下一次回复里回来。
- 后台(run_in_background) :解决"结果要不要现在要 "------这个操作不阻塞模型 ,先回个占位符,结果以后以通知注入。
一句话:并行 = 同一轮里 N 个工具同时干活;后台 = 把某个活丢出去,结果晚点再要。 一个是"并发执行",一个是"延迟取结果"。
先打个比方
- 并行像同时炒三个菜,然后一起吃。你还是守在厨房,只是三个灶台同时开火,总时间从"三道菜依次炒"变成"最慢那道菜的时间"。
- 后台像点了外卖,然后接着写代码。菜到了(可能十分钟后)再吃,这期间你不等它。
逐个拆解
并行:发生在"工具派发层"
模型在一条回复里返回多个 tool_use(比如同时读 3 个文件、git status + ls 一起来)。CC 把这些并行安全 的工具同时执行,受 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 限制(默认 10 个并发)。
关键特征:
- 结果必然在下一次回复里全部回来------模型一次就能看到 N 个结果
- agent 循环会等------等待所有并行工具完成,把结果批量喂回给模型。只是等待的墙钟时间从"串行之和"变成"最慢一个"
- 生命周期活在当前 turn 内,跑完就完了
后台:发生在"任务生命周期层"
模型在某个 bash 调用上带 run_in_background: true。CC 把这个调用从当前 turn 里摘出来,变成一个独立的后台任务 ,回一个占位结果,完成后再通过通知队列(enqueueTaskNotification)注入后续 turn。
关键特征:
- 结果不一定什么时候回来------可能是下一个 turn,也可能是下下个,甚至你换了话题它才冒出来
- agent 循环不等------立刻拿到占位符,继续跑
- 生命周期超出当前 turn ,独立存在。这也是 CC 里 7 种后台任务类型(
local_bash、local_agent、remote_agent等)那一层的事
对比表
| 维度 | 并行(parallel_tool_calls) | 后台(run_in_background) |
|---|---|---|
| 解决什么问题 | 同一轮多个工具同时执行 | 慢操作不阻塞模型,结果延迟给 |
| 结果什么时候到 | 下一次回复,必定一起到 | 以后某个 turn 边界,以通知注入 |
| agent 循环等不等 | 等(所有并行工具完成) | 不等(立刻占位) |
| 生命周期 | 活在当前 turn 内 | 独立,可超出 turn |
| 控制方式 | 模型一次返回多个 tool_use | 单个调用带 run_in_background 参数 |
| 并发上限 | 有(默认 10) | 无硬性限制(独立子进程) |
关键问题一:进并行的一定不会进后台吗?
对,单个工具调用是"二选一"的派发------要么前台(可能并行),要么后台,不会两边都进。但一次回复里可以混着两种。
看派发的实际机制。模型一次返回 N 个 tool_use,CC 对每个调用做一次二元派发:
模型返回 [bash(npm install, 后台), read_file(a), read_file(b)]
│ │ │
│ └──────┬──────┘
后台路径 前台路径
立刻回占位符 并行执行,等真结果
结果晚点通知 下次回复一起给
- 带
run_in_background=true的那个 → 后台路径:立刻回占位符,不再参与"等待结果"这件事 - 前台的多个调用 → 并行路径:一起执行,下次回复里真结果一起回来
所以"进并行的一定不会进后台"这句话,在单个调用的层面是对的------一个调用派发时只能走其中一条路。后台化的调用已经用占位符"退出"了并行组,并行组等的是那些要真结果的前台调用。
但一个 turn 里可以同时出现两者(上面的例子就是)。所以它不是"整个 turn 二选一",而是"每个调用二选一,turn 可以混合"。
关键问题二:进并行和进后台,都是大模型自主决策吗?
不完全一样。后台是模型直接决定;并行是"模型提议,harness 拍板"。
后台:模型说了算
run_in_background 是 bash 工具 schema 里的一个参数,模型显式决定某个调用进后台。这是模型自己的选择,harness 不干预,也不会把前台工具偷偷挪去后台。
注意:很多教学实现里会有"关键词启发式兜底"(命令里出现 install/build/test 就自动丢后台),这是教学的简化设计,真实 CC 没有------模型不指定,就是前台。
并行:模型提议,harness 执行时决定
关键在于:模型并没有一个"run_in_parallel"参数。 模型"决定并行"的方式是间接的------它选择一次回复里请求多个工具(tool_use 数量是模型定的)。但真正是否并发执行,是 CC 的执行引擎决定的:
- 判断每个工具是否"并行安全"(concurrency-safe)------有些工具会改动共享状态,必须串行
- 受并发上限
CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY(默认 10)约束 - 安全且没超限的,才真正并发跑;否则退化成串行
所以拆开是两层:
| 决策 | 谁决定 | 怎么决定 |
|---|---|---|
| 一次要几个工具 | 模型 | 一次返回 N 个 tool_use |
| 后台不后台 | 模型 | 调用时带 run_in_background 参数 |
| 实际并发不并发 | harness | 判断并行安全 + 并发上限 |
一句话:模型决定"请求什么、要不要后台";harness 决定"能不能真的并行跑"。
顺带:Anthropic API 和 OpenAI 协议的一个差异
我用的是 OpenAI 兼容协议(DeepSeek),API 里有个显式的 parallel_tool_calls=True 参数------这是 OpenAI 风格的显式开关。而 Anthropic Messages API 没有这个参数 ,模型返回多个 tool_use 就隐含"可以并行",CC 的执行引擎负责决定怎么跑。
如果从零写 Agent,很容易在这个地方踩坑:API 参数开了并行,不代表执行层真并行。
回到自研实现:两个机制都处于"半成品"状态
很多人从零手写 Agent,两个机制往往都没真正落地:
并行:开关开了,执行是串行的。 比如我自己的代码,虽然给 API 传了 parallel_tool_calls=True,但工具执行循环是逐个 for 循环跑的------模型确实一次返回了多个工具,实际却是一个一个执行,墙钟时间退化成了串行之和。要做真并行,得把执行循环改成"并行安全的工具分组、线程池执行"。
后台:管理类实例化了,主循环没接。 BackgroundManager 写好了(background_tasks 字典、锁、daemon 线程、通知收集),但主循环的工具执行里根本没调用它------所有工具还是同步阻塞。
两个机制的改造方向是独立的两块工作,互不依赖:
- 把工具执行循环改成
ThreadPoolExecutor并发执行,但只对无冲突的只读工具并发 (读文件、bash 查询等);会改动共享状态的工具(todo、write_file、建任务这类)保持串行,否则有顺序/竞态问题。 - 把
BackgroundManager接入工具执行:让run_in_background参数真正生效,慢操作走 daemon 线程 + 占位结果 + 通知注入的完整链路。
总结
把这次学习钉死的几个认知:
- 并行和后台是两个正交维度,不是一回事:并行管"同一轮怎么跑",后台管"结果要不要现在要"。
- 单个调用二选一:要么前台(可能并行),要么后台;但一个 turn 可以同时混两者。
- 决策权不同:后台模型说了算(显式参数);并行是"模型提议(返回多个 tool_use)+ harness 拍板(并行安全 + 上限 10)"。
- API 开关 ≠ 执行层并行 :
parallel_tool_calls=True只是让模型可以返回多个工具,真正并发与否取决于执行引擎。 - 并行安全是关键约束:不是所有工具都能并行,改共享状态的必须串行。
如果你也想弄懂 Claude Code 这类 Coding Agent 到底是怎么工作的,这个仓库也许能帮你少走一些弯路。目前我的github的项目(OpenAI SDK 版),我学习的课程为learn-claude-code的v2版本课程(感谢原作者),大家一起学习,共同进步。如果对你有帮助,请为我点一个Star。
项目地址:https://github.com/peijiping/learn-claude-code-langchain