Skip to content
Go back

Subagent 与 Skill 注入:多 Agent 编排的艺术

Subagent 与 Skill 注入:多 Agent 编排的艺术

结论先行

单 Agent 什么都做,等于什么都很烂。根因是三个,都在「上下文窗口」这件事上:

解法是两件事,一硬一软:

一句话:Subagent 管「谁来做」,Skill 管「怎么做」。一个拆身份,一个拆知识。

核心原理

多 Agent 编排不是「多叫几个 AI 一起干活」这么简单。它解决的是一个更底层的问题:单个 Agent 的上下文窗口是它唯一的「心智状态」,而这个状态不可分、会互相干扰、只能串行推进。

你无法让一个 Agent「既用建造者的脑子写代码,又用攻击者的脑子审查代码」——因为这两个脑子共享同一个上下文窗口。写代码时读进来的文件、当时的假设、走过的弯路,全都在窗口里。审查时这些「污染」还在,模型必须假装它没看过、假装不知道这段代码为什么这样写——但装不出来。

Subagent 的价值就在这里:它是一次「心智重置」。派一个全新的 subagent 出去,它带着一个空的上下文、一个专门的系统提示词、一个受限制的工具集,从零开始读代码。它天然就是「第一次看到」,不需要假装。

Skill 注入解决的是另一个问题:能力多了怎么办。当你的系统里积攒了几十个技能、每个技能一份几百行的说明书时,如果全塞进启动 prompt,每次会话还没干活就先烧掉几万 token,模型还会被信息淹没。正确做法是「按需加载」——启动时只装一张「地图」,走到哪一步再读「那一页的说明书」。

这两个机制是一对:一个拆身份,一个拆知识,都指向同一件事——让上下文窗口里只装当下这一步真正需要的东西。

💭 思考:为什么「拆身份(Subagent)」和「拆知识(Skill)」能归到同一件事上?—— 一步步推:单 Agent 的所有病,根子都在「上下文窗口 = 它唯一的心智状态,而这个状态不可分、会互相干扰、只能串行推进」。Subagent 解决的是「状态不可分」——把「做」和「查」放进不同上下文、不同身份,让上一个身份不在窗口里;Skill 解决的是「知识全堆在一起」——把几十份说明书按需注入,让上一个技能不在窗口里。两者手段不同(一个是派生新进程,一个是注入文档),但目标是同一个:窗口里只装当下这一步真正需要的。所以它们不是两个孤立功能,而是同一约束(窗口是钱、也是注意力)的两种解法。

一、为什么单 Agent 不够

先不急着讲方案,把「单 Agent 不够」拆成三个具体症状。

1.1 上下文互相污染

一个 Agent 同时承担「实现」和「审查」两个环节时,会发生一件很微妙的事:审查时的上下文窗口里,还残留着实现时读进来的所有东西——代码、当时的思路、踩过的坑、做过的假设。

审查的核心动作是「切换视角」:假设你第一次看到这段代码,且你不在乎它为什么这样写,你只在乎它哪里会坏。但这个「第一次看到」在单 Agent 里是装出来的:模型已经跟着实现走了一遍,它知道这段代码为什么长这样,这个「知道」就是污染。

反过来也一样:实现时读进来的一大堆上下文(需求文档、相关文件的旧代码、改动的试错记录),会把审查时的注意力冲散。审查者本应只盯着「这段 diff 哪里会坏」,但它的窗口里塞满了「这段代码是怎么一步步写出来的」。

Subagent 的解法是物理性的:派出去的审查 agent 只有一个全新上下文,它读到的只有「这是要审查的代码」加「审查规则」,没有实现过程的历史包袱。隔离不是靠 prompt 请求「你忘了刚才的事」,而是靠架构让「刚才的事」根本不在它的窗口里。

1.2 运动员兼裁判:自我肯定偏差

这是单 Agent 最隐蔽、也最致命的问题。写完代码的人,天然倾向证明代码是对的——这在心理学上叫自我肯定偏差(self-confirmation bias)。工程上的表现是:同一个模型,让它审查自己刚写的代码,它会不自觉地沿着「这条逻辑是对的」的正向路径走,而攻击者视角需要的是「这条逻辑怎么才能错」。

你可以用 prompt 提示「你现在要批判」,但身份没变、上下文没变,批判就只是「形式上严厉一点」。这就是为什么严肃的审查方法论里塞满了「反合理化检查点」——「我是否因改动太小而准备跳过检查?」「我是否写了『通过』但没有绑定具体代码位置?」——这些检查点存在的原因,恰恰是单个 Agent 会不断给自己的放行找合理化理由

对抗验证的解法是:给「找茬」一个独立的身份,让它天然不承担「证明代码对」的义务。 一个以批判为唯一职责的 subagent,系统提示词里第一句就该是「你的工作是挑战和反驳」。它没有「放行」这个选项,它唯一的工作就是不断追问:证据够吗?有没有替代解释?什么边界会击穿这个结论?什么没被测到?

这不是「给同一个模型换了个 prompt」,而是给「批判」这个职能分配了一个独立的身份和独立的上下文。它不会因为「代码是我写的」而下不去手,因为代码根本不是它写的。

💭 思考:为什么对抗验证不是「换个 prompt」,而是「换个身份 + 换上下文」?—— 从「偏差的成因」反推:自我肯定偏差来自「同一个模型、同一段上下文、既写了代码又审代码」——它会沿着「这条逻辑是对的」的正向路径走。你只改 prompt 说「请批判」,身份没变、上下文没变,它顶多「形式上严厉一点」。约束于是落到「身份」上:要真正产生攻击者视角,就得给「找茬」一个独立的身份——系统提示词第一句就是「你的工作是挑战和反驳」、它没有「放行」选项、它天然不承担证明代码对的义务。而独立身份要成立,上下文也必须独立——否则写代码的历史还在窗口里,它就还是「知道为什么这样写」。所以「换身份」和「换上下文」是同一件事的两半,缺了任一半,「批判」都只是演戏。

1.3 长任务难以并行

单 Agent 是严格串行的:读完才能写、写完才能查、查完才能测。这在大多数场景没问题,但一旦任务本身有可并行的结构,串行就成了纯损失。

典型的例子是「多视角审查」:一段高风险代码,需要同时从安全(注入、越权、明文存储)、性能(N+1 查询、内存泄漏、复杂度)、兼容性(API 签名变更、依赖冲突、数据迁移)三个视角去查。这三个视角互相独立,单 Agent 只能一个一个来,延迟叠加;而三个 subagent 可以同步跑,总时长等于最慢的那个。

更重要的并行价值不在「快」,而在「独立性」:三个并行的审查者各自带着独立的上下文和独立的视角,它们之间不会互相影响判断。串行审查时,前一个维度的发现会「污染」后一个维度的注意力——你查完安全发现一堆问题,再去查性能时,脑子里全是安全问题。并行的三个 agent 没有这个问题。

二、Subagent:分工隔离、对抗验证、并行执行

Subagent 本质是「一个带独立上下文 + 独立身份 + 独立工具集的子循环」。它和主 Agent 的 Tool Loop(本系列第 2 篇讲的 think → act → observe 循环)结构一样,区别在于:它从父任务里拿走一块「职责」,用一个干净的上下文去执行,然后把结论交回来。

这套机制在两个主流实现里都有源码级对应。Claude Code 的 Agent 工具把「派一个 subagent」落成一个显式动作:每个 agent 类型在定义里声明名字(agentType)、用途(whenToUse)和可用工具——tools 是允许清单,disallowedTools 是禁止清单,二者共同圈定这个子 agent 的能力边界;派发时主 Agent 指定 subagent_type,可选指定 modelisolation: "worktree"。Codex 的对应物在 codex_delegate:它「启动一个交互式子 Codex 线程」(interactive sub-Codex thread),源码注释里直白写明「委托从不请求审批」(Delegates never request approvals),并且代码在入口处强制审批策略必须为 never,否则直接报错;取消则用一个 child token,让父级的取消能级联传导、又能把取消范围限定在这个子任务上。三个价值正好对应上一节的三个症状。

2.1 分工隔离:独立上下文与工作副本

Claude Code 的 Agent 工具把隔离做成了一个参数:isolation: "worktree" 让 subagent 在一个独立的临时 git 工作副本里执行,它看到的是主仓库的一个独立副本,而不是主工作区本身——如果它没做任何改动,这个副本会被自动清理。Codex 的 codex_delegate 走的是另一条隔离路线:子线程以 ForkPersistence::Copied 复制一份历史、parent_thread_id 指向父线程,且入口强制审批策略为 never——「委托从不请求审批」,子线程没有停下来等人确认的资格,只能自己跑完、把公开事件交回来。这带来两层隔离:

  1. 文件隔离:审查 agent 即使「手滑」写了个临时文件、改了什么,也污染不到主工作区。审查本应是只读的,但「只读」靠约束不如靠物理隔离——独立副本让「越权写」从「违规」变成「根本影响不到主仓库」。
  2. 状态隔离:工作副本是一个独立的 git 状态空间,subagent 在里面怎么折腾(切换分支、回滚、跑测试)都不会影响主会话正在进行的其它工作。

隔离的价值用一个词就能说清:干净的上下文。一个审查者如果带着一整个项目的实现历史去审查,它就不是在「发现风险」,而是在「为已知的决策辩护」。独立工作副本让「第一次看到」从一句 prompt 里的客套话,变成架构上的既定事实。

2.2 对抗验证:给「找茬」一个独立身份

对抗验证的核心设计是一个「身份非对称」:生产者负责证明「对」,对抗者只负责证明「错」。 这两个身份如果合并到一个人身上,就必然妥协;拆开,才能各自极端。

怎么让「找茬」这个职能稳定地被执行,而不是靠一句「请保持批判性」的祈祷?答案是把它固化成一套每次必问的问题模板。一个合格的对抗者,每次拿到一个结论都应该问这四类问题:

1. 证据充分吗?——支持这个结论的证据是什么,够不够硬?
2. 有替代解释吗?——同样的证据,能不能套上别的解释?
3. 什么边界会击穿它?——什么输入、什么规模、什么环境会让这个结论失效?
4. 什么没被测到?——哪些路径、哪些分支、哪些条件是「没验证过」而非「验证过没问题」?

这四问的设计很讲究:第一问打「证据」,第二问打「归因」,第三问打「普适性」,第四问打「覆盖盲区」。合起来,任何一个结论都很难同时顶住这四问——而这正是对抗验证要的效果:不是让结论「看起来对」,而是让「经不起反驳」的地方全部暴露出来。

这套「身份非对称」在实现里不是一句祈祷,而是写死在 agent 定义里的。Claude Code 内置的 verification agent,系统提示词开篇就是「你的工作不是确认实现能跑,而是想办法搞坏它」(Your job is not to confirm the implementation works — it’s to try to break it),并且强制要求最后输出 VERDICT: PASS / FAIL / PARTIAL,还得附上至少一次「对抗性探测」——并发、边界值、幂等、孤儿操作之类。它同时是「砍权限」的样本:定义里用 disallowedTools 禁掉了文件编辑、写入这类工具,整个系统提示词反复强调「不要改动项目」。

这里还有一个容易被忽略的成本设计:按任务强度分级指定模型,按职责最小化工具集。 这在实现里是硬配置而非口头建议——Claude Code 的 Explore 这种只读搜索 agent 默认跑在便宜的 haiku 模型上,做判断的 verification agent 则继承主模型。跑测试、捕获输出、报告 pass/fail 这种机械执行的 agent,用便宜的快模型就够了;做判断、挑战结论这种知识密集型的 agent,才值得用强模型。工具集也一样——只读的审查者不该有写文件的权限,纯批判的对抗者连执行命令的权限都可以不给。工具集就是能力边界,砍掉写权限和执行权限,就是物理上不允许它越权。

2.3 并行执行:可并行的结构就并行跑

当一个任务里有多个互相独立的视角时,最自然的做法就是并行派发。以「高风险代码的多视角审查」为例:

主审查(整体扫描)
    +
并行交叉审查者(多个子 Agent,同步运行):
  ├── 安全视角   → 注入、越权、敏感信息、依赖漏洞
  ├── 性能视角   → N+1 查询、内存泄漏、算法复杂度
  └── 兼容性视角 → API 签名变更、依赖冲突、数据迁移

并行不是「你尽力同时跑」,而是「怎么让它们真的同时跑」——实现里对这一点写得很具体。Claude Code 的 Agent 工具在提示词里明说:要并行,就在一条消息里同时发多个 Agent 工具调用,并且「只要可能就并发启动多个 agent」。Codex 的 explorer 角色描述更是把并行当成委托的核心优势:有多个彼此独立的代码库问题时,鼓励一次并行派出多个 explorer,等结果的同时还能继续干本地不依赖这些结果的事。并行也不是无上限的——Codex 的 AgentRegistry 专门给「每个用户会话的子 agent 总数」设上限,每个子 agent 还要从一份名单池里领唯一昵称(Euclid、Archimedes、Turing……),撞名了就自动编号成「XX the 2nd」。

并行一旦跑起来,还有个现实问题:谁改什么、怎么不互相踩踏。Claude Code 用 Task 工具维护一张结构化任务清单(团队与任务表一一对应),用 TaskUpdateowner 字段把任务明确分配给某个 agent;Codex 的 worker 角色则要求显式分配任务的所有权(文件、职责范围),并提醒 worker「代码库里不只有你一个,别回滚别人的改动」——所有权是并行下避免合并冲突和互相踩踏的机制。

合并规则必须是硬性的:主审查和所有并行审查者都无阻断发现才放行;任一并行审查者发现阻断问题,就汇总回退重写。并行不是为了「更快地通过」,而是为了「更不容易漏」。 快只是副产品,独立性才是目的。

三、Skill 注入:按需加载,而不是全堆进 prompt

解决了「谁来做」,还要解决「怎么做」。当你的 Agent 系统积攒了几十个技能时,一个现实的工程问题摆在面前:这些技能的说明书怎么给到模型?

3.1 为什么不能全堆进 prompt

一个技能通常是一份结构化文档——头部声明「这个技能是干嘛的、允许用什么工具、适合什么模型」,正文写执行方法论、反合理化检查点、踩坑记录。一份几百行。

如果把这几十份说明书全塞进启动 prompt,会发生两件事:

  1. token 爆炸:每次会话还没开始干活,就先烧掉几万 token 的「背景知识」,而其中绝大多数这次会话根本用不到。
  2. 注意力稀释:模型被几十套方法论淹没,反而抓不住当前任务真正需要的那一套。信息不是越多越好,是「越准越好」。

这其实就是本系列上一篇(上下文压缩与 Memory)讲的同一个问题:上下文窗口是钱,也是注意力。 往窗口里塞一份用不到的东西,等于同时浪费了钱和注意力。

💭 思考:为什么「几十份说明书全堆进 prompt」会同时伤害钱和注意力,而不是「多给点背景没坏处」?—— 反推:token 是钱——每次会话还没干活就先烧掉几万 token 背景知识,其中绝大多数这次用不到,是纯浪费;注意力是稀缺的——模型被几十套方法论淹没后,反而抓不住当前任务真正需要的那一套,正确率不升反降。所以「多给背景」有个反直觉的代价曲线:过了某个点,信息越多、决策越差。这正是「薄路由」的由来——启动时只装「名字 + 描述」一张地图,等真正触发再读正文,把「钱和注意力」都花在「当前这一步」上。

3.2 按需加载:薄路由 + 语义匹配

Claude Code 的 Skill 机制是「注入式加载」的一个公开实现:每个技能一个声明文件(SKILL.md),头部用 namedescription 声明它是什么、解决什么问题。启动时只加载「名字 + 描述」这一层(也就是一张地图),等某个技能真正被触发时,才把它的完整内容读进上下文。

这里有两个关键设计,值得分别展开。

第一,薄路由——启动时只装地图,不装说明书。 一张「意图 → 技能」的速查表,每个格子只写「用户会说什么 → 对应哪个技能」,不写技能怎么执行。真正的执行规则在各自的技能文档里。这张表的关键是「薄」——只存指针,不存内容。为什么?因为上下文窗口是钱:启动时加载的是「地图」,不是「几十本说明书」。

第二,语义匹配——靠 description 做锚点,而不是靠关键词命中。 每个技能的 description 是一段话,说明「这个技能是干嘛的」。模型读完整张地图后,靠语义去匹配用户意图,而不是靠关键词命中。

这一点最值得展开。触发词是示例,不是枚举。用户说「我有段代码感觉很乱,但说不上来哪里有问题」——这句话可能没有命中任何一行字面关键词,但模型能靠 description 的语义锚点,把它匹配到「代码审查」加「根因分析」这类能力上。这正是用 LLM 做路由器、而不是用 if-else 做路由器的区别:你不需要穷举用户会说什么,你只需要给模型「每个技能是干嘛的」这个语义锚点。

description 的质量因此直接决定路由的准确率。它本质是「技能的语义锚点」——写得含糊,模型就会在一堆技能里选错;写得精准,模型一眼就能定位。这也提醒我们:在注入式架构里,「怎么写清楚这个技能是干嘛的」和「怎么写这个技能的执行规则」同样重要,甚至更重要——因为描述错了,再好的规则也不会被加载。

💭 思考:为什么路由要「靠语义匹配」,而不是「靠关键词命中」?—— 从「用户意图的分布」反推:用户不会按你的关键词表说话。他们说「我有段代码感觉很乱但说不上来哪里有问题」,这句话可能一个技能名都不命中,但模型能靠 description 的语义锚点把它匹配到「代码审查 + 根因分析」。如果你用 if-else 或关键词命中,就得穷举用户会说什么——而真实意图是不可穷举的。所以用 LLM 做路由器,本质是把「匹配规则」从「枚举」换成「语义锚点」:你不需要预测用户会怎么说,只需要写清「每个技能是干嘛的」。这也把压力转移到了 description 的质量上——它是技能的语义锚点,写糊了,再好的规则也不会被加载。

3.3 单技能加载:用完即弃

注入的最后一个环节是「何时卸载」。原则很明确:单技能加载——每步只载当前要用的那一份,用完即弃。

意思是:走一个多步骤流程时,实现阶段只加载「实现」的说明书,进了审查阶段就把它丢出上下文,只加载「审查」的说明书。几十个技能永远不会同时出现在窗口里,任何时刻上下文里只有「当前这一步要用的那一份」。

这和 Subagent 是同一哲学的两个侧面:Subagent 让「上一个身份」不在窗口里,Skill 注入让「上一个技能」不在窗口里。两者都在做同一件事——让上下文窗口里只装当下这一步真正需要的东西。

总结

把这篇文章压缩成一段话:

单 Agent 的瓶颈不是「不够聪明」,而是「状态不可分、身份不可拆、执行只能串行」。Subagent 的价值是一次「心智重置」——把「做」和「查」拆到不同上下文、不同身份、独立工作副本里,让审查者真的「第一次看到」,让对抗者真的「只负责找茬」,让多个视角真的「并行跑」。Skill 注入的价值是「按需加载」——靠薄路由加语义匹配注入,单技能加载、用完即弃,任何时刻窗口里只有当下这一步需要的那一份。

落到工程上,就是一套清晰的职责划分:Subagent 管「谁来做」,Skill 管「怎么做」——一个拆身份,一个拆知识。两者加上一个不依赖模型的机械兜底(比如固定流程里不可跳过的检查项),才构成一个能跑远、能跑稳、敢放行的多 Agent 系统。

下一篇,讲这个系列最后一块拼图:长任务状态管理——一个几十步、跨多个会话的 Agent 任务,中途断了怎么断点续传。

章末提问

追问 1:为什么单 Agent 审查自己写的代码不靠谱,Subagent 怎么解决?

结论先行:因为上下文污染 + 自我肯定偏差,模型「假装第一次看到」装不像、也下不去手;Subagent 用一次「心智重置」——独立上下文 + 独立身份 + 独立工具集,让它真的「第一次看到」。

因为:写代码时读进来的文件、假设、弯路全在窗口里,审查时这些污染还在;且同一个模型天然倾向证明自己写的代码对。Subagent 派出去一个空上下文、专门「找茬」身份、只读工具集的子循环,隔离不是靠 prompt 请求「忘了刚才的事」,而是靠架构让「刚才的事」根本不在它窗口里。

追问 2:对抗验证为什么必须「身份非对称」?

结论先行:因为生产者负责证明「对」、对抗者只负责证明「错」,两个身份合并到一个人身上就必然妥协。

因为:自我肯定偏差会让「写代码的人」不自觉沿「这条逻辑是对的」走,而攻击者视角要的是「这条逻辑怎么才能错」。你只换 prompt 不换身份,批判就只是「形式上严厉」。所以对抗者被固化成「系统提示词第一句就是挑战和反驳 + 每次必问四类问题(证据/归因/普适性/盲区)+ 禁掉写和执行权限」——它没有「放行」选项,唯一工作就是让经不起反驳的地方暴露。

追问 3:几十个技能为什么不能全塞进启动 prompt?

结论先行:因为会同时浪费「钱」(token)和「注意力」——绝大多数技能这次用不到,还会把模型淹没。

因为:上下文窗口是钱也是注意力。全塞进去,每次会话没干活先烧几万 token,且模型被几十套方法论淹没后反而抓不住当前要的那套。正确做法是「薄路由 + 语义匹配」:启动只装「名字 + 描述」一张地图(只存指针不存内容),靠 description 的语义锚点匹配意图,等真正触发才读正文。

追问 4:Subagent 和 Skill 注入是什么关系?

结论先行:Subagent 管「谁来做」,Skill 管「怎么做」——一个拆身份,一个拆知识,但都指向同一件事:上下文窗口里只装当下这一步真正需要的。

因为:Subagent 让「上一个身份」不在窗口里(独立上下文 + 身份 + 工具集),Skill 注入让「上一个技能」不在窗口里(按需加载、用完即弃)。两者是同一约束(窗口是钱、是注意力)的两种解法,一个硬一个软:Subagent 用进程/工作副本做物理隔离,Skill 用文档注入做知识裁剪。再加上一个不依赖模型的机械兜底,才构成能跑远、能跑稳、敢放行的多 Agent 系统。


Share this post on:

Previous Post
长任务状态管理:跑两小时的任务怎么不丢状态
Next Post
水桶取水——扩展欧几里得算法的编程题转化