Claude Code 学习站

Agent SDK 子代理(Subagents)使用参考

介绍如何在 Claude Agent SDK 中用 agents 参数以编程方式定义、调用、恢复子代理,并控制其嵌套深度、并发与花费上限。

本页目录26
AI 摘要 · 已核查整理于 2026-08-07原文:Subagents in the SDK(Anthropic)Claude Agent SDK子代理多代理编排TypeScript/Python
要点速览
  • 子代理通过 query() 的 agents 参数以编程方式定义(AgentDefinition),也可用 .claude/agents/ 下的 Markdown 文件定义,同名时编程定义优先
  • 必须在 allowedTools 中包含 Agent,子代理调用才会自动批准,否则会走 canUseTool 回调或在 dontAsk 模式下被拒绝
  • tools 字段省略则继承子代理可用的全部工具;列出则仅限所列工具,未列出的工具在该子代理会话中完全不可见
  • TypeScript SDK v0.3.219+ / Python SDK v0.2.127+(对应 Claude Code v2.1.219+)可通过环境变量 CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH、CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS 及查询选项 maxBudgetUsd/max_budget_usd 分别限制子代理嵌套深度、并发数与总花费
  • 可通过捕获 session_id 与 Agent 工具结果文本中的 agentId,并在后续 query 中传入 resume 来恢复子代理的完整会话历史
  • 数十到数百个代理规模的协同建议改用 Workflow 工具(TypeScript SDK v0.3.149+),而非在单轮对话中层层委派子代理

本文是对 Claude Agent SDK 官方文档「Subagents in the SDK」页面的中文整理,完整与最新内容以原文为准:https://code.claude.com/docs/en/agent-sdk/subagents

概述

子代理(Subagent)是主代理可以派生出来处理专项子任务的独立代理实例。用它们可以隔离上下文、并行执行多项分析,并在不增加主代理提示词负担的前提下应用专门指令。

创建子代理有三种方式:

  • 编程方式:在 query() 选项中使用 agents 参数。参见 TypeScript 与 Python 参考文档中的 AgentDefinition
  • 基于文件系统:在 .claude/agents/ 目录下以 Markdown 文件定义代理。
  • 内置的通用(general-purpose)子代理:即使你没有定义任何代理,Claude 也可以随时通过 Agent 工具调用内置的 general-purpose 子代理。

本文聚焦于编程方式,这是 SDK 应用中推荐的做法。

使用子代理的好处

上下文隔离

每个子代理运行在全新的独立对话中。中间的工具调用与结果都留在子代理内部,只有其最终消息会返回给父代理。例如,一个 research-assistant 子代理可以探索数十个文件而不会把这些内容累积进主对话——父代理收到的是简明摘要,而不是子代理读过的每个文件。

并行化

多个子代理可以并发运行,独立子任务的完成时间取决于最慢的那个,而不是所有任务耗时之和。例如代码评审时可以同时运行 style-checkersecurity-scannertest-coverage 三个子代理,而不是依次运行。

专门的指令与知识

每个子代理可以拥有针对性的系统提示词,包含特定专业知识、最佳实践与约束。例如 database-migration 子代理可以拥有关于 SQL 最佳实践、回滚策略、数据完整性检查的详细知识,这些内容对主代理来说是不必要的噪音。

工具限制

子代理可以被限制只能使用特定工具,降低意外操作的风险。例如 doc-reviewer 子代理可能只拥有 Read 和 Grep 权限,确保它只能分析而不会意外修改文档文件。

创建子代理

编程方式定义(推荐)

直接在代码中用 agents 参数定义子代理。Claude 通过 Agent 工具调用子代理,所以要在 allowedTools 中包含 Agent,以便子代理调用自动获批、无需权限提示。

本页大多数示例只打印最终结果。要确认 Claude 确实委派给了子代理而不是自己直接回答,参见下文「检测子代理调用」。

以下示例创建两个子代理:一个只读访问的代码评审员,一个可以执行命令的测试执行器。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition


async def main():
    async for message in query(
        prompt="Review the authentication module for security issues",
        options=ClaudeAgentOptions(
            # Auto-approve these tools, including Agent for subagent invocation
            allowed_tools=["Read", "Grep", "Glob", "Agent"],
            agents={
                "code-reviewer": AgentDefinition(
                    # description tells Claude when to use this subagent
                    description="Expert code review specialist. Use for quality, security, and maintainability reviews.",
                    # prompt defines the subagent's behavior and expertise
                    prompt="""You are a code review specialist with expertise in security, performance, and best practices.

When reviewing code:
- Identify security vulnerabilities
- Check for performance issues
- Verify adherence to coding standards
- Suggest specific improvements

Be thorough but concise in your feedback.""",
                    # tools restricts what the subagent can do (read-only here)
                    tools=["Read", "Grep", "Glob"],
                    # model overrides the default model for this subagent
                    model="sonnet",
                ),
                "test-runner": AgentDefinition(
                    description="Runs and analyzes test suites. Use for test execution and coverage analysis.",
                    prompt="""You are a test execution specialist. Run tests and provide clear analysis of results.

Focus on:
- Running test commands
- Analyzing test output
- Identifying failing tests
- Suggesting fixes for failures""",
                    # Bash access lets this subagent run test commands
                    tools=["Bash", "Read", "Grep"],
                ),
            },
        ),
    ):
        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Review the authentication module for security issues",
  options: {
    // Auto-approve these tools, including Agent for subagent invocation
    allowedTools: ["Read", "Grep", "Glob", "Agent"],
    agents: {
      "code-reviewer": {
        // description tells Claude when to use this subagent
        description:
          "Expert code review specialist. Use for quality, security, and maintainability reviews.",
        // prompt defines the subagent's behavior and expertise
        prompt: `You are a code review specialist with expertise in security, performance, and best practices.

When reviewing code:
- Identify security vulnerabilities
- Check for performance issues
- Verify adherence to coding standards
- Suggest specific improvements

Be thorough but concise in your feedback.`,
        // tools restricts what the subagent can do (read-only here)
        tools: ["Read", "Grep", "Glob"],
        // model overrides the default model for this subagent
        model: "sonnet"
      },
      "test-runner": {
        description:
          "Runs and analyzes test suites. Use for test execution and coverage analysis.",
        prompt: `You are a test execution specialist. Run tests and provide clear analysis of results.

Focus on:
- Running test commands
- Analyzing test output
- Identifying failing tests
- Suggesting fixes for failures`,
        // Bash access lets this subagent run test commands
        tools: ["Bash", "Read", "Grep"]
      }
    }
  }
})) {
  if ("result" in message) console.log(message.result);
}

AgentDefinition 配置字段

字段类型必填说明
descriptionstring何时使用该代理的自然语言描述
promptstring定义代理角色与行为的系统提示词
toolsstring[]允许使用的工具名数组。若省略,继承子代理可用的全部工具
disallowedToolsstring[]从该代理工具集中移除的工具名数组。也接受 MCP 服务器级模式:mcp__servermcp__server__* 会移除该服务器的全部工具,mcp__* 会移除任何服务器的全部 MCP 工具
modelstring该代理的模型覆盖。可接受别名如 'fable''opus''sonnet''haiku''inherit',或完整模型 ID。省略时默认使用主模型
skillsstring[]启动时预加载进该代理上下文的技能名列表。未列出的技能仍可通过 Skill 工具调用
memory'user' | 'project' | 'local'该代理的记忆来源
mcpServers(string | object)[]该代理可用的 MCP 服务器,按名称或内联配置指定
initialPromptstring当该代理作为主线程代理运行时,自动提交为第一个用户轮次。作为子代理调用时被忽略
maxTurnsnumber该代理停止前的最大代理轮次数
backgroundboolean调用时以非阻塞的后台任务方式运行该代理
effort'low' | 'medium' | 'high' | 'xhigh' | 'max' | number该代理的推理努力等级
permissionModePermissionMode该代理内工具执行的权限模式

在 Python SDK 中,像 disallowedToolsmcpServers 这类多单词字段名保留其驼峰式拼写以匹配线上格式,而不遵循 Python 的 snake_case 约定。详见 Python 参考文档中的 AgentDefinition

Claude Code v2.1.198 起,子代理有两处行为变化:

  • 子代理默认在后台运行。省略 run_in_background 输入的 Agent 工具调用会启动一个后台子代理;当 Claude 需要在继续之前拿到结果时,会设置 run_in_background: false。在 v2.1.198 之前,省略 run_in_background 会同步运行子代理。将 background 字段设为 true 可强制该特定代理无视 Claude 的请求、始终后台执行。
  • 子代理继承主会话的扩展思考(extended thinking)配置。

子代理也可以派生自己的子代理。要限制这种嵌套的深度、同时运行的数量以及查询花费,参见下文「限制子代理深度、并发与花费」。

基于文件系统的定义(替代方案)

也可以在 .claude/agents/ 目录下用 Markdown 文件定义子代理,详见 Claude Code 子代理文档。以编程方式定义的代理优先于同名的基于文件系统的代理。

注意:即使没有定义自定义子代理,Claude 也可以派生内置的 general-purpose 子代理,这对于委派研究或探索类任务而无需创建专门代理很有用。要让这类调用自动获批而无需权限提示,请在 allowedTools 中包含 Agent

当 Claude 调用 Agent 工具但未指定 subagent_type 时,会得到这个内置的 general-purpose 子代理。如果设置了 CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1,这个默认值也会消失。此时该调用会失败,报错 subagent_type is required: the general-purpose agent is not available in this session,消息末尾会列出当前会话仍可用的子代理类型。在 TypeScript SDK v0.3.235 之前(Python SDK 对应打包的 Claude Code v2.1.235 之前),同样的调用会报错 Agent type 'general-purpose' not found

子代理继承了什么

子代理的上下文窗口从零开始,没有父对话,但并非完全为空。从父代理传给子代理的唯一内容是 Agent 工具的 prompt 字符串,因此需要将子代理所需的任何文件路径、错误信息或决策直接写入该 prompt。

拥有 SendMessage 工具的子代理,启动时会附带一份该会话中其他已命名代理的列表,以便知道可以向哪些名称发消息。Claude Code 会自动将该列表加入子代理的首个轮次。fork(分叉当前对话)不会得到该列表,因为它继承的是父对话本身。该列表功能需要 Claude Code v2.1.206 或更高版本。

子代理会收到子代理不会收到
自身的系统提示词(AgentDefinition.prompt)与 Agent 工具的 prompt父代理的对话历史或工具结果
项目 CLAUDE.md(通过 settingSources 加载)预加载的技能内容,除非在 AgentDefinition.skills 中列出
工具定义(继承自父代理,或 tools 中指定的子集,后台运行时会被过滤)父代理的系统提示词

注意:父代理会将子代理的最终消息作为 Agent 工具结果收到,但可能会在自己的回复中对其进行摘要。若要在面向用户的回复中原样保留子代理输出,需要在传给主 query() 调用的 prompt 或 systemPrompt 选项中加入相应指令。

在 v2.1.210 及更高版本中,Claude Code 会在父代理读取最终消息之前,扫描其中形似指令的模式。该扫描对三类模式做不同处理:

  • 模仿控制标签:Claude Code 会就地中和只有 harness 才会发出的标签(如 <system-reminder> 块),做法是在开头尖括号后插入一个反斜杠,不删除任何内容。
  • 提及权限配置:Claude Code 会原样保留对权限配置的引用,例如 .claude/settings.jsonbypassPermissions--dangerously-skip-permissions
  • 对话轮次标记:以 Human:Assistant: 开头的行,会在冒号前加一个反斜杠,使该消息无法冒充对话轮次边界。

对于控制标签或权限配置匹配,Claude Code 会在前面加一行 [harness: ...] 标记,列出匹配到的模式;轮次标记匹配则不会加这行标记。这些是扫描所做的唯一修改——它绝不会删除或改写子代理的文本。

导致子代理提前结束的 API 错误(例如速率限制)不会作为其结果被传递。前台与后台行为的细节参见原文「API errors in subagents」章节。

调用子代理

自动调用

Claude 会根据任务与每个子代理的 description 自动决定何时调用子代理。例如,若你定义了一个描述为「Performance optimization specialist for query tuning」的 performance-optimizer 子代理,当你的提示词提到优化查询时,Claude 就会调用它。

请编写清晰、具体的描述,以便 Claude 能把任务匹配到正确的子代理。

显式调用

要确保 Claude 使用某个特定子代理,可以在提示词中直接点名:

"Use the code-reviewer agent to check the authentication module"

这会绕过自动匹配,直接调用指定名称的子代理。

动态代理配置

可以根据运行时条件动态创建代理定义。以下示例创建一个安全评审员,根据严格程度使用更强的模型进行严格审查。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition


# Factory function that returns an AgentDefinition
# This pattern lets you customize agents based on runtime conditions
def create_security_agent(security_level: str) -> AgentDefinition:
    is_strict = security_level == "strict"
    return AgentDefinition(
        description="Security code reviewer",
        # Customize the prompt based on strictness level
        prompt=f"You are a {'strict' if is_strict else 'balanced'} security reviewer...",
        tools=["Read", "Grep", "Glob"],
        # Key insight: use a more capable model for high-stakes reviews
        model="opus" if is_strict else "sonnet",
    )


async def main():
    # The agent is created at query time, so each request can use different settings
    async for message in query(
        prompt="Review this PR for security issues",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Grep", "Glob", "Agent"],
            agents={
                # Call the factory with your desired configuration
                "security-reviewer": create_security_agent("strict")
            },
        ),
    ):
        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query, type AgentDefinition } from "@anthropic-ai/claude-agent-sdk";

// Factory function that returns an AgentDefinition
// This pattern lets you customize agents based on runtime conditions
function createSecurityAgent(securityLevel: "basic" | "strict"): AgentDefinition {
  const isStrict = securityLevel === "strict";
  return {
    description: "Security code reviewer",
    // Customize the prompt based on strictness level
    prompt: `You are a ${isStrict ? "strict" : "balanced"} security reviewer...`,
    tools: ["Read", "Grep", "Glob"],
    // Key insight: use a more capable model for high-stakes reviews
    model: isStrict ? "opus" : "sonnet"
  };
}

// The agent is created at query time, so each request can use different settings
for await (const message of query({
  prompt: "Review this PR for security issues",
  options: {
    allowedTools: ["Read", "Grep", "Glob", "Agent"],
    agents: {
      // Call the factory with your desired configuration
      "security-reviewer": createSecurityAgent("strict")
    }
  }
})) {
  if ("result" in message) console.log(message.result);
}

检测子代理调用

Claude 通过 Agent 工具调用子代理。要检测子代理何时被调用,检查 name"Agent"tool_use 块。子代理上下文内产生的消息包含 parent_tool_use_id 字段。

注意:该工具在 Claude Code v2.1.63 中从 "Task" 更名为 "Agent"。当前 SDK 版本在 tool_use 块中输出 "Agent",但在 system:init 的 tools 列表以及 result.permission_denials[].tool_name 中仍使用 "Task"。在 block.name 中同时检查这两个值可以保证跨 SDK 版本兼容。

两个 SDK 的消息结构不同:Python 中可通过 message.content 直接访问内容块;TypeScript 中 SDKAssistantMessage 包装了 Claude API 消息,因此要通过 message.message.content 访问内容。

以下示例遍历流式消息,记录子代理何时被调用,以及后续哪些消息来自该子代理的执行上下文。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ToolUseBlock


async def main():
    async for message in query(
        prompt="Use the code-reviewer agent to review this codebase",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Glob", "Grep", "Agent"],
            agents={
                "code-reviewer": AgentDefinition(
                    description="Expert code reviewer.",
                    prompt="Analyze code quality and suggest improvements.",
                    tools=["Read", "Glob", "Grep"],
                )
            },
        ),
    ):
        # Check for subagent invocation. Match both names: older SDK
        # versions emitted "Task", current versions emit "Agent".
        if hasattr(message, "content") and message.content:
            for block in message.content:
                if isinstance(block, ToolUseBlock) and block.name in (
                    "Task",
                    "Agent",
                ):
                    print(f"Subagent invoked: {block.input.get('subagent_type')}")

        # Check if this message is from within a subagent's context
        if hasattr(message, "parent_tool_use_id") and message.parent_tool_use_id:
            print("  (running inside subagent)")

        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Use the code-reviewer agent to review this codebase",
  options: {
    allowedTools: ["Read", "Glob", "Grep", "Agent"],
    agents: {
      "code-reviewer": {
        description: "Expert code reviewer.",
        prompt: "Analyze code quality and suggest improvements.",
        tools: ["Read", "Glob", "Grep"]
      }
    }
  }
})) {
  const msg = message as any;

  // Check for subagent invocation. Match both names: older SDK versions
  // emitted "Task", current versions emit "Agent".
  for (const block of msg.message?.content ?? []) {
    if (block.type === "tool_use" && (block.name === "Task" || block.name === "Agent")) {
      console.log(`Subagent invoked: ${block.input.subagent_type}`);
    }
  }

  // Check if this message is from within a subagent's context
  if (msg.parent_tool_use_id) {
    console.log("  (running inside subagent)");
  }

  if ("result" in message) {
    console.log(message.result);
  }
}

恢复子代理

可以恢复子代理以从中断处继续,而不是重新开始。恢复的子代理会保留完整的对话历史,包括所有先前的工具调用、结果与推理过程。

子代理完成后,Agent 工具结果中会包含一个含 agentId: <id> 的文本块。内置的 ExplorePlan 代理是一次性的,不会返回 agentId,所以需要恢复时应使用自定义代理或 general-purpose。要以编程方式恢复子代理:

  1. 捕获会话 ID:在首次 query 期间从消息中提取 session_id
  2. 提取代理 ID:从 Agent 工具结果文本中解析 agentId
  3. 恢复会话:在第二次 query 的选项中传入 resume: sessionId,并在提示词中包含代理 ID

注意:必须恢复同一个会话才能访问子代理的记录(transcript)。每次 query() 调用默认会开启一个新会话,因此需要传入 resume: sessionId 以在同一会话中继续。

使用自定义代理时,需要在两次 query 中都通过 agents 参数传入相同的代理定义。

以下示例定义了一个自定义 endpoint-finder 代理。第一次 query 运行它,并从 Agent 工具结果中捕获会话 ID 与代理 ID;第二次 query 恢复该会话,提出需要第一次分析上下文的后续问题。

import asyncio
import re
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ToolResultBlock

AGENTS = {
    "endpoint-finder": AgentDefinition(
        description="Locates and catalogs API endpoints in a codebase.",
        prompt="You find and document API endpoints. Report each endpoint's path, method, and handler.",
        tools=["Read", "Grep", "Glob"],
    )
}


def extract_agent_id(block: ToolResultBlock) -> str | None:
    """Extract agentId from an Agent tool result's text content."""
    parts = block.content if isinstance(block.content, list) else [{"text": block.content}]
    for part in parts:
        if match := re.search(r"agentId:\s*([\w-]+)", part.get("text") or ""):
            return match.group(1)
    return None


async def main():
    agent_id = None
    session_id = None

    # First invocation - run the endpoint-finder subagent
    try:
        async for message in query(
            prompt="Use the endpoint-finder agent to find all API endpoints in this codebase",
            options=ClaudeAgentOptions(allowed_tools=["Read", "Grep", "Glob", "Agent"], agents=AGENTS),
        ):
            # Capture session_id from ResultMessage (needed to resume this session)
            if hasattr(message, "session_id"):
                session_id = message.session_id
            # Search tool results for the agentId trailer
            for block in getattr(message, "content", None) or []:
                if isinstance(block, ToolResultBlock):
                    agent_id = extract_agent_id(block) or agent_id
            # Print the final result
            if hasattr(message, "result"):
                print(message.result)
    except Exception as error:
        # A single-shot query() raises after yielding an error result,
        # so session_id and agent_id have already been captured by the loop above.
        print(f"Session ended with an error: {error}")

    # Second invocation - resume and ask follow-up
    if agent_id and session_id:
        async for message in query(
            prompt=f"Resume agent {agent_id} and list the top 3 most complex endpoints",
            options=ClaudeAgentOptions(
                allowed_tools=["Read", "Grep", "Glob", "Agent"], agents=AGENTS, resume=session_id
            ),
        ):
            if hasattr(message, "result"):
                print(message.result)
    else:
        print("No agentId found in the first query, so there is no subagent to resume.")


asyncio.run(main())
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";

const agents = {
  "endpoint-finder": {
    description: "Locates and catalogs API endpoints in a codebase.",
    prompt: "You find and document API endpoints. Report each endpoint's path, method, and handler.",
    tools: ["Read", "Grep", "Glob"]
  }
};

// Stringify content to search for agentId without traversing nested block types
function extractAgentId(message: SDKMessage): string | undefined {
  if (message.type !== "assistant" && message.type !== "user") return undefined;
  const content = JSON.stringify(message.message.content);
  const match = content.match(/agentId:\s*([\w-]+)/);
  return match?.[1];
}

let agentId: string | undefined;
let sessionId: string | undefined;

// First invocation - run the endpoint-finder subagent
try {
  for await (const message of query({
    prompt: "Use the endpoint-finder agent to find all API endpoints in this codebase",
    options: { allowedTools: ["Read", "Grep", "Glob", "Agent"], agents }
  })) {
    // Capture session_id from ResultMessage (needed to resume this session)
    if ("session_id" in message) sessionId = message.session_id;
    // Search message content for the agentId (appears in Agent tool results)
    const extractedId = extractAgentId(message);
    if (extractedId) agentId = extractedId;
    // Print the final result
    if ("result" in message) console.log(message.result);
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result,
  // so sessionId and agentId have already been captured by the loop above.
  console.error(`Session ended with an error: ${error}`);
}

// Second invocation - resume and ask follow-up
if (agentId && sessionId) {
  for await (const message of query({
    prompt: `Resume agent ${agentId} and list the top 3 most complex endpoints`,
    options: { allowedTools: ["Read", "Grep", "Glob", "Agent"], agents, resume: sessionId }
  })) {
    if ("result" in message) console.log(message.result);
  }
} else {
  console.log("No agentId found in the first query, so there is no subagent to resume.");
}

子代理的会话记录存储在单独的文件中,独立于主对话持久保存。压缩(compaction)行为与 cleanupPeriodDays 清理周期,参见原文「Claude Code 子代理」文档中的「恢复子代理」章节。

工具限制

tools 字段限制子代理能做什么:

  • 省略 tools:子代理获得子代理可用的全部工具
  • 列出工具:子代理只获得所列工具。例如永远不该编辑文件的代码评审员,可以只给 ["Read", "Grep", "Glob"]

未列出的工具根本不会出现在该子代理的会话中:Claude 会在没有该工具的情况下工作,不会出现权限提示或报错。

以下示例创建一个只读分析代理,可以检查代码但不能修改文件或执行命令。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition


async def main():
    async for message in query(
        prompt="Analyze the architecture of this codebase",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Grep", "Glob", "Agent"],
            agents={
                "code-analyzer": AgentDefinition(
                    description="Static code analysis and architecture review",
                    prompt="""You are a code architecture analyst. Analyze code structure,
identify patterns, and suggest improvements without making changes.""",
                    # Read-only tools: no Edit, Write, or Bash access
                    tools=["Read", "Grep", "Glob"],
                )
            },
        ),
    ):
        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Analyze the architecture of this codebase",
  options: {
    allowedTools: ["Read", "Grep", "Glob", "Agent"],
    agents: {
      "code-analyzer": {
        description: "Static code analysis and architecture review",
        prompt: `You are a code architecture analyst. Analyze code structure,
identify patterns, and suggest improvements without making changes.`,
        // Read-only tools: no Edit, Write, or Bash access
        tools: ["Read", "Grep", "Glob"]
      }
    }
  }
})) {
  if ("result" in message) console.log(message.result);
}

常见工具组合

使用场景工具说明
只读分析ReadGrepGlob可以检查代码但不能修改或执行
测试执行BashReadGrep可以执行命令并分析输出
代码修改ReadEditWriteGrepGlob完整的读写访问,但不能执行命令
完全访问全部工具继承子代理可用的全部工具(省略 tools 字段)

限制子代理深度、并发与花费

注意:本节描述的是 TypeScript SDK v0.3.219 及 Python SDK v0.2.127 及以后版本(对应打包 Claude Code v2.1.219 及以后)。在更早版本上,部分限制缺失或默认行为不同,依赖这些限制来约束一次运行前请先升级。每个环境变量新增所在的 Claude Code 版本、以及花费上限对子代理的执行方式,记录在环境变量参考文档与「轮次与预算」文档中。

一旦在 allowedTools 中包含 Agent,Claude 就会自行决定何时派生子代理以及派生多少个。每个子代理都会发起自己的 API 请求,这些请求都计入该次查询的 total_cost_usd,而子代理本身也可以派生自己的子代理,因此一条提示词可能演变成一整棵代理树。

可以从三个维度限制这种增长:子代理嵌套的深度、同时运行的数量、以及整个查询的花费。深度与并发限制通过 env 选项设置为环境变量,花费限制则作为查询选项设置:

限制设置方式默认值Claude Code 在达到限制时的行为
深度CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH主代理之下 3 层子代理。1 表示子代理不能再派生自己的子代理使最底层的子代理无法再派生,它会自行完成被委派的工作。参见「嵌套子代理」
并发CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS同时运行 20 个子代理,计入 Claude 用 Agent 工具派生的每一个子代理拒绝再派生新的子代理,返回 Concurrent subagent limit reached,直到运行中的数量降到限制以下。启用 ultracode 的会话永远不会被拒绝。参见「并发子代理限制」
花费TypeScript 中的 maxBudgetUsd,Python 中的 max_budget_usd无限制。与 total_cost_usd 比较,因此子代理的请求也计入以三种方式强制执行该上限:拒绝派生更多子代理并返回 Budget limit reached;停止仍在运行的后台子代理;以 error_max_budget_usd 结果子类型结束该查询。参见「轮次与预算」

两个 SDK 对 env 选项的处理不同:TypeScript SDK 会用它替换整个子进程环境,因此需要把 process.env 展开进去以保留 PATH 等变量;Python SDK 则是把它合并进继承的环境中。以下示例关闭嵌套、最多允许五个子代理同时运行,并在预估花费达到 5 美元时停止查询:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    try:
        async for message in query(
            prompt="Audit every service in this repo for unhandled promise rejections",
            options=ClaudeAgentOptions(
                allowed_tools=["Read", "Grep", "Glob", "Agent"],
                # env is merged on top of the inherited environment
                env={
                    "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "1",
                    "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "5",
                },
                max_budget_usd=5.0,
            ),
        ):
            if isinstance(message, ResultMessage):
                print(f"{message.subtype}: ${message.total_cost_usd}")
    except Exception as error:
        # A single-shot query() raises after yielding an error result,
        # so the budget-capped result has already been printed above.
        print(f"Session ended with an error: {error}")


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

try {
  for await (const message of query({
    prompt: "Audit every service in this repo for unhandled promise rejections",
    options: {
      allowedTools: ["Read", "Grep", "Glob", "Agent"],
      // env replaces the subprocess environment, so spread process.env to keep PATH
      env: {
        ...process.env,
        CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH: "1",
        CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS: "5",
      },
      maxBudgetUsd: 5,
    },
  })) {
    if (message.type === "result") {
      console.log(`${message.subtype}: $${message.total_cost_usd}`);
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result,
  // so the budget-capped result has already been logged above.
  console.error(`Session ended with an error: ${error}`);
}

查询最终看到什么取决于是否触发了限制:

  • 未达到花费上限:看到 success 与预估花费。
  • 达到花费上限:看到 error_max_budget_usd,花费值等于或高于 5,随后你的错误处理逻辑会被触发。
  • 达到并发限制:消息流中会出现一个携带 Concurrent subagent limit reachedtool_result 块;Claude 收到的 Agent 工具结果就是这个同样的块。

在使用子代理时运行 Opus 5

Claude Opus 5 比早期模型更倾向于委派给子代理,因此在运行 Opus 5 的查询中,深度、并发与花费限制尤为重要。Opus 5 提示工程指南中有一条可加入任何提示词的委派控制指令。Claude Code 是否会自行添加此类指令,取决于所用的系统提示词:

  • claude_code 预设:当模型为 Opus 5 时,Claude Code 会在其系统提示词中加入一行,告诉 Claude 除非被要求,否则不要调用 Agent 工具。Agent 工具本身仍然可用。
  • 自定义提示词,或未设置 systemPrompt:Claude Code 不会构建自己的系统提示词,因此该行指令不存在,需要自行把提示工程指南中的委派指令加入自己的提示词。

无论哪种指令都只是引导 Claude,因此仍需设置限制——Claude Code 会按 Claude 实际的委派决策来强制执行这些限制。

用动态工作流(Workflow)扩展规模

子代理适合每轮委派少量任务。若要协调数十到数百个代理的运行,应使用 Workflow 工具,它将编排逻辑移入运行时在对话上下文之外执行的脚本中。差异详见原文「dynamic workflows」文档。

Workflow 工具在 TypeScript Agent SDK v0.3.149 及以后版本中可用。在 allowedTools 中包含 Workflow 以自动批准工作流运行。工具的输入输出 schema 列在 TypeScript 参考文档中。

故障排查

Claude 没有委派给子代理

如果 Claude 直接完成任务而不是委派给你定义的子代理:

  • 检查 Agent 调用是否已获批:在 allowedTools 中包含 Agent 以自动批准子代理调用。否则,Agent 调用会走 canUseTool 回调,或在 dontAsk 模式下被拒绝。
  • 使用显式提示:在提示词中直接点名子代理,例如「Use the code-reviewer agent to...」。
  • 写清楚的描述:准确说明何时使用该子代理,以便 Claude 能正确匹配任务。

基于文件系统的代理未加载

Claude Code 会监视 ~/.claude/agents/.claude/agents/,并在几秒内自动拾取新建或修改的代理文件,无需重启。若某个定义始终不出现,可依次排查以下原因:

  • 新建的 agents 目录:监视器只覆盖会话启动时已存在的目录,因此新目录中的第一个文件需要重启会话才能生效。这是最常见的原因。
  • YAML frontmatter 无效或 name 重复:检查文件的 YAML,以及是否已有代理使用了相同的 name
  • --disable-slash-commands:以该标志启动的会话不会监视这些目录,始终需要重启才能加载新文件。
  • 位于通过 add-dir 添加的目录下的文件:Claude Code 会从通过 add_dirs(Python)或 additionalDirectories(TypeScript)选项、或 CLI 的 --add-dir//add-dir 添加的目录中加载 .claude/agents/,但不会监视这些目录,因此该目录下新建或修改的文件需要重启会话。
  • 同名的编程方式代理:传给 query()agents 会覆盖同名的基于文件系统的代理。

文件格式细节参见原文「how to write subagent files」章节。

相关文档

  • Claude Code subagents:全面的子代理文档,包括基于文件系统的定义方式
  • Dynamic workflows:如何用脚本编排大量子代理,适用于单次对话难以承载的任务
  • SDK overview:Claude Agent SDK 入门