1. 为什么主从式串行调度在 OpenClaw 里值得单独拆一节
OpenClaw 的多 Agent 能力很容易让人第一反应就是“并发拉满”:一个用户任务丢进去,六个子 Agent 同时开工,谁先返回谁先汇总。听起来很爽,但只要你在单 GPU 环境里跑过一次就会明白,这种玩法基本等于自找麻烦——显存被多个活跃会话同时占用,模型上下文互相挤占,最后不是某个 Agent 卡死,就是返回内容串味。
主从式(main-sub)串行调度的核心思路其实很朴素:main Agent 不亲自干活,它只做四件事——理解任务、拆子任务、按顺序派发、汇总兜底。真正执行的是固定的一组子 Agent,而且同一时刻只允许一个子任务处于 running 状态。这样做的代价是整体耗时变长,换来的是链路可控、状态可查、异常可纠偏。
这套结构适合谁?适合那些已经把 OpenClaw 跑起来、想让多个职责不同的 Agent 协同完成一条完整任务链的开发者。比如一条“检索事实 → 技术预研 → 写代码 → 部署验证 → 成稿”的链路,天然就是有先后依赖的,硬要并行反而会乱。下面我把 config.toml 骨架、角色定义、状态机、轮询参数和一次完整的串行验证动作拆开讲,你可以直接照着改。
2. TaoToken 前置:把模型接入层先固定下来
在动 config.toml 之前,建议先把模型接入这一层固定住,否则后面调 Agent 的时候,你分不清是调度逻辑的问题还是接入层的问题。我这边统一走 TaoToken 的 API 入口,官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址用 https://taotoken.net/api (这个不加 UTM)。
为什么强调“先固定接入层”?因为主从式调度里,main 和每个子 Agent 可能配不同模型。如果每个 Agent 各自去填一套不同的接入参数,排障时你会疯掉。统一走一个 API 基址、一套 Key 管理,config.toml 里只区分模型名,问题定位会快很多。
你需要准备的东西不多:一个可用的 API Key,以及确认你的 OpenClaw 版本支持在 config.toml 里为每个 Agent 单独指定 model 字段。Key 的创建入口在控制台的 API Keys 页面,地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。创建完先别急着写进配置,拿模型对话页做个最小连通性验证,地址是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,能正常出字再往下走。
注意:接入层没通之前,不要去调 Agent 调度。否则你看到的“子任务失败”很可能只是 Key 或基址写错了,白白浪费排查时间。
3. config.toml 骨架:主从角色与固定子 Agent 定义
OpenClaw 的配置我习惯分成三段:全局接入段、main 段、sub agents 段。下面这份骨架你可以直接复制,把 model 和 api_key 换成自己的即可。注意子 Agent 我固定成 6 个,和 excerpt 里的职责划分对齐,但数量本身是可调的——越多,串行链路越长,单任务耗时越久。
# ============ 全局接入 ============ [provider] base_url = "https://taotoken.net/api" api_key = "sk-你的Key" timeout = 120 # 单次请求超时,秒 # ============ main Agent:只调度,不执行 ============ [agents.main] role = "orchestrator" model = "claude-sonnet" # 调度用,建议选指令遵循稳的 max_subtasks = 8 # 单个用户任务最多拆几个子任务 serial_only = true # 强制串行,核心开关 # ============ 固定子 Agent(6 个) ============ [agents.retrieval] role = "sub" model = "claude-haiku" # 检索类,轻量模型够用 desc = "新闻检索、事实核验" [agents.research] role = "sub" model = "claude-sonnet" desc = "技术分析、方案预研" [agents.devops] role = "sub" model = "claude-sonnet" desc = "代码实现、修改、修复" [agents.toolkit] role = "sub" model = "claude-haiku" desc = "环境、部署、SSH、排障" [agents.content] role = "sub" model = "claude-sonnet" desc = "总结、整理、成稿" [agents.medical] role = "sub" model = "claude-sonnet" desc = "医学学习、科普" # ============ 调度与轮询参数 ============ [scheduler] poll_interval = 25 # 轮询间隔,秒,建议 20-30 max_poll_times = 8 # 最大轮询次数,建议 5-10 retry_limit = 1 # 重试上限 stall_threshold = 3 # 连续 N 次无有效输出判定假死几个字段值得单独说。serial_only = true是这套架构的命门,它保证调度器不会一次性把多个子任务派发出去。stall_threshold配合poll_interval决定假死判定:25 秒轮询一次,连续 3 次只拿到心跳没有实际结果,就触发纠偏。max_subtasks是防止 main 把一个简单任务拆成十几段,串行跑起来没完没了。
子 Agent 的role = "sub"和 main 的role = "orchestrator"是路由层识别的依据。main 在派发时只认子任务里写的目标 Agent 名,不会动态创建新 Agent,这一点和 excerpt 里“固定 6 个、不可动态新增”的原则一致。
4. 任务状态机与任务表字段
串行调度能不能跑稳,一半取决于状态机设计得够不够细。我用的是这套:pending → running → succeeded → failed → timeout → stalled → aborted。pending 是已生成未派发,running 是当前唯一活跃态,succeeded 和 failed 是终态,timeout 是超时,stalled 是假死,aborted 是人工或 main 主动终止。
任务表至少要留这几个字段,缺一个后面排障都会难受:
| 字段 | 说明 |
|---|---|
| subtask_id | 子任务唯一标识,建议 main 生成时带序号 |
| target_agent | 对应子 Agent 名,如 devops |
| status | 当前状态,取值见状态机 |
| updated_at | 最近更新时间,判断假死的依据 |
| retry_count | 已重试次数,配合 retry_limit |
| summary | 返回摘要,汇总阶段用 |
updated_at这个字段特别关键。轮询器判断假死,本质就是看updated_at多久没变、以及这期间返回的内容是不是只有心跳。如果只靠“有没有返回”来判断,一个一直回心跳的 Agent 会被误判成正常。
5. 一次串行调度的完整验证动作
配置写好后,别急着上复杂任务。先用一个三段式的小任务验证链路:检索 → 预研 → 成稿。这个链路依赖清晰,任何一段出问题都能立刻定位。
第一步,启动 OpenClaw 并加载配置:
openclaw run --config ./config.toml --log-level debugdebug 日志会打印每次派发和轮询,验证阶段非常有用。
第二步,在对话里下发任务,措辞尽量明确,让 main 容易拆:
请完成一个关于“边缘设备上小模型推理延迟优化”的简报。 先检索近一年的公开资料,再做技术预研,最后整理成 500 字成稿。第三步,观察日志里的派发顺序。正常情况你会看到类似这样的流转:
[main] split into 3 subtasks: retrieval -> research -> content [main] dispatch subtask#1 -> retrieval status=running [scheduler] poll #1 subtask#1 status=running [scheduler] poll #2 subtask#1 status=succeeded [main] dispatch subtask#2 -> research status=running [scheduler] poll #1 subtask#2 status=running [scheduler] poll #2 subtask#2 status=succeeded [main] dispatch subtask#3 -> content status=running [scheduler] poll #1 subtask#3 status=succeeded [main] all subtasks done, aggregating...关键验证点有三个:同一时刻只有一个status=running;下一个子任务的派发一定发生在上一个succeeded之后;最后 main 汇总时能拿到三段 summary。如果日志里出现两个 running 并存,说明serial_only没生效,回去检查配置段名有没有写错。
第四步,确认从 Agent 回传结果。可以在汇总输出里核对,成稿内容是否引用了检索和预研的结论。如果成稿是凭空生成的,说明 summary 没正确传递,检查任务表的summary字段有没有在派发下一段时带上。
6. 本篇常见错排查
报错一:serial_only不生效,多个子任务同时 running。最常见原因是配置段名写成了[agent.main]而不是[agents.main],OpenClaw 读不到就退回默认并发。另一个可能是你的版本较旧,不支持该字段,升级后再试。
报错二:子任务一直停在 running,轮询不推进。先看updated_at有没有更新。如果一直不变且超过poll_interval × stall_threshold,说明触发了假死但纠偏没生效。检查retry_limit是否为 0,为 0 时不会重试,直接标记 failed。
报错三:main 把任务拆得太碎,串行跑很久。调小max_subtasks,或者在 main 的提示里明确“最多拆 3 段”。拆解粒度是 main 模型决定的,配置只能设上限,不能强制下限。
报错四:某个子 Agent 返回内容与任务无关。这通常不是调度问题,而是该 Agent 的模型选得太弱或提示词没约束好。把retrieval这类轻量 Agent 换成指令遵循更强的模型试试,或者在该 Agent 的 desc 里补一句职责边界。
报错五:接入层 401 或超时。回到第 2 节,先用模型对话页确认 Key 和基址可用。如果对话页正常但 Agent 报错,检查 config.toml 里base_url有没有多写斜杠或漏写。
7. 长期跑编码链路,建议单独配 Coding Plan
如果你这套主从调度主要是为了长期跑代码类任务——比如 devops 和 toolkit 两个 Agent 反复做实现、修复、部署——那单次调用按量计费在长链路上成本会不太可控。这种情况可以看下 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,它更适合固定周期内高频调用的编码场景。
接入细节和字段说明如果和你的 OpenClaw 版本对不上,以接入文档为准:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。文档里对 provider 段和 Agent 段的字段有完整列表,遇到配置不生效时对照一遍通常能定位。
最后留一个我踩过的坑:串行调度下,main 的汇总阶段不要让它再调用子 Agent。汇总就是纯文本聚合,一旦 main 在汇总时又派发任务,链路会多出一段不可控的尾巴。把汇总逻辑写死在 main 的提示里,明确“只汇总,不派发”,这条链路才算真正闭环。