本教程教你用 Claude Agent SDK(Python 版) 把 Claude Code 从"命令行工具"升级为"可编程的自动化引擎"。你将掌握 query() 异步查询、ClaudeSDKClient 双向会话、进程内自定义工具、Hooks 拦截、并行子代理编排与 Session 复用,最终亲手造一条批量代码安全审计流水线。适合已用过 Claude Code CLI、希望用代码精确控制 Agent 行为、跑无人值守批处理任务的后端工程师与 DevOps 自动化开发者。
pip 包管理器ANTHROPIC_API_KEY,或已通过 claude 登录)async/await 异步编程有基本了解(SDK 全异步)核心认知:Claude Agent SDK 是把 Claude Code 的整个 Agent 循环(调用模型→判断是否用工具→执行工具→回喂结果)封装成了 Python 库。CLI 自带、无需单独安装。
Claude Agent SDK 的 Python 包名为 claude-agent-sdk,安装时自动捆绑 Claude Code CLI,开箱即用:
pip install claude-agent-sdk
安装完成后,创建 hello.py,写入官方 Quick Start 的最小示例:
import anyio
from claude_agent_sdk import query
async def main():
# query() 返回一个异步消息迭代器,逐条 yield Agent 产生的消息
async for message in query(prompt="What is 2 + 2?"):
print(message)
anyio.run(main)
运行:
python hello.py
终端会逐条打印 Agent 循环过程中产生的消息对象,包括 SystemMessage(初始化)、AssistantMessage(模型回复)、ResultMessage(含 token 用量与耗时统计的最终结果)。你会看到 4 这个答案被包含在某个 AssistantMessage 的 TextBlock 里。
| 问题 | 原因 | 解决 |
|---|---|---|
CLINotFoundError |
找不到捆绑 CLI 或系统 CLI | 重装包;或手动 curl -fsSL https://claude.ai/install.sh \| bash 后用 ClaudeAgentOptions(cli_path=...) 指定 |
Weekly/Monthly Limit Exhausted |
账户额度耗尽 | 切换 API Key 或等待额度重置 |
| 长时间无输出 | 首次启动 CLI 子进程较慢 | 等待 3-5 秒;后续调用会复用 |
为什么用
anyio.run而不是asyncio.run? SDK 内部基于anyio抽象层,用anyio.run能保证事件循环兼容性。如果你坚持用asyncio.run(main())也能跑,但遇到Trio后端时会出问题。
query() 的第二参数 options 是一切配置的入口。它取代了旧版的 ClaudeCodeOptions(升级时注意改名)。核心字段如下:
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock
options = ClaudeAgentOptions(
system_prompt="你是资深 Python 安全审计员,只读分析、不修改文件", # 系统提示词
max_turns=6, # Agent 循环最多迭代几轮,防止失控
cwd="/home/me/myproject", # Agent 的工作目录(影响 Read/Bash 的相对路径)
allowed_tools=["Read", "Bash"], # 白名单:列出的工具自动放行,不再弹确认
disallowed_tools=["Write", "Edit", "NotebookEdit"], # 黑名单:彻底移除写操作能力
permission_mode="acceptEdits", # 兜底权限模式
)
async for message in query(prompt="列出项目里所有硬编码的密钥", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text)
Agent 进入指定 cwd,仅用 Read 和 Bash(如 grep)扫描代码,不会修改任何文件,扫描完毕输出一份密钥清单。因为 Write/Edit 被加入 disallowed_tools,即使模型想改文件也无工具可用。
这是新手最常踩的坑。allowed_tools 是权限放行清单,不是工具清单:
allowed_tools=["Bash"]:Bash 自动放行(不弹确认);其它工具并未消失,只是会走 permission_mode / can_use_tool 决策。disallowed_tools=["Write"]:真正从工具集中移除 Write,模型根本看不到它。安全审计场景务必用
disallowed_tools移除所有写工具,而不是依赖allowed_tools——后者只是少弹确认,模型仍可能请求写操作。
query() 是"一问一答"的快捷函数。若要多轮追问、随时插入新指令,需要用 ClaudeSDKClient:
import anyio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
async def main():
options = ClaudeAgentOptions(
allowed_tools=["Read", "Bash"],
cwd=".",
)
# async with 自动管理 CLI 子进程的生命周期
async with ClaudeSDKClient(options=options) as client:
await client.query("读取 main.py 并总结它的职责")
async for msg in client.receive_response():
print("第一轮:", msg)
# 同一会话内继续追问,Agent 记得上文
await client.query("那它有哪些潜在的安全风险?")
async for msg in client.receive_response():
print("第二轮:", msg)
anyio.run(main)
两轮对话共享同一上下文:第二轮的"那它有哪些风险"能正确指代第一轮读到的 main.py,无需重复喂文件。
async with:直接 client = ClaudeSDKClient(...) 后不进入上下文管理器,会导致 CLI 子进程泄漏、连接未初始化报 CLIConnectionError。务必用 async with。receive_response() 提前结束:它只在当前一轮响应完成后结束迭代;若想持续监听,需在每次 client.query() 后再次调用。ClaudeSDKClient 相比 query() 的最大增值能力是自定义工具与 Hooks——两者都用普通 Python 函数定义。
自定义工具通过 @tool 装饰器 + create_sdk_mcp_server 实现,本质是一个进程内 MCP 服务器(无需像传统 MCP 那样起独立子进程):
import anyio
from claude_agent_sdk import (
tool, create_sdk_mcp_server,
ClaudeAgentOptions, ClaudeSDKClient,
)
# 定义一个工具:@tool(工具名, 描述, 参数类型签名)
@tool("count_secrets", "统计文件中疑似密钥的行数", {"file_path": str})
async def count_secrets(args):
path = args["file_path"]
try:
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
hits = sum(1 for ln in lines if any(k in ln.upper() for k in ("API_KEY", "SECRET", "TOKEN", "PASSWORD")))
text = f"{path} 中发现 {hits} 行疑似密钥"
except FileNotFoundError:
text = f"文件不存在: {path}"
# 返回 MCP 标准内容块
return {"content": [{"type": "text", "text": text}]}
# 把函数打包成进程内 MCP 服务器
server = create_sdk_mcp_server(name="audit-tools", version="1.0.0", tools=[count_secrets])
options = ClaudeAgentOptions(
mcp_servers={"t": server},
allowed_tools=["mcp__t__count_secrets"], # 命名规则:mcp__<服务器名>__<工具名>
)
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("帮我统计 ./config.py 里有多少疑似密钥,调用 count_secrets 工具")
async for msg in client.receive_response():
print(msg)
anyio.run(main)
Claude 会自主决定调用 count_secrets 工具,传入 "./config.py",工具在同一 Python 进程内执行,返回命中行数,模型再据此组织最终回答。
| 维度 | 传统 MCP(stdio 子进程) | SDK 进程内工具 |
|---|---|---|
| 进程模型 | 每个服务器独立子进程 | 与你的应用同进程 |
| IPC 开销 | 每次调用走 stdin/stdout | 直接 Python 函数调用 |
| 调试 | 跨进程难断点 | 同进程可单步 |
| 部署 | 多进程编排 | 单进程 |
混合使用:
mcp_servers字典里可同时放进程内 server(create_sdk_mcp_server返回值)与外部 stdio server({"type":"stdio","command":"..."}字典),SDK 会分别处理。
Hooks 与自定义工具不同:Hook 由 Claude Code 应用层(不是模型)在 Agent 循环的特定节点调用,用于注入确定性逻辑——比如无论模型怎么想,运行 rm -rf 前先拦截。
最经典的是 PreToolUse Hook,在工具执行前拦截:
import anyio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher
async def guard_bash(input_data, tool_use_id, context):
"""PreToolUse 钩子:在 Bash 执行前检查命令"""
if input_data["tool_name"] != "Bash":
return {} # 非 Bash 工具,放行
command = input_data["tool_input"].get("command", "")
blocked = ["rm -rf", "DROP TABLE", "sudo ", "> /dev/sda", "mkfs"]
for pattern in blocked:
if pattern in command:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny", # 拒绝执行
"permissionDecisionReason": f"命中危险模式: {pattern}",
}
}
return {} # 放行
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Read"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[guard_bash]),
],
},
)
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("用 rm -rf 删除 ./tmp 目录")
async for msg in client.receive_response():
print(msg)
anyio.run(main)
模型虽然请求执行 rm -rf,但 Hook 在执行前返回 deny,Bash 工具被拒绝,模型收到拒绝原因后会改用更安全的方式(如 rmdir 或 shutil)或放弃。这就是"确定性护栏"——不依赖模型自觉,由代码硬性保证。
disallowed_tools=["Bash"]:彻底不让用 Bash(连只读命令 ls 也用不了)。PreToolUse Hook:仍允许用 Bash,但能按命令内容动态放行/拒绝(放行 ls、拒绝 rm -rf)。粒度更细,是生产环境首选。这是把 Claude Code 真正变成"批处理引擎"的关键。每个 query() 调用是独立会话、独立 Agent 实例,因此可以用 asyncio.gather 同时跑 N 个任务,互不干扰。
子代理(Subagent)的本质就是:一个拥有独立上下文窗口、独立工具权限的 Agent 实例,专注于一个子任务。在 SDK 里,你不需要"定义"它——每次 query() 调用就是一个临时子代理。
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock
def make_options():
"""每个子代理用相同的安全配置"""
return ClaudeAgentOptions(
system_prompt="你是代码安全审计员。只读分析,输出 JSON:{\"file\":..,\"risks\":[..],\"severity\":\"high|mid|low\"}",
allowed_tools=["Read", "Bash"],
disallowed_tools=["Write", "Edit"],
max_turns=4,
cwd=".",
)
async def audit_one(file_path: str) -> dict:
"""一个子代理审一个文件,返回结构化结果"""
options = make_options()
collected = []
prompt = f"审计文件 {file_path},找出硬编码密钥、SQL 注入、命令注入风险,按约定 JSON 输出。"
async for message in query(prompt=prompt, options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
collected.append(block.text)
return {"file": file_path, "report": "\n".join(collected)}
async def batch_audit(files: list[str]) -> list[dict]:
# 并发扇出:所有文件同时审计
return await asyncio.gather(*[audit_one(f) for f in files])
if __name__ == "__main__":
targets = ["src/auth.py", "src/db.py", "src/utils.py", "src/api.py"]
results = asyncio.run(batch_audit(targets))
for r in results:
print(f"=== {r['file']} ===")
print(r["report"])
4 个文件的审计同时进行(而非串行排队),总耗时接近"最慢的一个文件",而非"四个文件之和"。每个子代理的上下文窗口只装着自己那个文件的内容,互不污染,输出互不串扰。
asyncio.gather 会同时启动所有任务,若文件有 500 个,会瞬间起 500 个 CLI 子进程,耗尽内存与 API 速率配额。务必加信号量限流:
import asyncio
sem = asyncio.Semaphore(5) # 最多 5 个子代理并发
async def audit_one(file_path: str) -> dict:
async with sem: # 拿到许可才跑
options = make_options()
collected = []
async for message in query(prompt=f"审计 {file_path}", options=options):
# ... 同上
pass
return {"file": file_path, "report": "\n".join(collected)}
经验值:并发数建议设为
min(CPU核心数-2, API速率限制/2)。Anthropic API 有 RPM/TPM 限制,并发过高会触发429。
| 问题 | 解决 |
|---|---|
偶发 429 Rate Limit |
降低 Semaphore 值,或对 query() 包一层指数退避重试 |
| 某个子代理卡死不返回 | 设小 max_turns(如 4-6),防止模型无限探索 |
| 结果里混入工具调用日志 | 只收 AssistantMessage 的 TextBlock,过滤掉 ToolUseBlock |
Session 是 SDK 在 Agent 工作期间累积的完整对话历史——你的 prompt、每一次工具调用、每一次工具结果、每一条回复。它让自动化任务具备三个能力:
这在批处理里价值巨大:一个跑了一半的大型重构任务,可以 fork 出一个"试验性改法"的分支,主线不受影响;崩了也能用 session id 续跑。
ClaudeSDKClient 在会话层面提供 session 管理能力。典型用法是在 ClaudeAgentOptions 中维持/传入 session 标识,使跨进程、跨时间的会话得以衔接:
import anyio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
async def long_task():
options = ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash"],
cwd=".",
)
async with ClaudeSDKClient(options=options) as client:
# 阶段一:分析
await client.query("分析整个 src 目录的测试覆盖率缺口")
async for _ in client.receive_response():
pass
# 阶段二:在阶段一的上下文基础上继续
await client.query("针对最大的三个缺口,各写一个失败的单元测试")
async for _ in client.receive_response():
pass
# 关键:这条 session 的历史在 client 存活期内始终累积
当你想"基于一个已经分析完毕的会话,同时尝试两种不同的修复方案"时:fork 主 session 得到两条独立分支,各自演进,互不干扰,最后比较结果择优。这正是单线程 CLI 难以做到、而 SDK 编程模型天然擅长的场景。
具体的 session id 获取与 fork 调用细节,以官方 Work with sessions 文档为准——不同 SDK 小版本字段可能调整。
把上面七步串成一条真实可跑的流水线:扫描整个项目 → 并行审计 → 危险操作拦截 → 汇总报告。
# audit_pipeline.py
import asyncio
import json
from pathlib import Path
from claude_agent_sdk import (
query, ClaudeAgentOptions, ClaudeSDKClient, HookMatcher,
AssistantMessage, TextBlock,
)
# ---- 1. 危险命令护栏 ----
async def guard_bash(input_data, tool_use_id, context):
if input_data["tool_name"] != "Bash":
return {}
cmd = input_data["tool_input"].get("command", "")
for bad in ["rm -rf", "DROP TABLE", "sudo ", ":(){:|:&};:"]:
if bad in cmd:
return {"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"危险命令: {bad}",
}}
return {}
# ---- 2. 单文件审计子代理 ----
def audit_options():
return ClaudeAgentOptions(
system_prompt=(
"你是安全审计员。审一个文件,输出严格 JSON:"
'{"file":str,"risks":[{"line":int,"type":str,"desc":str}],'
'"severity":"high|mid|low|none"}。只读,禁止修改文件。'
),
allowed_tools=["Read", "Bash"],
disallowed_tools=["Write", "Edit", "NotebookEdit"],
max_turns=5,
cwd=".",
hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[guard_bash])]},
)
sem = asyncio.Semaphore(4)
async def audit_one(file_path):
async with sem:
opts = audit_options()
text_parts = []
async for msg in query(
prompt=f"审计 {file_path} 的安全风险并输出 JSON。",
options=opts,
):
if isinstance(msg, AssistantMessage):
for b in msg.content:
if isinstance(b, TextBlock):
text_parts.append(b.text)
raw = "\n".join(text_parts)
# 尝试从回复里抠出 JSON(容错)
try:
start, end = raw.index("{"), raw.rindex("}") + 1
return json.loads(raw[start:end])
except (ValueError, json.JSONDecodeError):
return {"file": file_path, "risks": [], "severity": "none",
"raw": raw[:200]}
# ---- 3. 编排器 ----
async def run_pipeline(root="."):
files = [str(p) for p in Path(root).rglob("*.py") if "venv" not in str(p)]
print(f"发现 {len(files)} 个 Python 文件,开始并行审计(并发上限 4)...")
results = await asyncio.gather(*[audit_one(f) for f in files])
high = [r for r in results if r.get("severity") == "high"]
print(json.dumps({"total": len(files), "high_risk": len(high),
"details": high}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
asyncio.run(run_pipeline("."))
运行:
python audit_pipeline.py
这条流水线综合运用了:白名单+黑名单权限隔离、进程内 Hooks 护栏、信号量限流的并行子代理、结构化 JSON 输出——这正是 SDK 编程模型相比 CLI 脚本的核心优势:每一条策略都是普通 Python 代码,可测试、可版本化、可纳入 CI。
flowchart TD
A[Python 应用] -->|asyncio.run| B[编排器 batch_audit]
B -->|asyncio.gather 并发扇出| C1[子代理1: query]
B -->|asyncio.gather 并发扇出| C2[子代理2: query]
B -->|asyncio.gather 并发扇出| C3[子代理N: query]
B -.->|Semaphore=4 限流| C1
C1 --> D1[ClaudeAgentOptions]
D1 --> D1a[allowed_tools 白名单]
D1 --> D1b[disallowed_tools 黑名单]
D1 --> D1c[hooks PreToolUse]
C1 -->|Agent 循环| E1[模型决策]
E1 -->|调用工具前| F1[Hook guard_bash 拦截]
F1 -->|deny 危险命令| G1[拒绝执行]
F1 -->|allow 安全命令| H1[执行 Read/Bash]
H1 --> E1
E1 -->|完成| I1[结构化 JSON 结果]
C2 --> I2[结果]
C3 --> I3[结果]
I1 --> J[汇总报告]
I2 --> J
I3 --> J
J --> K[CI / PR 评论 / 告警]
style F1 fill:#fde68a,stroke:#b45309
style G1 fill:#fecaca,stroke:#b91c1c
style B fill:#bfdbfe,stroke:#1d4ed8
platform.claude.com/docs/en/agent-sdk/python —— 最权威的 API 参考code.claude.com/docs/en/agent-sdk/subagents —— 子代理隔离上下文与并行执行详解code.claude.com/docs/en/agent-sdk/sessions —— continue / resume / fork 机制platform.claude.com/docs/en/agent-sdk/hooks —— Hook 全节点参考github.com/anthropics/claude-agent-sdk-python 的 examples/ 目录(mcp_calculator.py、hooks.py、streaming_mode.py)可直接运行学习| 维度 | 评估 |
|---|---|
| 适合的训练营类型 | Python 后端进阶营、DevOps/SRE 自动化营、AI 工程化实战营、企业级 LLM 应用开发营 |
| 难度 | 中高级(需异步编程基础 + 对 Agent 循环的理解) |
| 建议教学时长 | 4-6 课时(含实操):1 课时安装与 query、1 课时配置与权限、1 课时自定义工具+Hooks、2 课时并行编排实战、1 课时 Session 与答疑 |
| 前置课程 | 建议先完成"读懂智能体循环,跑通首个项目"(CLI 基础)与本系列 MCP 服务器开发章节 |
| 可交付产出 | 学员独立交付一条可接入 CI 的代码审计/文档生成/测试批量生成流水线 |
| 延伸挑战 | 引入 ccusage 做 token 成本监控、把 fork 用于 A/B 重构方案对比、用 ClaudeSDKClient 造交互式运维排障助手 |
从 query() 一行代码起步,到 asyncio.gather 并行扇出、Hooks 硬性护栏、Session 可复用编排——Claude Agent SDK 把 Agent 循环彻底变成了可编程的库。当你能用 Python 精确控制"跑几个、怎么限流、拦什么、记什么",批处理自动化就从"写一堆 shell 脚本"升级成了"写工程化的并发程序"。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
