资讯动态

zcode源码解析 Day1:动态 Subagent 与 AIMD 并发治理器

发布时间:2026/10/18 20:44:48 来源:尧图企业网站定制
1. 从 runtime.ts 到并发治理器zcode 动态 Subagent 调度到底在解决什么问题如果你正在做多任务并发的 Agent 系统大概率踩过这两个坑一是任务一多就把上游 API 打到 429整个流程雪崩二是并发数写死在配置里流量低谷时资源闲置、高峰时又扛不住。zcode 这套源码里动态 Subagent 调度配合 AIMD 并发治理器就是专门治这两个毛病的。先把这个场景说清楚。假设你有一个代码审查工作流需要同时给 30 个文件各派一个子代理去读、去改、去验证。如果无脑并发 30 个请求供应商那边大概率直接限流如果保守到只开 2 个跑完要等半天。zcode 的做法是让运行时自己观察请求成功还是被限流动态把并发水位往上探或往下砍脚本和模型都不许插手。这套机制适合谁适合正在自建 Agent 编排框架、或者用 zcode 做多任务批处理的开发者。你不需要改模型也不需要改业务脚本只要理解治理器的输入信号和调整规则就能把并发参数调到既稳又快的状态。本文会从源码结构讲起给出可复制的并发参数配置、Subagent 注册示例以及一套压测验证步骤让你能自己复现限流来了砍下去、恢复了爬回来的过程。我试过把这套逻辑套到一个批量文档处理任务上30 个文件并发初始 cap 设 8跑起来后确实能看到 effective 被压到 5、再慢慢回到 8。下面按源码顺序拆。1.1 动态工作流计划即代码的设计思路zcode 的agent 自助规划没有单独的规划器组件它的核心思路是 plan-as-code让模型把规划直接写成一段可执行的 TypeScript 编排脚本阶段图、人员分配、验证门全部从这份代码里长出来。给模型的积木定义在packages/dynamic-workflow/src/facade/dts.ts主要 API 有这么几类积木作用agent(名字, { system: 角色 })雇一个子代理本质是一段独立的模型对话actor.ask(任务说明)给子代理派活拿到类型化结果phase(汇总并产出报告)划阶段就是确认图上的节点world.run(npm, [test])跑真实命令当验证门files.glob / read / grep、git.changedFiles / diff脚本自己读仓库决定怎么拆活report()、artifact.*汇报进度、发布交付物触发规则写得很死用户明确说了用工作流模型必须走工作流没说模型禁止自作主张开——哪怕任务再复杂。开一条要确认、要跑很久的 run决定权必须在用户嘴里。模型写完脚本不直接跑先过 strict 模式的 TypeScript 类型检查analyzeScript编不过就把诊断回给模型改。这里有两处刻意设计phase()的名字必须是编译期字面量这样确认图能在你批准前画出来world.run的命令名也必须是字面量确认窗能给你看将要执行哪些命令的白名单。编译干净后才弹确认窗展示完整脚本加阶段图用户批准后 run 才在后台开跑。1.2 子代理的注册与缓存键机制每个agent()调用创建一个持久对话上下文上下文跨任务累积同一个 actor 的多个ask排队串行执行。每个ask才是真正的一次模型生成。一个容易被忽略的细节子代理的名字是缓存键。改脚本重跑AmendWorkflow时同名的子代理已完成的工作直接复用、不花 token。所以 facade 规则要求名字稳定、有意义循环里的重复子代理要起计算出来的名字比如agent(reviewer- file)。如果你把名字写成随机数或者时间戳缓存全失效重跑一次等于全部重做token 哗哗地烧。1.3 运行中改设置三态语义与换班机制工作流跑起来后有两项设置可以改subagentModel子代理用哪个模型和maxConcurrency最多几个子代理同时干活。每项字段有三种填法含义完全不同填法含义不填省略这项不动保持原样填null清掉自定义回到默认会话模型 / 本机上限填具体值改成这个值没改这一项和要清空这一项是两码事三态就是为区分它们而生的。修改入口有两个一是人改GUI 配置面板上点「应用」不经过模型轮也不弹二次确认二是 agent 改模型调用 AmendWorkflow 工具走审批门弹窗让用户点头。两条路底层共用同一套三态解析、同一个port.amend。改动不是原地生效而是换班用同一份脚本、按新设置起一个新 run 接替旧 run旧 run 已完成的活按子代理名字作为缓存继承不重做。port.amend之前每一步失败都是零副作用旧 run 照旧在跑两项设置与现状完全一致时直接拒绝不为零改动付停一次运行中工作流的代价。2. TaoToken 前置准备拿到 Base URL、API Key 和 Model ID在跑通 zcode 的并发压测之前你需要一个能稳定调用的模型入口。TaoToken 提供 OpenAI 兼容的 APIBase URL 是https://taotoken.net/api你可以在控制台创建 API Key然后在模型列表里挑一个适合并发压测的 Model ID。具体操作路径打开 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 创建 Key复制保存再打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 查看可用模型记下你要用的 Model ID。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite里面有完整的请求示例。如果你只是想先验证模型能不能通可以用模型对话页面直接发一条消息https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。这一步能帮你排除 Key 或网络配置的问题再去跑并发压测就不会把环境问题和治理器行为混在一起。拿到三件套之后把它们写进环境变量后面所有配置都从这里读export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_MODEL_ID你的ModelID注意Base URL 不要带 UTM 参数API 调用路径就是https://taotoken.net/api后面接/v1/chat/completions这类标准路径。如果你用的是 Claude Code 这类工具配置方式略有不同需要设置ANTHROPIC_BASE_URL和ANTHROPIC_AUTH_TOKEN具体参考接入文档里的 ClaudeCodeAnthropic 章节https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。长期跑编码任务或者 Agent 工作流的话Coding Plan 会更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。3. 可复制配置AIMD 并发参数与 Subagent 注册示例这一节给你可以直接抄的配置。先看 AIMD 治理器的核心参数它们在dynamic-workflow/src/engine/concurrency.ts里定义export const CONCURRENCY_DECREASE_FACTOR 0.75; // 限流 → cap × 0.75 export const CONCURRENCY_INCREASE_STEP 1; // 加性递增步长 export const CONCURRENCY_INCREASE_AFTER_SUCCESSES 4; // 连续 4 次成功才 1 export const CONCURRENCY_FLOOR 1; // 地板永远留一个探针 export const CONCURRENCY_IDLE_RESET_MS 300_000; // 5 分钟空闲 → 失忆这五条规则用大白话说被限流就砍cap max(1, floor(cap × 0.75))注释解释了为什么不减半——闸门是请求级的一次 429 说明稍微高了很少说明高了一倍持续成功就加连续 4 次成功加队里有人等加没到天花板才 1谨慎加、大胆砍这就是 AIMD加性增、乘性减Retry-After来了就完全冷却供应商说等多久就锁多久探测失败退回已证明水位状态机记着lastGood往上试新高撞了 429 就直接退回lastGood损失只是一次试探5 分钟空闲就失忆学到的 cap 全忘掉回天花板从头学。治理器按供应商/模型分桶一个 CLI 进程里所有 run 加主代理共用一个供应商配额所以每个${providerId}/${modelId}一个桶。闸门粒度是模型请求runner 每次尝试前 acquire、结束 release。一个子代理同时只有一个在飞请求所以≤ N 个请求等价于≤ N 个子代理在跑。最终放行公式是本 run 实际并发 min(治理器 cap, 本 run 的 maxConcurrency)。治理器管供应商受得了几条maxConcurrency 管这个 run 最多许几条两者取小。下面是一个 Subagent 注册与并发配置的完整示例你可以直接放进你的工作流脚本// workflow-config.ts export const workflowConfig { maxConcurrency: 8, // 本 run 最多 8 个子代理同时跑 subagentModel: null, // null 表示回到会话默认模型 agents: [ { name: reviewer, // 稳定、有意义的名字作为缓存键 system: 你是一个代码审查专家只关注逻辑错误和安全问题。, tasks: [read-file, check-logic, report-issues] }, { name: tester, system: 你是一个测试工程师负责为指定文件生成单元测试。, tasks: [read-file, generate-test, run-test] } ] };如果你用 JSON 格式管理配置可以这样写{ maxConcurrency: 8, subagentModel: null, concurrencyGovernor: { decreaseFactor: 0.75, increaseStep: 1, increaseAfterSuccesses: 4, floor: 1, idleResetMs: 300000 } }注意subagentModel填null和省略是两回事。省略表示这项不动填null表示清掉自定义回到默认。改配置时想清楚你要哪种。如果你用 Cline MCP 或者 Codex 的auth.json接入三件套要写全Base URL 填https://taotoken.net/apiKey 填你创建的 API KeyModel ID 填你选的模型。缺任何一个都会在请求阶段报错。4. 验证请求与成功结果压测看治理器怎么呼吸配置写好后怎么验证治理器真的在动造一个大扇出的工作流几十个文件各派一个子代理跑起来盯详情页。限流一来 effective 被砍下去、原因显示rate_limited过一会儿recovered慢慢爬回来那就是这台状态机在呼吸。先写一个最小压测脚本用 Node.js 直接打请求观察并发行为// concurrency-test.mjs const BASE_URL process.env.TAOTOKEN_BASE_URL; const API_KEY process.env.TAOTOKEN_API_KEY; const MODEL_ID process.env.TAOTOKEN_MODEL_ID; async function singleRequest(id) { const start Date.now(); const res await fetch(${BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY} }, body: JSON.stringify({ model: MODEL_ID, messages: [{ role: user, content: 这是第 ${id} 个并发请求请回复 OK。 }], max_tokens: 16 }) }); const elapsed Date.now() - start; return { id, status: res.status, elapsed }; } async function runBatch(concurrency) { const tasks Array.from({ length: concurrency }, (_, i) singleRequest(i)); const results await Promise.allSettled(tasks); const ok results.filter(r r.status fulfilled r.value.status 200).length; const limited results.filter(r r.status fulfilled r.value.status 429).length; console.log(并发 ${concurrency}: 成功 ${ok}, 限流 ${limited}); return { ok, limited }; } // 从低到高试探 for (const c of [2, 4, 8, 16]) { await runBatch(c); await new Promise(r setTimeout(r, 2000)); }跑起来后你会看到类似这样的输出并发 2: 成功 2, 限流 0 并发 4: 成功 4, 限流 0 并发 8: 成功 7, 限流 1 并发 16: 成功 10, 限流 6这说明供应商的舒适区大概在 8 附近。治理器看到 429 后会把 cap 从 8 砍到floor(8 × 0.75) 6然后连续成功 4 次后加回 7、8。如果你在 zcode 的 run 详情页看会看到concurrency-changed事件带rate_limited或recovered原因。治理器每次调压都记一条concurrency-changed事件进 journalrun 详情页汇成并发现状effective实际放行数、cap本 run 的界、原因、起始时间。刻意的设计是这个指标只在effective cap时出现——跑满自己那条界的 run 没有可说的一出现就等于它正被限着。还有一个防误伤设计叫 epoch并发 10 时放出去 10 个请求可能 5 个先后撞 429。每次砍完翻一页epoch 1只有新页里发出的请求撞墙才再砍旧页的 429 只清计数、刷冷却。这样避免一次限流被重复计算、把 cap 砍到底。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth跑并发压测时最常见的几类报错和对应排查方法如下。401 UnauthorizedKey 没带对或者过期了。检查Authorization头是不是Bearer sk-xxx格式Key 有没有多余空格。如果你用的是 Claude Code检查ANTHROPIC_AUTH_TOKEN是不是设置成了你的 TaoToken Key而不是别的。重新去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 创建一个新 Key 试试。local proxy failed本地代理配置有问题。检查你的环境变量里有没有残留的HTTP_PROXY、HTTPS_PROXY指向一个不可用的地址。Base URL 应该直接是https://taotoken.net/api不要经过额外的本地转发。如果你在 Cline MCP 里配置确认 MCP server 的启动参数里没有写错的代理地址。reading choices 报错通常是响应体解析失败。可能是 Model ID 写错了供应商返回了错误结构而不是标准的choices数组。检查TAOTOKEN_MODEL_ID是不是控制台里列出的可用模型大小写要一致。也可能是max_tokens设得太小响应被截断。OAuth 相关报错如果你用 Codex 的auth.json接入确认文件里的字段名和格式正确。三件套要写全Base URL、Key、Model ID。OAuth 流程和 API Key 流程不要混用用 Key 就直接填 Key不要走 OAuth 授权。并发上不去检查maxConcurrency是不是设得太小或者治理器的 cap 被限流砍到 1 之后一直没恢复。看 run 详情页的effective和cap如果effective一直是 1说明供应商那边持续限流等Retry-After冷却结束或者降低初始并发。子代理缓存不生效检查agent()的名字是不是稳定。如果你在循环里用了随机数或者时间戳做名字每次重跑都是新名字缓存全失效。改成agent(reviewer- file)这种基于文件名的确定性名字。6. 把并发治理器用起来从压测到长期编码到这里你已经有了完整的配置、压测脚本和排障清单。接下来可以做的事把maxConcurrency从 8 调到 16再跑一遍压测看治理器多久能把 cap 从 16 砍到稳定水位或者把CONCURRENCY_INCREASE_AFTER_SUCCESSES从 4 改成 8观察恢复速度变慢后对整体吞吐的影响。如果你要长期跑编码任务或者 Agent 工作流建议用 Coding Plan配额更充足https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。需要看完整接入示例的话文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。想先验证模型响应质量用模型对话页面发几条消息就行https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。下一篇我们拆会话恢复与回退ResumeSession / Rewind 那一族类型看看 zcode 怎么在 run 中断后把上下文接回来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑