TECH NOTE
7175 字 36 分钟 系列 19 / 2202 Dynamic subagents:用代码编排子智能体(解释器运行时)
ON THIS PAGE / 目录21 节
一句话讲透:静态子智能体像给主管一本通讯录,让他自己一个一个打电话;动态子智能体像主管直接写了一段脚本——打给谁、打几个、谁先谁后、结果怎么比,全由脚本说了算,主管只负责最后拍板。
先分清:静态子智能体 vs 动态子智能体
01 篇 讲的是静态子智能体的配置面:subagents=[...] 怎么写、name/description/system_prompt 各管什么、mode 怎么选。那一篇里,派活这个动作是模型做的——模型看到一个 task 工具,自己决定调谁、调几次、什么时候停。
本篇完全换了一个位置:派活这个动作交给代码做。
| 维度 | 静态(01 篇) | 动态(本篇) |
|---|---|---|
| 谁决定派谁 | 模型:每次一次 task 工具调用 | 代码:task() 在一段 JS 循环 / 并发里被调用 |
| 派几次 | 模型边看结果边决定 | 代码写死,比如 files.length 次 |
| 并发 | 串行,一次一个 | Promise.all 并发一批 |
| 结果怎么处理 | 文本回灌对话,模型自己记 | JS 值,可以 filter / sort / 投票 |
| 适合 | 单次委派、职责隔离 | 批量、多视角、递归、需要明确收敛 |
一句话:静态 = 模型决定派谁;动态 = 代码决定什么时候派、派几个、怎么收敛。
底层机制(subagents= 本质上就是一个 task 工具)见 14 篇 和模式原理 04 篇;异步版见 03 篇。
它解决什么问题
静态派活有三个绕不过去的局限:
① 一次只能派一个。 模型调用 task 工具,等结果回来,再决定下一次调用。审查 20 个文件就是 20 个串行的来回,墙钟时间线性叠加。
② 派几次、按什么顺序,由模型决定。 “把这批工单都处理一遍”这种需求,模型可能处理 8 个就”觉得差不多了”,也可能重复处理同一个。这是概率行为,不是确定性行为——你没法保证它一定遍历完 src/routes/ 下的每个文件。
③ 子智能体之间不能互相比较、筛选。 模型拿到的是一堆文本消息。让它”从 5 个方案里按这个标准挑最好的”,它只能靠”读一遍再拍脑袋”。它没法真的 sort,也没法真的做多数投票。
换成代码编排,换来的是三件事:
- 确定性:循环次数、并发度、停止条件都写在代码里,可复现、可单测。
- 并发:
Promise.all一次把 N 个task()同时发出去,墙钟时间从 N× 变成约 1×。 - 可计算的结果处理:结构化输出直接是 JS 对象,能在代码里排序、去重、投票、打分——真正意义上的”结果收敛(reduce)”。
代价也很直白:编排逻辑从提示词搬到了代码里,出问题时要看代码而不是看提示词;而且并发和循环都是 token 放大器,失控的 while 和无限扇出能把账单打穿。
整体机制
graph TB U["用户任务<br/>User Task"] --> S["主管智能体<br/>Supervisor"] S -->|"写一段 JS 交出去"| I["解释器 Interpreter<br/>QuickJS 运行时"] I --> C{"代码编排<br/>Code Orchestration<br/>循环 / 分支 / Promise.all"} C -->|"task()"| A1["子智能体 A<br/>Subagent A"] C -->|"task()"| A2["子智能体 B<br/>Subagent B"] C -->|"task()"| A3["子智能体 C<br/>Subagent C"] A1 --> R["结果收敛<br/>Reduce / Synthesize<br/>filter / sort / 投票"] A2 --> R A3 --> R R --> F["最终答案<br/>Final Answer"]这张图在说什么:
① 主管不再直接调子智能体,而是写一段 JavaScript 丢给解释器(Interpreter)——也就是把”怎么编排”这件事从模型脑子里搬进代码里。
② 解释器里的代码负责编排(Code Orchestration):循环、分支、Promise.all 并发,通过内置的 task() 把活派给子智能体(Subagent)。派几个、什么顺序、要不要重试,全由这段代码说了算。
③ 子智能体返回的是 JS 值,不是对话文本,所以代码能做真正的收敛(Reduce)——过滤、排序、投票、打分——最后只把收敛后的结论交回主管。
换成 ASCII 看形状:
用户任务:"审查 src/routes/ 下每个文件,汇总 top 风险" │ ▼ ┌────────────────────────────┐ │ 主管 Supervisor(LLM) │ 产出:一段 JS 代码 └─────────────┬──────────────┘ │ eval ▼┌──────────────── 解释器 Interpreter(QuickJS)────────────────┐│ files = (await tools.glob({pattern:"src/routes/**/*.ts"})) ││ .split("\n").filter(Boolean); ← PTC 打开才有 ││ ││ reviews = await Promise.all( ← 代码决定并发几个 ││ files.map(f => task({ description: `Review ${f}`, ││ subagentType: "reviewer" }))); ││ issues = reviews.flatMap(r => r.issues).sort(bySeverity); ││ ← 结果收敛 Reduce│└──────────────────────────────┬───────────────────────────────┘ ┌──────────┬────────────┴────────────┬──────────┐ ▼ ▼ ▼ ▼ reviewer reviewer ...... reviewer reviewer file 1 file 2 file N-1 file N └──────────┴────────────┬────────────┴──────────┘ issues(已排序、已去重)→ 最终答案 → 主管 → 用户⚠️ 版本要求:解释器(interpreter)运行时目前是 beta,官方明确说 API 和生命周期行为可能随版本变化。它依赖
langchain-quickjs>=0.2.0和 Python 3.11+。本机实测(2026-09-01):本机
deepagents是 0.7.5,包内没有解释器运行时——deepagents包下不存在interpreters相关模块(deepagents.interpreters的模块 spec 为None),顶层导出里也没有任何解释器 /CodeInterpreter相关符号;langchain_quickjs也未安装。所以动态子智能体在本机不可用,下面的 Quickstart 和模式代码都跑不起来,需要pip install langchain-quickjs并升级到包含解释器运行时的深版本(beta)后再试。本篇的价值不在”照抄能跑”,而在于讲清编排思路与六种模式。这些代码骨架可以直接迁移到任何”代码编排 + 调用 Agent”的实现上——用 LangGraph 自己搭图,或者就用普通 Python 写循环和线程池,思路完全一样。
快速上手(Quickstart)
动态子智能体不是一套新 API,而是给已有的 agent 多挂一个中间件。三步:
- 装解释器运行时:
pip install "langchain-quickjs>=0.2.0"(Python 3.11+)。 - 在
create_deep_agent(...)的middleware=[...]里挂上CodeInterpreterMiddleware()。 - 在
subagents=[...]里配好子智能体——它们的name就是代码里subagentType能填的取值,description是模型判断”该找谁”的依据。
原文档给了 7 个模型厂商的版本(Google / OpenAI / Anthropic / OpenRouter / Fireworks / Baseten / Ollama),差别只在 model= 那一行;这里只保留 OpenAI 版:
from deepagents import create_deep_agentfrom langchain_quickjs import CodeInterpreterMiddleware
agent = create_deep_agent( model="openai:gpt-5.5", subagents=[{ "name": "reviewer", "description": "Reviews code for security issues, citing lines and severity", "system_prompt": "You are a security-focused code reviewer. Report issues with line numbers and severity.", }], middleware=[CodeInterpreterMiddleware()], # 关键:挂上解释器,task() 才会出现)说明几点:
- 只要
subagents非空、又挂了CodeInterpreterMiddleware(),解释器里就会自动多出一个全局函数task()——不需要你额外注册什么。 - 不需要额外配置就能做基础扇出:Deep Agents 内置的
general-purpose子智能体(默认子智能体)可以直接被task()调用。 - 想要专业分工,才需要自己配子智能体,把名字、描述、系统提示词写清楚。
再用一句”workflow”触发它:
result = agent.invoke({ "messages": [{"role": "user", "content": "Run a workflow that reviews every file in src/routes/ and summarizes the top risks."}]})💡 提示:“workflow” 这个词是一个有用的触发器。解释器的系统提示词把 “workflow” 当成信号——看到它就倾向于”通过解释器组织工作”,用代码里的
task()派子智能体,而不是一个一个地做模型选的工具调用。把需求描述成一个 “workflow”,就是你主动打开动态编排的拉杆。反过来,如果只是一次直接的委派,就平铺直叙地说,别用这个词,免得白白绕进解释器。
📌 注意:
dcode(LangChain 的终端编码 agent)自带了代码解释器,所以在dcode里动态子智能体开箱即用,不用自己挂中间件。
它是怎么工作的
这一段是全篇重点,把机制拆开讲。
三个零件
① 解释器运行时(interpreter runtime):一个跑在 agent 旁边的 JavaScript(QuickJS)沙箱。模型不是”调用解释器工具”,而是写一段代码交给它执行(底层是一个 eval 工具)。它平时就很有用——算数、处理大文本、保存中间变量;挂了子智能体之后,它多了一个能力:在代码里派活。
② task() 全局函数:解释器里的内置入口,等价于”从代码里调用子智能体”。它接收三个入参:
| 入参 | 含义 |
|---|---|
description | 交给子智能体的提示词 |
subagentType | 用哪个已配置的子智能体(填 subagents=[...] 里的 name) |
responseSchema(可选) | 结构化输出的 JSON Schema |
一次 task() 会跑完一整轮完整的 agent loop(子智能体自己思考、调工具、再思考),然后 resolve 出这个子智能体的最终结果。
③ 子智能体(subagent):还是 01 篇 里那套配置——只不过在静态模式里它是模型手上的一个 task 工具,在动态模式里它是代码里的一个函数调用。
它和 task 工具到底差在哪
这是最容易被含糊过去的地方。两者最终跑的是同一批子智能体,但发起方和结果形态完全不同:
| 维度 | task 工具(静态) | task() 函数(动态) |
|---|---|---|
| 谁发起 | 模型,每轮一次工具调用 | 解释器里的 JS 代码 |
| 一次能派几个 | 一个 | 想几个就几个(Promise.all) |
| 次数谁定 | 模型 | 代码(files.length、while 条件) |
| 并发的实现 | 靠框架 / 无 | JS 原生并发原语 |
| 返回什么 | 文本,回灌进对话上下文 | JS 值,可以 filter / sort / 投票 |
| 结构化输出 | 需要额外解析 | 传 responseSchema 即可 |
| 审批链路 | interrupt_on 逐个生效 | task() 在 eval 里派发,不逐次经过审批 |
看一段完整形态的 task():
const review = await task({ description: "Review src/auth/login.ts for auth issues. Cite line numbers.", subagentType: "reviewer", responseSchema: { type: "object", properties: { issues: { type: "array", items: { type: "object", properties: { file: { type: "string" }, line: { type: "number" }, severity: { type: "string" }, description: { type: "string" }, }}}, }, },});
// 带 responseSchema 时,拿到的已经是类型化对象,不需要 JSON.parseconst critical = review.issues.filter((issue) => issue.severity === "high");关键细节:只有子智能体故意返回了一个 JSON 字符串时,你才需要 JSON.parse;只要传了 responseSchema,resolved 出来的就是现成的类型化对象。这是”结果能在代码里算”的前提。
为什么”确定性”是重点
原文的原话是:一个跨很多独立单元的任务(审查目录下的每个文件、批量分诊工单),变成一个把工作扇出去的循环,于是它确定性地运行,而不是”一次一个由模型挑选的工具调用”。
这不只是性能问题。串行 20 次工具调用,中间任何一次模型判断失误都可能提前收工;而 files.map(...) 的循环次数是 files.length,不依赖模型心情。
两个配套能力
递归语言模型(RLM, Recursive Language Models):出自论文 Recursive Language Models。做法是——把工作集放在解释器变量里,按需切出片段,用 task() 派子智能体处理,再把结果汇总。本文这六种模式,本质上都是这个思路的不同切法。它的价值在于:你不用把 200 个文件塞进上下文,只把”当前这一片”喂给子智能体。
程序化工具调用(PTC, programmatic tool calling):很多编排流程要先”发现输入”(列出文件、筛出待处理项),这就需要从解释器代码里调普通工具,也就是 tools.*:
const files = (await tools.glob({ pattern: "src/routes/**/*.ts" })) .split("\n") .filter(Boolean); // 在代码里发现 / 过滤输入const reviews = await Promise.all(files.map((f) => task({ // 再用 task() 派活 description: `Review ${f}`, subagentType: "reviewer",})));PTC 默认是关的,必须在解释器中间件上给显式白名单才开——比如 CodeInterpreterMiddleware(ptc=["glob"])。不开白名单,解释器里的代码就看不见文件系统。
task() 和 PTC 是一对对称的能力桥:tools.* 是”代码调到工具”,task() 是”代码调子智能体”。安全默认值、审批边界、中间件选项见官方 Interpreter 页的 Security / Configuration 小节。
📌 注意:用
mode="thread"(默认值)时,解释器变量可以跨多轮对话持久化。也就是说上一轮存在解释器里的seen、found这些变量,下一轮还在——多轮编排能接着上一轮的进度干。
🧭 笔记补充的示意伪代码(非原文):本机跑不了动态子智能体,但下面这套”代码编排 + 调用 Agent”的骨架是通用的——换成 LangGraph 或普通 Python 一样成立:
# 纯文本伪代码:同一套编排思路,与语言和框架无关files = discover_inputs("src/routes/**/*.ts") # 发现输入(对应 tools.glob)reviews = parallel_map(files, limit = 4, # 并发要有上限(对应 Promise.all) fn = call_subagent("reviewer", 结构化输出))issues = flatten(r.issues for r in reviews) # 显式收敛(对应 flatMap)issues = dedupe(sort_by_severity(issues)) # 排序 + 去重return issues # 收敛后的结论才是答案⚠️ 警告:
task()是在一个已经在运行的eval调用内部派发的。它不走常规的工具调用路径,所以父 agent 上配的interrupt_on审批流程不会对每一次派发生效。如果你需要在子智能体编排开始前有人点”同意”,正确做法是拦截eval工具本身(gate theevaltool),而不是去拦task。
六种编排模式
原文的 Patterns 一节说得很清楚:这些模式不是配置出来的,是模型根据任务形状自己写的解释器代码”长”出来的;你能控制的只有”放哪些子智能体进去”。但六种模式共享同一套编排姿势:把工作握在 JS 变量里 → 用 task() 派子智能体 → 在代码里合并结果。
下面逐个讲,每种给:适用场景 / 代码骨架 / 为什么这么写 / 代价。
分类后分发(Classify and act)
先说结论:先给每个 item 打标签,再按标签路由给对应的专家。
适用场景:客服工单分诊、错误日志归类、用户反馈分流——凡是”一锅里混了好几种类型”的批量输入。
流程:items → 分类器 → bug 交给 bug-fixer / feature 交给 feature-analyst / question 交给 support-agent → 汇总成分诊报告
需要的子智能体:bug-fixer(调查 bug 报告并给出复现步骤)、feature-analyst(评估需求可行性与工作量)、support-agent(基于文档回答用户问题)。
// 前提:tickets 已经带上 category 字段(分类可以在同一段代码里先派一个分类子智能体做)const SPECIALIST = { bug: "bug-fixer", feature: "feature-analyst", question: "support-agent" };
const handled = await Promise.all( tickets.map((ticket) => task({ description: `Handle this ${ticket.category}:\n${ticket.text}`, subagentType: SPECIALIST[ticket.category], }), ),);// ... 把 handled 按 category 分组,拼成一份分诊报告handled;为什么这么写:路由表 SPECIALIST 就是一张普通的 JS 对象——改路由不用改模型提示词,加一个类型加一行就行。Promise.all 让不同专家并行开工,而且每个 item 只被处理一次。
代价:分类错了整条就错了,而且没人复核。新类型(比如 refund)进来时路由表里没有键,subagentType 会是 undefined——要么补兜底键,要么加一道人工抽检。
扇出后汇总(Fan-out and synthesize)
先说结论:同一件活复制 N 份并行干,干完在代码里合并。
适用场景:整个目录的代码审查、批量文档分析、日志文件处理、同一项检查跑遍多个服务。
流程:items → worker ×N(并发)→ collect → synthesize → 一份合并结论
需要的子智能体:reviewer(安全审查,标明行号与严重级别);中间件要开 PTC:CodeInterpreterMiddleware(ptc=["glob"])。
// 每个文件派一个 reviewer 并行审,最后把发现合并const files = (await tools.glob({ pattern: "src/routes/**/*.ts" })) .split("\n") .filter(Boolean);
const reviews = await Promise.all( files.map((file) => task({ description: `Review ${file} for authentication issues. Cite line numbers.`, subagentType: "reviewer", responseSchema: issuesSchema, // -> { issues: [{ file, line, severity }] } }), ),);
const issues = reviews.flatMap((r) => r.issues);// ... 按严重级别排序、去重,总结 top 风险issues;为什么这么写:两点。一是 tools.glob —— 从解释器代码里发现文件必须开 PTC,并且把 glob 加进 ptc=[...] 白名单,否则代码看不见文件系统,只能靠模型把文件列表贴进来(那就退化了)。二是 flatMap —— 每个子智能体返回的是 { issues: [...] } 这种结构,得先摊平才能统一排序去重。
代价:N 个文件 = N 个并发 agent loop,token 和速率限制都是线性涨的。绝大多数”扇出事故”都出在这里没分批。
对抗式验证(Adversarial verification)
先说结论:两趟。第一趟出结论,第二趟让独立的验证者逐条挑刺,只有活下来的才留。
适用场景:误报代价很高的安全审计、合规检查,以及任何”宁缺毋滥、置信度比速度重要”的复核。
流程:items → workers → findings → verifier ×N → 多数投票 → confirmed
需要的子智能体:reviewer(找潜在漏洞,报文件/行号/描述)+ verifier(独立验证某个漏洞是否真的可利用,提示词要求 “Be skeptical”,只确认真实问题)。
// 第一趟:审计;第二趟:逐条独立验证,只保留被确认的const { findings } = await task({ description: "Audit the payments module for vulnerabilities.", subagentType: "reviewer", responseSchema: findingsSchema, // -> { findings: [{ id, file, line, description }] }});
const verdicts = await Promise.all( findings.map((f) => task({ description: `Verify ${f.file}:${f.line} (${f.description}). Confirm or refute.`, subagentType: "verifier", responseSchema: verdictSchema, // -> { confirmed: boolean } }), ),);
const confirmed = findings.filter((_, i) => verdicts[i]?.confirmed);// ... 只报告被确认的漏洞confirmed;为什么这么写:验证者用的是另一个子智能体(verifier 而不是 reviewer),提示词也明确要求怀疑态度——独立性才有意义,同一个人复核自己会一路点头。原文的图是三个 verifier 加多数投票;代码骨架为了短只放了一个 verifier per finding,要投票就在 Promise.all 里再套一层,按 confirmed 的比例判。注意 findings.filter((_, i) => verdicts[i]?.confirmed) 里用了 ?.——验证失败留下 undefined 时不会被误判为确认。
代价:成本至少翻倍——findings 数量 × 验证者数量 次 task()。而且验证者本身也会犯错,它只是把”单点判断”换成”多数判断”,不是把错误变成零。
生成后筛选(Generate and filter)
先说结论:让多个子智能体各自独立出方案,再在代码里打分、排序、留最好的。
适用场景:架构方案、重构策略、内容变体——凡是”先多探索再定稿”收益明显的任务。
流程:prompt → generator ×3 → filter + rank → best result
需要的子智能体:architect(给出 schema 设计,附权衡分析、迁移考虑和理由)。
// 并发生成多个独立方案,再按需求打分留最优const proposals = await Promise.all( [1, 2, 3].map((n) => task({ description: `Approach ${n}: redesign the orders schema, with tradeoffs.`, subagentType: "architect", responseSchema: designSchema, // -> { design, tradeoffs } }), ),);
// ... 按需求给每个方案打分const best = proposals.sort((a, b) => score(b) - score(a))[0];best;为什么这么写:生成阶段故意不让生成者看到彼此的方案——独立性带来多样性,互相抄就会收敛到同一个平庸答案。筛选阶段放在代码里,因为规则要可见可测。responseSchema 是这一模式的命门:没有结构化输出,你拿到的是三段散文,根本没法 sort。
代价:N 倍生成成本加一次筛选成本。另外 score() 在原文里是省略号——打分可以再派一个子智能体,也可以是代码里的确定性规则;如果打分也交给模型,那这个模式就变成了下面的锦标赛。
锦标赛(Tournament)
先说结论:变体两两 PK,赢的进下一轮,直到只剩一个冠军。
适用场景:主观标准下的择优——代码风格选择、文案变体、在两个能跑的竞争实现里挑一个。
流程:attempt ×5 → judge(两两对战)→ ... → winner
需要的子智能体:writer(重写函数,以可读性为目标,并解释选择)+ judge(比较两份实现,选更可读的那个,并要求给出具体评判依据)。
// 先生成变体,再两两判胜负,直到决出唯一冠军let bracket = await Promise.all( [1, 2, 3, 4, 5].map((n) => task({ description: `Rewrite processOrder for readability (variant ${n}).`, subagentType: "writer" }), ),);
while (bracket.length > 1) { const winners = []; for (let i = 0; i < bracket.length; i += 2) { if (bracket[i + 1] === undefined) { winners.push(bracket[i]); break; } // 轮空直接晋级 const { winner } = await task({ description: `Pick the more readable:\n\nA:\n${bracket[i]}\n\nB:\n${bracket[i + 1]}`, subagentType: "judge", responseSchema: pickSchema, // -> { winner: "A" | "B" } }); winners.push(winner === "A" ? bracket[i] : bracket[i + 1]); } bracket = winners;}bracket[0]; // 冠军为什么这么写:模型的绝对打分很不稳,相对判断稳得多——“给这三份方案各打 1-10 分”远不如”这两份哪个更好读”。淘汰赛把问题从”N 份一起排序”换成 N-1 场两两比较,轮数是对数级。代码里显式处理了奇数长度时的轮空(bracket[i + 1] === undefined)。
代价:场次 = N-1,每场都是一轮完整 agent loop(await 在 for 里,是串行的)。冠军的稳定性完全取决于 judge 提示词的质量;判官最好和选手是不同的子智能体,否则容易自我偏袒。
循环直到达标(Loop until done)
先说结论:一轮轮派活,每轮把新发现并进集合,某一轮没有新东西了就停。
适用场景:穷尽式搜索、死代码检测、依赖审计——凡是”要的是完整,不是固定条数”的清扫任务。
流程:agent → 有新发现?→ 是:再来一轮 / 否:done
// 反复派活,每轮和"已找到的"去重,直到某一轮没有新东西const seen = new Set();const found = [];
while (true) { const { items } = await task({ description: `Find dead code. Already found: ${[...seen].join(", ") || "(none)"}.`, subagentType: "analyzer", responseSchema: itemsSchema, // -> { items: [{ id, file }] } }); const fresh = items.filter((i) => !seen.has(i.id)); if (fresh.length === 0) break; // 收敛:没有新东西了 for (const i of fresh) { seen.add(i.id); found.push(i); }}found;为什么这么写:把”已经找到的东西”回灌进 description,子智能体才知道该往哪儿找新的,否则它每轮都会重复报同一批。seen 就是一个普通的 Set,去重逻辑在代码里——可解释、可调试、可单测,这是静态模式给不了的。
代价:这是六种里唯一一个轮数不确定的模式,token 消耗事先估不出来。而且去重键 id 必须稳定——如果子智能体每轮给出的 id 都不太一样,这个循环永远不会收敛。while (true) 上生产前一定要加最大轮数上限。
六种模式对照表
| 模式 | 一句话 | 什么时候用 | 主要成本 | 用 token 换质量? |
|---|---|---|---|---|
| 分类后分发 | 先打标,再按标路由 | 混合类型的批量输入 | 一次分类 + N 次处理 | 否(反而省) |
| 扇出后汇总 | 同一件活并发干 N 份再合并 | 目录级审查、批量文档、日志 | N 个并发 agent loop | 否 |
| 对抗式验证 | 二次独立复核,活下来才算 | 误报代价高的安全 / 合规审计 | findings × 验证者数量 | 是 |
| 生成后筛选 | 多方案并发生成,代码里打分 | 架构选型、方案探索 | N 倍生成 + 打分 | 是 |
| 锦标赛 | 两两 PK,淘汰晋级 | 主观标准下选最优 | N-1 场 judge(串行) | 是 |
| 循环直到达标 | 一轮轮找,没新的就停 | 穷尽搜索、死代码检测 | 轮数不可预知 | 是(换的是完整性) |
选型口径:前两种是”把活干完”,后四种是”把活干好”。前两种该用就用,收益是确定性的;后四种要先问一句”这个场景的误报/平庸方案的代价,值不值这几倍 token”。
关掉动态子智能体
只要有子智能体,动态派发就是默认开的。 如果你想保留子智能体、但只允许它们从常规的 task 工具路径被调用(模型一个个派,不接受代码编排),就在中间件上关掉:
from deepagents import create_deep_agentfrom langchain_quickjs import CodeInterpreterMiddleware
agent = create_deep_agent( model="openai:gpt-5.5", subagents=[{"name": "reviewer", "description": "Reviews code", "system_prompt": "Review code."}], middleware=[CodeInterpreterMiddleware(subagents=False)], # 关掉代码里的 task())原文档同样给了 7 个厂商的版本,差别只在 model= 一行,这里保留一版。
这个开关适合的场景:你已经有一套稳定的静态委派提示词和工作流,不想让模型某天突然”自由发挥”写一段解释器代码;或者你需要在每次派发前做审批——因为 task() 不逐次经过 interrupt_on,关掉它是拿到逐次审批最省事的办法。
常见坑
① 子智能体依然是无状态的。 每次 task() 都是全新的上下文,上一次的结论不会自动带过去。想让第二轮知道第一轮发现了什么,必须显式写进 description(就像”循环直到达标”里回灌 seen 那样),或者塞进 responseSchema 要求它自己带上。这点和 01 篇 的静态子智能体完全一致。
② 并发一定要有上限。 Promise.all(files.map(...)) 看着优雅,但 200 个文件就是 200 个同时跑的 agent loop。上线前按 chunk 分批、按 limit 限流、或者只对筛选后的输入扇出。
③ 结果必须显式收敛。 代码块最后要有一个”值”被交回去(原文每个例子的末尾都孤零零站着一个 handled; / issues; / confirmed; / best; / found;——那不是笔误,那是把结果送回模型的方式)。只循环不收敛,模型拿不到任何东西。
④ 不能把不收敛的循环留在生产。 while (true) 只适合”每轮严格去重、必然收敛”的场景。去重键不稳定、子智能体不听话、或者任务本身是开放的,都会变成烧钱黑洞。加最大轮数兜底。
⑤ task() 绕过了逐次审批。 它从 eval 内部派发,interrupt_on 不生效。要审批就拦 eval 工具本身,或者直接 subagents=False。
⑥ 没有 responseSchema 就没有”可编程的结果”。 拿到一段散文,filter / sort / 投票全都无从谈起,动态编排就退化成了”并发地要几段文本”。能用 schema 就用 schema。
⑦ PTC 默认关闭。 代码里想用 tools.* 发现输入,必须在中间件上给白名单(ptc=["glob"]),否则解释器看不见文件系统。
⑧ 这是 beta,本机也跑不起来。 本机 deepagents 0.7.5 没有解释器运行时、langchain_quickjs 未安装,所以上面的代码骨架在本机只能读不能跑。API 和生命周期行为在正式版前都可能变——迁移时记得锁版本。
小结
- 动态子智能体 = 把编排逻辑从提示词搬进代码。 模型不再”一次一个”地调
task工具,而是在解释器里写 JS:循环、分支、Promise.all、投票、排序。 - 机制上只多了一个零件:
CodeInterpreterMiddleware()挂上、subagents非空,解释器里就出现内置的task()全局函数,入参description/subagentType/responseSchema,一次调用跑完一整轮子智能体的 agent loop。 - 它和
task工具是同一个能力的两种用法:同一批子智能体、同一套配置,区别只在谁发起、并发几个、结果以什么形态回来。但task()不逐次走审批路径。 - 六种模式共享一套姿势:把工作握在变量里 → 用
task()派活 → 在代码里filter/sort/投票收敛。分类、扇出是”把活干完”;对抗验证、生成筛选、锦标赛、循环到达标是”用 token 换质量”。 - 两个配套能力:
tools.*(PTC,默认关,需白名单)负责”发现输入”,mode="thread"负责”跨轮持久化解释器变量”;底层思路来自递归语言模型(RLM)。 - 本机不可用:
deepagents 0.7.5包内无解释器运行时、langchain_quickjs未安装,官方标注 beta。本篇价值在于编排思路,代码骨架可平移到 LangGraph 或普通 Python。
一句话选型
一次委派、职责隔离 → 用 01 篇 的静态子智能体;一批活要遍历完、要多视角互验、要按结果排序筛选 → 用代码编排。
判断标准很简单:如果”派几次、派给谁”这件事你希望它是确定的,就把它写进代码。
延伸阅读:01 静态子智能体配置面 | 03 异步子智能体 | 04 Subagents 模式原理 | 14 底层机制与抓包