第十五章:Agent 与 Skill 如何改写运行时
把 AgentTool 和 SkillTool 放在一起看,理解 Claude Code 怎样动态扩展执行主体和执行策略。
1. 为什么这两个要放在一起拆
前面几篇文档拆的,大多还是“当前这个 agent 自己怎么干活”。
到了 AgentTool 和 SkillTool,味道就完全变了。
它们不再只是“多一个能力”,而是在改运行时本身:
AgentTool会创建新的执行主体SkillTool会改写当前执行主体接下来的行为边界
所以这两个工具放在一起看,才能真正看清 Claude Code 的一个关键设计判断:
Claude Code 不是单个模型循环 + 一堆工具,而是一个可以动态扩展执行主体和执行策略的 agent runtime。
2. 先说结论
我对这两个工具的判断可以压成两句话:
AgentTool的本质是:把“再开一个 agent 去做事”做成正式工具协议SkillTool的本质是:把“加载一段专门工作说明并改变当前上下文”做成正式工具协议
如果换成更偏架构的话说:
AgentTool扩展的是 execution subjectSkillTool扩展的是 execution context
这两个东西一组合,Claude Code 就不再只是“一个会调工具的大模型”,而是一个:
- 可以自己分工
- 可以自己装载专门能力
- 可以按上下文切换工作模式
- 可以把不同执行单元隔离开
的运行时系统。
3. 源码锚点
这次主要看的源码是:
src/tools/AgentTool/AgentTool.tsxsrc/tools/AgentTool/prompt.tssrc/tools/AgentTool/UI.tsxsrc/tools/SkillTool/SkillTool.tssrc/tools/SkillTool/prompt.tssrc/tools/SkillTool/UI.tsx
配套依赖还包括:
src/tools/AgentTool/runAgent.tssrc/tools/AgentTool/loadAgentsDir.tssrc/tools/AgentTool/forkSubagent.tssrc/tools/AgentTool/agentToolUtils.tssrc/utils/forkedAgent.tssrc/utils/processUserInput/processSlashCommand.jssrc/commands.js
4. 总体结构图
flowchart TD
A["当前主 agent"] --> B{"选择哪条扩展路径"}
B -->|"AgentTool"| C["创建新的执行主体"]
B -->|"SkillTool"| D["改造当前执行上下文"]
C --> C1["选择 fork / specialized agent / teammate / remote"]
C1 --> C2["校验 agent 类型、权限、MCP 依赖、隔离方式"]
C2 --> C3["重建子 agent 的工具池与运行上下文"]
C3 --> C4["同步完成 / 后台运行 / 远端运行"]
C4 --> C5["把结果映射回父 agent transcript"]
D --> D1["查找本地 skill / MCP skill / 远程 canonical skill"]
D1 --> D2["校验 skill 名、类型、disable-model-invocation"]
D2 --> D3["按 skill 名做权限决策"]
D3 --> D4["展开 slash command 或加载 SKILL.md"]
D4 --> D5["向当前会话注入 newMessages + contextModifier"]
D5 --> D6["后续回合按新上下文继续执行"]
这张图里最重要的一点是:
这两个工具都不是“立即返回一个字符串答案”。
它们真正做的,是改 Claude Code 后面怎么运行。
5. AgentTool 不是辅助函数,而是真正的子 agent 启动器
从 src/tools/AgentTool/AgentTool.tsx 的 call() 往下看,AgentTool 做的事情远比“spawn 一个子任务”复杂得多。
它至少有四种执行分支:
- 启动普通同步 sub-agent
- 启动后台 async agent
- 启动 teammate / team agent
- 启动 worktree 或 remote 隔离 agent
而且它不是把一个 prompt 扔给别的模型就完了,它会重新做一遍子 agent 的运行时配置:
- 选 agent definition
- 算最终 model
- 检查当前权限模式
- 检查能不能 background
- 检查能不能 team spawn
- 检查 required MCP server 是否真的可用
- 为子 agent 重建 tool pool
- 生成新的 agent id / worktree / output file / progress tracker
也就是说,子 agent 不是一个“模型调用分支”,而是一个新的 runtime instance。
6. AgentTool 的设计重点
6.1 它先解决“该不该生这个 agent”
这层判断很重,主要包括:
team_name + name是否真的是 teammate spawn- 当前是不是 teammate,teammate 能不能继续生 teammate
- 当前是不是 in-process teammate,它能不能开 background
- 指定的
subagent_type是否存在 - 这个
subagent_type是否被权限规则 deny - 这个 agent 要求的 MCP server 是否都已连接且已认证
Claude Code 在这里特别谨慎的一点是:
它不会因为 MCP server “正在连接中” 就立刻失败。
如果发现需要的 MCP server 还在 pending,它会轮询等待一段时间,再决定能不能启动。这个处理很 runtime,不是简单 schema 校验。
6.2 fork 和 fresh subagent 是两条明确不同的路
AgentTool 里一个很关键的设计是:
- 指定
subagent_type,表示启动一个 fresh specialized agent - 不指定
subagent_type,在 fork gate 打开时,表示 fork 自己
这两个分支的设计目标完全不同。
fresh agent 的假设是:
- 它没有当前上下文
- 需要你把背景讲清楚
- 它更像“新拉进来的同事”
fork agent 的假设是:
- 它直接继承当前上下文
- 适合把中间噪音留在子上下文里
- 重点是节省主 agent 的上下文预算
所以 src/tools/AgentTool/prompt.ts 专门花了很大篇幅讲:
- 什么时候应该 fork
- 什么时候不要 fork
- fork prompt 应该写 directive,而不是背景复述
- 不要在 fork 完成前“偷看结果”
- 不要猜测 fork 的结论
这说明 Claude Code 很清楚:多 agent 系统最大的问题不是“能不能开”,而是“开了以后父 agent 会不会胡乱同步、胡乱猜结果”。
6.3 它把“结果未返回之前,父 agent 不知道答案”写成了正式规则
这是我觉得 AgentTool 最成熟的一点。
它不是默认父 agent 可以“差不多知道子 agent 在干嘛”,而是明确约束:
- 子 agent 没回,就等于不知道
- 用户中途追问,也只能报状态,不能猜结论
- 如果是后台 agent,告诉用户“我启动了什么”就该停
也就是说,Claude Code 把“不要脑补异步任务结果”写进了工具协议和 prompt 规范里。
这对 C# 版很重要,因为很多人做多 agent 时最容易犯的错,就是把子任务还没返回时的推测,当成了真实信息。
6.4 它会重建子 agent 的工具池,而不是粗暴继承
从实现上看,子 agent 的工具不是父 agent 原样拷过去,而是会重新经过 assembleToolPool(...) 之类的装配逻辑。
这意味着子 agent 的工具可见性依赖于:
- agent definition 自身的 allowlist / denylist
- 当前权限上下文
- MCP 可用性
- fork / built-in / worker 场景
也就是说,Claude Code 的 agent 不是“共享一份万能工具箱”,而是每个执行主体都有自己的工具视图。
这点非常值得在 C# 版里保留。
7. AgentTool 的额外 runtime 处理
除了“能启动 agent”之外,Claude Code 还给它加了很多额外处理。
7.1 权限语义不是简单 allow/deny
AgentTool.isReadOnly() 返回的是 true,这看起来有点反直觉,因为子 agent 明明可能去改文件、跑 bash。
但 Claude Code 的意思是:
AgentTool 自己不直接做副作用,它只是委派。真正的副作用权限,交给子 agent 里的具体工具去判。
这是一种“包装器只声明自己行为,不替子工具背锅”的设计。
7.2 并发安全也被单独声明
AgentTool.isConcurrencySafe() 返回 true。
意思不是“所有子 agent 都不会冲突”,而是:
- 这个工具协议本身允许并发发起
- 真正的冲突控制靠子任务隔离、worktree、提示词约束和后续调度
所以 Claude Code 在这里分清了两层:
- 工具调用协议是否能并发
- 具体任务内容是否会互相踩
7.3 自动模式下走 passthrough
AgentTool.checkPermissions() 在 auto mode 下会返回 passthrough,其他模式直接 allow。
这说明 Claude Code 对“再开一个 agent”这件事是有单独敏感度的:
- 普通模式默认更宽松
- 自动模式下交给更上层的 classifier / 审批逻辑继续判断
换句话说,AgentTool 不是完全免审批的。
7.4 结果映射不是一种,而是四种
mapToolResultToToolResultBlockParam() 至少区分了几类结果:
teammate_spawnedremote_launchedasync_launchedcompleted
而 completed 里又会根据情况追加:
agentId- worktree 信息
- token / tool use / duration usage trailer
但如果是一次性 built-in agent,还会主动省掉 trailer,减少 token 浪费。
这个细节很说明问题:
Claude Code 不把 tool result 当“日志原样输出”,而是当 prompt 预算和后续行为的控制面。
8. AgentTool 的 UI 不是装饰,而是降噪层
src/tools/AgentTool/UI.tsx 里有个非常 runtime 的设计:它会主动折叠子 agent 的搜索、读取、REPL 类进度。
比如它会把连续的:
- 搜索
- 读文件
- 子工具结果
压缩成更高层的摘要。
这是因为多 agent 一旦开始跑,transcript 很容易被子 agent 的工具噪音淹没。
所以 Claude Code 在 UI 层专门做了几件事:
- 折叠连续 search/read/repl 消息
- 只展示最近几条进度
- 支持 condensed 风格
- 避免 user tool_result 在终端里刷出大片空白
这说明它把“可读性”当成运行时设计的一部分,而不是前端美化问题。
9. SkillTool 本质上是 slash-command runtime
如果说 AgentTool 是“生一个新的执行主体”,那 SkillTool 做的就是:
把一条 skill 命令扩展成当前会话后续要遵循的一段工作协议。
从 src/tools/SkillTool/SkillTool.ts 看,它至少做了四层事情:
- 发现 skill
- 校验 skill
- 审批 skill
- 把 skill 展开进当前对话上下文
它的核心不是“执行脚本”,而是:
- 找到 skill 对应的 prompt command
- 把 skill 的内容转成新的消息
- 必要时修改当前上下文里的允许工具、模型、effort
所以 skill 更像“热加载工作模式”,不是普通命令。
10. SkillTool 的设计重点
10.1 skill 是阻塞要求,不是可选建议
src/tools/SkillTool/prompt.ts 里写得很重:
当用户请求和某个 skill 匹配时,这是一个 BLOCKING REQUIREMENT。
意思是:
- 先调
SkillTool - 再继续回答
- 不能嘴上提到 skill 却不真的调用
这件事很关键,因为 Claude Code 明确不希望模型“看见 skill 目录,但继续靠自己编”。
10.2 它拿到的不是只有本地 skill
getAllCommands(context) 会把 skill 来源合并起来:
- 本地 command / skill
- bundled skill
- MCP skill
但它又明确只允许 MCP skills 进来,不让 plain MCP prompts 混进 SkillTool。
这个边界很重要,因为它说明 Claude Code 不是“凡是 prompt 都叫 skill”,而是把可被 SkillTool 调用的能力单独分类。
10.3 validateInput() 做的是协议级校验
这部分至少做了这些事情:
- 去掉前导
/,兼容 slash command 习惯 - 校验 skill 名不能为空
- 支持实验性的远程 canonical skill
_canonical_<slug> - 校验 skill 是否存在
- 拒绝
disable-model-invocation - 要求命令必须是 prompt-based skill
也就是说,SkillTool 不是一个“万能命令调度口”,它明确只接受某一类命令对象。
10.4 它的权限模型不是按工具,而是按 skill 名
这是 SkillTool 最有意思的地方。
它的权限规则匹配对象不是“整个 SkillTool”,而是:
- 精确 skill 名
- skill 名前缀,比如
review:*
并且顺序也很讲究:
- 先查 deny
- 再处理远程 canonical skill 的自动放行
- 再查 allow
- 再判断是不是只包含 safe properties
- 否则 ask
这意味着 Claude Code 把 skill 看成“可审计的能力名字空间”,不是一把大锤。
10.5 safe skill 会自动放行
skillHasOnlySafeProperties(...) 这套逻辑很关键。
Claude Code 的思路是:
- 不是所有 skill 都要每次问
- 但只有显式确认安全属性集合内的 skill,才能自动放行
- 新增属性默认视为不安全,除非被加入 allowlist
这是一种很稳的默认策略。
11. SkillTool 真正返回的不是“结果”,而是“环境变了”
从 call() 的实现看,inline skill 的核心输出不是一段最终答案,而是两样东西:
newMessagescontextModifier
这里的设计非常重要。
11.1 newMessages
skill 展开后,会往当前会话里注入新的消息。
这些消息可能来自:
processPromptSlashCommand(...)展开的 prompt- 本地 skill 的内容
- 远程 skill 加载后的 SKILL.md 内容
也就是说,skill 的主要效果是把新的“操作说明”喂回当前回合上下文。
11.2 contextModifier
这部分更像运行时补丁,它会改后续上下文:
- 把 skill 指定的
allowedTools注入alwaysAllowRules.command - 如果 skill 指定了 model,就覆写
mainLoopModel - 如果 skill 指定了 effort,就覆写
effortValue
这说明 SkillTool 干的不是“做完一个动作”,而是“把当前 agent 切换到一种新的执行姿态”。
所以它最后映射成 tool result 时,inline 分支只是:
Launching skill: xxx
真正重要的内容不在这句文案,而在前面那批 newMessages + contextModifier。
12. forked skill:SkillTool 自己也会借 AgentTool 的路子
SkillTool 里一个很值得注意的点是:
如果 skill 的 command context 是 fork,它不会在当前上下文里 inline 展开,而是转去 executeForkedSkill(...)。
这条路径会:
- 先用
prepareForkedCommandContext(...)准备 skill 上下文 - 生成独立 agent id
- 直接调用
runAgent(...) - 采集子 agent 的 tool use / tool result 进度
- 最后抽取结果文本返回
也就是说,skill 并不总是“当前 agent 的提示词宏”。
有些 skill 会进一步变成“用一个隔离 agent 跑完这套 skill 协议”。
这再次说明 Claude Code 的 skill 系统并不是壳子语法,而是运行时一级公民。
13. 远程 canonical skill 说明 Claude Code 在做“技能分发”
SkillTool 里还有一条更平台化的路线:远程 canonical skill。
它的流程大致是:
- 先通过
_canonical_<slug>命中远程 skill 名 - 要求这个 skill 已经在当前 session 里被 discover 过
loadRemoteSkill(...)从远程拉内容并做本地缓存- 去 frontmatter
- 注入 base directory
- 替换
${CLAUDE_SKILL_DIR}/${CLAUDE_SESSION_ID} - 用
addInvokedSkill(...)注册到 compaction 保留状态 - 最后把内容直接包成 user meta message 注入当前上下文
这说明 Claude Code 已经不满足于“本地技能目录”了,它其实在往“技能市场 / 技能分发网络”方向走。
换句话说,skill 不是本地文件夹技巧,而是平台能力包。
14. AgentTool 和 SkillTool 的真正区别
可以把它们并排看:
| 维度 | AgentTool | SkillTool |
|---|---|---|
| 目标 | 新建执行主体 | 改当前执行上下文 |
| 核心产物 | 子 agent runtime | 新消息 + 上下文修饰 |
| 典型场景 | 分工、并行、隔离、后台运行 | 装载专门工作协议 |
| 主要风险 | 父子上下文错位、结果猜测、任务冲突 | skill 越权、重复加载、技能污染 |
| Claude Code 的控制点 | agent 类型、MCP、隔离、结果映射、UI 降噪 | skill 名权限、safe property、prompt 展开、contextModifier |
所以你后面做 C# 版时,千万不要把它们统一成一个 InvokeExtensionAsync()。
它们长得都像“扩展能力”,但其实修改的是两层完全不同的运行时结构。
15. Claude Code 做了哪些额外处理
把前面零散细节收一下,大概有这些:
AgentTool会过滤 denied agent,不是所有 agent definition 都对模型可见AgentTool会检查 required MCP server 是否真的有工具,不是“连上了就算可用”AgentTool对 fork child 做递归保护,避免无限 forkAgentTool会区分 sync / async / remote / teammate 四类结果映射AgentTool会在 UI 层折叠子 agent 的搜索和读文件噪音SkillTool会把 MCP skill 纳入统一命令视图,但排除 plain MCP promptSkillTool会校验disable-model-invocationSkillTool会按 skill 名和前缀做权限决策,不是只看工具名SkillTool会把安全属性 skill 自动放行SkillTool会把 skill 对运行时的修改显式编码到contextModifierSkillTool会把远程 skill 内容做本地缓存和 compaction 保活
这些额外处理合起来说明一件事:
Claude Code 真正在构建的是“可治理的扩展运行时”,不是“模型能顺手用的几个高级命令”。
16. 转成 C# 时我建议怎么建模
如果你后面准备做 C# 版,我建议至少拆成下面几层接口:
public interface IAgentLauncher
{
Task<AgentLaunchResult> LaunchAsync(AgentLaunchRequest request, CancellationToken ct);
}
public interface ISkillRuntime
{
Task<SkillExecutionResult> ExecuteAsync(SkillExecutionRequest request, CancellationToken ct);
}
public interface IAgentRegistry
{
Task<IReadOnlyList<AgentDefinition>> GetAvailableAgentsAsync(CancellationToken ct);
}
public interface ISkillRegistry
{
Task<IReadOnlyList<SkillDefinition>> GetAvailableSkillsAsync(CancellationToken ct);
}
public interface IContextMutation
{
RuntimeContext Apply(RuntimeContext context);
}
然后在语义上明确分开:
AgentLaunchResult表示启动了谁、在哪里跑、是否后台、结果什么时候回SkillExecutionResult表示注入了哪些消息、修改了哪些上下文字段
我会特别建议你保留这几个概念,不要拍扁:
AgentDefinitionSkillDefinitionPermissionDecisionContextModifierBackgroundTaskHandleIsolationModeToolPoolSnapshot
其中 ContextModifier 很关键。
因为如果没有这个概念,你最后很容易把 skill 退化成“返回一个 prompt 字符串”,那就丢了 Claude Code 最有意思的一层设计。
17. 我的总体评价
拆完这两个工具之后,我对 Claude Code 的判断更明确了:
它真正先进的地方,不是“会开子 agent”或者“会读 SKILL.md”,而是它把这两件事都纳入了统一 runtime 语义。
也就是:
- 什么时候能扩展
- 扩展成什么
- 扩展后权限怎么算
- 扩展后 UI 怎么展示
- 扩展后 transcript 怎么回灌
- 扩展后 compaction 怎么保活
这些问题它都认真处理了。
所以如果你后面要做 C# 版,我会建议把这两个子系统看成 Claude Code 从“工具型 agent”跨到“平台型 agent runtime”的分界线。
这是非常值得抄的设计思路。