首页 › tutorials › 用Python SDK编程造批量Agent流水线

用 Python SDK 编程造批量 Agent 流水线:Claude Agent SDK 自动化编排实战

概述

本教程教你用 Claude Agent SDK(Python 版) 把 Claude Code 从"命令行工具"升级为"可编程的自动化引擎"。你将掌握 query() 异步查询、ClaudeSDKClient 双向会话、进程内自定义工具、Hooks 拦截、并行子代理编排与 Session 复用,最终亲手造一条批量代码安全审计流水线。适合已用过 Claude Code CLI、希望用代码精确控制 Agent 行为、跑无人值守批处理任务的后端工程师与 DevOps 自动化开发者。

前置条件

核心认知:Claude Agent SDK 是把 Claude Code 的整个 Agent 循环(调用模型→判断是否用工具→执行工具→回喂结果)封装成了 Python 库。CLI 自带、无需单独安装。


步骤一:安装 SDK 与跑通第一个 query()

操作说明

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 后端时会出问题。


步骤二:用 ClaudeAgentOptions 精确配置 Agent 行为

操作说明

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,即使模型想改文件也无工具可用。

关键概念:白名单 vs 黑名单

这是新手最常踩的坑。allowed_tools 是权限放行清单,不是工具清单:

安全审计场景务必用 disallowed_tools 移除所有写工具,而不是依赖 allowed_tools——后者只是少弹确认,模型仍可能请求写操作。


步骤三:用 ClaudeSDKClient 跑双向交互式会话

操作说明

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,无需重复喂文件。

常见问题


步骤四:用进程内自定义工具扩展 Agent(零额外进程)

操作说明

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 强

维度 传统 MCP(stdio 子进程) SDK 进程内工具
进程模型 每个服务器独立子进程 与你的应用同进程
IPC 开销 每次调用走 stdin/stdout 直接 Python 函数调用
调试 跨进程难断点 同进程可单步
部署 多进程编排 单进程

混合使用:mcp_servers 字典里可同时放进程内 server(create_sdk_mcp_server 返回值)与外部 stdio server({"type":"stdio","command":"..."} 字典),SDK 会分别处理。


步骤五:用 Hooks 拦截危险操作(确定性护栏)

操作说明

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)或放弃。这就是"确定性护栏"——不依赖模型自觉,由代码硬性保证。

Hook 与 disallowed_tools 的区别


步骤六:用并行子代理编排批量任务(核心实战)

操作说明

这是把 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 个文件的审计同时进行(而非串行排队),总耗时接近"最慢的一个文件",而非"四个文件之和"。每个子代理的上下文窗口只装着自己那个文件的内容,互不污染,输出互不串扰。

并发控制:别让 N 无限大

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 实现可中断、可复用的长任务

操作说明

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

当你想"基于一个已经分析完毕的会话,同时尝试两种不同的修复方案"时: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

延伸阅读


训练营适用性评估

维度 评估
适合的训练营类型 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 脚本"升级成了"写工程化的并发程序"。

参考来源

  1. claude-agent-sdk-python (GitHub README) — 官方代码库,本文所有 API 示例(query/ClaudeAgentOptions/ClaudeSDKClient/@tool/HookMatcher/错误处理)的权威来源
  2. Subagents in the SDK — Claude Code Docs — 子代理隔离上下文与并行执行的官方说明,支撑步骤六并行编排
  3. Work with sessions — Claude Code Docs — session 历史、continue/resume/fork 机制,支撑步骤七
  4. Claude Agent SDK Python Guide 2026 — AI Workflow Lab — 生产级 Agent 构建(自定义工具、Hooks、子代理)的实战博客
  5. Claude Agent SDK (PyPI) — 包安装入口与版本/Python 3.10+ 依赖说明

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入