跳到正文

TECH NOTE

7175 字 36 分钟 系列 19 / 22

02 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 多挂一个中间件。三步:

  1. 装解释器运行时:pip install "langchain-quickjs>=0.2.0"(Python 3.11+)。
  2. 在 create_deep_agent(...) 的 middleware=[...] 里挂上 CodeInterpreterMiddleware()。
  3. 在 subagents=[...] 里配好子智能体——它们的 name 就是代码里 subagentType 能填的取值,description 是模型判断”该找谁”的依据。

原文档给了 7 个模型厂商的版本(Google / OpenAI / Anthropic / OpenRouter / Fireworks / Baseten / Ollama),差别只在 model= 那一行;这里只保留 OpenAI 版:

from deepagents import create_deep_agent
from 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.parse
const 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 the eval tool),而不是去拦 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_agent
from 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 和生命周期行为在正式版前都可能变——迁移时记得锁版本。


小结#

  1. 动态子智能体 = 把编排逻辑从提示词搬进代码。 模型不再”一次一个”地调 task 工具,而是在解释器里写 JS:循环、分支、Promise.all、投票、排序。
  2. 机制上只多了一个零件:CodeInterpreterMiddleware() 挂上、subagents 非空,解释器里就出现内置的 task() 全局函数,入参 description / subagentType / responseSchema,一次调用跑完一整轮子智能体的 agent loop。
  3. 它和 task 工具是同一个能力的两种用法:同一批子智能体、同一套配置,区别只在谁发起、并发几个、结果以什么形态回来。但 task() 不逐次走审批路径。
  4. 六种模式共享一套姿势:把工作握在变量里 → 用 task() 派活 → 在代码里 filter/sort/投票收敛。分类、扇出是”把活干完”;对抗验证、生成筛选、锦标赛、循环到达标是”用 token 换质量”。
  5. 两个配套能力:tools.*(PTC,默认关,需白名单)负责”发现输入”,mode="thread" 负责”跨轮持久化解释器变量”;底层思路来自递归语言模型(RLM)。
  6. 本机不可用:deepagents 0.7.5 包内无解释器运行时、langchain_quickjs 未安装,官方标注 beta。本篇价值在于编排思路,代码骨架可平移到 LangGraph 或普通 Python。

一句话选型#

一次委派、职责隔离 → 用 01 篇 的静态子智能体;一批活要遍历完、要多视角互验、要按结果排序筛选 → 用代码编排。

判断标准很简单:如果”派几次、派给谁”这件事你希望它是确定的,就把它写进代码。


延伸阅读:01 静态子智能体配置面 | 03 异步子智能体 | 04 Subagents 模式原理 | 14 底层机制与抓包

02 Dynamic subagents:用代码编排子智能体(解释器运行时)
https://aicooleo.cn/blog/posts/deepagents-02-dynamic/
作者
aicooleo
发布于
2026-09-01
许可协议
CC BY-NC-SA 4.0
多 Agent 精讲 · 第 19 / 22 篇 返回合集