子代理的核心价值是独立上下文窗口——避免中间结果污染父级、接收摘要而非完整记录、可定制独立 system prompt 和工具列表,给出了 Search-and-distil、并行调查、需隔离的评审角色等具体场景判断。
一旦一个 agent 起作用,下一步显而易见就是搞更多 agent——一个研究员、一个写手、一个评审员,每个都有各自的 prompt 和工具。
有时候这样做是对的。但更多时候,这只是一个伪装成 agent 的工具调用——你把一个函数变成了一个完整的额外模型循环,有自己的轮次、自己的预算、自己迷失的机会。
真正值得问的问题很简单:sub-agent 相比工具,到底买到了什么?
它真正买到的是:独立的上下文窗口
这就是全部。一个 sub-agent 有自己的消息历史,所以:
产生大量中间文本的工作不会污染父级 父级收到的是摘要,而不是完整对话记录 sub-agent 获得一个专注的系统 prompt 和一个小工具列表
相比之下,工具调用把它的整个结果放入父级的上下文,之后每一轮都会重新发送。
这个框架给出了一个可用的规则。如果工作产生的中间材料远多于最终答案,隔离就是值得的。如果结果很小且工作是确定性的,它就是一个工具。
三个值得用 sub-agent 的场景
搜索并提炼。读取十二份文档,返回一段话。如果是内联,这十二份文档会在父级的窗口中保留到整个运行结束,而且每一轮你都要为此付费。
独立并行工作。三个互不依赖的调查任务。独立的上下文让它们可以并发运行,而不会交织成一段令人困惑的历史。
真正不同的姿态。一个不能看到作者推理过程的评审者,或者一个被故意只给予论点而不给来源的核查者。隔离本身就是特性——正是它让第二种意见变得独立。
四个不值得用 sub-agent 的场景
确定性工作。解析、格式化、验证、用已知参数调用 API。这是一个函数。模型夹在中间只会增加延迟、成本和方差,毫无意义。
带有包装 prompt 的单一工具调用。如果 sub-agent 的工作是"调用 get_order 并告诉我状态",你构建的只是一个昂贵的别名。
需要父级完整上下文的工作。如果你发现自己把大部分对话都传给了 sub-agent,隔离就没有发生,你只是在为相同的 token 付两次钱。
任何在延迟路径上的东西。sub-agent 是一个完整的循环:多个模型调用,顺序执行。对于一个需要秒级响应的交互功能,这种延迟是无法隐藏的。

从父级的角度看,一个 sub-agent 应该看起来像一个工具。相同的接口,不同的实现。
export type SubAgentSpec<S extends z.ZodType, R extends z.ZodType> = {
name: string;
description: string;
input: S;
output: R; // the contract that keeps the summary small
system: string;
tools: readonly string[]; // narrower than the parent's
limits: { maxTurns: number; maxCostUsd: number };
};
export function asTool<S extends z.ZodType, R extends z.ZodType>(
spec: SubAgentSpec<S, R>,
) {
return {
name: spec.name,
description: spec.description,
schema: spec.input,
async run(args: z.infer<S>, ctx: Ctx) {
const child = childCtx(ctx, spec);
const out = await runAgent(render(spec, args), child, spec.limits);
return spec.output.parse(out.structured); // small, typed, validated
},
};
}
output 是让这一切工作的关键部分。没有 schema,sub-agent 返回任意长度的 prose,你为之付出的隔离就径直泄漏回父级窗口。
const researcher = asTool({
name: "research_topic",
description:
"Investigate a topic across internal docs and return findings. " +
"Use for open questions needing several sources. Not for a single lookup.",
input: z.object({ question: z.string().min(10) }),
output: z.object({
findings: z.array(z.object({
claim: z.string().max(300),
sourceIds: z.array(z.string()).min(1),
})).max(6),
gaps: z.array(z.string()).max(3),
}),
system: RESEARCHER_PROMPT,
tools: ["search_docs", "fetch_doc"],
limits: { maxTurns: 8, maxCostUsd: 0.20 },
});
.max(6) 和 .max(300) 不是装饰——它们是"父级拿到摘要"的强制执行。
继承预算和能力,绝不扩展
两个不变式防止 sub-agent 变成一个逃逸出口:
function childCtx(parent: Ctx, spec: SubAgentSpec<any, any>): Ctx {
const caps = parent.caps.filter((c) => spec.tools.includes(capToTool(c)));
return {
...parent,
caps, // subset, never a superset
budget: parent.budget.child(spec.limits.maxCostUsd),
depth: parent.depth + 1,
runId: `${parent.runId}.${spec.name}`,
};
}
能力收窄。预算从父级的分叉出来,所以一个 sub-agent 花了 20 美分,父级就少 20 美分,而不是一个新的配额:
child(limitUsd: number): Budget {
const cap = Math.min(limitUsd, this.remaining);
const b = new Budget(cap, this.maxCalls);
b.onSpend = (usd) => this.record(usd); // charges flow upward
return b;
}
没有这个,三个各带"小预算"的 sub-agent 实际上是一场没有上限的运行。
还要有深度限制,因为能生成 sub-agent 的 sub-agent 迟早会出问题:
if (parent.depth >= 2) throw new TooDeep(parent.depth);
两级覆盖了我见过的所有合法场景。再深几乎必然是缺工具。

并行处理,同时处理失败情况
const results = await Promise.allSettled(
topics.map((t) => limit(() => researcher.run({ question: t }, ctx))),
);
const findings = results.flatMap((r) =>
r.status === "fulfilled" ? r.value.findings : []);
const failed = results.filter((r) => r.status === "rejected").length;
if (failed) {
parentMessages.push({ role: "user", content:
`${failed} of ${topics.length} investigations failed. Continue with what you have.` });
}
用 allSettled 而不是 all——一个 sub-agent 触达它的轮次上限不应该丢弃另外两个成功的结果。告诉父级有多少失败了,这才能阻止它把部分答案当作完整答案来呈现。
限制并发数量:三个各八个轮次的 sub-agent 最多会有二十四个并发模型调用。
可观测性需要这棵树
logger.info("subagent finished", {
parentRunId: ctx.runId,
spec: spec.name,
turns: out.turns,
costUsd: out.costUsd,
outcome: out.status,
});
将 runId 构造成 parent.child 的形式,意味着一条日志查询就能重建整棵树。没有它,sub-agent 的调用显示为互不相关的运行,"为什么这个请求花了 40 美分"就成了无法回答的问题。
值得关注的指标是 sub-agent 成本占运行成本的比例。超过一半,父级就变成了一个路由器——如果是有意为之则没问题,如果不是,那就是过度分解的信号。
生成之前,先做这个测试
先把 sub-agent 的工作写成一个函数签名。
research(question: string): Promise<{ findings: Finding[]; gaps: string[] }>
如果能用一次搜索调用加一些过滤来实现,就那样做。如果它真的需要阅读、判断、追踪线索、决定何时停止——那它才需要一个循环,而循环需要自己的上下文。
大多数"sub-agent"过不了这个测试。能通过的那些通常是搜索并提炼类的,而且它们确实值得花每一分钱。
《AI That Plans》涵盖多 agent 结构——何时分解有帮助、监督者和工作者拓扑、预算和能力继承、以及保持整棵树可观测。
