入门篇


1. 一个比喻理解 SubAgent

想象你是一个项目经理(主 Agent),手下有几个专员(SubAgent)。你不会自己去翻 200 个文件找答案——你会把任务交给调研专员,让他去翻,他翻完了最终再把结论汇报给你。

这就是 SubAgent 做的事:主 Agent 把任务派给一个独立的子进程去执行,子进程干完后只把结论带回来。

比如 Claude Code 内部的工具调用:

Agent({
  SubAgent_type: "Explore",
  prompt: "搜索整个代码库,找出所有 API 端点定义"
})

这段调用会启动一个 Explore 类型的子代理,它自己去搜索、读取文件、分析代码,最后把结果摘要返回。主 Agent 只看到结论不看到过程。

一句话总结:SubAgent = 一个拥有独立上下文窗口的自治 Worker,干完活只交结论。


2. SubAgent 解决的三个核心问题

问题一:上下文污染

Claude 的上下文窗口再大也是有限的。如果让主 Agent 自己去搜 30 个文件,那些搜索结果、文件内容、中间分析全部留在主对话里,等真正要做决策时,那上下文窗口可能已经快满了。

SubAgent 的解决方案是让自己天然拥有一个独立的上下文窗口。即中间过程全都留在子代理里,主对话只看结论。也就是说子代理执行完毕后,这些中间内容就消失了。

简单判断:如果信息对当下执行是必要的,但对后续决策是噪声——用子代理。

问题二:行为不可控

主 Agent 通常拥有完整的工具权限(读文件、写文件、执行命令)。但某些任务你只想让它"看",不想让它"改"。

对于这个问题 SubAgent 的解决方案是精确的工具权限控制。即我们可以定义一个只读型子代理,只给它 ReadGrepGlob 三个工具,这样它想改也改不了了。

# 只读型子代理(代码审查)
tools: Read, Grep, Glob

# 开发型子代理(bug 修复)
tools: Read, Write, Edit, Bash

# 研究型子代理(技术调研)
tools: Read, WebFetch, WebSearch

问题三:经验无法沉淀

每次都要手动告诉 Claude "去查这个、用那个方式分析"。这些操作步骤无法复用。

针对这个问题 SubAgent 的解决方案是配置即文件。子代理的定义保存在 .md 文件中,可以放进 Git 与团队共享,好用的配置可以复制到其他项目。

所以 SubAgent 可以用三个词概括:隔离、约束、复用。那么再从更高层面看 SubAgent 的设计哲学,其实就是将一个大脑拆成多个岗位角色,每个岗位只做一件事,并且有明确的权限边界。


3. 你可能已经在使用了

Claude Code 内置了几个 SubAgent。当你在对话里说”帮我看看代码库结构”、”先规划一下怎么做”、或者 Claude 自动走验证流程的时候,这些 SubAgent 就在干活。而你可能根本没注意到。

Explore(代码库的搜索引擎)

Explore 是最常用的内置 SubAgent。它的定位很明确:快速搜索、只读分析。

当我们在对话里说比如”帮我找一下所有 API 端点的定义”或者”这个函数在哪些地方被调用了”,Claude 就会启动 Explore 去干活。它会把成百上千行的 grep 结果、文件读取、路径分析全吞进自己的上下文里,最后只给你一份干净的摘要。

搜索深度分三档:quick、medium(默认)、very thorough。这个档位是可以在 prompt 里指定的,Explore 会据此调整搜多广。这不是代码层面的硬限制,纯粹是 prompt 级别的指导:

  • quick 就是跑几条 grep 就收工,适合”某个 class 在哪个文件”这种目标明确的问题
  • medium 则会多搜几个路径、多读几个文件,适合”这个模块的结构是怎样的”
  • very thorough 会在多个目录和命名规范下反复搜,尽量不留死角——适合”梳理认证流程从入口到数据库的完整调用链”。

工具方面 Explore 能用 Glob(按文件名搜)、Grep(按内容搜)、Read(读文件)、Bash(但只能跑只读命令)。在前段时间 Claude Code 暴露的源码里使用 disallowedTools 硬性屏蔽了 Edit、Write、NotebookEdit。说明它确实改不了东西。

外部用户跑 Explore 用的是 Haiku,快且便宜。Anthropic 内部用户则会继承主 Agent 的模型。

Claude Code 暴露的源码里有个不太起眼的阈值:EXPLORE_AGENT_MIN_QUERIES = 3。这个参数的作用是,主 Agent 被告知任务只需要 1-2 次搜索就搞定的别启动 Explore,直接用 Grep/Read,只有明确需要 3 次以上查询时才值得派出去。

另外,Explore 默认省略 CLAUDE.md 和 gitStatus(能到 40KB)。只读代理不需要知道 commit 规范和 PR 流程,自己会跑 git status。这一项每周会省 5-15 Gtok。

Plan(动手之前先想清楚)

Plan 的定位是软件架构师。它不写代码,专门在动手之前把方案想透。

比如当我们跟 Claude 说”我想给系统加个支付模块”,这个时候 Claude 就会先派 Plan 去调研,Plan 会读现有代码、找已有的模式和约定、理清依赖关系、最后输出一份分步实施计划。

系统提示给 Plan 定义了四步流程:

  • 理解需求
  • 深入探索(读代码、追踪调用链、参考已有实现)
  • 设计方案(考虑取舍)
  • 输出计划(分步策略、依赖关系、可能的坑)。

输出必须以”Critical Files for Implementation”结尾,并列出最关键的 3-5 个文件,这样主 Agent 拿到这份计划就知道下一步该读什么、改什么了。

Plan 跟 Explore 一样只读——同样的使用了 disallowedTools,改不了文件。但模型不同:Plan 继承主 Agent 的模型,不会降级到 Haiku。架构设计需要更强的推理能力,用便宜模型容易翻车。

Explore 和 Plan 的分工边界是:Explore 搜完就交结果,Plan 搜完还要分析、权衡、给建议。找函数在哪用 Explore,搞清”加这个功能要改哪些文件、按什么顺序改”用 Plan。

General-purpose(什么都干的全能选手)

Explore 和 Plan 都被硬性禁止了 Edit、Write 等工具,但 General-purpose 没这个限制。tools: ['*'],即父 Agent 有什么它就能用什么,这是它跟 Explore/Plan 的根本区别。搜索和规划是只读的活儿,而 General-purpose 要真刀真枪改代码。

系统提示很短,两段话完事:

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.

意思是把活干完,别画蛇添足,但也别半途而废。

General-purpose 适合的场景是那种连贯的多步骤流程:先读代码定位问题、再改代码、再跑测试验证。比如”修复认证模块的登录 bug”这种任务。

模型字段故意留空,由 getDefaultSubagentModel() 在运行时决定,是跟着会话配置走的。

Claude Code Guide——产品文档专家

Claude Code 还有一个不太起眼的内置 SubAgent:claude-code-guide。当你问”Claude Code 怎么配 hooks?”、”Agent SDK 怎么用?”的时候,Claude 会派它去查官方文档。

它的工具是 Glob、Grep、Read、WebFetch、WebSearch。Haiku 模型,dontAsk 权限(不弹确认框)。干活流程是先抓 code.claude.com 和 platform.claude.com 的文档索引,再定位到具体页面拿答案。

Verification——专门来挑刺的

Verification 的系统提示第一句话就说:

Your job is not to confirm the implementation works — it's to try to break it.

它不是来验证”代码能跑”的,它是来找茬的。

当主 Agent 完成一项实现任务后,Verification 被自动调用。它会跑构建、测试、lint,然后根据变更类型(前端、后端、CLI、数据库迁移等各有各的检查套路)做针对性验证,还要跑边界值测试和对抗性探测。

输出格式要求严格:每条检查必须附带实际执行的命令和输出,不能只说”看起来没问题”。最后给出 VERDICT:PASS、FAIL 或 PARTIAL。默认后台运行,模型继承主 Agent。


这五个内置 SubAgent 各管一摊:搜索、规划、执行、查文档、找茬。共同点是它们都把高噪声的工作留在子进程里,不让垃圾信息堆到主对话中。


4. 什么时候该用,什么时候不该用

其实判断标准很简单:主对话需不需要承载过程本身?

适合用 SubAgent 的场景

  1. 有高噪声输出的任务——主对话只关心结论,不关心过程。比如搜索 30 个文件找一个 API 定义。
  2. 角色边界非常明确的任务——天然需要和其他任务隔离开。比如代码审查只看不改。
  3. 可以并行执行的研究型任务——比如同时调研三个模块的实现方式。
  4. 可以拆成清晰阶段的流水线式任务——比如先调研,再规划,再实现。

不适合用 SubAgent 的场景

你想做的事 该用什么
读取一个已知路径的文件 Read 工具
搜索 "class Foo" 在哪 Grep 工具
在 2-3 个文件里找东西 Read 工具
简单的文本修改 Edit 工具直接改

重要提醒:子代理不能再嵌套调用子代理。所有编排都必须由主对话完成,流水线的调度中心只有一个。


实践篇


5. 三步创建自定义 SubAgent

方式一:交互式(推荐新手)

在 Claude Code 中输入 /agents,按照向导操作即可。

方式二:手写配置文件(推荐进阶)

直接创建 .claude/agents/your-agent.md 文件。优势是更精细的控制、方便版本管理、可以从其他项目复制。

方式三:CLI 参数临时创建(适合 CI/CD)

通过 --agents 参数在启动时传入 JSON 格式的子代理定义。仅在当前会话中存在,不会保存到磁盘。

claude --agents '[{"name":"lint-checker","tools":["Bash","Read"]}]'

这种方式特别适合 CI/CD 自动化:在流水线中临时创建任务专用的子代理。


6. 配置文件完全指南

一个完整的子代理配置文件长这样:

---
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. 提供改进建议

输出格式:
## 审查结果
- 安全问题:[列表]
- 规范问题:[列表]
- 建议:[列表]

frontmatter 字段详解

字段 作用 备注
name 子代理的唯一标识 如 code-reviewer
description 决定 Claude 何时自动调用这个子代理 说清楚做什么和什么时候用
tools 工具白名单 只开放必要的工具
disallowedTools 工具黑名单 不要和 tools 同时用
model 选择模型 sonnetopushaiku 等
permissionMode 权限模式 控制遇到权限操作时如何处理
skills 预加载的技能列表 子代理不继承主对话的 Skill,需要显式列出
hooks 生命周期钩子 只在子代理运行期间生效,结束后自动清理
maxTurns 最大执行轮次 防止无限循环
effort 思考努力级别(0-1) 简单任务用低值,复杂任务用高值

工具权限的最小特权原则

遵循一个原则:能用 Read 完成的任务,就不要给 Edit。

只读型(审计/检查)         研究型(信息收集)         开发型(读写改)
├── Read                    ├── Read                   ├── Read
├── Grep                    ├── Grep                   ├── Write
└── Glob                    ├── Glob                   ├── Edit
                            ├── WebFetch               ├── Bash
                            └── WebSearch              ├── Glob
                                                       └── Grep

子代理存放位置与优先级

子代理定义有六种来源,同名冲突时高优先级覆盖低优先级。从高到低:

1. Built-in agents(内置)

源码里写死的,比如 Explore、Plan、General-purpose、Verification。你不能改它们也不能删。

2. Plugin agents(插件提供)

装了插件之后,插件自带的子代理会自动注册。名字带命名空间前缀(plugin-name:agent-name),避免跟自定义代理撞名。

插件代理有个安全限制:frontmatter 里写了 permissionModehooksmcpServers 会被直接忽略。源码注释说得很直白——插件是第三方代码,这些字段会让代理的权限超出用户安装时批准的范围。如果你需要这些控制能力,得在 .claude/agents/ 里手写,那里的定义是你自己审核过的。

3. User agents(用户级)

放在 ~/.claude/agents/ 目录下(Windows 是 %USERPROFILE%\.claude\agents\)。对当前用户所有项目生效。比如你有一个通用的代码审查代理,放到这里,不管在哪个项目里都能用。

创建方式:直接往这个目录丢 .md 文件就行,或者在 Claude Code 里输入 /agents 选择"用户级"位置。

4. Project agents(项目级)

放在项目根目录的 .claude/agents/ 下。只对当前项目生效。好处是可以提交到 Git,团队共享。

your-project/
└── .claude/
    └── agents/
        ├── code-reviewer.md
        └── deploy-checker.md

5. Flag agents(CLI 参数)

通过 claude --agents 参数在启动时传入 JSON 格式定义。只存在于当前会话,关掉就没了。适合 CI/CD 流水线或者临时用一下的场景。示例:

claude --agents '[{"name":"quick-check","tools":["Read","Grep"]}]'

6. Managed agents(企业管理)

这是最低优先级,也是最少人知道的一种。源码里的 source 叫 policySettings

Managed agents 存放在系统级的管理目录里:

  • macOS: /Library/Application Support/ClaudeCode/.claude/agents/
  • Windows: C:\Program Files\ClaudeCode\.claude\agents\
  • Linux: /etc/claude-code/.claude/agents/

由 IT 管理员配置,普通用户改不了。它的设计目的是让企业管理员给团队统一下发子代理定义——比如全公司通用的安全审计代理、合规检查代理。

Managed agents 的加载路径来自 getManagedFilePath(),这个目录也存放企业级的 managed-settings.json 配置。源码里的 getManagedFilePath() 还支持一个 drop-in 目录(managed-settings.d/),里面可以放多个配置文件按字母顺序叠加上去。

因为优先级最低,如果用户或项目里有同名的代理,企业下发的版本会被覆盖。这是有意为之:让本地自定义优先于企业默认。

正文部分(子代理的系统提示词)

--- 之间的 frontmatter 是配置,下面的 markdown 正文是子代理的系统提示词。子代理只会收到这段系统提示词和基本环境信息,不会继承主对话的完整系统提示词。


7. 前台、后台与恢复

前台模式(Foreground)

子代理在执行期间阻塞主对话。权限弹窗和问题会实时传递给用户。适用于需要人工审批、人工交互的任务。

后台模式(Background)

子代理并行执行,用户可以继续在主对话中工作,适合独立的探索或分析任务。

Claude 会根据任务自动选择前台或后台。也可以手动控制:

  • 对 Claude 说 "run this in the background"
  • 正在运行的前台子代理可以按 Ctrl+B 切换到后台

切换到后台时,Claude Code 会预先请求子代理可能需要的所有权限,因为后台运行时无法弹出交互式确认。

恢复(Resume)

每个子代理执行完成后,Claude 会自动获得它的 agent ID。你可以让 Claude 在之前的基础上继续:

用 code-reviewer 子代理审查认证模块
[子代理完成]

继续刚才的审查,再看一下授权逻辑
[Claude 恢复之前的子代理,保留完整上下文]

恢复会保留之前的对话历史,让它从上次停下的地方继续,而不是重新开始。

但注意:Explore 和 Plan 是一次性代理,执行完毕后不能通过 SendMessage 继续对话。


8. 最佳实践:Prompt 怎么写

核心原则

源码里有一段系统提示,是 Claude Code 告诉自己怎么写子代理 prompt 的:

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.

因为 Fresh Agent(你指定了 subagent_type 的那种)从零开始,没有父 Agent 的任何对话历史。所以 prompt 里必须包含子代理完成任务所需的全部信息。

写得差的 prompt 长什么样

查一下认证模块

这种 prompt 的问题:子代理不知道"认证模块"指的是哪部分代码,不知道你已经看过什么,不知道你查完之后要干嘛。它会瞎逛一圈,大概率找出一堆不相关的东西。

写得好的 prompt 长什么样

我需要了解这个项目中用户认证的完整流程。具体来说:

1. 项目是一个 Next.js 应用,认证相关代码可能在 src/auth/ 或 src/middleware/ 目录下
2. 我已经知道用了 NextAuth.js,但不确定具体配置在哪个文件
3. 我需要找到:登录入口、session 管理、权限校验中间件
4. 每个模块用了什么文件、关键函数名叫什么

最后给我一个调用链的总结,从用户点击登录到请求被校验通过,中间经过了哪些函数。

差别在哪?背景信息(Next.js、NextAuth.js)、已知信息(已经知道用了 NextAuth)、明确目标(找调用链)、输出格式(总结调用链)。子代理拿到这些,就能精准行动。

五条规则

给背景。 你在做什么项目、用了什么技术栈、为什么需要这个信息。不要假设子代理知道任何上下文。

说目标,别说步骤。 告诉它你要什么结果,让它自己决定怎么搜。"找出认证流程的调用链"比"先搜 auth 相关文件,再读每个文件,再找出函数调用"好得多。后者是把你的猜测当成了搜索方案,万一前提错了就白费。

交代已知信息。"我已经看过 src/auth/login.ts,排除了 cookie 方案"。这样子代理不会重复你已经做过的工作。

指定输出格式。"200 字以内"、"列出每个模块对应的文件路径和关键函数名"。没有格式约束的输出要么太长要么太短。

不要甩锅。"基于你的发现,修复 bug"——反面教材。子代理跑完调研,你拿到结果,你自己判断怎么修。让它既调研又修复,等于把决策外包了。

源码里的原话:

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.

并行和串行

并行和串行是 Claude Code 内部的调度策略,了解它可以帮助你更有效地给 Claude 下指令。

并行:如果你跟 Claude 说"同时帮我调研三个模块的实现方式",Claude 会在同一条消息里发出多个子代理调用。这些子代理同时启动、同时跑、各自独立返回结果。

适合的场景:多个互相不依赖的调研任务。比如"帮我同时看一下前端路由、后端 API、数据库 schema 分别怎么设计的"。

串行:后一个任务依赖前一个的结果。比如先调研认证模块的结构,再基于调研结果决定怎么加一个新功能。这时候 Claude 会先跑第一个子代理,等结果回来再决定下一步。

适合的场景:有依赖关系的流水线任务。

你该怎么利用这点?在对话里说清楚任务之间的关系就行:

  • "同时帮我查 A 和 B" → Claude 会并行派两个 Explore
  • "先帮我查 A,查完再基于结果做 B" → Claude 会串行执行
  • "帮我查 A、B、C,它们之间没有依赖" → Claude 会并行派三个

原理篇

读源码不只是看它做了什么,更重要的是为什么这么做。基于 v2.1.88 源码聊下 Claude Code SubAgent 系统背后的设计决策。


9. 为什么 SubAgent 是一个微型会话

Claude Code 团队没有把 SubAgent 当成一个"轻量级的任务派发"。他们把它当成一个完整的、独立的 Claude Code 会话的微缩版本

每次启动一个子代理,系统会:

  1. 从磁盘或内存找到对应的 AgentDefinition
  2. 为它组装一套独立的工具池
  3. 构建一段独立的系统提示
  4. 创建一个隔离的 ToolUseContext(权限、文件状态、拒绝追踪全是新的)
  5. 可选地为它初始化专属的 MCP Server
  6. 启动一个独立的 query() 循环
  7. 跑完后在 finally 块里做十项清理

这个流程跟启动一个新的 Claude Code 会话几乎没有区别,只是它跑在父进程内部、生命周期由父 Agent 管理

为什么要做得这么重?轻量级的做法是共享父 Agent 的上下文和状态,只在工具层面做点过滤就行了。但 Claude Code 选了隔离路线。原因在于一个核心判断:在 LLM 系统里,上下文污染比上下文缺失更危险。

共享上下文意味着子代理的中间输出会回溢到父对话里。一个 Explore 代理读 30 个文件产生的中间内容,如果留在主对话里,后面做决策时有效信息就被稀释了。相比之下,子代理从零开始需要你在 prompt 里多写几句背景信息——这是可控的成本。上下文污染是不可控的风险。

这就是为什么源码里 Fresh Agent 路径(标准路径)选择了零上下文继承。这是经过权衡的设计决策。


10. 两条路径的设计取舍:专业化 vs 缓存效率

SubAgent 有两条执行路径:Fresh Agent 和 Fork。

Fresh Agent:为专业化做的选择

当指定 subagent_type 时,系统走 Fresh 路径。它的四个特征:

  • 零上下文继承
  • 专用系统提示
  • 独立工具池
  • 独立权限模式

全部服务于同一个目标:让每个子代理成为某个领域的专家

Explore 只有搜索工具、Plan 继承父模型做架构分析、Verification 默认跑在后台专门找茬。工具池、系统提示、模型、运行模式,全都围绕这个代理的职责量身定制。

这种专业化是有代价的。因为每个子代理有独立的系统提示和工具池,它跟父 Agent 的 API 请求前缀不同,没法共享 Prompt Cache。每次启动一个 Fresh Agent,基本等于一次全新的 API 调用。源码里还特意把普通子代理的 thinkingConfig 设成 { type: 'disabled' },省输出 token,但进一步确保了缓存不可能命中。

设计取舍很清晰:Fresh Agent 用缓存效率换专业化。只要你指定了 subagent_type,你就选择了"让这个代理在自己的领域内做最好",而不是"让它尽量便宜"。

Fork:为缓存效率做的选择

Fork 是反过来的取舍。它不追求专业化,追求的是让并行子代理尽量便宜

看 Fork 的定义:

export const FORK_AGENT = {
  tools: ['*'],            // 不过滤工具——保持跟父一致
  model: 'inherit',        // 不换模型——保持跟父一致
  getSystemPrompt: () => '',  // 不生成新提示——保持跟父一致
  permissionMode: 'bubble',
}

每一行都在做同一件事:保持跟父 Agent 的 API 请求前缀字节级一致。因为 Anthropic API 的 Prompt Cache 要求前缀完全匹配。系统提示、工具定义、模型、消息前缀、思考配置五个维度全部一致,缓存才能命中。

Fork 通过 buildForkedMessages() 克隆父 Agent 的完整对话历史,给每个 tool_use 塞一个占位符 result,然后附上各自的指令文本。所有 Fork 子代理的前 N 条消息完全相同,只有最后一个文本块不同。

父 Agent 的请求:
  [system][tools][msg_1]...[msg_N][assistant_tool_uses]

Fork #1:
  [system][tools][msg_1]...[msg_N][assistant_tool_uses] ← 缓存命中
  [user: "调查模块A"]

Fork #2:
  [system][tools][msg_1]...[msg_N][assistant_tool_uses] ← 缓存命中
  [user: "调查模块B"]

派两个 Fork 子代理并行调研,理论上只有各自最后的那个指令文本是新的 token。相比派两个 Fresh Agent,成本可以低一个数量级。

但 Fork 的限制也来自这个设计:

  • 不能换模型(换了缓存就废了)
  • 不能自定义工具池(过滤了工具定义就变了)
  • 不能有独立的系统提示(换了前缀就不同了)。

它是一个"跟父 Agent 一模一样,只是干不同的活"的并行执行单元。

还有一个设计约束:Fork 不能嵌套。isInForkChild() 通过扫描 <fork-boilerplate> 标签来阻止 Fork 套 Fork。原因也很实际,如果 Fork 可以嵌套,内层 Fork 会继承外层 Fork 已经被污染的上下文,隔离性就保不住了。

为什么不让 Fresh Agent 也共享缓存

因为 Fresh Agent 的系统提示不同、工具池被过滤、思考配置被禁用、模型可能不同。四个维度的差异导致缓存键完全不同。这是 Fresh Agent 选择专业化的必然代价。

两条路径的设计本质上是一个光谱的两端:

专业化 <──────────────────> 缓存效率
   Fresh Agent              Fork
   独立提示+工具+模型        继承一切
   不共享缓存                字节级共享
   适合特定职责              适合并行调研

11. 权限模型:单向棘轮原则

源码里有一个关于权限的硬性规则,看着简单,背后的设计思路值得细想:

父 Agent 的 bypassPermissionsacceptEdits 和 auto 模式永远优先,子代理降级不了。

if (
  agentPermissionMode &&
  state.toolPermissionContext.mode !== 'bypassPermissions' &&
  state.toolPermissionContext.mode !== 'acceptEdits' &&
  !(feature('TRANSCRIPT_CLASSIFIER') && state.toolPermissionContext.mode === 'auto')
) {
  toolPermissionContext = { ...toolPermissionContext, mode: agentPermissionMode }
}

这是一个单向棘轮(ratchet)设计。权限只能往更严格的方向调,不能往更宽松的方向调。

考虑一个场景:你用 --allowedTools 参数限制了子代理只能用 Read 和 Grep,结果子代理定义里写了 tools: ['*']。如果子代理的权限覆盖了你的限制,你花心思设的工具白名单就白费了。这违背了"调用者控制安全边界"的原则。

类似的棘轮设计还有好几个:

  • allowedTools 参数替换所有会话级规则,但保留 CLI 参数级规则。会话级规则是你运行时手动批准的,CLI 参数级规则是你启动时明确指定的。后者优先级更高,因为它是更早、更明确的安全决策。

  • 异步 SubAgent 有独立的拒绝计数器localDenialTracking),不影响父 Agent。因为异步代理跑在后台,它的权限拒绝不应该污染父 Agent 的交互体验。

  • 异步代理强制设置 shouldAvoidPermissionPrompts = true即不弹确认框,未授权操作直接拒绝。这同样是为了保安全边界:后台跑着的代理没法跟你交互确认,所以宁可直接拒绝也不默认放行。

所有这些设计的共同点是:宁可让子代理功能受限,也不让安全边界被突破。这在 LLM 系统里尤其重要,因为 LLM 的行为不可预测。权限边界的严谨性是最后一道防线。


12. 工具池设计:最小权限原则的实际落地

工具池组装的逻辑看代码就几行,但设计思路值得琢磨。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐