让 Agent、Skill、Command 做同一件事,会怎样?- Claude9

38 阅读11分钟

这篇讲 command / agent / skill 自动触发的优先级、三者分工,以及「用 agent 代替 command」的问题。同一件事用不同扩展做有什么差异,什么时候该选哪一个?这是系列文章的第9篇。

目录概要

  1. 同一件事、三种写法——"打开时间"的 A/B/C 实验
  2. Claude 选哪一个:自动触发的优先级规则
  3. 分工原则:command 是入口、agent 是外包、skill 是工具
  4. 两种 skill 模式:preload vs on-demand
  5. 三层编排的真实结构图
  6. 反面实验:用 agent 代替 command 会发生什么
  7. 坏味道(过度编排)
  8. 小结

一、同一件事、三种写法

我做过一个很有意思的对照实验——"显示巴基斯坦当前时间(PKT)"这个需求,同时写成三种扩展:

  • .claude/commands/time-command.md
  • .claude/agents/time-agent.md
  • .claude/skills/time-skill/SKILL.md

都是读 TZ='Asia/Karachi' date,都是输出同一行时间字符串——实现完全等价。但三个版本在 Claude Code 的行为空间里扮演的角色完全不同

1.1 三个版本能干什么

维度time-commandtime-agenttime-skill
用户能手动调吗/time-command❌ 不在 / 菜单/time-skill
Claude 能自动触发吗❌ 永远需要 /✅ 通过 description✅ 通过 description
独立上下文吗❌ 共享主上下文✅ 独立 subagent 进程❌ 共享主上下文(除非 context: fork
有没有 memory✅ 可配 memory: user/project/local
能不能预加载到 agent 里✅ 通过 skills: 字段
接受参数吗$ARGUMENTSprompt 参数$ARGUMENTS

光看表格就能发现——三者能力差得非常远。"实现一样"不代表"作用一样"。

1.2 用户问"现在几点"会发生什么

现在做一个思想实验:用户不用 /,直接在对话里打一句——

"现在几点了?"

Claude 内部会怎么决定调哪一个?

flowchart TD
  U["用户: '现在几点了?'"] --> D{Claude 解析意图}
  D --> M1{匹配到 skill?}
  M1 -->|描述匹配| S1[time-skill 触发]
  M1 -->|未匹配| M2{匹配到 agent?}
  M2 -->|描述匹配| A1[time-agent 触发]
  M2 -->|未匹配| F[Claude 直接跑 TZ=Asia/Karachi date]
  M3[time-command] -. 用户没敲 slash 时不触发 .-> D

  style S1 fill:#afa
  style A1 fill:#ff9
  style F fill:#fcc
  style M3 fill:#fcc

关键事实——

  • time-command 永远不会被自动触发。Commands 在设计上没有自动匹配的通路,必须用户手动敲 /
  • time-agent 和 time-skill 都有自动触发的可能,因为它们都有 description 字段
  • Claude 的偏好:如果两者都匹配,skill 优先,因为 skill inline 执行没有额外 context 开销;agent 要另开一个上下文窗口,属于"高射炮打蚊子"

选型的本质:当一个任务既能写成 skill 也能写成 agent 时,默认 skill。只有在任务需要自主探索、独立上下文或持久 memory 时才升格到 agent。

1.3 如果 skill 关了自动调用呢

disable-model-invocation: true 能把 skill 从"Claude 可自动触发"里摘掉。这时候 Claude 回到上面的决策树——往下找到 agent,触发 time-agent。代价是:本来 inline 一行 bash 就能跑完的事,开了一个独立 context 去做。

如果 skill 和 agent 都禁用自动触发,Claude 就没有扩展可用了——它会回落到自己原生的能力,直接生成并执行 TZ='Asia/Karachi' date

这一路推演下来可以看出——扩展的作用域是"Claude 的默认行为不够好时的补丁",它们不是为了绕过 Claude 的能力存在的,是为了在特定场景里比 Claude 的默认做法更优


二、为什么不把三者合并?

这是作者当初反复问过自己的问题。三个东西都是 markdown + frontmatter,都能"执行一段逻辑"。为什么 Anthropic 不设计成"一种扩展类型 + 不同配置"?

仔细想一下,差异的根源在人和机器谁来触发这件事上——

graph LR
  subgraph 触发源
    U[User 主动敲] --> C[Command]
    Claude[Claude 自动判断] --> S[Skill]
    Claude --> A[Agent]
    API[另一个 Command/Agent] --> A
    API --> S
  end

  style C fill:#afa
  style S fill:#aaf
  style A fill:#ff9
  • Command用户是触发源。只有用户敲 / 才启动,Claude 不会自己去调。所以它的设计里不需要 description 做匹配、不需要独立 context(因为是用户当前会话里要的东西)
  • SkillClaude 是触发源。Claude 读了 description 决定要不要调。所以 description 是必需的,context 是共享的(就像是 Claude 临时掏出来用一下工具)
  • Agent既能被 Claude 也能被 Command/其他 Agent 触发。它本身是个完整的"执行体",有自己的工具集、memory、hooks。必须有独立 context(不然调用它的 Claude 会被它的噪声污染)

设计哲学上的权衡:合并之后意味着一个字段集合要同时表达"用户触发/Claude 触发"、"共享/独立 context"、"工具箱/员工"——这张表会膨胀到没法用。拆成三种,每种专注表达一种角色,反而简单。

这就是 03 篇那个类比的由来——

Command 是按钮、Skill 是工具、Agent 是员工

按钮按下就响;工具摆在那儿 Claude 想用就用;员工雇来就给他任务让他自己干。三种东西本质不同,勉强合一反而不好。


三、两种 skill 模式:预加载 vs 直接调用

理解了 Command/Agent/Skill 的粗分工,还有一个更细的问题:Skill 到底怎么被使用

03 篇里那套 release-notes 生成器两种 skill 模式同时出现——一个预加载进 agent,一个被 command 直接调用。这不是随手一摆,是设计上的核心分工。

3.1 预加载(agent skill)

在 agent 定义里的 skills: 字段:

---
name: release-notes-agent
skills:
  - git-log-reader    # 启动时 git-log-reader 的全部内容注入到 agent 的上下文
---

git-log-reader 的 SKILL.md 内容在 agent 启动的那一刻就被塞进了 agent 的 system prompt。对 agent 来说,这不是"调用一个工具",而是"我天生就会的东西"。

3.2 直接调用(tool-invoked skill)

在 command 里(或者在 agent 里、在主会话里)用 Skill 工具调:

# release-notes-crafter.md

Step 3: 调用 release-notes-formatter 排版生成 RELEASE_NOTES.md
Skill(skill: "release-notes-formatter")

release-notes-formatter 不会被任何人预加载——只有调用那一刻才展开。

3.3 两种模式的本质区别

graph TB
  subgraph 预加载模式
    A1[Agent 启动] --> B1[SKILL.md 全文注入 system prompt]
    B1 --> C1[Agent 的每一轮都能访问这段知识]
  end

  subgraph 直接调用模式
    A2[主上下文] --> B2[Skill 工具被调用]
    B2 --> C2[SKILL.md 即时展开]
    C2 --> D2[执行完毕,context 消化]
  end

  style B1 fill:#afa
  style B2 fill:#ff9
维度预加载直接调用
加载时机agent 启动时被调用时
谁能用只有那个 agent任何能用 Skill 工具的地方(command、agent、主会话)
消耗整个 session 一直占 context只有调用时占
适用这个 skill 是 agent 的"专业知识"这个 skill 是"偶尔用一下的工具"

类比

预加载是给员工背的岗位技能(必须随身带);直接调用是放在工具柜里等人来借的扳手(要用再拿)。

不能搞错——把 release-notes-formatter 预加载到 release-notes-agent 里,结果就是 agent 每次都带着排版模板,但它其实只跑 git log;反过来把 git-log-reader 做成直接调用的 skill,agent 每次用之前都要调用一次 skill 工具,多一轮开销。


四、三层编排的真实结构

看完了理论,回到 03 篇那套 release-notes 生成器的实际结构——

graph TB
  U[用户敲 /release-notes-crafter] --> C[release-notes-crafter<br>Command 层]
  C --> Q[AskUserQuestion<br>问目标版本号 / 起止 tag]
  Q --> AT[Agent tool]
  AT --> A[release-notes-agent<br>Agent 层]

  subgraph AgentContext[独立 Agent 上下文]
    A --> PL[预加载的 git-log-reader]
    PL --> GL[Bash: git log v1.2.0..v1.3.0]
    GL --> R[返回分类后的 commit 列表]
  end

  R --> ST[Skill tool]
  ST --> S[release-notes-formatter<br>Skill 层]

  subgraph SkillExec[在主上下文内 inline]
    S --> OS[输出 RELEASE_NOTES_v1.3.0.md]
    S --> OM[输出 output.md]
  end

  style C fill:#afa
  style A fill:#ff9
  style S fill:#aaf
  style PL fill:#fcc

这张图里每一层各司其职:

4.1 Command 层——"对外接口"

release-notes-crafter 做了三件事:

  1. 接用户输入:问一下目标版本号和起止 tag
  2. 调 agent:拿 commit 分类数据
  3. 调 skill:生成排版好的 RELEASE_NOTES.md

它不自己跑 git、不自己写模板——它只做"编排"。对应真实代码:

# .claude/commands/release-notes-crafter.md

Step 1: Ask the user for the target version and base/head git tags
Step 2: Invoke release-notes-agent via the Agent tool
Step 3: Invoke release-notes-formatter via the Skill tool

简洁、清晰、职责单一。

4.2 Agent 层——"专业外包"

release-notes-agent 是一个有独立上下文的 subagent。关键设计:

---
name: release-notes-agent
tools: Bash(git *), Read
model: sonnet
memory: project
skills:
  - git-log-reader
---
  • 独立上下文:就算 git log 返回几百条 commit,也不会污染主会话
  • 受限工具集:只有 Bash(git *) 和 Read,别的不给——避免它"自作聪明"去改代码
  • 预加载 git-log-reader:Conventional Commits 分类规则、日志字段解析这些知识,直接注入它的 system prompt

从主会话角度看,agent 就是一个黑盒——输入"v1.2.0..v1.3.0",输出"按类型分好类的 commit 列表"。里面怎么搞的不关心。

4.3 Skill 层——"一次性工具"

release-notes-formatter 被 command inline 调用,Markdown 写完就结束。它不是一个常驻 agent,不是一个要人去敲 / 的命令——它是个被调用的工具

关键点:它需要的数据(分类好的 commit 列表)已经在主上下文里(agent 刚刚返回过来),所以 skill 不需要重新获取——这就是"inline 共享 context"的好处。

4.4 为什么这样分层

这种分层背后有一条很清晰的数据流

用户输入 → 问 version → 拉 commit → 分类 → 排版 → 输出文件
   ↑          ↑           ↑         ↑       ↑          ↑
 command    command     agent     agent   skill      skill

每个组件只做一件事。换 git 命令就改 agent、换 Markdown 模板就改 skill、换提示语就改 command——互相不干扰。


五、反面实验:能不能都用 agent?

这是很多人踩过的坑——"既然 agent 最灵活,我全用 agent 不就行了?"

试一下——把 release-notes 生成器改成"主 agent + 子 agent + 孙 agent"三层嵌套:

graph TB
  U[用户] --> MA[主 agent: orchestrator]
  MA --> SA1[子 agent: release-notes-agent]
  SA1 --> GA[孙 agent: formatter-agent]

  style MA fill:#fcc
  style SA1 fill:#fcc
  style GA fill:#fcc

两个问题立刻冒出来:

问题 1:subagent 不能直接调另一个 subagent

这是 Claude Code 的硬约束。Subagent 调 subagent 需要用 Agent(...) 工具——注意是工具调用,不是 bash 命令。而且嵌套超过一层时,上下文切换开销会翻倍。

这一点项目级 CLAUDE.md 里通常会明确写上:

Subagents cannot invoke other subagents via bash commands. Use the Agent tool (renamed from Task in v2.1.63; Task(...) still works as an alias)

问题 2:每层都是独立 context

三层嵌套意味着三个独立 context 窗口同时存在,数据要层层手动传递。主 agent 拿到温度值,要传给孙 agent 去画 SVG——这个数据在三个 agent 之间来回塞,成本很高。

对比一下真实实现——

命令 (主会话 context)
  ├ Agent tool → agent (独立 context, 拿完数据返回)
  └ Skill tool → skill (主会话 context, inline 生成)

主会话只开了一个 subagent context,skill 在主会话 inline 跑,数据零拷贝。这就是"正确分工"和"错误分工"的差距。

实用经验只在必须独立 context 的地方用 agent。数据转换、格式化、输出生成这类能在主 context 搞定的,一律用 skill。


六、过度编排的坏味道

作者踩过不止一次的坑,总结成几条"闻到这个味儿就退一步想想"的信号:

6.1 三层以上嵌套 agent

不管你的业务多复杂,三层以上 agent 嵌套都意味着设计出了问题。正常场景 1-2 层 agent 已经顶天。

6.2 一个 command 里同时调 5 个 agent

这不是编排,这是调度器。要写调度器去 code 里写,不要在 command 里用 markdown 伪代码写调度逻辑。

6.3 "万能 agent"

见过一个 agent 的 description 写:"Handles all user requests, delegates to sub-agents when needed"——这等于没设计。agent 的 description 要精确到"我只处理 X 类任务",否则 Claude 匹配时会乱套。

6.4 command 里塞业务逻辑

Command 是入口,不是业务容器。如果你的 command 有 200 行业务步骤,那 200 行应该拆成 skill 或者 agent——command 只负责调用串场

6.5 skill 里又调 agent,agent 又调 skill,回环

这是最危险的一种——循环调用。Claude Code 不会死循环(有 maxTurns),但 context 会被反复污染,最后谁也看不清到底在干嘛。

一条能自检的小原则:画出调用图,如果有环就是错了


七、一个小决策树

综合前面所有讨论,给个选型的决策树:

flowchart TD
  Start[我想加一个扩展] --> Q1{谁触发它?}
  Q1 -->|只有用户手动| CMD[用 Command]
  Q1 -->|Claude 自动 或 被其他扩展调用| Q2

  Q2{任务需要独立 context 或 memory?} -->|是| AGT[用 Agent]
  Q2 -->|否| Q3

  Q3{要被预加载到某个 agent?} -->|是| PS[Preloaded Skill]
  Q3 -->|否| TS[Tool-invoked Skill]

  style CMD fill:#afa
  style AGT fill:#ff9
  style PS fill:#aaf
  style TS fill:#aaf

简单说——

  1. 用户必须手动才能触发? → Command
  2. 需要隔离或持久记忆? → Agent
  3. 是某个 agent 的专业知识? → 预加载 Skill
  4. 其他一切通用工具? → 直接调用 Skill

八、编排之道

这一篇拎出来的主题是"编排"——当你手里同时有 Command、Agent、Skill 三种扩展,怎么组装它们去干一件有点复杂的事。

核心观点三条——

第一,三种扩展不是可以互相替换的。Command 是用户入口,Agent 是外包员工,Skill 是工具。合并后字段会爆,拆开各自专注。

第二,Skill 有两种用法——预加载是"背在员工身上的专业",直接调用是"公共工具柜里的扳手"。一个 skill 该用哪种方式,取决于它是某人专属的还是大家都要用的

第三,正确的编排是"用最轻的方式达成目的"——能 skill 不上 agent,能一层 agent 不上两层。嵌套 / 回环 / 万能 agent 都是过度工程化的信号。

写完这篇,Claude Code 内部怎么用它自己造的扩展这件事基本说清了。但前面说过 Claude Code 不只是"本地工具"——它能连 GitHub、Slack、Notion、各种内部系统,这些"外面的世界"是怎么插进来的?答案是 MCP(Model Context Protocol)。下一篇把 MCP 拆开看——它的协议层长什么样、它跟 skill/agent 的关系是什么、为什么它让 Claude Code 从"单机工具"跳到了"工作中枢"。


外部链接