拆解 Claude Code SubAgent:隔离、专业化与权限设计
<blockquote><p>提一嘴博主开源的以 Claude Code 为基础的 GUI 图形化桌面客户端应用 <strong>Misaka</strong>(<br>
GitHub 地址:<strong>https://github.com/knqiufan/Misaka</strong></p>
<p>项目完全开源,是个学习项目,还有比较多的地方待优化,欢迎大家来提 Issue 和 PR</p>
</blockquote>
<p>另外 Claude Code 的其他相关文章:</p>
<ul>
<li>关于完全指南: <strong>Claude Code 完全指南:使用方式、技巧与最佳实践</strong></li>
<li>关于 Skill: <strong>从对话到协作,Skills 如何改变我们与 AI 共事的方式</strong></li>
</ul>
<hr>
<h2 id="section">引言</h2>
<p>从"这东西有什么用"聊到"它底下是怎么跑的",一篇讲完。</p>
<hr>
<h2 id="section-1">目录</h2>
<p><strong>入门篇</strong></p>
<ol>
<li>一个比喻理解 SubAgent</li>
<li>SubAgent 解决的三个核心问题</li>
<li>你可能已经在使用了</li>
<li>什么时候该用,什么时候不该用</li>
</ol>
<p><strong>实践篇</strong></p>
<ol start="5">
<li>三步创建自定义 SubAgent</li>
<li>配置文件完全指南</li>
<li>前台、后台与恢复</li>
<li>最佳实践:Prompt 怎么写</li>
</ol>
<p><strong>原理篇</strong></p>
<ol start="9">
<li>为什么 SubAgent 是一个微型会话</li>
<li>两条路径的设计取舍:专业化 vs 缓存效率</li>
<li>权限模型:单向棘轮原则</li>
<li>工具池设计:最小权限原则的实际落地</li>
<li>生命周期管理</li>
<li>Agent 定义的加载策略:信任的梯度</li>
<li>MCP Server 的隔离策略:共享 vs 专属的取舍</li>
<li>Worktree 隔离:让子代理在自己的沙箱里改代码</li>
<li>从 Sub-Agent 到 Multi-Agent:架构选型的三角博弈</li>
</ol>
<p><strong>附录</strong></p>
<ol start="18">
<li>总结一下源码里藏着的设计巧思</li>
<li>源码关键文件索引</li>
</ol>
<hr>
<h1 id="section-2">入门篇</h1>
<hr>
<h2 id="subagent">1. 一个比喻理解 SubAgent</h2>
<p>想象你是一个项目经理(主 Agent),手下有几个专员(SubAgent)。你不会自己去翻 200 个文件找答案——你会把任务交给调研专员,让他去翻,他翻完了最终再把结论汇报给你。</p>
<p>这就是 SubAgent 做的事:<strong>主 Agent 把任务派给一个独立的子进程去执行,子进程干完后只把结论带回来。</strong></p>
<p>比如 Claude Code 内部的工具调用:</p>
<pre><code>Agent({
SubAgent_type: "Explore",
prompt: "搜索整个代码库,找出所有 API 端点定义"
})
</code></pre>
<p>这段调用会启动一个 Explore 类型的子代理,它自己去搜索、读取文件、分析代码,最后把结果摘要返回。主 Agent 只看到结论不看到过程。</p>
<blockquote>
<p><strong>一句话总结</strong>:SubAgent = 一个拥有独立上下文窗口的自治 Worker,干完活只交结论。</p>
</blockquote>
<hr>
<h2 id="subagent-1">2. SubAgent 解决的三个核心问题</h2>
<h3 id="section-3">问题一:上下文污染</h3>
<p>Claude 的上下文窗口再大也是有限的。如果让主 Agent 自己去搜 30 个文件,那些搜索结果、文件内容、中间分析全部留在主对话里,等真正要做决策时,那上下文窗口可能已经快满了。</p>
<p>SubAgent 的解决方案是<strong>让自己天然拥有一个独立的上下文窗口</strong>。即中间过程全都留在子代理里,主对话只看结论。也就是说子代理执行完毕后,这些中间内容就消失了。</p>
<blockquote>
<p><strong>简单判断</strong>:如果信息对当下执行是必要的,但对后续决策是噪声——用子代理。</p>
</blockquote>
<h3 id="section-4">问题二:行为不可控</h3>
<p>主 Agent 通常拥有完整的工具权限(读文件、写文件、执行命令)。但某些任务你只想让它"看",不想让它"改"。</p>
<p>对于这个问题 SubAgent 的解决方案是<strong>精确的工具权限控制</strong>。即我们可以定义一个只读型子代理,只给它 <code>Read</code>、<code>Grep</code>、<code>Glob</code> 三个工具,这样它想改也改不了了。</p>
<pre><code class="language-markdown"># 只读型子代理(代码审查)
tools: Read, Grep, Glob
# 开发型子代理(bug 修复)
tools: Read, Write, Edit, Bash
# 研究型子代理(技术调研)
tools: Read, WebFetch, WebSearch
</code></pre>
<h3 id="section-5">问题三:经验无法沉淀</h3>
<p>每次都要手动告诉 Claude "去查这个、用那个方式分析"。这些操作步骤无法复用。</p>
<p>针对这个问题 SubAgent 的解决方案是<strong>配置即文件</strong>。子代理的定义保存在 <code>.md</code> 文件中,可以放进 Git 与团队共享,好用的配置可以复制到其他项目。</p>
<p>所以 SubAgent 可以用三个词概括:<strong>隔离、约束、复用</strong>。那么再从更高层面看 SubAgent 的设计哲学,其实就是<strong>将一个大脑拆成多个岗位角色,每个岗位只做一件事,并且有明确的权限边界。</strong></p>
<hr>
<h2 id="section-6">3. 你可能已经在使用了</h2>
<p>Claude Code 内置了几个 SubAgent。当你在对话里说”帮我看看代码库结构”、”先规划一下怎么做”、或者 Claude 自动走验证流程的时候,这些 SubAgent 就在干活。而你可能根本没注意到。</p>
<h3 id="explore">Explore(代码库的搜索引擎)</h3>
<p>Explore 是最常用的内置 SubAgent。它的定位很明确:快速搜索、只读分析。</p>
<p>当我们在对话里说比如”帮我找一下所有 API 端点的定义”或者”这个函数在哪些地方被调用了”,Claude 就会启动 Explore 去干活。它会把成百上千行的 grep 结果、文件读取、路径分析全吞进自己的上下文里,最后只给你一份干净的摘要。</p>
<p>搜索深度分三档:quick、medium(默认)、very thorough。这个档位是可以在 prompt 里指定的,Explore 会据此调整搜多广。这不是代码层面的硬限制,纯粹是 prompt 级别的指导:</p>
<ul>
<li>quick 就是跑几条 grep 就收工,适合”某个 class 在哪个文件”这种目标明确的问题</li>
<li>medium 则会多搜几个路径、多读几个文件,适合”这个模块的结构是怎样的”</li>
<li>very thorough 会在多个目录和命名规范下反复搜,尽量不留死角——适合”梳理认证流程从入口到数据库的完整调用链”。</li>
</ul>
<p>工具方面 Explore 能用 Glob(按文件名搜)、Grep(按内容搜)、Read(读文件)、Bash(但只能跑只读命令)。在前段时间 Claude Code 暴露的源码里使用 <code>disallowedTools</code> 硬性屏蔽了 Edit、Write、NotebookEdit。说明它确实改不了东西。</p>
<p>外部用户跑 Explore 用的是 Haiku,快且便宜。Anthropic 内部用户则会继承主 Agent 的模型。</p>
<p>Claude Code 暴露的源码里有个不太起眼的阈值:<code>EXPLORE_AGENT_MIN_QUERIES = 3</code>。这个参数的作用是,主 Agent 被告知任务只需要 1-2 次搜索就搞定的别启动 Explore,直接用 Grep/Read,只有明确需要 3 次以上查询时才值得派出去。</p>
<p>另外,Explore 默认省略 CLAUDE.md 和 gitStatus(能到 40KB)。只读代理不需要知道 commit 规范和 PR 流程,自己会跑 <code>git status</code>。这一项每周会省 5-15 Gtok。</p>
<h3 id="plan">Plan(动手之前先想清楚)</h3>
<p>Plan 的定位是软件架构师。它不写代码,专门在动手之前把方案想透。</p>
<p>比如当我们跟 Claude 说”我想给系统加个支付模块”,这个时候 Claude 就会先派 Plan 去调研,Plan 会读现有代码、找已有的模式和约定、理清依赖关系、最后输出一份分步实施计划。</p>
<p>系统提示给 Plan 定义了四步流程:</p>
<ul>
<li>理解需求</li>
<li>深入探索(读代码、追踪调用链、参考已有实现)</li>
<li>设计方案(考虑取舍)</li>
<li>输出计划(分步策略、依赖关系、可能的坑)。</li>
</ul>
<p>输出必须以”Critical Files for Implementation”结尾,并列出最关键的 3-5 个文件,这样主 Agent 拿到这份计划就知道下一步该读什么、改什么了。</p>
<p>Plan 跟 Explore 一样<strong>只读</strong>——同样的使用了 <code>disallowedTools</code>,改不了文件。但模型不同:Plan 继承主 Agent 的模型,不会降级到 Haiku。<strong>架构设计需要更强的推理能力,用便宜模型容易翻车。</strong></p>
<p>Explore 和 Plan 的分工边界是:Explore 搜完就交结果,Plan 搜完还要分析、权衡、给建议。找函数在哪用 Explore,搞清”加这个功能要改哪些文件、按什么顺序改”用 Plan。</p>
<h3 id="general-purpose">General-purpose(什么都干的全能选手)</h3>
<p>Explore 和 Plan 都被硬性禁止了 Edit、Write 等工具,但 General-purpose 没这个限制。<code>tools: ['*']</code>,即父 Agent 有什么它就能用什么,这是它跟 Explore/Plan 的根本区别。搜索和规划是只读的活儿,而 General-purpose 要真刀真枪改代码。</p>
<p>系统提示很短,两段话完事:</p>
<blockquote>
<p>Given the user's message, you should use the tools available to complete the task. Complete the task fully — don't gold-plate, but don't leave it half-done.</p>
</blockquote>
<p>意思是把活干完,别画蛇添足,但也别半途而废。</p>
<p>General-purpose 适合的场景是那种连贯的多步骤流程:先读代码定位问题、再改代码、再跑测试验证。比如”修复认证模块的登录 bug”这种任务。</p>
<p>模型字段故意留空,由 <code>getDefaultSubagentModel()</code> 在运行时决定,是跟着会话配置走的。</p>
<h3 id="claude-code-guide">Claude Code Guide——产品文档专家</h3>
<p>Claude Code 还有一个不太起眼的内置 SubAgent:<strong>claude-code-guide</strong>。当你问”Claude Code 怎么配 hooks?”、”Agent SDK 怎么用?”的时候,Claude 会派它去查官方文档。</p>
<p>它的工具是 Glob、Grep、Read、WebFetch、WebSearch。Haiku 模型,dontAsk 权限(不弹确认框)。干活流程是先抓 <code>code.claude.com</code> 和 <code>platform.claude.com</code> 的文档索引,再定位到具体页面拿答案。</p>
<h3 id="verification">Verification——专门来挑刺的</h3>
<p>Verification 的系统提示第一句话就说:</p>
<blockquote>
<p>Your job is not to confirm the implementation works — it's to try to break it.</p>
</blockquote>
<p>它不是来验证”代码能跑”的,它是来找茬的。</p>
<p>当主 Agent 完成一项实现任务后,Verification 被自动调用。它会跑构建、测试、lint,然后根据变更类型(前端、后端、CLI、数据库迁移等各有各的检查套路)做针对性验证,还要跑边界值测试和对抗性探测。</p>
<p>输出格式要求严格:每条检查必须附带实际执行的命令和输出,不能只说”看起来没问题”。最后给出 VERDICT:PASS、FAIL 或 PARTIAL。默认后台运行,模型继承主 Agent。</p>
<hr>
<p>这五个内置 SubAgent 各管一摊:搜索、规划、执行、查文档、找茬。共同点是它们都把高噪声的工作留在子进程里,<strong>不让垃圾信息堆到主对话中。</strong></p>
<hr>
<h2 id="section-7">4. 什么时候该用,什么时候不该用</h2>
<p>其实判断标准很简单:<strong>主对话需不需要承载过程本身?</strong></p>
<h3 id="subagent-2">适合用 SubAgent 的场景</h3>
<ol>
<li><strong>有高噪声输出的任务</strong>——主对话只关心结论,不关心过程。比如搜索 30 个文件找一个 API 定义。</li>
<li><strong>角色边界非常明确的任务</strong>——天然需要和其他任务隔离开。比如代码审查只看不改。</li>
<li><strong>可以并行执行的研究型任务</strong>——比如同时调研三个模块的实现方式。</li>
<li><strong>可以拆成清晰阶段的流水线式任务</strong>——比如先调研,再规划,再实现。</li>
</ol>
<h3 id="subagent-3">不适合用 SubAgent 的场景</h3>
<table>
<thead>
<tr>
<th>你想做的事</th>
<th>该用什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>读取一个已知路径的文件</td>
<td><code>Read</code> 工具</td>
</tr>
<tr>
<td>搜索 "class Foo" 在哪</td>
<td><code>Grep</code> 工具</td>
</tr>
<tr>
<td>在 2-3 个文件里找东西</td>
<td><code>Read</code> 工具</td>
</tr>
<tr>
<td>简单的文本修改</td>
<td><code>Edit</code> 工具直接改</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>重要提醒</strong>:子代理不能再嵌套调用子代理。所有编排都必须由主对话完成,流水线的调度中心只有一个。</p>
</blockquote>
<hr>
<h1 id="section-8">实践篇</h1>
<hr>
<h2 id="subagent-4">5. 三步创建自定义 SubAgent</h2>
<h3 id="section-9">方式一:交互式(推荐新手)</h3>
<p>在 Claude Code 中输入 <code>/agents</code>,按照向导操作即可。</p>
<h3 id="section-10">方式二:手写配置文件(推荐进阶)</h3>
<p>直接创建 <code>.claude/agents/your-agent.md</code> 文件。优势是更精细的控制、方便版本管理、可以从其他项目复制。</p>
<h3 id="cli-cicd">方式三:CLI 参数临时创建(适合 CI/CD)</h3>
<p>通过 <code>--agents</code> 参数在启动时传入 JSON 格式的子代理定义。<strong>仅在当前会话中存在,不会保存到磁盘。</strong></p>
<pre><code class="language-bash">claude --agents '[{"name":"lint-checker","tools":["Bash","Read"]}]'
</code></pre>
<p>这种方式特别适合 CI/CD 自动化:<strong>在流水线中临时创建任务专用的子代理。</strong></p>
<hr>
<h2 id="section-11">6. 配置文件完全指南</h2>
<p>一个完整的子代理配置文件长这样:</p>
<pre><code class="language-markdown">---
name: code-reviewer
description: Review code for security issues and best practices. Use after code changes.
tools:
- Read
- Grep
- Glob
permissionMode: plan
model: sonnet
skills:
- chain-knowledge
- recent-incidents
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate-readonly-query.sh"
---
你是一个代码审查专家。
当被调用时:
1. 首先理解代码变更的范围
2. 检查安全问题
3. 检查代码规范
4. 提供改进建议
输出格式:
## 审查结果
- 安全问题:[列表]
- 规范问题:[列表]
- 建议:[列表]
</code></pre>
<h3 id="frontmatter">frontmatter 字段详解</h3>
<table>
<thead>
<tr>
<th>字段</th>
<th>作用</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td>子代理的唯一标识</td>
<td>如 <code>code-reviewer</code></td>
</tr>
<tr>
<td><code>description</code></td>
<td>决定 Claude <strong>何时自动调用</strong>这个子代理</td>
<td>说清楚做什么和什么时候用</td>
</tr>
<tr>
<td><code>tools</code></td>
<td>工具白名单</td>
<td>只开放必要的工具</td>
</tr>
<tr>
<td><code>disallowedTools</code></td>
<td>工具黑名单</td>
<td>不要和 <code>tools</code> 同时用</td>
</tr>
<tr>
<td><code>model</code></td>
<td>选择模型</td>
<td><code>sonnet</code>、<code>opus</code>、<code>haiku</code> 等</td>
</tr>
<tr>
<td><code>permissionMode</code></td>
<td>权限模式</td>
<td>控制遇到权限操作时如何处理</td>
</tr>
<tr>
<td><code>skills</code></td>
<td>预加载的技能列表</td>
<td>子代理不继承主对话的 Skill,需要显式列出</td>
</tr>
<tr>
<td><code>hooks</code></td>
<td>生命周期钩子</td>
<td>只在子代理运行期间生效,结束后自动清理</td>
</tr>
<tr>
<td><code>maxTurns</code></td>
<td>最大执行轮次</td>
<td>防止无限循环</td>
</tr>
<tr>
<td><code>effort</code></td>
<td>思考努力级别(0-1)</td>
<td>简单任务用低值,复杂任务用高值</td>
</tr>
</tbody>
</table>
<h3 id="section-12">工具权限的最小特权原则</h3>
<p>遵循一个原则:<strong>能用 Read 完成的任务,就不要给 Edit。</strong></p>
<pre><code>只读型(审计/检查) 研究型(信息收集) 开发型(读写改)
├── Read ├── Read ├── Read
├── Grep ├── Grep ├── Write
└── Glob ├── Glob ├── Edit
├── WebFetch ├── Bash
└── WebSearch ├── Glob
└── Grep
</code></pre>
<h3 id="section-13">子代理存放位置与优先级</h3>
<p>子代理定义有六种来源,同名冲突时高优先级覆盖低优先级。从高到低:</p>
<p><strong>1. Built-in agents(内置)</strong></p>
<p>源码里写死的,比如 Explore、Plan、General-purpose、Verification。你不能改它们也不能删。</p>
<p><strong>2. Plugin agents(插件提供)</strong></p>
<p>装了插件之后,插件自带的子代理会自动注册。名字带命名空间前缀(<code>plugin-name:agent-name</code>),避免跟自定义代理撞名。</p>
<p>插件代理有个安全限制:frontmatter 里写了 <code>permissionMode</code>、<code>hooks</code>、<code>mcpServers</code> 会被直接忽略。源码注释说得很直白——插件是第三方代码,这些字段会让代理的权限超出用户安装时批准的范围。如果你需要这些控制能力,得在 <code>.claude/agents/</code> 里手写,那里的定义是你自己审核过的。</p>
<p><strong>3. User agents(用户级)</strong></p>
<p>放在 <code>~/.claude/agents/</code> 目录下(Windows 是 <code>%USERPROFILE%\.claude\agents\</code>)。对当前用户所有项目生效。比如你有一个通用的代码审查代理,放到这里,不管在哪个项目里都能用。</p>
<p>创建方式:直接往这个目录丢 <code>.md</code> 文件就行,或者在 Claude Code 里输入 <code>/agents</code> 选择"用户级"位置。</p>
<p><strong>4. Project agents(项目级)</strong></p>
<p>放在<strong>项目</strong>根目录的 <code>.claude/agents/</code> 下。只对当前项目生效。好处是可以提交到 Git,团队共享。</p>
<pre><code>your-project/
└── .claude/
└── agents/
├── code-reviewer.md
└── deploy-checker.md
</code></pre>
<p><strong>5. Flag agents(CLI 参数)</strong></p>
<p>通过 <code>claude --agents</code> 参数在启动时传入 JSON 格式定义。只存在于当前会话,关掉就没了。适合 CI/CD 流水线或者临时用一下的场景。示例:</p>
<pre><code class="language-bash">claude --agents '[{"name":"quick-check","tools":["Read","Grep"]}]'
</code></pre>
<p><strong>6. Managed agents(企业管理)</strong></p>
<p>这是最低优先级,也是最少人知道的一种。源码里的 source 叫 <code>policySettings</code>。</p>
<p>Managed agents 存放在系统级的管理目录里:</p>
<ul>
<li>macOS: <code>/Library/Application Support/ClaudeCode/.claude/agents/</code></li>
<li>Windows: <code>C:\Program Files\ClaudeCode\.claude\agents\</code></li>
<li>Linux: <code>/etc/claude-code/.claude/agents/</code></li>
</ul>
<p>由 IT 管理员配置,普通用户改不了。它的设计目的是让企业管理员给团队统一下发子代理定义——比如全公司通用的安全审计代理、合规检查代理。</p>
<p>Managed agents 的加载路径来自 <code>getManagedFilePath()</code>,这个目录也存放企业级的 <code>managed-settings.json</code> 配置。源码里的 <code>getManagedFilePath()</code> 还支持一个 drop-in 目录(<code>managed-settings.d/</code>),里面可以放多个配置文件按字母顺序叠加上去。</p>
<p>因为优先级最低,如果用户或项目里有同名的代理,企业下发的版本会被覆盖。这是有意为之:<strong>让本地自定义优先于企业默认。</strong></p>
<h3 id="section-14">正文部分(子代理的系统提示词)</h3>
<p><code>---</code> 之间的 frontmatter 是配置,下面的 markdown 正文是<strong>子代理的系统提示词</strong>。子代理只会收到这段系统提示词和基本环境信息,不会继承主对话的完整系统提示词。</p>
<hr>
<h2 id="section-15">7. 前台、后台与恢复</h2>
<h3 id="foreground">前台模式(Foreground)</h3>
<p>子代理在执行期间<strong>阻塞主对话</strong>。权限弹窗和问题会实时传递给用户。适用于需要人工审批、人工交互的任务。</p>
<h3 id="background">后台模式(Background)</h3>
<p>子代理<strong>并行执行</strong>,用户可以继续在主对话中工作,适合独立的探索或分析任务。</p>
<p>Claude 会根据任务自动选择前台或后台。也可以手动控制:</p>
<ul>
<li>对 Claude 说 "run this in the background"</li>
<li>正在运行的前台子代理可以按 <strong>Ctrl+B</strong> 切换到后台</li>
</ul>
<blockquote>
<p>切换到后台时,Claude Code 会预先请求子代理可能需要的所有权限,因为后台运行时无法弹出交互式确认。</p>
</blockquote>
<h3 id="resume">恢复(Resume)</h3>
<p>每个子代理执行完成后,Claude 会自动获得它的 agent ID。你可以让 Claude 在之前的基础上继续:</p>
<pre><code>用 code-reviewer 子代理审查认证模块
[子代理完成]
继续刚才的审查,再看一下授权逻辑
</code></pre>
<p>恢复会保留之前的对话历史,让它从上次停下的地方继续,而不是重新开始。</p>
<p>但注意:<strong>Explore 和 Plan 是一次性代理</strong>,执行完毕后不能通过 SendMessage 继续对话。</p>
<hr>
<h2 id="prompt">8. 最佳实践:Prompt 怎么写</h2>
<h3 id="section-16">核心原则</h3>
<p>源码里有一段系统提示,是 Claude Code 告诉自己怎么写子代理 prompt 的:</p>
<blockquote>
<p>Brief the agent like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.</p>
</blockquote>
<p>因为 Fresh Agent(你指定了 subagent_type 的那种)从零开始,没有父 Agent 的任何对话历史。所以 prompt 里必须包含子代理完成任务所需的全部信息。</p>
<h3 id="prompt-1">写得差的 prompt 长什么样</h3>
<pre><code>查一下认证模块
</code></pre>
<p>这种 prompt 的问题:子代理不知道"认证模块"指的是哪部分代码,不知道你已经看过什么,不知道你查完之后要干嘛。它会瞎逛一圈,大概率找出一堆不相关的东西。</p>
<h3 id="prompt-2">写得好的 prompt 长什么样</h3>
<pre><code>我需要了解这个项目中用户认证的完整流程。具体来说:
1. 项目是一个 Next.js 应用,认证相关代码可能在 src/auth/ 或 src/middleware/ 目录下
2. 我已经知道用了 NextAuth.js,但不确定具体配置在哪个文件
3. 我需要找到:登录入口、session 管理、权限校验中间件
4. 每个模块用了什么文件、关键函数名叫什么
最后给我一个调用链的总结,从用户点击登录到请求被校验通过,中间经过了哪些函数。
</code></pre>
<p>差别在哪?背景信息(Next.js、NextAuth.js)、已知信息(已经知道用了 NextAuth)、明确目标(找调用链)、输出格式(总结调用链)。子代理拿到这些,就能精准行动。</p>
<h3 id="section-17">五条规则</h3>
<p><strong>给背景。</strong> 你在做什么项目、用了什么技术栈、为什么需要这个信息。不要假设子代理知道任何上下文。</p>
<p><strong>说目标,别说步骤。</strong> 告诉它你要什么结果,让它自己决定怎么搜。"找出认证流程的调用链"比"先搜 auth 相关文件,再读每个文件,再找出函数调用"好得多。后者是把你的猜测当成了搜索方案,万一前提错了就白费。</p>
<p><strong>交代已知信息。</strong>"我已经看过 src/auth/login.ts,排除了 cookie 方案"。这样子代理不会重复你已经做过的工作。</p>
<p><strong>指定输出格式。</strong>"200 字以内"、"列出每个模块对应的文件路径和关键函数名"。没有格式约束的输出要么太长要么太短。</p>
<p><strong>不要甩锅。</strong>"基于你的发现,修复 bug"——反面教材。子代理跑完调研,你拿到结果,你自己判断怎么修。让它既调研又修复,等于把决策外包了。</p>
<p>源码里的原话:</p>
<blockquote>
<p>Never delegate understanding. Don't write "based on your findings, fix the bug." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change.</p>
</blockquote>
<h3 id="section-18">并行和串行</h3>
<p>并行和串行是 Claude Code 内部的调度策略,了解它可以帮助你更有效地给 Claude 下指令。</p>
<p><strong>并行</strong>:如果你跟 Claude 说"同时帮我调研三个模块的实现方式",Claude 会在同一条消息里发出多个子代理调用。这些子代理同时启动、同时跑、各自独立返回结果。</p>
<p>适合的场景:多个互相不依赖的调研任务。比如"帮我同时看一下前端路由、后端 API、数据库 schema 分别怎么设计的"。</p>
<p><strong>串行</strong>:后一个任务依赖前一个的结果。比如先调研认证模块的结构,再基于调研结果决定怎么加一个新功能。这时候 Claude 会先跑第一个子代理,等结果回来再决定下一步。</p>
<p>适合的场景:有依赖关系的流水线任务。</p>
<p>你该怎么利用这点?在对话里说清楚任务之间的关系就行:</p>
<ul>
<li>"同时帮我查 A 和 B" → Claude 会并行派两个 Explore</li>
<li>"先帮我查 A,查完再基于结果做 B" → Claude 会串行执行</li>
<li>"帮我查 A、B、C,它们之间没有依赖" → Claude 会并行派三个</li>
</ul>
<hr>
<h1 id="section-19">原理篇</h1>
<blockquote>
<p>读源码不只是看它做了什么,更重要的是为什么这么做。基于 v2.1.88 源码聊下 Claude Code SubAgent 系统背后的设计决策。</p>
</blockquote>
<hr>
<h2 id="subagent-5">9. 为什么 SubAgent 是一个微型会话</h2>
<p>Claude Code 团队没有把 SubAgent 当成一个"轻量级的任务派发"。他们把它当成一个<strong>完整的、独立的 Claude Code 会话的微缩版本</strong>。</p>
<p>每次启动一个子代理,系统会:</p>
<ol>
<li>从磁盘或内存找到对应的 AgentDefinition</li>
<li>为它组装一套独立的工具池</li>
<li>构建一段独立的系统提示</li>
<li>创建一个隔离的 ToolUseContext(权限、文件状态、拒绝追踪全是新的)</li>
<li>可选地为它初始化专属的 MCP Server</li>
<li>启动一个独立的 query() 循环</li>
<li>跑完后在 finally 块里做十项清理</li>
</ol>
<p>这个流程跟启动一个新的 Claude Code 会话几乎没有区别,只是<strong>它跑在父进程内部、生命周期由父 Agent 管理</strong>。</p>
<p>为什么要做得这么重?轻量级的做法是共享父 Agent 的上下文和状态,只在工具层面做点过滤就行了。但 Claude Code 选了隔离路线。原因在于一个核心判断:<strong>在 LLM 系统里,上下文污染比上下文缺失更危险。</strong></p>
<p>共享上下文意味着子代理的中间输出会回溢到父对话里。一个 Explore 代理读 30 个文件产生的中间内容,如果留在主对话里,后面做决策时有效信息就被稀释了。相比之下,子代理从零开始需要你在 prompt 里多写几句背景信息——这是可控的成本。上下文污染是不可控的风险。</p>
<p>这就是为什么源码里 Fresh Agent 路径(标准路径)选择了零上下文继承。这是经过权衡的设计决策。</p>
<hr>
<h2 id="vs">10. 两条路径的设计取舍:专业化 vs 缓存效率</h2>
<p>SubAgent 有两条执行路径:Fresh Agent 和 Fork。</p>
<h3 id="fresh-agent">Fresh Agent:为专业化做的选择</h3>
<p>当指定 <code>subagent_type</code> 时,系统走 Fresh 路径。它的四个特征:</p>
<ul>
<li>零上下文继承</li>
<li>专用系统提示</li>
<li>独立工具池</li>
<li>独立权限模式</li>
</ul>
<p>全部服务于同一个目标:<strong>让每个子代理成为某个领域的专家</strong>。</p>
<p>Explore 只有搜索工具、Plan 继承父模型做架构分析、Verification 默认跑在后台专门找茬。工具池、系统提示、模型、运行模式,全都围绕这个代理的职责量身定制。</p>
<p>这种专业化是有代价的。因为每个子代理有独立的系统提示和工具池,它跟父 Agent 的 API 请求前缀不同,<strong>没法共享 Prompt Cache</strong>。每次启动一个 Fresh Agent,基本等于一次全新的 API 调用。源码里还特意把普通子代理的 <code>thinkingConfig</code> 设成 <code>{ type: 'disabled' }</code>,省输出 token,但进一步确保了缓存不可能命中。</p>
<p>设计取舍很清晰:<strong>Fresh Agent 用缓存效率换专业化</strong>。只要你指定了 subagent_type,你就选择了"让这个代理在自己的领域内做最好",而不是"让它尽量便宜"。</p>
<h3 id="fork">Fork:为缓存效率做的选择</h3>
<p>Fork 是反过来的取舍。它不追求专业化,追求的是<strong>让并行子代理尽量便宜</strong>。</p>
<p>看 Fork 的定义:</p>
<pre><code class="language-typescript">export const FORK_AGENT = {
tools: ['*'], // 不过滤工具——保持跟父一致
model: 'inherit', // 不换模型——保持跟父一致
getSystemPrompt: () => '',// 不生成新提示——保持跟父一致
permissionMode: 'bubble',
}
</code></pre>
<p>每一行都在做同一件事:保持跟父 Agent 的 API 请求前缀字节级一致。因为 Anthropic API 的 Prompt Cache 要求前缀完全匹配。系统提示、工具定义、模型、消息前缀、思考配置五个维度全部一致,缓存才能命中。</p>
<p>Fork 通过 <code>buildForkedMessages()</code> 克隆父 Agent 的完整对话历史,给每个 tool_use 塞一个占位符 result,然后附上各自的指令文本。所有 Fork 子代理的前 N 条消息完全相同,只有最后一个文本块不同。</p>
<pre><code>父 Agent 的请求:
...
Fork #1:
... ← 缓存命中
Fork #2:
... ← 缓存命中
</code></pre>
<p>派两个 Fork 子代理并行调研,理论上只有各自最后的那个指令文本是新的 token。相比派两个 Fresh Agent,成本可以低一个数量级。</p>
<p>但 Fork 的限制也来自这个设计:</p>
<ul>
<li>不能换模型(换了缓存就废了)</li>
<li>不能自定义工具池(过滤了工具定义就变了)</li>
<li>不能有独立的系统提示(换了前缀就不同了)。</li>
</ul>
<p>它是一个"跟父 Agent 一模一样,只是<strong>干不同的活"的并行执行单元。</strong></p>
<p>还有一个设计约束:Fork 不能嵌套。<code>isInForkChild()</code> 通过扫描 <code><fork-boilerplate></code> 标签来阻止 Fork 套 Fork。原因也很实际,如果 Fork 可以嵌套,内层 Fork 会继承外层 Fork 已经被污染的上下文,隔离性就保不住了。</p>
<h3 id="fresh-agent-1">为什么不让 Fresh Agent 也共享缓存</h3>
<p>因为 Fresh Agent 的系统提示不同、工具池被过滤、思考配置被禁用、模型可能不同。四个维度的差异导致缓存键完全不同。这是 Fresh Agent 选择专业化的必然代价。</p>
<p>两条路径的设计本质上是一个光谱的两端:</p>
<pre><code>专业化 <──────────────────> 缓存效率
Fresh Agent Fork
独立提示+工具+模型 继承一切
不共享缓存 字节级共享
适合特定职责 适合并行调研
</code></pre>
<hr>
<h2 id="section-20">11. 权限模型:单向棘轮原则</h2>
<p>源码里有一个关于权限的硬性规则,看着简单,背后的设计思路值得细想:</p>
<p><strong>父 Agent 的 <code>bypassPermissions</code>、<code>acceptEdits</code> 和 <code>auto</code> 模式永远优先,子代理降级不了。</strong></p>
<pre><code class="language-typescript">if (
agentPermissionMode &&
state.toolPermissionContext.mode !== 'bypassPermissions' &&
state.toolPermissionContext.mode !== 'acceptEdits' &&
!(feature('TRANSCRIPT_CLASSIFIER') && state.toolPermissionContext.mode === 'auto')
) {
toolPermissionContext = { ...toolPermissionContext, mode: agentPermissionMode }
}
</code></pre>
<p>这是一个单向棘轮(ratchet)设计。权限只能往更严格的方向调,不能往更宽松的方向调。</p>
<p>考虑一个场景:你用 <code>--allowedTools</code> 参数限制了子代理只能用 Read 和 Grep,结果子代理定义里写了 <code>tools: ['*']</code>。如果子代理的权限覆盖了你的限制,你花心思设的工具白名单就白费了。这违背了"调用者控制安全边界"的原则。</p>
<p>类似的棘轮设计还有好几个:</p>
<ul>
<li><p><code>allowedTools</code> 参数替换所有会话级规则,但保留 CLI 参数级规则。会话级规则是你运行时手动批准的,CLI 参数级规则是你启动时明确指定的。后者优先级更高,因为它是更早、更明确的安全决策。</p>
</li>
<li><p>异步 SubAgent 有独立的<strong>拒绝计数器</strong>(<code>localDenialTracking</code>),不影响父 Agent。因为异步代理跑在后台,它的权限拒绝不应该污染父 Agent 的交互体验。</p>
</li>
<li><p>异步代理强制设置 <code>shouldAvoidPermissionPrompts = true</code>即不弹确认框,未授权操作直接拒绝。这同样是为了保安全边界:后台跑着的代理没法跟你交互确认,所以宁可直接拒绝也不默认放行。</p>
</li>
</ul>
<p>所有这些设计的共同点是:**宁可让子代理功能受限,也不让安全边界被突破。**这在 LLM 系统里尤其重要,因为 LLM 的行为不可预测。权限边界的严谨性是最后一道防线。</p>
<hr>
<h2 id="section-21">12. 工具池设计:最小权限原则的实际落地</h2>
<p>工具池组装的逻辑看代码就几行,但设计思路值得琢磨。</p>
<p><code>resolveAgentTools()</code> 支持白名单(tools)和黑名单(disallowedTools)两种模式。白名单模式是**"只给这些"<strong>,黑名单是</strong>"除了这些都给"**。两者同时存在时,<strong>白名单先过一遍,黑名单再过一遍。</strong></p>
<p>比如有这么一个场景:你想让子代理能用大部分工具,但不能写文件也不能执行危险命令。用纯白名单你要列十几二十个工具名,漏了一个就出问题。用纯黑名单只列两个,但你要确保未来新增的工具默认是被允许的。</p>
<p>两阶段过滤给了你第三种选择:先用白名单框一个大致范围,再用黑名单精确排除。这在实践中更灵活。</p>
<p><code>tools: ['*']</code> 的处理也值得注意。它表示"用父的完整工具池",不做任何过滤。Fork 子代理和 General-purpose 代理用这个。设计意图是:当你信任这个子代理、不需要限制它的工具时,不要人为缩小它的能力范围。工具限制是为了约束你不信任的代理,不是为了约束所有代理。</p>
<p>MCP 工具的合并用了 <code>uniqBy(..., 'name')</code>,父工具优先。这也遵循了同样的安全逻辑:如果父 Agent 已经有一个叫 <code>search</code> 的 MCP 工具,子代理想加一个同名的,父的优先——子代理不能覆盖父的工具。</p>
<hr>
<h2 id="section-22">13. 生命周期管理</h2>
<p>子代理的 runAgent() 函数在 finally 块里做了十项清理。这是从实际生产事故里学来的。</p>
<pre><code class="language-typescript">finally {
await mcpCleanup() // 1. MCP 服务器
clearSessionHooks(rootSetAppState, agentId) // 2. 会话钩子
cleanupAgentTracking(agentId) // 3. 缓存追踪
agentToolUseContext.readFileState.clear() // 4. 文件状态缓存
initialMessages.length = 0 // 5. 消息数组
unregisterPerfettoAgent(agentId) // 6. 性能追踪
clearAgentTranscriptSubdir(agentId) // 7. 转录目录
rootSetAppState(prev => { // 8. todos
const { : _removed, ...todos } = prev.todos
return { ...prev, todos }
})
killShellTasksForAgent(agentId, ...) // 9. 后台 shell 任务
// 10. 还有内存释放等隐式清理
}
</code></pre>
<p>清理的这么彻底是因为子代理是一个长期运行的进程,它可能打开 MCP 连接、注册钩子、创建后台 shell 任务、写入文件状态缓存。如果不清理:</p>
<ul>
<li>MCP 连接泄漏——服务器进程不退出,资源浪费</li>
<li>钩子残留——下一个子代理可能意外触发前一个的钩子</li>
<li>文件状态缓存过期——下一个子代理可能读到脏数据</li>
<li>后台 shell 任务失控——子代理已经结束,但它启动的 <code>npm run dev</code> 还在跑</li>
<li>内存泄漏——消息数组、转录文件不被释放</li>
</ul>
<p>源码里对子代理的每一步清理都对应一个可能出问题的场景。这是一种防御性编程的思路:<strong>不假设子代理会正常结束,而是假设它随时可能失败或被取消,确保不管怎么结束都不留垃圾。</strong></p>
<p>生命周期管理的另一个设计是同步转异步的自动机制。**同步执行的子代理如果超时,会自动被切到后台。**源码里用一个竞速实现:</p>
<pre><code>for await (const message of runAgent({ ... })) {
// 正常处理
// 同时有个计时器在跑
// 计时器先到 → 切后台,返回 "launched in background"
}
</code></pre>
<p>这么做主要是因为子代理的执行时间不可预测。一个 Plan 代理分析大型代码库可能需要几分钟,直接超时报错会让用户白等。切到后台让用户可以继续干别的,子代理跑完了再通知。这样会有更好的用户体验。</p>
<hr>
<h2 id="agent">14. Agent 定义的加载策略:信任的梯度</h2>
<p>六种 Agent 来源的优先级:Built-in > Plugin > User > Project > Flag > Managed;这不是一个随意的排序。它反映了 Claude Code 对**"谁更可信"的判断梯度**。</p>
<p>代码里硬编码的 <strong>Built-in agents 可信度最高</strong>,因为它们经过 Anthropic 团队测试。<strong>插件其次</strong>,因为安装时用户批准了插件的权限清单。<strong>用户级和项目级是用户自己写的,可信度再低一档</strong>——不是因为用户不靠谱,而是因为项目级配置可能被 Git 提交者篡改(源码注释里明确提到了这个安全考虑)。<strong>Flag 是临时传的,生命周期最短</strong>。<strong>Managed 是 IT 管理员下发的,优先级最低</strong>,因为企业管理员不知道你的项目具体需要什么。</p>
<p>Plugin agents 有一个额外的安全限制:frontmatter 里的 <code>permissionMode</code>、<code>hooks</code>、<code>mcpServers</code> 会被直接忽略。源码注释说得很直白——插件是第三方代码,这些字段能让代理的权限超出用户安装时批准的范围。</p>
<p>这个限制说明了一个设计原则:<strong>安装时信任边界和运行时信任边界要一致。</strong> 用户安装插件时批准了一组能力,运行时不能通过 frontmatter 悄悄增加新能力。如果你需要这些控制,得在 <code>.claude/agents/</code> 里手写——因为那里的定义是你自己审核过的。</p>
<p>Agent 定义从 .md 文件解析的逻辑也值得一看。<code>parseAgentFromMarkdown()</code> 把文件拆成 frontmatter(YAML 配置)和正文(系统提示)。这个设计的妙处是:它让 Agent 定义既能被人直接阅读和编辑(就是 markdown 文件),又能被程序精确解析(YAML 是结构化的)。不需要额外的 schema 文件或编译步骤。这种"配置即文档"的思路贯穿了整个 Claude Code 的设计。</p>
<hr>
<h2 id="mcp-server-vs">15. MCP Server 的隔离策略:共享 vs 专属的取舍</h2>
<p>子代理可以在 frontmatter 里声明 MCP 服务器。源码里分了两种处理方式:引用(字符串)和内联(对象)。</p>
<p>引用方式使用全局配置里的 MCP 连接,子代理结束时不断开。内联方式创建专属连接,跑完就清理。</p>
<p>分两种是因为 MCP 服务器的启动成本不低。有些服务器(比如数据库连接、浏览器实例)初始化要好几秒。如果每个子代理都重新连一遍,并行跑三个子代理就要等三遍启动。引用方式通过共享连接避免了这个问题。</p>
<p>但共享连接有隐患:**如果子代理修改了 MCP 服务器的状态(比如改了数据库里的数据),下一个用同一个连接的子代理会看到脏状态。**内联方式通过"跑完就清理"来隔离——每个子代理拿到的是全新的 MCP 实例。</p>
<p>引用方式快但不隔离,内联方式隔离但慢。源码让你根据场景选。</p>
<p>还有一个安全限制:当 MCP 被锁定为"仅限插件"时,用户自定义的 Agent 不能声明 frontmatter MCP。<strong>这又是一个棘轮设计——企业管理员锁了 MCP 策略,用户级代理不能绕过。</strong></p>
<hr>
<h2 id="worktree">16. Worktree 隔离:让子代理在自己的沙箱里改代码</h2>
<p>Worktree 隔离的用途很直接:让子代理在一个独立的 Git Worktree 里修改文件,不影响你的工作目录。</p>
<p>设计上有个细节值得注意:如果子代理跑完没有产生任何文件改动,Worktree 和分支会被自动清理。如果有改动,则返回路径和分支名,由用户决定怎么处理。</p>
<p>并行跑多个子代理时,每个都会创建一个 Worktree。如果都保留下来,仓库里会堆满废弃的 Worktree。自动清理降低了运维成本。</p>
<p>Fork 子代理在 Worktree 里跑时还会收到一个路径翻译通知。这解决了 Fork 继承上下文带来的一个实际问题:Fork 继承了父 Agent 的对话历史,历史里的文件路径指向父的工作目录。但子代理现在在 Worktree 里,路径不同了。如果不做翻译,子代理会去父目录操作,Worktree 隔离就形同虚设。</p>
<p>这个细节说明了一个重要的思路:<strong>隔离不是一次性动作,而是需要在整个生命周期中持续维护的属性。</strong> 创建 Worktree 只是第一步,路径翻译、权限隔离、状态隔离、资源清理,每一层都要考虑隔离的一致性。</p>
<hr>
<h2 id="sub-agent-multi-agent">17. 从 Sub-Agent 到 Multi-Agent:架构选型的三角博弈</h2>
<p>把视线从 Claude Code 的源码里拔出来,看看更大图景。</p>
<p>Sub-Agent 是多 Agent 系统的基础形态。但在实际工程中,纯 Supervisor + Sub-Agent 模式会遇到四个挑战:</p>
<p><strong>状态复杂度。</strong> 一个 Sub-Agent 的轻微错误可能级联放大。应对方法是在每个 Agent 输出端设检查点。源码里的 Verification 代理干的就是这个。</p>
<p><strong>非确定性调试。</strong> LLM 的输出不固定,出问题时需要完整链路追踪。源码里的侧链转录(<code>recordSidechainTranscript</code>)和链式 UUID 就是为此设计的。</p>
<p><strong>部署复杂度。</strong> 多 Agent 系统不能简单"停机更新",得渐进式部署。源码里的 Feature Flag 门控(<code>FORK_SUBAGENT</code>、<code>tengu_agent_list_attach</code> 等)就是渐进式发布的工具。</p>
<p><strong>同步瓶颈。</strong> 当前大多数 Sub-Agent 是同步执行的——父 Agent 阻塞等待子代理完成。未来的方向是异步执行加上 Agent 间消息通道。源码里已经为这个做了准备:<code>registerAsyncAgent</code>、<code>enqueueAgentNotification</code>、独立的 AbortController。</p>
<p>架构选型归根结底是三个维度的博弈:性能、成本和可控性。纯 Sub-Agent 模式隔离性最好(可控性强),但 token 消耗大约 15 倍(成本高),研究时间最多缩短 90%(性能好)。你需要根据实际情况决定怎么取舍。</p>
<p>典型的演进路径:</p>
<pre><code>第一步:单 Agent + Tools
第二步:单 Agent + Skills
第三步:Supervisor + Sub-Agents
第四步:混合架构(Router 分类 + Sub-Agent 并行 + Handoff 顺序流程)
</code></pre>
<p>几条经验:从单 Agent 开始,碰到瓶颈再升级。先加工具再加 Agent。选对模型比堆 token 管用。多 Agent 的第一价值是隔离,不是并行。</p>
<hr>
<h1 id="section-23">附录</h1>
<hr>
<h2 id="section-24">18. 总结一下源码里藏着的设计巧思</h2>
<ol>
<li><p><strong>CLAUDE.md 瘦身。</strong> Explore 和 Plan 默认省略 CLAUDE.md 和 gitStatus(能到 40KB)。它们是只读代理,不需要 commit/PR/lint 规则,自己会跑 <code>git status</code> 拿最新数据。每周省 5-15 Gtok。这不是优化技巧——这是"只加载必要信息"的设计原则的体现。</p>
</li>
<li><p><strong>验证提示。</strong> 子代理连续完成 3+ 个任务没验证结果的话,系统会注入提醒。这反映了一个判断:LLM 在执行模式中倾向于"完成任务"而不是"验证质量"。系统需要主动干预来纠正这个倾向。</p>
</li>
<li><p><strong>一次性代理。</strong> Explore 和 Plan 被标记为一次性代理,跑完不能通过 SendMessage 继续。原因是这两个代理的职责是"搜索和规划",不需要多轮交互。如果允许继续,会增加复杂度但不会增加多少价值。<strong>不为不存在的场景做设计。</strong></p>
</li>
<li><p><strong>后台摘要。</strong> 长运行的子代理,系统每 30 秒跑一次后台摘要。这是在"让用户了解进度"和"不要产生太多输出"之间找平衡。</p>
</li>
<li><p><strong>Bash 禁止 cd。</strong> 子代理上下文里 Bash 工具不让切目录。一个在预期路径之外游荡的子代理是不可调试的。</p>
</li>
<li><p><strong>Agent 列表缓存优化。</strong> Agent 列表从工具描述挪到了 system-reminder 消息里注入。因为 MCP/插件/权限一变列表就变,放在工具描述里会频繁打碎 Prompt Cache。<strong>把经常变的东西从缓存关键路径上移走。</strong></p>
</li>
<li><p><strong>Fork 的"不偷看"规则。</strong> 系统提示写死了:主 Agent 不能读 Fork 子代理 output_file,除非用户明确要求。原因:读子代理的中间输出会把噪声拉回主对话,违背了 Fork 的设计初衷——把过程隔离在子代理里。</p>
</li>
<li><p><strong>跑完全面清理。</strong> finally 块清掉十项资源。子代理跑完不留垃圾。这不是代码洁癖,这是从生产事故里学来的教训。</p>
</li>
</ol>
<hr>
<h2 id="section-25">19. 源码关键文件索引</h2>
<table>
<thead>
<tr>
<th>文件</th>
<th>行数</th>
<th>核心设计职责</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>src/tools/AgentTool/AgentTool.tsx</code></td>
<td>~1387</td>
<td>路由决策,生命周期管理</td>
</tr>
<tr>
<td><code>src/tools/AgentTool/runAgent.ts</code></td>
<td>~974</td>
<td>执行引擎,资源管理,防御性清理</td>
</tr>
<tr>
<td><code>src/tools/AgentTool/forkSubAgent.ts</code></td>
<td>~211</td>
<td>缓存一致性,递归防护</td>
</tr>
<tr>
<td><code>src/tools/AgentTool/prompt.ts</code></td>
<td>~287</td>
<td>双路径提示策略</td>
</tr>
<tr>
<td><code>src/tools/AgentTool/loadAgentsDir.ts</code></td>
<td>~756</td>
<td>信任梯度加载</td>
</tr>
<tr>
<td><code>src/tools/AgentTool/agentToolUtils.ts</code></td>
<td>-</td>
<td>最小权限工具过滤</td>
</tr>
<tr>
<td><code>src/utils/forkedAgent.ts</code></td>
<td>~690</td>
<td>缓存共享,上下文隔离</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="section-26">写在最后</h2>
<p>Claude Code 的 SubAgent 系统不是"派个任务出去"这么简单,他有一些自己的核心判断:</p>
<p><strong>隔离比共享安全。</strong> 子代理从零开始、独立的工具池和权限、彻底的 finally 清理——每一层都在避免父 Agent 跟子代理互相干扰。上下文污染是不可控的风险,多写几句 prompt 背景是可控的成本。</p>
<p><strong>专业化跟缓存效率是一对矛盾。</strong> Fresh Agent 选了专业化,Fork 选了缓存效率。两条路径的存在不是因为技术上的巧合,而是因为这两种需求都是真实的。在写自定义 Agent 时也需要做同样的取舍: Agent 是要高度专业化(窄工具、专用提示),还是要尽量便宜(宽工具、继承上下文)?</p>
<p><strong>安全边界是单向的。</strong> 权限只能降不能升、插件不能声明 hooks 和 MCP、企业管理员锁了策略用户就绕不过去。这些棘轮设计背后的逻辑是:LLM 的行为不可预测,权限边界是最后一道防线。</p>
</div>
<div id="MySignature" role="contentinfo">
<p>本文来自博客园,作者:knqiufan,转载请注明原文链接:https://www.cnblogs.com/knqiufan/p/19838019</p><br><br>
来源:https://www.cnblogs.com/knqiufan/p/19838019 回复内容:
哇,这篇帖子太干货了!收藏+1
之前虽然一直在用Claude Code的Explore和Plan子代理,但确实没深入想过背后有这么多设计考量。特别是“隔离比共享安全”这个核心思路,讲得很通透。
看完最大的几个收获:
[*]终于搞清楚了Fresh Agent和Fork的区别——一个是专业化路线,一个是缓存效率优先,明白了什么时候该用哪个
[*]权限的单向棘轮设计很值得学习,子代理权限只能降不能升,这个思路在很多系统设计里都适用
[*]子代理的配置文件用markdown+frontmatter的方式确实优雅,既能human-readable又能程序解析
想请教一下:
关于自定义SubAgent的配置,如果我想在团队里推广,有没有推荐的配置管理方式?比如是把agent定义放在项目仓库里好,还是放在用户级目录比较好?看了帖子里的优先级说明,但还是有点纠结。
另外Misaka项目支持自定义Agent配置吗?之前fork下来看了下,感觉是个很好的学习项目,赞一个!
最后:
感谢博主的分享,期待更多关于Claude Code的深度解析!👍 看了这篇帖子,对 SubAgent 的设计理解又深了一层,感谢楼主的细致拆解!
特别是权限“只降不升”的棘轮机制,我之前在写自动化工作流的时候隐约有类似的直觉,但一直没提炼出这么清晰的原则。 想顺着这个点请教一下楼主:在实际的 Misaka GUI 客户端里,不同子代理的权限边界是怎么在 GUI 层体现给用户的呢?
是做了可视化的权限开关面板,还是完全靠 frontmatter 配置透明给用户看?因为如果能在图形界面里直观展示“当前子代理不可写外部文件”这类状态,对排查问题会友好很多。
另外也去瞟了一眼 Misaka 仓库,发现 issues 里已经有不少好建议了。感觉这个项目很适合做成“可插拔 SubAgent 商店”的形态,让各类专用子代理能以插件化方式接入。不知道楼主有没有这方面的规划?如果有机会一起贡献 PR 的话,我也很乐意参与 我也蹲个这个问题的答案!之前刚下Misaka玩的时候,调用写文档的子代理死活存不到指定文件夹,折腾半天最后才反应过来是默认给子代理开的是只读权限,要是界面上能直接把当前子代理的权限列出来真的会友好特别多,排查问题效率直接拉满。
楼主这篇拆解真的太戳痛点了,之前自己瞎搭多Agent流程的时候还出过子代理权限溢出改了系统文件的乌龙,看到那个权限棘轮机制瞬间就明白之前问题出在哪了,之前怎么就没总结出这么清晰的原则啊哈哈
已经去GitHub给Misaka点完star了,之前看楼主写的Claude Code完全指南就收获特别多,这次的SubAgent拆解也直接收藏存笔记了,等后面有空研究下代码,有能力的话也试着提个PR贡献点小功能~
頁:
[1]