☰
Claude code 工程化实战第24讲:Agent SDK 高级应用——用 @tool 与 create_sdk_mcp_server 搭建 SubAgent 配置骨架
2026/9/29 23:03:22 网站建设 项目流程

1. 为什么 SubAgent 配置总是跑不起来

Claude code 的 Agent SDK 到了高级阶段,绕不开三件事:用@tool把 Python 函数注册成 Claude 能调用的工具、用create_sdk_mcp_server把这些工具打包成进程内 MCP server、再让主 Agent 通过 SubAgent 做任务隔离。听起来是一条清晰的链路,但真正动手时,多数人卡在同一个地方——配置骨架搭不起来。

我见过太多人把@tool函数写好了,create_sdk_mcp_server也调用了,结果 Claude 在推理时压根看不到工具,或者看到了却传错参数。问题往往不在业务逻辑,而在骨架:type hints 漏了、docstring 没写、mcp_servers的 key 和 server name 对不上、SubAgent 的allowed_tools没放行对应工具。这些细节单看都不难,凑在一起就变成"明明照抄了代码却跑不通"。

这篇面向的是已经用过 Claude code 基础 SDK、想往工程化多工具编排走的人。我会给出一套可以直接复制的settings.json/config.toml骨架,配合 TaoToken 统一 Key 接入,然后演示一次 SubAgent 调用验证动作。目标很明确:让你在本地把"主 Agent 调 SubAgent、SubAgent 调自定义工具"这条链路跑通,而不是停在"代码看起来对"的阶段。

核心检索词先摆出来:Claude code 的 Agent SDK 高级应用,围绕@tool注册、create_sdk_mcp_server打包、SubAgent 协作三件事展开。适合谁?适合已经能跑通query()基础调用、想进一步做多工具编排和权限隔离的开发者。

2. TaoToken 前置:统一 Key 与接入配置

在搭 SubAgent 骨架之前,先把模型接入这一层固定下来。Agent SDK 的调用最终要落到一个可用的 API 端点上,TaoToken 在这里的作用是提供统一的 Key 和兼容的接入地址,省去在每个 SubAgent 里重复配置鉴权的麻烦。

你需要准备两样东西:一个 TaoToken 的 API Key,以及接入地址。地址分两个用途,官网入口是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,API 调用地址是https://taotoken.net/api(这个不加 UTM 参数)。Key 在控制台的 API Keys 页面生成,地址是https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite。

拿到 Key 之后,推荐用环境变量注入,而不是硬编码在脚本里。这样主 Agent 和所有 SubAgent 共享同一份鉴权,切换环境时只改一处:

export TAOTOKEN_API_KEY="sk-你的key" export ANTHROPIC_BASE_URL="https://taotoken.net/api"

注意:环境变量名要和 SDK 读取的字段对齐。如果你的 SDK 版本读的是ANTHROPIC_API_KEY,就把上面第一行的变量名改成它,值不变。变量名写错是"Key 明明对却报鉴权失败"的头号原因。

如果你更习惯用配置文件管理,可以在项目根目录放一个.env,然后用python-dotenv加载。但无论哪种方式,原则是同一条:Key 只出现一次,主 Agent 和 SubAgent 都从同一个来源读。SubAgent 是独立上下文,但它继承主进程的环境变量,所以不需要在每个 SubAgent 里单独传 Key。

这一步做完,接入层就固定了。后面所有@tool、create_sdk_mcp_server、SubAgent 的配置都建立在这个统一 Key 之上。

3. 可复制配置:settings.json 与 config.toml 骨架

这一节给两套骨架。settings.json管 Claude code 的运行时行为(权限、Hook、MCP server 引用),config.toml管项目级参数(模型、超时、SubAgent 定义)。两者配合,构成 SubAgent 配置的完整底座。

先看settings.json。放在项目根目录的.claude/settings.json:

{ "permissions": { "allow": [ "Read", "Grep", "Glob", "Bash(pytest:*)", "Bash(ruff:*)" ], "deny": [ "Bash(rm -rf:*)", "Bash(curl:*)" ] }, "mcpServers": { "my-custom-tools": { "command": "python", "args": ["-m", "my_tools.server"], "env": { "TAOTOKEN_API_KEY": "${TAOTOKEN_API_KEY}" } } }, "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash .claude/hooks/deny-dangerous.sh" } ] } ], "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "bash .claude/hooks/audit-log.sh" } ] } ] } }

这里有几个关键点。permissions.allow是白名单,SubAgent 能用的工具必须在这里放行,否则即使@tool注册成功,Claude 调用时也会被拦。mcpServers里的 key(这里是my-custom-tools)必须和后面create_sdk_mcp_server的name完全一致,这是最常见的对不上错误。hooks里的PreToolUse做危险命令拦截,PostToolUse做审计日志,这两层是 SubAgent 权限管理的基础。

再看config.toml,放在项目根目录:

[model] name = "claude-sonnet-4-20250514" max_turns = 20 timeout_seconds = 120 [api] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" [subagent.code_reviewer] description = "代码审查 SubAgent,只读分析" allowed_tools = ["Read", "Grep", "Glob"] permission_mode = "plan" system_prompt = "你是代码审查专家,只输出问题,不修改文件。" [subagent.test_runner] description = "测试执行 SubAgent" allowed_tools = ["Bash(pytest:*)", "Read"] permission_mode = "acceptEdits" system_prompt = "你是测试执行专家,只输出测试结果。" [subagent.linter] description = "代码风格检查 SubAgent" allowed_tools = ["Bash(ruff:*)", "Read"] permission_mode = "plan" system_prompt = "你是 lint 专家,只输出风格问题。"

config.toml的价值在于把 SubAgent 的定义从代码里抽出来。每个 SubAgent 的allowed_tools、permission_mode、system_prompt都在这里声明,主 Agent 启动时读取,动态构造ClaudeCodeOptions。这样加一个 SubAgent 只需要改配置,不用动主流程代码。

提示:permission_mode用plan表示只读分析,用acceptEdits表示允许编辑。SubAgent 做分析时用plan,做修复时用acceptEdits,这是两阶段工作流的基础。

两套骨架就位后,接下来写@tool和create_sdk_mcp_server的代码,把它们和上面的配置对接起来。

4. 验证请求:一次 SubAgent 调用跑通

现在把骨架填上血肉。先写自定义工具,用@tool装饰器注册,再用create_sdk_mcp_server打包:

#!/usr/bin/env python3 # my_tools/server.py import asyncio from claude_code_sdk import ( query, ClaudeCodeOptions, tool, create_sdk_mcp_server ) @tool def get_user(user_id: int) -> dict: """根据用户 ID 获取用户对象。 Args: user_id: 用户 ID(整数) Returns: 包含 id, name, email 的字典 """ return { "id": user_id, "name": f"User {user_id}", "email": f"user{user_id}@example.com" } @tool def count_lines(file_path: str) -> int: """统计文件行数。 Args: file_path: 文件路径 Returns: 文件行数 """ with open(file_path, "r", encoding="utf-8") as f: return len(f.readlines()) custom_server = create_sdk_mcp_server( name="my-custom-tools", version="1.0.0", tools=[get_user, count_lines] )

注意create_sdk_mcp_server的name是my-custom-tools,和settings.json里mcpServers的 key 一致。@tool装饰器会自动从 type hints 生成 JSON schema,从 docstring 生成工具描述,所以 type hints 和 docstring 一个都不能少。

接下来写主 Agent 调 SubAgent 的验证脚本:

#!/usr/bin/env python3 # main.py import asyncio from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage from my_tools.server import custom_server async def run_subagent(name: str, prompt: str, options: ClaudeCodeOptions) -> str: """跑一个 SubAgent,返回报告""" report = "" async for msg in query(prompt=prompt, options=options): if isinstance(msg, AssistantMessage): for block in msg.content: if hasattr(block, "text"): report += block.text return report async def main(): # 主 Agent 的 options,挂载 in-process MCP server main_options = ClaudeCodeOptions( mcp_servers={"my-custom-tools": custom_server}, allowed_tools=["mcp__my-custom-tools__get_user"], max_turns=5 ) # 验证 1:主 Agent 直接调自定义工具 print("=== 验证 1:主 Agent 调 get_user ===") async for msg in query( prompt="查询用户 ID 42 的信息", options=main_options ): if isinstance(msg, AssistantMessage): for block in msg.content: if hasattr(block, "text"): print(block.text, end="", flush=True) # 验证 2:SubAgent 隔离调用 print("\n\n=== 验证 2:SubAgent 调 count_lines ===") sub_options = ClaudeCodeOptions( mcp_servers={"my-custom-tools": custom_server}, allowed_tools=["mcp__my-custom-tools__count_lines", "Read"], system_prompt="你是文件分析 SubAgent,只统计行数。", max_turns=5 ) report = await run_subagent( name="file-analyzer", prompt="统计 main.py 的行数", options=sub_options ) print(report) asyncio.run(main())

跑起来之后,验证 1 应该看到 Claude 调用get_user(42)并返回用户信息,验证 2 应该看到 SubAgent 调用count_lines返回行数。如果验证 1 通过、验证 2 失败,问题多半在 SubAgent 的allowed_tools没放行mcp__my-custom-tools__count_lines。

工具命名规则要记牢:in-process MCP server 暴露的工具,在 Claude 眼里是mcp__<server_name>__<tool_name>。server_name是create_sdk_mcp_server的name,tool_name是@tool函数的函数名。这个命名规则对不上,工具就调不到。

5. 本篇常见错排查

骨架跑通之前,下面几个错误几乎每个人都会踩一遍。我按出现频率排一下。

错误一:@tool函数漏写 type hints。@tool装饰器依赖 type hints 生成 JSON schema。如果写成def get_user(user_id)而不是def get_user(user_id: int),Claude 看不到参数类型,工具无法调用。判断标准很简单:你的每个@tool函数,参数和返回值都标了类型吗?没标就补上。

错误二:mcp_servers的 key 和 server name 不一致。settings.json里写"my-custom-tools",代码里create_sdk_mcp_server(name="my-tools"),两者对不上,Claude 就找不到工具。排查方法:把两处的字符串打印出来对比,或者统一用一个常量。

错误三:SubAgent 的allowed_tools没放行 MCP 工具。SubAgent 是独立上下文,它的allowed_tools不会继承主 Agent。如果 SubAgent 要用mcp__my-custom-tools__count_lines,就必须在它自己的allowed_tools里显式列出。漏了这一步,SubAgent 会报"工具不可用"。

错误四:流式输出忘了flush=True。如果你用print(block.text, end="", flush=True)做流式输出,漏掉flush=True会导致输出卡在缓冲区,用户等很久才看到结果。这个错误不报错,但体验极差,容易被忽略。

错误五:SubAgent 报告没汇总。主 Agent 调多个 SubAgent 并行跑,SubAgent 完成后报告写在各自上下文里,主 Agent 如果没显式读取,就只看到"任务完成"信号,看不到具体报告。修正方式是让 SubAgent 把报告写到约定路径(比如.claude/state/<name>.json),主 Agent 显式读取并汇总。

错误六:重试没有次数上限。网络持续失败时,无限重试会让长任务变成超长任务。重试次数控制在 3 次以内,第 4 次走 fallback 降级。指数退避用 1s / 2s / 4s,避免持续打 API。

注意:排查顺序建议从"工具是否被 Claude 看到"开始,再到"SubAgent 是否有权限调",最后到"报告是否被汇总"。这个顺序能覆盖 90% 的配置问题。

6. 从骨架到工程化:下一步怎么走

骨架跑通之后,你已经有了一个可用的 SubAgent 配置底座。接下来可以往两个方向深化。

一个方向是权限分层。把permission_mode用起来:分析阶段用plan只读,修复阶段用acceptEdits允许编辑,再配合PreToolUseHook 拦截危险命令、PostToolUseHook 记录审计日志。四层叠加,形成纵深防御。这套配置在settings.json里已经预留了位置,你只需要补上 Hook 脚本。

另一个方向是多 SubAgent 编排。用asyncio.gather并行跑多个 SubAgent,比如代码审查、测试执行、lint 检查同时进行,主 Agent 汇总三份报告。并行比串行快数倍,但要注意 SubAgent 之间不能有依赖,有依赖就得串行。

如果你想把模型调用也统一管理,TaoToken 的模型对话入口可以配合调试:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite。长期做编码和 Agent 编排的话,Coding Plan 更适合持续使用:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,遇到 SDK 字段对不上时查这里最快。

最后留一个实操建议:把config.toml里的 SubAgent 定义和settings.json里的mcpServers当成两个独立的真相来源,用脚本在启动时校验它们的 key 是否一致。这个校验脚本不到二十行,但能省掉大量"配置对不上"的排查时间。骨架的价值不在于一次跑通,而在于跑通之后能稳定复用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询