Claude Agent SDK 进阶:子代理、skills 和插件,把 Claude Code 里的三样东西用进你的程序
子代理、skills、插件这三样在 SDK 里都能用。本站用 Python 逐样实测:三个子代理并行时,一次 query() 先后来了四个结果消息,第一个只是“它们还在跑”;这个小任务上用子代理比主代理自己做慢一倍、贵三倍多;子代理的模型别名在中转站上不一定有。skill 默认就会被发现,插件一行配置加载。
本站已实测,测试日期:2026-10-13。
这篇教程解决一个问题:前五篇做出来的智能体是“一个代理加几个自己写的工具”。Claude Code 里还有三样用来组织大一点的活的东西:子代理、skills、插件。它们在 SDK 里怎么用,各自有什么坑?
接着用前几篇的订单助手。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-13 |
| 版本 | claude-agent-sdk 0.2.165 |
| 系统 | Ubuntu 24.04,Python 3.12.3 |
| 模型 | 主代理用 Sonnet 5.5 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
| 示例 | 前几篇的订单库和三个工具:查订单、看详情、退款 |
一、子代理
定义一个
子代理是主代理可以派出去干一件具体的事的另一个代理:有自己的系统提示、自己的工具清单,可以用不同的模型。在 SDK 里直接写在选项里:
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
from shop_tools import shop # 前几篇的工具服务器
READ = ["mcp__shop__find_orders", "mcp__shop__get_order"]
ANALYST = AgentDefinition(
# description 是给主代理看的:什么时候该派它
description="订单分析员。给它一位客户的名字,它查出这位客户的全部订单并写一份三行以内的小结。",
# prompt 是子代理自己的系统提示
prompt="你是网店的订单分析员。用工具查出指定客户的全部订单,然后写一份三行以内的小结:有几张订单、合计多少钱、有没有需要跟进的。只根据查到的数据说话。",
tools=READ, # 只给两个只读的工具
model="sonnet",
)
options = ClaudeAgentOptions(
model="claude-sonnet-5-5",
mcp_servers={"shop": shop},
system_prompt="你是网店的客服主管。用中文回答。每次回答的最后另起一行写上【主管口径】。",
tools=["Agent"], # 内置工具只留派子代理用的这一个
allowed_tools=["Agent"] + READ,
permission_mode="dontAsk",
agents={"order-analyst": ANALYST},
)
和前几篇不同的地方有两处。前几篇用 tools=[] 去掉了全部内置工具;主代理是靠内置的 Agent 工具派子代理的,所以这里要把它留下,并且预先批准。
AgentDefinition 里多个单词的字段名(disallowedTools、mcpServers、maxTurns)保留了驼峰写法,没有改成 Python 的下划线风格,文档特意提了这一点。
实测一:三个子代理并行,结果消息来了好几个
任务是“给张伟、李娜、王芳各做一份订单小结,每位交给一个子代理,三个同时派出去”。按时间打印收到的消息:
4.0s [派出子代理] order-analyst 交代的话:客户姓名:张伟。请查出这位客户的全部订单……
4.6s [派出子代理] order-analyst 交代的话:客户姓名:李娜……
5.6s [派出子代理] order-analyst 交代的话:客户姓名:王芳……
8.7s (子代理内)[调用] mcp__shop__find_orders {"customer": "张伟"}
9.5s (子代理内)[调用] mcp__shop__find_orders {"customer": "李娜"}
9.9s (子代理内)[调用] mcp__shop__find_orders {"customer": "王芳"}
11.1s [主代理] 我已经同时派出三个 order-analyst 子代理……它们现在还在运行。三份小结都回来后,我会原样列给你。
11.8s [结束] success 累计花费估算=0.0349
17.8s [主代理] 李娜的小结已经回来了。张伟和王芳的两位子代理还在运行……
18.0s [结束] success 累计花费估算=0.0453
18.0s [结束] success 轮数=0 累计花费估算=0.0453
22.3s [主代理] 三位客户的订单小结如下,都是子代理的原文。……
22.3s [结束] success 累计花费估算=0.053
一次 query(),来了四个“结束”。 第一个在 11.8 秒,那时主代理说的是“它们还在运行”。第三个是空的,轮数是 0。真正的答案在 22.3 秒的最后一个里。
原因是子代理默认在后台运行。主代理派出去之后不等,先把这一轮结束掉;子代理陆续回来时,主代理被重新叫起来,每次又产生一个结果消息。
这和第一篇里“一次运行最后来一个 ResultMessage”的印象不一样,写程序时要改两处:
- 不要收到第一个
ResultMessage就停。 把消息流读到自然结束,取最后一个。 - 花费看最后一个。 每个结果上的
total_cost_usd是到那时为止的累计,依次是 0.0349、0.0453、0.0453、0.053,不要相加。
让它等齐了再答
在提示里说清楚要前台运行:
……子代理用前台方式运行(run_in_background 设为 false),三份结果都拿到之后再回答。
5.2s [派出子代理] order-analyst 后台=False ……
……
19.3s [主代理] 三位客户的小结如下,均为子代理的原文。……
19.3s [结束] success 花费估算=0.0484
这次只有一个结果消息,里面就是完整的答案。
AgentDefinition 有一个 background 字段,设成 True 是强制后台。反过来强制前台的字段没有,只能像上面这样在提示里要求,或者写进主代理的系统提示。
怎么认出子代理的消息
- 主代理派子代理,是一次名叫
Agent的工具调用,参数里的subagent_type是子代理的名字。文档提醒老版本里这个工具叫Task,要兼容就两个名字都认。本站看到启动信息的工具清单里它仍然显示为Task。 - 子代理内部产生的消息,
parent_tool_use_id不为空。上面输出里标着“子代理内”的行就是靠它分出来的。
实测二:值不值
同一个任务,四种做法:
| 做法 | 总用时 | 花费估算 | 结果消息 |
|---|---|---|---|
| 不用子代理,主代理自己查 | 10.4 秒 | 0.0145 美元 | 1 个 |
| 三个子代理,前台 | 19.9 秒 | 0.0484 美元 | 1 个 |
| 三个子代理,后台(默认) | 22.8 秒 | 0.053 美元 | 4 个 |
| 三个子代理,但并发上限设成 1 | 49.2 秒 | 0.0634 美元 | 4 个 |
在这个任务上,用子代理慢了一倍,贵了三倍多。
原因不难理解。每个子代理是一个新开的对话,有自己的系统提示和工具说明,都要从头付一遍。任务本身只是查两三次数据库,分出去省下的还不够开销。
子代理划算的情况是另一种:每个子任务要读很多东西。文档讲的第一条好处就是这个:子代理翻了几十个文件,这些内容留在它自己的对话里,回到主代理手上的只有一段结论。主对话不会被撑满。子任务又长又互不相干时,并行才真的省时间。
所以先问一句“子任务大不大”。小的,让主代理自己做。
实测三:子代理看得到什么
主代理的系统提示里有一条“每次回答末尾写上【主管口径】”。本站告诉主代理一个暗号,让它派子代理去问两件事,并且不许把暗号和标记告诉子代理。子代理的原话:
一,我不知道客户的暗号。依据:我手头只有系统提示和你的这条消息,里面都没有提到任何“暗号”。
二,不需要加固定标记。依据:系统提示只要求我写三行以内的小结……没有规定结尾要加标记。
两样都不知道。文档里的对照是这样的:
| 子代理拿得到 | 子代理拿不到 |
|---|---|
| 它自己的系统提示,和主代理派它时写的那段话 | 主代理的对话历史和工具结果 |
| 项目的 CLAUDE.md | 主代理的系统提示 |
工具的定义(继承主代理的,或者 tools 里列的那几个) |
没有在 skills 字段里列出的 skill 内容 |
主代理传给子代理的只有派它时写的那一段话。 子代理需要的文件路径、报错信息、已经做过的决定,都得写在那段话里。主代理自己写这段话,所以要在主代理的系统提示里交代清楚“派活时要带上什么”。
实测四:工具限制是硬的
子代理只有两个只读工具。让主代理派它去退款,它的原话:
我没能给订单 A1002 办理退款。我可用的工具只有
find_orders和get_order,都是只读的。我没有退款、取消订单或修改订单的工具,所以什么都没改。
tools 里没列的,子代理手上就没有。这是把“只许看、不许改”落到实处的办法,比在提示里写“请不要退款”可靠。
实测五:给它套上限
Claude 自己决定派几个子代理,子代理还能再派子代理。文档给了三道上限:
| 上限 | 怎么设 | 默认 |
|---|---|---|
| 嵌套几层 | 环境变量 CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH |
3 层。设成 1 就不许子代理再派 |
| 同时几个 | 环境变量 CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS |
20 个 |
| 花多少钱 | 选项 max_budget_usd |
不限。子代理的花费算在里面 |
环境变量通过 env 选项传:
ClaudeAgentOptions(..., env={"CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "1"})
本站把并发设成 1 试了一下。主代理同时派了三个,后两个被拒绝。它的反应:
目前只派出了张伟的那一个……李娜和王芳的两个没派出去。系统限制同时只能跑 1 个子代理,所以这两次调用被拒绝了。
然后它自己改成了一个一个地派,最后三份都做完了,用了 49 秒。上限生效,任务也没有因此失败。
一个坑:模型别名在中转站上不一定有
本站第一次跑的时候,子代理的模型写的是 haiku,想用便宜的模型干简单的活。结果三个子代理全部失败。主代理发现了:
三个子代理都失败了。原因是 order-analyst 配置的模型 claude-haiku-5-5 不存在,和订单数据无关。我换用 sonnet 模型重新派出这三个。
它自己换了模型重派,任务完成了,但多花了一轮:36 秒,0.0732 美元。
单独验证:主模型直接写 haiku,得到的是
There's an issue with the selected model (claude-haiku-5-5). It may not exist or you may not have access to it.
haiku 这个别名被解析成了 claude-haiku-5-5,而本站这把 key 用不了这个模型。这不是 SDK 的问题,是接入方式的问题:别名指向哪个模型是 Claude Code 定的,你的 key 支不支持是中转站定的,两边对不上就会这样。
三种处理办法:
- 子代理写一个你确认能用的完整模型 ID;
- 写
model="inherit",跟主代理用同一个; - 用环境变量
ANTHROPIC_DEFAULT_HAIKU_MODEL把别名改指到一个能用的模型。
这次主代理能自己绕过去,是运气。写程序时别指望它,先把每个要用的模型单独试一遍。
二、skills
它在 SDK 里是什么
skill 是一份 SKILL.md:一段说明,Claude 判断用得上的时候才去读。和子代理不一样,skill 只能是磁盘上的文件,SDK 没有用代码注册 skill 的办法。
本站给订单助手写了一份退款政策,放在项目的 .claude/skills/refund-policy/SKILL.md:
---
name: refund-policy
description: 网店的退款政策。用户问某张订单能不能退、怎么退时使用。
---
退款政策(内部条款):
- R-1:已付款、还没发货的订单,随时可以全额退。
- R-2:已发货、还没签收的订单,要先拦截物流,拦截成功后全额退。
- R-3:已签收的订单,签收后 7 天内可以退,运费由客户承担;超过 7 天不退。
- R-4:已经退过款的订单不能再退。
回答时先用工具查出订单的状态,再指出适用的条款编号。
实测:五种配置
问题都是“订单 A1001 现在还能退吗?”(它是一张已签收的订单)。选项里 cwd 指向项目目录,tools=["Skill"]。
| 配置 | 启动信息里有没有这个 skill | Claude 调用了它吗 | 回答 |
|---|---|---|---|
| 什么都不加 | 有 | 调用了 | 引用了 R-3,并指出系统里没有签收日期、没法确定是否在 7 天内 |
skills=[] |
有 | 没有 | 只说“已签收的订单系统不允许退款”,没有提政策 |
skills=["refund-policy"] |
有 | 调用了 | 引用了 R-3 |
setting_sources=["user"] |
没有 | 没有 | 同第二行 |
提示写成 /refund-policy 订单 A1004 能退吗 |
有 | 没有经过 Skill 工具,内容直接生效 | 引用了 R-4 |
读出四件事:
- 默认就会被发现。 不设
skills、不设setting_sources,项目目录里的 skill 就能用。文档说默认的query()会读用户和项目两层设置。 skills选项管的是“Claude 能不能调用”。 设成空列表,它就不调用了。但启动信息的skills数组里照样列着这个 skill,文档说明那个数组不随名单变化。不要拿那个数组判断 skill 有没有启用。- 不读项目设置,skill 就不存在。 第四行只读用户设置,项目里的 skill 没被发现。部署那篇讲多用户隔离时建议
setting_sources=[],那样做的同时,文件系统里的 skill 也一并没了,要换成下面的插件来加载。 /名字是直接点名。 不经过 Claude 的判断,内容直接进来。
有 skill 和没有的差别,在第一行和第二行的回答里很清楚:没有 skill 时,Claude 只知道退款工具的硬规则(“已签收不能退”);有了之后,它知道政策是“签收 7 天内可以退”,并且注意到数据里缺一个判断所需的日期。
文档里一条配置上的提醒:如果你像本站这样显式写了 tools 列表,要把 "Skill" 放进去,否则 Claude 没有调用 skill 的工具。
三、插件
插件是把 skills、子代理、hooks、MCP 服务器打成一包。在 SDK 里用一行配置加载一个本地目录:
options = ClaudeAgentOptions(
...,
plugins=[{"type": "local", "path": "/home/ccdemo/demo/c7-plugin/express-kit"}],
)
type 只能是 "local"。路径指向插件的根目录,相对路径按 cwd 算。市场里的插件要先下载到本地,再把目录给它。
本站加载的是做一个插件分享给团队里那个 express-kit。启动信息里多出来的:
插件:['express-kit']
skills 里:'express-kit:commit-message'
代理里:'express-kit:test-reviewer'
命令里:'express-kit:commit-message'
插件里的东西都带上了插件名做前缀。 skill 叫 express-kit:commit-message,子代理叫 express-kit:test-reviewer。这样两个插件里有同名的 skill 也不会撞。
两种用法都试了:
| 提示 | 发生了什么 | 回答 | 花费估算 |
|---|---|---|---|
/express-kit:commit-message 我把 getUser 改名成了 fetchUser…… |
直接点名,1 轮 | refactor: rename getUser to fetchUser and update call sites |
0.0094 美元 |
| “帮我写一条提交说明:我把 getUser 改名成了 fetchUser……” | Claude 自己选了这个 skill,调用时用的是带前缀的名字 | refactor: rename getUser to fetchUser and update all call sites |
0.0225 美元 |
两次都得到了符合团队格式的一行。直接点名更便宜,少了“判断该用哪个”的那一轮。
确认插件加载上没有,看启动信息里的 plugins 数组。
三样怎么选
| 你想要的 | 用 | 在 SDK 里怎么给 |
|---|---|---|
| 把一件大事分给几个互不干扰的帮手,或者让某个帮手只有一部分工具 | 子代理 | agents={...},写在代码里 |
| 一份只在用得上时才加载的说明、政策、流程 | skill | 磁盘上的 SKILL.md,靠设置来源发现 |
| 把上面这些连同 hooks 打成一包,在几个程序之间复用 | 插件 | plugins=[{"type": "local", "path": ...}] |
| 让它能查你的数据、调你的接口 | 自定义工具 | 见第二篇 |
子代理和 skill 的区别可以这样记:skill 是往当前对话里加一段说明,干活的还是同一个代理;子代理是另开一个对话,有自己的上下文,只把结论带回来。
这一趟花了多少
约 0.45 美元。子代理的八次运行约 0.3 美元,最贵的是模型别名失败又重派的那一次;skills 和插件的七次运行约 0.13 美元。主代理和子代理用的都是 Sonnet 5.5。这是 SDK 按官方价格估算的数字,不是中转站实际扣的金额。
这次实测没有覆盖到的
- 用文件定义子代理(
.claude/agents/)没有在 SDK 里试,只试了写在代码里的。 - 子代理的嵌套、恢复一个中断的子代理、
max_budget_usd对子代理的限制没有试。 - “小任务上不划算”只比较了一个任务,每种做法只跑了一次。 读很多文件的大任务上子代理能省多少,没有数据。
AgentDefinition的skills(预先加载)、mcpServers、permissionMode等字段没有试。- 插件里的 hook 和 MCP 服务器在 SDK 里有没有正常工作,没有验证。 只看了 skill 和子代理。
setting_sources=[]时用plugins加载 skill 是否可行,没有实测,这个组合要给密钥,原因见部署那篇。- TypeScript 没有实测。
- 只在酷站AI 一家中转站上测过。 别家支持哪些模型,各不相同。