Codex 和 Claude Code 这类编码助手讨论到现在,真正卡住人的已经不是模型本身能不能写代码,而是当一个主 agent 要同时调度多个子代理时,运行环境能不能把任务、上下文、权限和日志都安排好。最近开源社区里出现了一批 runtime,目标很直接:给 Codex 和 Claude 提供更好的 subagent 体验。这里的 subagent,简单理解就是主 agent 拆分出来的更小、更专注的执行单元,比如让一个子代理去改测试、另一个去查文档、第三个去扫静态检查结果。
很多人在聊多 agent 设计时喜欢谈“主从模式”,或者强调“本质上就是把 subagent 当成另一种 tool 去调用”。这两种说法都有用,但它们都没有回答一个更底层的问题:谁来管理这些 subagent 的生命周期?谁来隔离它们的上下文?谁来处理失败重试和文件冲突?这正是 runtime 要解决的。本文不打算把某个具体仓库的 README 抄一遍,而是从实际接入和部署的角度,拆解这一类 runtime 到底解决了什么问题、运行前要准备什么、接入时有哪些关键参数、遇到报错应该按什么顺序排查。
如果你正在用 Codex CLI 或 Claude Code 跑自动化编程任务,想做批量重构、并行的代码审查、文档生成,或者想在公司内部做一个比较规范的 coding agent 接入层,这篇文章值得往下看。最值得先记住的一点是:subagent 体验的瓶颈,通常不是模型提示词,而是运行时的任务编排。
1. subagent 体验差的根源,不在模型,而在运行时
1.1 从“主从模式”到“另一种 tool”,是一次设计转变
多 agent 的协作方式,现在基本被归成两类。
一类是严格的主从模式。主 agent 作为 planner,负责理解用户目标、拆分任务、把子任务派给 subagent,最后汇总结果。这种模式的优点是结构清晰,每个 subagent 只负责一个明确目标,主 agent 拥有最终决策权。缺点是主 agent 容易成为瓶颈:如果子任务太多,所有中间结果都要回到主 agent 这里做判断,主 agent 的上下文很快就会被占满。
另一类是把 subagent 当成一种特殊的 tool 调用。主 agent 不需要知道“对面是一个完整的 agent”,它只需要知道:自己调用了某个工具,传入了待办事项,过一段时间拿回一段结构化结果。这种做法更接近函数调用模型,Agent 本身不需要为“另一个 Agent 正在思考”负责。
从工程角度看,第二种设计通常更稳。因为工具调用的输入输出边界是明确的,一个工具返回什么、出错时抛什么异常,都可以定义。而 subagent 如果把“思考过程”“对话记录”“文件改动日志”全混在一起返回,主 agent 反而不知道怎么处理。所以把 subagent 当作工具,不等于弱化它,而是强调它必须遵守约定的接口。
1.2 subagent 和普通 tool 到底有什么不一样
但 subagent 又不能完全等同于普通工具。普通工具比如“读取文件”“执行测试”,通常是无状态或者轻状态的,一次调用很快结束,返回的内容也很轻。subagent 不一样:
- 它可能运行几十秒甚至几分钟;
- 它可能产生多个中间文件改动;
- 它需要自己的上下文窗口;
- 它的输出可能很长,也可能不一致;
- 它在失败时需要重试,但重试成本非常高,因为又要消耗一轮模型调用。
这些差异带来一个直接结论:普通工具调用只需要关心超时和异常,subagent 还需要关心生命周期、资源回收、结果归因和并发冲突。而这些,恰恰是单独使用 Codex 或 Claude Code 时很容易被忽略的。
我自己在切换到这类 runtime 之前,遇到过蛮典型的问题:主 agent 派了两个子代理,一个负责重构工具函数,一个负责补测试。结果两个子代理同时改了同一个文件,后写的人把先写的人的逻辑覆盖了。从主 agent 的日志看,两个任务都“成功完成”,但代码已经坏了。这就是纯粹的运行时问题,不是模型能力不足。
2. runtime 调度的核心:任务生命周期、上下文隔离和可见性
2.1 任务生命周期要覆盖哪些状态
一个可复用的 subagent runtime,至少要能清楚表达任务的状态流转。我见过一些简单实现,只用“开始”和“结束”两个状态,结果一旦任务卡住,根本不知道它到底卡在“等待模型响应”还是“写完文件没返回”。
比较实用的状态模型大致是这样:
| 状态 | 含义 | 判断方式 |
|---|---|---|
| pending | 任务已进入队列,还没被派发 | 队列里能看到任务标识 |
| running | subagent 已在执行 | 进程日志中有启动记录 |
| waiting_input | subagent 需要主 agent 补充信息 | 日志停在某个提问处 |
| completed | 返回了预期结果 | 拿到结构化 result |
| failed | 执行过程出错 | 有堆栈、退出码或错误消息 |
| cancelled | 被主 agent 主动终止 | 有取消信号记录 |
如果没有这些状态,runtime 的调度能力几乎为零。主 agent 能做的只是“发出任务、等待最终输出”,一旦中间有问题,或者任务被卡住,你没法取消、没法重试、也没法定位。
所以,接入开源 runtime 时,第一件事不是看它支持多少个模型,而是看它有没有一套任务状态机,以及任务日志是不是完整。
2.2 上下文不能全量透传,要按任务裁剪
第二个关键点是上下文。
单个 agent 工作时,上下文窗口大小决定一次能处理多少代码。多 agent 工作时,情况复杂得多:每个 subagent 理论上都应该有自己的上下文,但主 agent 往往还会把大量仓库内容传递给子代理。如果每个任务都传三四个文件、再加一大堆历史对话,token 消耗会成倍增长。
比较合理的做法是:主 agent 只传任务描述、目标文件路径、相关规范文档和约束条件。不是把整个代码库都留给 subagent 去看,而是让 subagent 自己按需读取指定文件。
这里有一个常见误判:看到某个 subagent 输出质量差,第一反应是模型不行,换一个更大的模型。排查后发现,主 agent 给它塞了太多无关历史,导致真实任务信息占不了多少上下文。runtime 的作用恰恰是“裁剪”:在任务入队前,把主 agent 的历史对话过滤掉,只保留当前任务真正需要的输入。
2.3 没有可见性,多 agent 协作就是黑盒
第三个核心是可见性。单 agent 出错时,你可以看它的对话记录和文件改动。多 agent 出错时,如果不能区分“到底是哪个 subagent 改了哪个文件”,后面的问题基本没法查。
开源的 subagent runtime 通常会加一层日志或事件总线,把每次任务派发、每次工具调用、每次文件写入都记录成事件。比如:
- task-start,带 task_id;
- file-written,带文件路径和摘要;
- tool-call,带工具名和参数;
- task-end,带最终结果。
接入时不要只看日志是否漂亮,要重点确认事件是否包含 task_id 和文件路径。否则,日志在表面上很完整,但你还是一样无法回放当时的执行过程。
3. 先解决本地环境和 CLI 问题,再谈调度
3.1 环境检查清单
不管 runtime 设计得多好,它最终还是要调用本地的 Codex 或 Claude Code CLI。很多人在引入 runtime 前后连 CLI 都跑不起来,一接到报错就先怀疑 runtime。但其实问题大多数出在环境本身。
我先说一个最常踩的坑:命令行识别不了程序名。网上常见这类报错:
'claude' 不是内部或外部命令,也不是可运行的程序或批处理文件unable to locate the codex cli binary. set codex cli path or ensure the elec...claude : 无法将“claude”项识别为 cmdlet、函数、脚本文件或可运行程序
这一类问题和解不解释模型无关,核心是环境变量 PATH 没有设置到正确位置。很多安装包默认不会自动加 PATH,装完之后要新开一个终端,或者手动把可执行文件所在目录加入系统 PATH。如果在 VS Code 的终端里跑,有时候还需要重启 VS Code,让系统环境变量生效。
我的建议是按下面的顺序检查:
- 先确认安装位置;
- 再确认 PATH 是否包含安装目录;
- 在同一个终端里运行
which codex或codex --version; - 确认 Electron 或桌面版场景下,插件的“CLI Path”设置是否指向真实可执行文件;
- 新开终端再试。
很多报错看起来像是程序坏了,其实就是路径没对上。这里先不要急着改 runtime 参数。
3.2 几个常见报错可以先按这种思路排查
我搜集了一轮相关讨论,发现有几个高频报错很值得提前说清楚。
model is not supported when using codex with a chatgpt account意思是当前账号类型或当前 CLI 版本不支持你配置的模型。它不是网络问题,也不是机器问题。先确认模型名称是否在支持列表里,再看 CLI 版本是否需要升级。如果你接入的是第三方模型,这类报错出现概率更高,因为第三方模型名未必在官方版本的白名单里。ChatGPT failed to start. unable to locate the codex cli binary通常发生在桌面版、编辑器插件或某个封装前端与 CLI 交互时。前端去找 codex 可执行文件的路径,找不到就会报这个。解决方式和上面 PATH 问题一样,但要注意有些插件需要单独设置 CLI Path,不是系统 PATH 调好就自动识别。deepseek-v4-flash is not a model this version of claude code recognizes这种通常出现在给 Claude CLI 配置自定义模型时。当前版本无法识别你写进去的模型名。先确认模型名是不是真实存在,再看配置的模型格式是否符合要求。不要直接去改并发参数,方向不对。cc switch local proxy failed while handling codex endpoint /responses这条看关键词大致发生在 codex endpoint 请求路径上,常见原因是本地网络端点没有起来,或者 endpoint 配置指向了无法访问的地址。排查时先确认本地服务是否在运行、端口是否被占用、endpoint 的地址是不是 localhost 或内网可达地址、证书是否可信。如果只是临时搭的本地测试端点,优先检查进程状态,而不是怀疑模型配置。
网络相关的配置要特别注意,在团队里不同人的网络环境可能差异很大。遇到 endpoint 无法访问,最直接的排查动作是:先用 curl 或浏览器单独访问同一个 endpoint,看是否能拿到正常响应。如果单独访问都不行,那就是本地服务或网络配置问题;如果单独访问可以,但 CLI 不行,再去检查 CLI 的配置文件和版本。
另外,多 agent 并发运行时往往需要比较多的 token,如果 API key 是共享的,要留意配额和速率限制。很多所谓的“突然失败”其实是触发了限流。不要把限流错误当成代码错误去排查。
4. 一个工程化的接入顺序:先单 agent,再 subagent-as-tool
4.1 为什么建议先跑通单 agent
接入 runtime 最大的错误,是环境还没稳定就直接开多 subagent 并发。我建议把接入过程拆成四步,每一步都有明确验证标准,不要跳步。
第一步,先把 Codex 或 Claude Code 单独跑通。找一个很小的任务,比如“给当前目录写一个 README.md”,确认 CLI 能启动、能调用模型、能写文件。很多人嫌这一步啰嗦,但它能帮你把环境变量、登录状态、网络访问和模型配置一次性检查完。跳过这一步,后面出现的任何错误都要同时怀疑环境和 runtime,排查成本高很多。
所谓验证标准,不是简单地看到输出就行,而是确认:
- CLI 能返回可读文本;
- 对文件系统有正确的读写权限;
- 不会因为缺依赖或者路径不对而中断;
- 运行日志能落到指定文件。
4.2 定义一份最小 Task 协议
单 agent 跑通后,再进入 subagent 设计。此时需要先定义一份最小协议,不能用自然语言随便描述。至少要包含这几种信息:
| 字段 | 说明 | 示例 |
|---|---|---|
| task_id | 每次派发的唯一标识 | task_001 |
| goal | 子代理要完成的最终目标 | 补充 utils.py 的单元测试 |
| context | 参考文件或相关代码位置 | src/utils.py |
| constraints | 不能做的事 | 不要修改现有接口 |
| expected_output | 主 agent 期望拿回什么 | 测试文件路径 + 测试结果摘要 |
| retry_policy | 失败后如何处理 | 最多重试 1 次,超过则标记失败 |
这里的关键是 expected_output。很多 subagent 协作出问题,是因为没有约定“完成”的标准。主 agent 问“你在吗”,subagent 回答“我在”,主 agent 以为完事了;或者 subagent 自我感觉完成,但只返回了一句“我已经修改代码”,主 agent 不知道具体改了哪里、怎么验证。
比较稳妥的做法是让 subagent 返回结构化结果,例如 JSON。早期调试阶段不需要太复杂,能表达“我做了哪些操作、改了哪些文件、测试通过没有、下一步建议做什么”就够了。
调用方视角大致是这样:
{ "task_id": "task_001", "goal": "给 src/utils.py 新增单元测试并运行", "context": { "target_file": "src/utils.py", "test_dir": "tests/" }, "constraints": [ "不要重构 src/utils.py 的内部逻辑" ], "expected_output": [ "修改后的测试文件路径", "单测运行结果", "发现的隐患列表" ] }subagent 的返回可以是:
{ "task_id": "task_001", "status": "completed", "files_changed": ["tests/test_utils.py"], "test_result": "12 passed", "issues": [ "utils.load_config 对空文件没有异常处理" ], "next_actions": [ "建议修复 load_config 的异常路径" ] }不要觉得这种格式死板。它最大的价值在于,主 agent 拿到结果后不需要再去“猜测”下一步。它能直接判断当前任务是否完成了,如果没完成,也能根据 files_changed、issues、next_actions 决定是继续追问同一个 subagent,还是派一个新的 subagent。
4.3 调用 subagent 并验证返回
第三步是验证 subagent 能力。可以把 subagent 当成一个函数来测:给一个最简单的 task,比如“统计当前目录下 Python 文件的数量,并把结果写入 count.txt”。看它能不能返回一个合法 JSON,能不能正确写入文件。
这个阶段要重点看两件事:
- 每次返回是否都能被主 agent 正确解析;
- subagent 是否严格只执行任务范围内的操作。
如果 subagent 在写 count.txt 时顺手改了 src 目录,说明权限边界没有约束好。很多 runtime 会提供 worktree、临时目录或 sandbox 机制,让 subagent 在隔离目录里操作,最后再把必要产物同步回来。
如果这一步验证没过,不要进批量任务。因为一个 subagent 的输出都不可控,多个 subagent 并发之后只会更乱。
4.4 加入并发和失败重试
最后一步才是并发。我第一次接入时最大的教训就是:不要一上来就开三四个并发。先开两个 subagent,跑一个耗时短的任务,确认事件日志里能区分出两个独立 task,并且它们没有同时写同一个文件。能跑通后,再逐步增加并发数。
并发场景下需要额外关注一件事:幂等。如果一个 subagent 第一次跑到一半失败了,重试的时候是整个任务重来,还是从断点继续?重试会不会重复创建文件、重复执行副作用操作?尤其是执行数据库迁移、发送消息、删除文件这类不可逆操作时,不做幂等设计会导致严重问题。
可以给每个任务配置一个幂等 key,subagent 在写文件前先检查是否已经执行过当前步骤。也可以强制规定:首次重试永远只做“读取当前状态”,不直接执行写操作。总之,不能把重试简单理解为“把同一个指令再发一遍”。
5. 核心参数怎么调:token、并发、超时、上下文
5.1 参数表
runtime 跑起来之后,多数人面对的不是代码问题,而是参数取舍。下面这张表是我在实际调试中比较关注的参数,不一定每个 runtime 都叫这个名字,但在底层基本都存在对应的配置项。
| 参数 | 作用 | 调试建议 |
|---|---|---|
| max_concurrency | 允许同时运行的 subagent 数量 | 开始用 1,稳定后逐步调到 2 或 3 |
| max_retries | 单个任务失败后的重试次数 | 默认 1 或 2,不要设 5 以上 |
| timeout_seconds | 单个任务最大执行时间 | 先设为单任务实际耗时的 2 倍 |
| context_buffer | 为每个 subagent 预留的上下文窗口 | 看模型限制,超过 80% 就裁剪输入 |
| task_output_limit | 返回结果的最大长度 | 防止 subagent 返回几千行文本 |
| artifact_dir | 文件产物保存目录 | 每个 task 建议单独子目录 |
| log_level | 日志详细级别 | 调试用 debug,平时用 info |
先说 max_concurrency。很多人以为越大效率越高,但多 agent 场景不是这么简单。每个并发任务都要占用模型上下文、系统进程和临时文件空间。如果在同一台机器上跑,CPU、内存、磁盘都会跟着涨。更麻烦的是,如果多个 subagent 共用同一个项目目录,并发一上来,文件冲突概率指数增长。低配置机器能跑一支任务,不代表能扛住五个任务并发。
再说 token。多 agent 相对单 agent 的 token 开销并不是线性增长,它经常是爆炸式增长。因为主 agent 要维护全局状态,每个 subagent 又各自维护局部上下文,两边都会有大量重复内容。控制 token 消耗最有效的方式,还是裁剪输入。只把任务相关的文件路径和关键约束传给 subagent,不要带大段无关历史。
timeout 也很重要。subagent 跑起来不像单次请求,可能是一个长任务。如果 timeout 设得太短,任务明明在做但被误杀;设得太长,一旦模型卡死,整个队列都会被堵住。我一般会先用一个小任务测出正常耗时,然后把 timeout 设成两倍左右,后续再根据日志调整。
task_output_limit 是很容易被忽略的点。subagent 一旦拿到比较大的上下文,可能返回特别长的结果。如果主 agent 的上下文有限,几千行返回会把后续决策能力挤没。所以控制输出长度很有必要,可以让 subagent 只返回文件路径、状态摘要和关键测试结论,完整日志写到 artifact_dir,由外部统一存储。
5.2 建议的调试顺序
参数不要一次性全改完。我的经验是一个一个来:
- 先验证单 subagent 跑通,记录耗时和 token 消耗;
- 调 timeout,让它能覆盖正常执行但不会无限等;
- 再增加并发,从 1 到 2,观察日志是否能区分任务;
- 发现结果重复或任务重叠时,再调上下文缓存和幂等机制;
- 最后才调 retry,因为重试必须在任务状态稳定之后才有意义。
如果调完并发后任务频繁失败,先不要降低 timeout,也不要盲目减小 max_concurrency。先去日志里找有没有文件锁冲突、资源配额、模型限流这一类错误。很多时间浪费在参数上,但问题根本是输入端被反复塞入了重复内容。
6. 怎么判断这套 runtime 真正可用
6.1 判断标准
前面说了很多接入方法,最后还是要回到一个朴素的问题:你搭的这套 runtime 到底算不算合格。
我的判断标准有五条,都比较直接:
- 单个任务能稳定返回结构化结果;
- 任务日志能还原出“哪个 subagent 在什么时候做了什么”;
- 并发任务不会修改彼此的文件;
- 失败任务重试后,执行过程对输出目录没有重复污染;
- 主 agent 拿到 subagent 返回后,能基于返回内容继续做下一步判断。
如果你搭建的 runtime 这五条都能满足,说明你已经在“把 subagent 当成一个可靠的工程组件”来用了。如果不能,说明还停留在“碰运气式多 agent”,每次跑的结果可能都不一样。
6.2 一个简单的验证样例
我会定期用同一个任务做回归验证。比如在一个模拟项目里,构造一个带少量 bug 的函数,然后让主 agent 派两个 subagent:第一个负责检查代码规范和潜在 bug,第二个负责补测试,最后主 agent 汇总判断是否修复。
这个验证样例的关键不在于任务复杂,而在于它覆盖了三个必要环节:subagent 读取代码、改了文件、返回结构化结论。如果这三个环节都能稳定走通,那这个 runtime 就具备继续扩展的基础。
6.3 出现问题时按什么顺序排查
最后给一份排查顺序表,直接按步骤来,速度会快很多。
| 优先级 | 排查点 | 具体动作 |
|---|---|---|
| 1 | 队列状态 | 任务是否真的进入队列?状态是否正常流转 |
| 2 | 输入协议 | 传给 subagent 的 task JSON 是否完整、无多余字段 |
| 3 | CLI 可执行状态 | 能不能单独跑通 codex 或 claude 命令 |
| 4 | 模型配置 | 当前模型名是否被支持、token 额度是否够 |
| 5 | 文件冲突 | 多个 subagent 是否操作了同一路径 |
| 6 | 日志 | 日志里有没有记录到每个任务的开始、结束和异常 |
| 7 | 系统资源 | CPU、内存、磁盘是否被打满,进程是否还活着 |
实际踩坑多了以后,你会发现自己经常在第二和第三步之间切换。有些人一看到任务失败就怀疑 runtime 不支持某个模型,结果真实原因只是 subagent 收到的 task 描述里把文件路径写错了。也有一些人疯狂调并发参数,最后发现 CLI 二进制路径根本找不到,任务压根没有启动。
这里我再提醒一下:如果 subagent 一直“看起来成功,但结果不可用”,请先检查 expected_output 定义。很多 runtime 的问题是“完成标准”太模糊,subagent 不知道该在什么时候停下,于是它输出一堆过程性内容,看起来忙了很久,但没有产出真正能被主 agent 消费的结果。
多 agent 形态本身不是目的。把 subagent 当成另一种 tool 调用,是一个很好的设计方向,但它需要配套的任务协议、状态管理、日志追踪和环境治理。开源的 runtime 给我们提供了一个更稳的底座,不过真正决定体验高低的,还是接入时你有没有把任务边界、上下文、并发、重试这些老问题想清楚。
我自己更建议把这个过程拆成单任务、批量、并发三个阶段来推进。先让一个 subagent 稳定完成任务,再让多个 subagent 并行而不互相干扰,最后才考虑引入更复杂的编排逻辑。这个顺序可能显得不够“炫”,但长期看,它才是能让 Codex 和 Claude 这类编程助手真正进入日常生产流程的路径。