手写 Agent ⑦,Subagent,给 Agent 加分身

用 Subagent 隔离大规模代码调研,让主 Agent 只接收结论;讲解 task 工具、并行、层级限制与调用成本。

outlier 17 分钟 6625 字 0 0

开场:读完二十个文件,它忘了要干什么

有一次我接手一个陌生的项目,想先让 mini-agent 帮我摸摸底:"看看这个仓库的每个模块都是干什么的,然后告诉我,要给订单加一个'部分退款'的功能,应该改哪几个地方。"

它很认真。list_files,然后一个模块一个模块地读:order/、payment/、refund/、notify/……每个模块读三四个文件。二十多个文件读完,上下文已经涨到了十几万 token,触发了一次自动压缩(第 28 篇)。

压缩之后,它给出了每个模块的介绍,写得还不错。然后它说:"以上是各模块的概况。请问您接下来想做什么?"

它忘了我问题的后半句。 那句"要加部分退款,应该改哪几个地方",在二十多个文件的内容和一次压缩之后,已经不在它的注意力范围里了。

我把问题的后半句又说了一遍,它才开始回答,而且回答得有点含糊,因为那些文件的细节已经在压缩里丢了。我当时就想:读这二十个文件,是一件"读得多、但最后只需要一个结论"的事情。为什么要让这些文件全部堆在主对话的上下文里?如果能派一个"分身"去读,让它只把结论带回来,主对话的上下文就能一直保持干净。

这就是 subagent(子 Agent)。这一篇我们给 mini-agent 加上分身的能力。读完本篇,你应该能回答这几个问题:

  • subagent 解决的是什么问题?
  • task 工具是怎么实现的?子 Agent 和父 Agent 共享什么、不共享什么?
  • 怎么让多个子 Agent 并行干活?并行有什么限制?
  • 怎么防止子 Agent 再开子 Agent,无限套娃?
  • 多开分身的代价是什么?什么时候不该用?
  • 一个分身失败了,或者偷偷改了文件,会发生什么?

一、分身解决的问题

1.1 上下文隔离

第 14 篇讲过上下文工程的四个基本动作:写入、选择、压缩、隔离。subagent 就是"隔离"的那一个。

flowchart TB subgraph M["主 Agent 的上下文"] M1["用户:看看每个模块是干什么的,
然后告诉我加部分退款要改哪里"] M2["task:调研各模块"] M3["报告:300 字"] M4["……继续回答后半个问题"] end subgraph C["子 Agent 的上下文(用完就扔)"] C1["任务说明"] C2["读了 20 个文件
十几万 token"] C3["写报告"] end M2 -->|"派出去"| C1 C3 -->|"只带回报告"| M3 style M fill:#e8f5e9,stroke:#2e7d32 style C fill:#fff9c4

子 Agent 有一个全新的、独立的上下文。它在自己的上下文里读文件、跑命令,可能塞进去十几万 token。但这些都留在它自己那里;它完成任务后,只把最后那份报告交回给主 Agent,作为 task 工具的结果。然后它的上下文就被丢弃了。

主 Agent 的上下文里,只多了一个工具调用和一段几百字的报告。用户问题的后半句,依然清清楚楚地在那里。

1.2 本质:用多花 token 换主上下文干净

agent/subagent.go 开头的注释把这件事说得很直白:

// task 工具开一个拥有全新上下文的子循环。子 Agent 可能读了几十个文件、跑了一堆命令,
// 但这些都留在它自己的上下文里,主 Agent 只拿到最后那份报告。
// 本质上是用"多花 token"换"主上下文干净"。

它不省 token,反而更费。 这一点后面专门讲。

二、task 工具

2.1 一个工具,开一个新的 Agent

subagent 的实现出奇地简单:它就是一个工具,这个工具的 Run 方法里,新建了一个 Agent,然后调用它的 Send。

func (t *taskTool) Run(ctx context.Context, input json.RawMessage) (string, error) {
    // ……解析 description 和 prompt

    p := t.parent
    child := New(t.model, subagentPrompt+"\n\n"+t.env, t.tools, &childUI{parent: p.UI, label: in.Description})
    child.Gate = p.Gate
    child.MaxSteps = 30
    child.ContextBudget = p.ContextBudget
    child.MaxTokens = p.MaxTokens
    // ……

    report, err := child.Send(ctx, in.Prompt)
    p.addUsage(child.Usage) // 分身花的钱也是钱
    // ……
    return strings.TrimSpace(report) + stats, nil
}

child.Send(ctx, in.Prompt):子 Agent 用的就是第 24 到 29 篇写的那个 Send,同一个循环、同一套工具执行、同一套上下文管理。Agent 套 Agent,不需要任何新机制。 这是前面几篇把 Agent 设计成一个独立的、可复用的结构体所带来的回报。

2.2 工具定义

Description: "启动一个子 Agent 独立完成一项任务,它有自己全新的上下文,完成后只返回一份简短的报告。" +
    "适合:需要读很多文件才能回答的调研问题(比如\"找出所有调用了 X 的地方并总结用法\");" +
    "几个互不相关、可以同时进行的调查(在一次回复里发起多个 task 调用,它们会并行执行)。" +
    "不适合:简单的、一两次工具调用就能完成的事情——直接自己做更快更省。" +
    "子 Agent 看不到当前对话,prompt 里必须写清楚背景、要做什么、要报告什么。",

两个参数:

  • description:3 到 10 个字的任务标题,用于在界面上显示;
  • prompt:交给子 Agent 的完整任务说明。

描述里最重要的是最后一句:"子 Agent 看不到当前对话,prompt 里必须写清楚背景、要做什么、要报告什么。"

这是用 subagent 时最容易犯的错误:主 Agent 写了一个"帮我查一下那个问题"的 prompt,可子 Agent 根本不知道"那个问题"是什么。第 01 篇的老话:模型知道的一切,都在这次请求的输入里。 对子 Agent 来说,它的全部输入就是 system prompt 加上这一句 prompt。

2.3 子 Agent 的 system prompt

const subagentPrompt = `你是一个子 Agent,主 Agent 把一项具体任务委派给了你。你看不到主 Agent 和用户之间的对话,
你知道的只有下面的任务描述。

<rules>
- 只做任务描述里要求的事,做完就停。
- 你的最终回答会原样交给主 Agent,它看不到你中间读过的文件和命令输出。
  所以最终回答要自成一体:结论、关键证据(文件路径和行号)、不确定的地方。
- 报告要精炼,通常不超过 300 字。不要贴大段代码,引用位置就够了。
</rules>`

这段 prompt 解决的是报告的质量。子 Agent 的报告会作为工具结果进入主 Agent 的上下文,所以:

  • 要自成一体:主 Agent 看不到子 Agent 读过的东西,报告里说"如上所述",主 Agent 不知道"上"是什么;
  • 要带证据:文件路径和行号。主 Agent 如果需要细节,可以自己去读那几行,而不用重新调研一遍;
  • 要精炼:不超过 300 字,不贴大段代码。否则隔离就失去了意义,子 Agent 把十几万 token 的内容又原样搬了回来。

后面拼上的 t.env,是第 24 篇的环境信息(工作目录、操作系统、日期),主 Agent 和子 Agent 共用。

2.4 共享什么,不共享什么

flowchart LR subgraph S["共享"] S1["模型(可以换成更便宜的,第 37 篇)"] S2["权限策略 Gate"] S3["上下文预算、max_tokens"] S4["记账(trace)"] end subgraph N["不共享"] N1["消息历史:全新的"] N2["system prompt:子 Agent 专用的"] N3["工具:只有一部分"] N4["读过哪些文件:各记各的"] end style S fill:#e3f2fd,stroke:#1565c0 style N fill:#fff9c4

几个值得说的:

权限策略是共享的。 child.Gate = p.Gate:子 Agent 执行命令时,过的是同一套权限检查,用户之前选过"本次会话都允许"的命令,子 Agent 也不用再问。子 Agent 不能绕过父 Agent 的权限。

工具只给一部分。 mini-agent 在 main.go 里给子 Agent 的工具是:

// 第 30 篇:子 Agent 只有只读工具和终端,用自己的 Workspace(它读过的文件不算主 Agent 读过)。
childWS := tools.NewWorkspace(wd)
a.EnableSubagents(tools.NewRegistry(
    tools.ReadFile{WS: childWS},
    tools.ListFiles{WS: childWS},
    tools.RunBash{WS: childWS},
), childModel, agent.EnvInfo(wd))

读文件、列目录、跑命令,没有 edit_file。子 Agent 是去调研的,不是去改代码的。改代码这种需要整体把握的事,留给主 Agent。

读过哪些文件,各记各的。 第 26 篇的"改之前必须读过",靠的是 Workspace 记录读取时间。子 Agent 用一个单独的 childWS,它读过的文件,不算主 Agent 读过。否则主 Agent 可能在没有亲自读过一个文件的情况下就去改它,而它对这个文件的了解只来自子 Agent 那份 300 字的报告。

2.5 没有 edit_file,不等于不能改文件

这里要老实交代一个漏洞:子 Agent 虽然没有 edit_file,但它有 run_bash。而 run_bash 能做的事情,第 27 篇已经说过了:sed -i 能改文件,rm 能删文件,echo > 能写文件。

所以"子 Agent 只调研、不改代码",靠的不是工具箱里少了一个工具,而是两件事一起:

  • 子 Agent 的 system prompt 里写着"只做任务描述里要求的事",主 Agent 写的任务说明也是调研性质的;
  • 共享的权限策略:sed 和 rm 都不在白名单里,echo > 属于第 27 篇说的"变味"写法,在 default 模式下都要问你。子 Agent 想偷偷改文件,你会在确认提示里看到它,提示前面还带着 ↳ [任务名]。

如果你在 yolo 模式下运行,这两道防线就只剩下第一道了:一段 prompt。更严格的做法是给子 Agent 一个"只读版"的 run_bash:只允许白名单里的命令,其他的一律拒绝,不问人。这是一个不错的练习,改的地方是 main.go 里创建子 Agent 工具的那几行,给子 Agent 单独配一个更严的 Gate。

能力的边界,要用权限来画,而不是用"少给一个工具"来暗示。 这一点在第 38 篇讲安全时还会再说一次。

2.6 子 Agent 的界面

子 Agent 说的话不显示给用户,只有它的工具调用会缩进显示在主界面上:

// childUI 把子 Agent 的工具调用缩进显示在父 Agent 的界面上;子 Agent 说的话不显示,只有最终报告会回到父 Agent。
func (u *childUI) ToolCall(name string, input json.RawMessage) {
    u.parent.ToolCall("  ↳ ["+u.label+"] "+name, input)
}

终端里看起来是这样的:

⏺ task {"description":"调研订单模块","prompt":"……"}
⏺   ↳ [调研订单模块] list_files {"path":"order"}
⏺   ↳ [调研订单模块] read_file {"path":"order/service.go"}
⏺   ↳ [调研订单模块] read_file {"path":"order/model.go"}

用户能看到分身在干什么,但不会被它的中间输出淹没。这又一次用到了第 24 篇的 UI 接口:子 Agent 的 UI 是一个包装了父 UI 的 childUI,Agent 本身完全不知道自己是不是一个"子"。

三、并行

3.1 一次回复,多个分身

第 19 篇讲过,模型可以在一次回复里调用多个工具。如果它一次发起了三个 task 调用,这三个子 Agent 完全可以同时干活:

// parallelSafe 里的工具互不影响,一次响应里调用多个时可以并发执行(第 30 篇)。
// 会改东西的工具(edit_file、run_bash)一律串行:两个并发的编辑可能互相覆盖。
var parallelSafe = map[string]bool{"read_file": true, "list_files": true, "task": true}

runTools 在执行一批工具调用时,如果它们全部都在这个"可以并行"的名单里,就并发执行:

// runTools 执行一批工具调用,结果顺序与调用顺序一致。
func (a *Agent) runTools(ctx context.Context, calls []llm.ContentBlock) []llm.ContentBlock {
    results := make([]llm.ContentBlock, len(calls))
    concurrent := len(calls) > 1
    for _, c := range calls {
        concurrent = concurrent && parallelSafe[c.Name]
    }
    if !concurrent {
        // ……依次执行
        return results
    }
    var wg sync.WaitGroup
    for i, call := range calls {
        a.UI.ToolCall(call.Name, call.Input)
        wg.Add(1)
        go func() {
            defer wg.Done()
            results[i] = a.runTool(ctx, call)
            a.UI.ToolResult(call.Name, results[i].Content, results[i].IsError)
        }()
    }
    wg.Wait()
    return results
}

注意 results[i]:结果按调用的顺序存放,而不是按完成的顺序。第 19 篇说过,结果要通过 id 和调用一一对应,按顺序放能避免很多麻烦。

sequenceDiagram participant M as 主 Agent participant A as 子 Agent:订单 participant B as 子 Agent:支付 participant C as 子 Agent:退款 M->>A: task(并发) M->>B: task(并发) M->>C: task(并发) Note over A,C: 各自读文件、跑命令
同时进行 A-->>M: 报告 C-->>M: 报告 B-->>M: 报告 Note over M: 三份报告按调用顺序
放进同一条 user 消息

3.2 什么能并行,什么不能

只有互不影响的工具才能并行:读文件、列目录、开子 Agent(子 Agent 本身只有只读工具和终端)。

会改东西的工具一律串行。两个 edit_file 同时改同一个文件,后写的会覆盖先写的;两个 run_bash 同时跑,一个在 go build、一个在 git checkout,结果谁也说不清。并行的前提是互不干扰,没法保证就别并行。

3.3 并发时的权限确认

子 Agent 跑命令时,可能需要问用户"允许吗"。三个子 Agent 同时跑,就可能同时有三个问题要问。如果三个问题同时打印到终端,用户的回答就不知道是给谁的了。

所以权限策略里有一把锁,一次只问一个问题:

askMu     sync.Mutex      // 多个子 Agent 并发运行时,一次只问一个问题

其他子 Agent 在问题排队时会等着。

3.4 记账

子 Agent 花的 token,要记到父 Agent 的账上:

p.addUsage(child.Usage) // 分身花的钱也是钱

多个子 Agent 并发运行时,会同时往父 Agent 身上记账,所以 addUsage 也加了锁(第 24 篇的 usageMu)。task 的结果最后还会附上一行统计:

(子 Agent「调研订单模块」:14 条消息,输入 52031 token,输出 1203 token)

主 Agent 和用户都能看到这个分身花了多少。

3.5 并行时,run_bash 也在并行

3.2 节说"会改东西的工具一律串行"。但仔细看那个名单:task 在可并行的名单里,而每个子 Agent 内部都有 run_bash。三个子 Agent 并行,就意味着它们各自的 run_bash 也可能同时在跑。

大部分时候这没问题:三个子 Agent 都在 grep、git log、go list,都是只读的。但如果其中一个跑了 go test ./...,另一个也跑了 go test ./...,它们会抢同一份构建缓存;如果一个在 git stash,另一个在 git diff,结果就说不清了。

这是并行带来的一个真实的风险,mini-agent 没有完全解决它,只是靠前面说的两点降低了概率:子 Agent 是调研性质的,修改性的命令要过权限检查。第 34 篇讲多 Agent 并行时,会用 git worktree 给每个 Agent 一份独立的工作目录,那才是彻底的隔离。

3.6 失败和取消

几个分身里,有一个没做完怎么办?

taskTool.Run 的处理是:子 Agent 的 Send 返回错误(比如 30 步用完了),就把它已经写出的部分报告连同统计信息,作为一个出错的工具结果交回去,错误信息是"子 Agent 没有完成任务"。主 Agent 看到的是:这个分身做了一部分,没做完。它可以基于这部分结果继续,也可以换一个说法再派一次,或者自己去读。一个分身失败,不影响其他分身,也不会让主 Agent 崩掉。

取消则是一起取消:所有子 Agent 用的是主 Agent 传下来的同一个 context。你按下 Ctrl+C,正在跑的几个分身会同时停下,各自给出"用户中断了操作"的结果(第 24 篇)。

四、防止无限套娃

子 Agent 能不能再开子 Agent?如果能,一个子 Agent 觉得任务太大,又派出三个分身;每个分身又派出三个……指数级的爆炸,账单也是指数级的。

防止的办法很简单:子 Agent 的工具箱里没有 task。

// EnableSubagents 给 Agent 加上 task 工具。reg 是子 Agent 能用的工具(不应包含 task 本身,防止无限套娃)。

子 Agent 的工具注册表,是 main.go 里单独创建的那一个(只有读文件、列目录、跑命令),task 工具只加到父 Agent 的注册表里。有一个测试专门检查这件事:

func TestTaskToolNotAvailableToChild(t *testing.T) {
    a := New(&llmtest.Fake{}, "", tools.NewRegistry(), &recordUI{})
    childTools := tools.NewRegistry()
    a.EnableSubagents(childTools, nil, "")
    if _, ok := childTools.Get("task"); ok {
        t.Fatal("子 Agent 不应该能再开子 Agent")
    }
    // ……
}

另外,子 Agent 的步数上限是 30,比主 Agent 的 50 小。一个调研任务转 30 圈还没完成,多半是任务说明没写清楚,或者它迷路了。

4.1 好的任务说明长什么样

主 Agent 写给子 Agent 的那段 prompt,决定了分身能不能把事做好。一个好的任务说明,通常有四部分:

  • 背景:用户最终想做什么,为什么需要这次调研。比如"用户想给订单加部分退款功能,现在要先搞清楚现有的退款流程";
  • 目标:这次具体要查清楚什么。比如"找出退款从发起到完成经过了哪些函数";
  • 范围:去哪里找、不用管哪些。比如"重点看 refund/ 和 payment/ 目录,不用看测试文件";
  • 报告格式:要什么样的结果。比如"按调用顺序列出函数,每个给出文件路径和行号,最后指出哪里最可能需要修改"。

对比一下"帮我看看退款是怎么做的",你会发现,好的任务说明,其实就是第 15 篇讲的 system prompt 原则,用在了一次委派上:说清楚场景、目标、边界和格式。

这段说明是主 Agent 写的,你不能直接控制它的质量。但可以在 task 工具的描述里给出要求(现在的描述里已经有"写清楚背景、要做什么、要报告什么"),也可以在主 Agent 的 system prompt 里加一个例子。如果你发现分身经常带回答非所问的报告,先去看看主 Agent 给它写的任务说明。

五、分身的代价

5.1 更费 token

这一点值得反复强调:subagent 通常比主 Agent 自己干更费 token。

  • 每个子 Agent 都要从零建立上下文:重新发一遍 system prompt、工具定义,重新读一遍可能主 Agent 已经读过的文件;
  • 子 Agent 的上下文是全新的,前缀和主 Agent 不同,缓存也要重新积累(第 10 篇);
  • 报告本身要额外生成一次;
  • 主 Agent 写任务说明,也要花输出 token。

它换来的是主 Agent 上下文的干净。在开场那种"读得多、报得少"的场景里,这笔交易很划算:主 Agent 的上下文少了十几万 token 的文件内容,后面的每一圈都便宜了,也不容易变笨。但在一两次工具调用就能完成的小事上,开分身纯粹是浪费。

5.2 信息损失

子 Agent 的报告是一份摘要,摘要就会丢信息。主 Agent 拿到"退款逻辑在 refund/service.go 的 Process 函数里",但不知道这个函数的具体实现。如果它接下来要改这个函数,还得自己去读一遍。

这其实是好事(读一个函数比读二十个文件便宜得多),但要意识到:子 Agent 的报告是线索,不是全部。 这也是为什么子 Agent 的 prompt 要求报告里带上文件路径和行号。

5.3 让分身用便宜一点的模型

既然分身干的大多是"读文件、搜代码、写一份摘要"这类杂活,它真的需要和主 Agent 一样强的模型吗?

很多时候不需要。EnableSubagents 的第二个参数就是子 Agent 用的模型,传 nil 表示和主 Agent 用同一个;mini-agent 的 -sub-model 参数可以把它换掉:

go run ./cmd/mini-agent -model deepseek-v4-pro -sub-model deepseek-flash

主 Agent 用更强的模型做规划和拍板,分身用更便宜的模型去打杂。第 06 篇的价目表里,两个模型的单价差了好几倍。第 37 篇讲成本时,会把这叫做"模型路由",并且用账本看看它到底省了多少。

代价是分身的报告质量可能会差一点。如果你发现便宜模型的报告经常漏掉关键的地方,主 Agent 就得自己再去读一遍,省下的钱又花了回去。换不换,用数字说话,别凭感觉。

5.4 什么时候用

适合不适合
需要读很多文件、但最后只要一个结论的调研一两次工具调用就能完成的事
几个互不相关、可以同时进行的调查需要整体把握、步骤之间强依赖的任务
主 Agent 的上下文已经很宝贵的长任务后续还要频繁用到中间细节的事

第 24 篇的基础 prompt 里有一条对应的规则:"需要大范围调研(读很多文件才能回答)时,用 task 交给子 Agent,保持自己的上下文干净;互相独立的调研可以一次发起多个 task 并行进行。"

六、动手:派出分身

实验 1:一个需要大范围调研的问题

要看模型自己决定什么时候派分身,需要接真实模型:先 export DEEPSEEK_API_KEY=sk-...。会读很多文件,留意费用。在线 Playground 跑不了 mini-agent。

在配套仓库的根目录启动 mini-agent:

export DEEPSEEK_API_KEY=sk-...
go run ./cmd/mini-agent

说:"列出这个仓库里每个 examples 目录分别演示了什么,每个目录用一个子 Agent 去看。"

观察:

  1. 它是不是在一次回复里发起了多个 task 调用?
  2. 界面上那些缩进的 ↳ [xxx] 行,是不是交错出现的(说明它们在并行执行)?
  3. 每个 task 的结果最后那行统计:每个分身各花了多少 token?
  4. 全部完成之后,输入 /usage,看看累计用量;再看看当前的上下文大小,是不是远小于累计用量?这个差距,就是子 Agent 替主 Agent 隔离掉的部分。

实验 2:对比一下不用分身

/reset 之后,说:"列出这个仓库里每个 examples 目录分别演示了什么,不要用子 Agent,自己读。"

完成之后再 /usage。对比两次的累计用量(不用分身可能更省)和当前上下文大小(不用分身会大很多)。这就是"用多花 token 换主上下文干净"。

实验 3:给分身一个更严的权限

按 2.5 节的思路,在 cmd/mini-agent/main.go 里给子 Agent 单独配一个权限策略:它的 Ask 设成 nil。第 27 篇第五节说过,Ask 为 nil 时,凡是需要确认的操作都会被自动拒绝。这样,子 Agent 只能跑白名单里的命令,想改文件、想跑别的命令,一律被拒,连问都不会问你。

改的时候要注意:taskTool.Run 里有一行 child.Gate = p.Gate,它会把父 Agent 的权限策略原样传给子 Agent,你需要让它改用这个单独的策略。改完之后,派一个分身去"把 README 里的错别字改掉",看看它会收到什么样的拒绝,以及它在报告里怎么说。

实验 4:跑测试

go test ./agent -run 'TestSubagentsRunInParallelWithFreshContext|TestTaskToolNotAvailableToChild' -v

TestSubagentsRunInParallelWithFreshContext 用一个按 system prompt 区分主、子 Agent 的假模型,检查了四件事:两个子 Agent 是并行执行的(同时在跑的最多有 2 个,总耗时小于两个串行的耗时);子 Agent 的上下文是全新的(只有一条任务消息);父 Agent 只拿到了报告;子 Agent 的用量记到了父 Agent 身上。

七、踩坑记录:分身的七个坑

坑 1:任务说明写得太简略。
"帮我查一下那个问题",子 Agent 不知道"那个问题"是什么,只能瞎猜。子 Agent 看不到对话,prompt 里要写清楚背景、目标、要报告什么。工具描述里要反复强调这一点。

坑 2:报告太长,把隔离的意义抵消了。
子 Agent 把读到的代码大段大段地贴进报告,十几万 token 的内容又搬回了主上下文。要求它精炼,只给结论和位置。

坑 3:给子 Agent 改代码的能力。
几个子 Agent 并行地改不同的文件,彼此不知道对方改了什么,最后拼起来对不上。调研交给分身,改代码留给主 Agent。

坑 4:子 Agent 读过的文件,被当成主 Agent 读过。
主 Agent 基于一份 300 字的报告就去改一个它没亲自读过的文件。子 Agent 用独立的 Workspace,edit_file 会强制主 Agent 自己先读。

坑 5:子 Agent 还能开子 Agent。
无限套娃,账单爆炸。子 Agent 的工具箱里不放 task,并且有测试守着。

坑 6:小事也开分身。
"看一下 go.mod 里 Go 的版本",开一个子 Agent,它从零建立上下文、读文件、写报告,比主 Agent 自己读一下 go.mod 贵十倍。工具描述里明确写"不适合简单的事"。

坑 7:以为没给 edit_file,子 Agent 就改不了文件。
它有 run_bash,sed -i、rm、echo > 都能改文件。真要限制,就给子 Agent 单独配一个更严的权限策略,而不是指望"少一个工具"就够了。

一句话总结

subagent 是一个 task 工具,它在 Run 里新建一个拥有全新上下文的 Agent 去干活,只把一份精炼的报告带回来;子 Agent 共享模型、权限和记账,但不共享历史,只给只读工具和终端,不能再开子 Agent;互不影响的调用可以并行;它用多花 token 换主上下文干净,适合"读得多、报得少"的调研,不适合两步就能做完的小事。

flowchart LR M["主 Agent"] -->|"task × N(并行)"| C["子 Agent × N
全新上下文、只读工具"] C -->|"每个只带回 300 字报告"| M C -.->|"用完就扔"| X["十几万 token 的中间内容"] style M fill:#e8f5e9,stroke:#2e7d32 style X fill:#fff9c4

评论

评论已关闭
Enter 搜索Esc 关闭