一、为啥要关心 Claude Agent SDK?#
1.1 它到底在解决什么问题?#
如果你已经写过几次 LLM 应用,大概率遇到过这些情况:
- 直接 HTTP 调 Claude API,每次都要自己拼 JSON,tools / messages / metadata 写多了非常痛苦;
- 想玩工具调用 / MCP / 多轮 Agent,对话状态、工具结果拼接、流式增量解析一多,业务代码迅速变成一坨状态机;
- 随着功能增多,你需要:
- 明确的错误类型(区分鉴权、限流、超时、工具执行异常…);
- 比较好的流式接口(不要自己手撸 event parser);
- 更清晰的测试、版本、发布流程。
claude-agent-sdk-python 想解决的,就是这些问题中的大头:
- 用一个清晰的
ClaudeSDKClient/query()抽象,把「请求参数 + 工具 + 对话历史」统一收口; - 用完善的类型(
types.py)建模消息、工具、响应、错误等核心概念; - 提供流式模式、工具调用、MCP 集成、Hook 系统等高级能力;
- 自带完整的测试与工程化体系,作为正式 SDK 发布,而不是「demo 脚本」。
1.2 核心设计:通过子进程与 Claude Code CLI 通信#
这是理解这套 SDK 最关键的一点:它并不是直接调用 Claude API,而是通过子进程启动 Claude Code CLI,然后用一套自定义的「控制协议(Control Protocol)」进行双向通信。
架构层次如下:
用户代码
↓
┌───────────────────────────────────────────────┐
│ 公共 API 层(claude_agent_sdk/) │
│ ├── query() → 简单的一次性查询 │
│ ├── ClaudeSDKClient → 双向交互式对话客户端 │
│ └── types.py → 所有类型定义 │
└───────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────┐
│ 内部实现层(_internal/) │
│ ├── InternalClient → 核心客户端实现 │
│ ├── Query → 控制协议 + 消息路由 │
│ └── message_parser → 消息解析器 │
└───────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────┐
│ 传输层(_internal/transport/) │
│ └── SubprocessCLITransport → 子进程通信 │
└───────────────────────────────────────────────┘
↓
Claude Code CLI(子进程)
↓
Claude APItext下面这张图更直观地展示了整个分层架构,以及各层之间的数据流向:

从图中可以看到:
- 左侧是你的业务代码,通过
query()或ClaudeSDKClient发起调用; - 中间是 SDK 的三层实现:Public API → Internal Implementation → Transport;
- 右侧是 Claude Code CLI 子进程,它再去调用真正的 Claude API。
所有的 Hook、Permission、MCP 等横切关注点都在 Internal Implementation 层处理,对上层使用者透明。
这意味着:
- SDK 的核心工作是管理与 CLI 子进程的双向通信;
- 所有高级功能(工具调用、Hook、MCP)都通过控制协议在 SDK 和 CLI 之间流转;
- 你不需要自己处理 Claude API 的 HTTP 请求、认证、重试等细节。
1.3 和 Claude Code 的关系与区别#
很多人第一次看到这个 SDK 名字时,会下意识以为它是「给 Claude Code 编辑器用的 SDK」。其实更准确的说法是:
- Claude Agent SDK 是一个通用的 Agent / 工具 / MCP SDK,用来在你自己的项目里构建智能体、工具调用、MCP 集成等能力;
- Claude Code 则是一个具体的产品形态(在编辑器里写/改代码),它内部当然也要调用 Claude 模型和一堆工具,但那是产品实现细节;
- 两者的关系:Claude Agent SDK 通过子进程启动 Claude Code CLI 来工作,所以它们共享同一套底层协议和能力。
可以简单这么区分:
| 场景 | 选择 |
|---|---|
| 想在 VS Code / Web IDE 里用 Claude 帮你写代码 | Claude Code 产品 |
| 想在自己的服务里做一个「用 Claude 作为大脑」的 Agent | Claude Agent SDK |
| 想让 Claude 调用你的业务工具、MCP 工具 | Claude Agent SDK |
1.4 本系列的节奏#
本系列会按这样的节奏来拆这套 SDK:
- 第 1 期:整体架构 + 快速上手(你正在看的这篇)
- 第 2 期:类型系统与公共 API 设计(
types.py、__init__.py、错误体系) - 第 3 期:Client & Query 的实现(
client.py/query.py/_internal/client.py) - 第 4 期:消息解析与流式输出(
_internal/message_parser.py) - 第 5 期:工具调用与 MCP 集成(MCP 示例 + 工具回调)
- 第 6 期:传输层与子进程 CLI(
transport/subprocess_cli.py) - 第 7 期:测试体系与版本管理(
tests/、e2e-tests/、CI / 发布)
二、项目结构鸟瞰:公共 API vs 内部实现#
本系列分析的是仓库里的 claude-agent-sdk-python 目录,大致结构如下:
claude-agent-sdk-python/
pyproject.toml
README.md
CHANGELOG.md
CLAUDE.md
src/claude_agent_sdk/
__init__.py # 对外暴露的主入口
client.py # ClaudeSDKClient 实现
query.py # query() 函数
types.py # 所有类型定义
_errors.py # 错误体系
_version.py # 版本信息
py.typed
_internal/
__init__.py
client.py # InternalClient 实现
query.py # Query 类(控制协议核心)
message_parser.py # 消息解析器
transport/
__init__.py
subprocess_cli.py # 子进程传输层
examples/
quick_start.py # 快速上手示例
agents.py # 多轮对话示例
streaming_mode.py # 流式输出示例
mcp_calculator.py # MCP 工具示例
...
tests/
test_client.py
test_types.py
test_message_parser.py
...
e2e-tests/
test_agents_and_settings.py
test_hooks.py
...text2.1 明确的「公共 API / 内部实现」分层#
-
顶层
src/claude_agent_sdk/下的几个模块,构成公共 API:__init__.py:决定import claude_agent_sdk能拿到什么;client.py:对外的ClaudeSDKClient类;query.py:封装一次请求的query()函数;types.py:消息、工具、响应、错误等核心类型;
-
_internal/则是内部实现细节:- 这里是底层 client、query、message parser、传输层;
- 对使用者来说是”不保证兼容”的区域;
- 源码分析系列会重点钻到这里。
2.2 示例与测试就是 “活的文档”#
examples/目录提供了各种使用场景的示例;tests/+e2e-tests/从测试可以看出设计意图和边界 case。
三、两种使用模式:query() vs ClaudeSDKClient#
SDK 提供了两种主要入口,分别面向不同使用场景:
3.1 query() 函数 —— 简单一次性查询#
async def query(
*,
prompt: str | AsyncIterable[dict[str, Any]],
options: ClaudeAgentOptions | None = None,
transport: Transport | None = None,
) -> AsyncIterator[Message]:python特点:
- 单向的、无状态的查询
- 适合「Fire-and-Forget」场景:一次性问答、批处理、脚本自动化
- 内部会自动创建
InternalClient,调用完就销毁
典型用法:
from claude_agent_sdk import query
async for message in query(prompt="What is 2+2?"):
print(message)python适用场景:
- 简单一次性问题(“2+2 等于几?”)
- 批量处理独立的 prompts
- 代码生成或分析任务
- 自动化脚本和 CI/CD 流水线
3.2 ClaudeSDKClient 类 —— 双向交互式对话#
class ClaudeSDKClient:
async def connect(self, prompt: str | AsyncIterable[dict] | None = None)
async def send_user_message(self, content: str | list, parent_tool_use_id: str | None = None)
async def messages() -> AsyncIterator[Message]
async def disconnect()python特点:
- 双向、有状态的对话客户端
- 支持中断、追问、动态发送消息
- 维护对话上下文
典型用法:
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
client = ClaudeSDKClient(options=ClaudeAgentOptions(...))
await client.connect("Hello!")
async for msg in client.messages():
print(msg)
if need_followup:
await client.send_user_message("Tell me more")
await client.disconnect()python适用场景:
- 构建聊天界面或对话式 UI
- 交互式调试或探索会话
- 需要根据响应发送后续消息
- 需要中断能力的场景
3.3 两种模式的对比#
| 特性 | query() | ClaudeSDKClient |
|---|---|---|
| 通信方向 | 单向 | 双向 |
| 状态管理 | 无状态 | 有状态 |
| 复杂度 | 简单 | 较复杂 |
| 中断支持 | ❌ | ✅ |
| 追问能力 | ❌ | ✅ |
| 适用场景 | 自动化、批处理 | 交互式应用 |
四、核心类型系统(types.py)#
src/claude_agent_sdk/types.py 是整个 SDK 的「词汇表」,定义了所有数据结构。
4.1 消息类型#
@dataclass
class UserMessage:
"""用户消息"""
content: str | list[ContentBlock]
parent_tool_use_id: str | None = None
@dataclass
class AssistantMessage:
"""助手消息,包含内容块"""
content: list[ContentBlock]
model: str
parent_tool_use_id: str | None = None
error: AssistantMessageError | None = None
@dataclass
class SystemMessage:
"""系统消息,包含元数据"""
subtype: str
data: dict[str, Any]
@dataclass
class ResultMessage:
"""结果消息,包含费用和使用信息"""
subtype: str
duration_ms: int
duration_api_ms: int
is_error: bool
num_turns: int
session_id: str
total_cost_usd: float | None = None
usage: dict[str, Any] | None = None
# 消息联合类型
Message = UserMessage | AssistantMessage | SystemMessage | ResultMessage | StreamEventpython4.2 内容块类型#
@dataclass
class TextBlock:
"""文本内容块"""
text: str
@dataclass
class ThinkingBlock:
"""思考内容块(Claude 的推理过程)"""
thinking: str
signature: str
@dataclass
class ToolUseBlock:
"""工具调用块"""
id: str
name: str
input: dict[str, Any]
@dataclass
class ToolResultBlock:
"""工具结果块"""
tool_use_id: str
content: str | list[dict] | None = None
is_error: bool | None = None
ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlockpython4.3 配置选项(ClaudeAgentOptions)#
这是 SDK 最重要的配置类,字段非常丰富:
@dataclass
class ClaudeAgentOptions:
# 工具相关
tools: list[str] | ToolsPreset | None = None
allowed_tools: list[str] = field(default_factory=list)
disallowed_tools: list[str] = field(default_factory=list)
# 提示词
system_prompt: str | SystemPromptPreset | None = None
# MCP 服务器
mcp_servers: dict[str, McpServerConfig] | str | Path = field(default_factory=dict)
# 权限与安全
permission_mode: PermissionMode | None = None
can_use_tool: CanUseTool | None = None
sandbox: SandboxSettings | None = None
# 模型配置
model: str | None = None
fallback_model: str | None = None
max_turns: int | None = None
max_budget_usd: float | None = None
# Hook 系统
hooks: dict[HookEvent, list[HookMatcher]] | None = None
# 高级选项
cwd: str | Path | None = None
cli_path: str | Path | None = None
include_partial_messages: bool = False
agents: dict[str, AgentDefinition] | None = None
output_format: dict[str, Any] | None = None # 结构化输出
...python源码里的 ClaudeAgentOptions 其实还要更丰富一些,例如:
continue_conversation/resume:用于继续之前的会话;betas:开启一些 Beta 能力(例如特定日期的 1M context);settings/add_dirs/env/extra_args:给 CLI 传递 settings 文件、附加目录和环境变量,以及任意额外的 CLI 参数;stderr:自定义 stderr 输出回调,用于打日志或 UI 展示;plugins:本地插件目录(SdkPluginConfig);max_thinking_tokens:控制思考 token 上限;
这些字段更多偏工程与运维向,适合在后续实战篇里结合具体配置文件来展开。
五、控制协议(Control Protocol)#
这是 SDK 最核心的设计之一:SDK 和 CLI 之间不是简单的「发请求 → 收响应」,而是一个双向的控制协议。
5.1 协议消息类型#
# SDK → CLI 的控制请求
class SDKControlRequest(TypedDict):
type: Literal["control_request"]
request_id: str
request: (
SDKControlInterruptRequest # 中断
| SDKControlPermissionRequest # 工具权限请求
| SDKControlInitializeRequest # 初始化
| SDKHookCallbackRequest # Hook 回调
| SDKControlMcpMessageRequest # MCP 消息
)
# CLI → SDK 的控制响应
class SDKControlResponse(TypedDict):
type: Literal["control_response"]
response: ControlResponse | ControlErrorResponsepython5.2 Query 类 —— 协议的核心实现#
_internal/query.py 中的 Query 类是整个控制协议的核心,主要职责:
- 消息路由:从 transport 读取消息,区分普通消息和控制消息
- 控制请求处理:
can_use_tool→ 调用用户提供的权限回调hook_callback→ 调用注册的 hook 函数mcp_message→ 路由到 SDK MCP 服务器
- 状态管理:维护 pending requests、hook callbacks 等状态
class Query:
async def initialize(self) -> dict | None:
"""初始化控制协议,注册 hooks"""
async def _read_messages(self):
"""从 transport 读取消息并路由"""
async def _handle_control_request(self, request):
"""处理 CLI 发来的控制请求"""python这里可以把 Query 想象成 SDK 的「事件循环 + 路由器」:一边从传输层不断读取 JSON 行,一边把控制请求分发给权限回调、Hook、MCP 服务器等组件。
下面这张时序图展示了控制协议的完整生命周期,包括初始化、消息流、权限检查、Hook 回调和 MCP 调用:

从图中可以看到几个关键阶段:
- 初始化阶段:
InternalClient启动 CLI 子进程,Query发送initialize控制请求完成 Hook / MCP 注册; - 消息流阶段:CLI 从 Claude API 拿到响应后,通过 JSON Lines 流式返回,
message_parser负责把原始 JSON 转成强类型 Message; - 权限检查:当 CLI 要调用工具时,会发
can_use_tool控制请求,SDK 调用你的回调函数做决策; - Hook 回调:
PreToolUse/PostToolUse等 Hook 也是通过控制协议触发,让你能在关键节点拦截或修改行为; - SDK MCP 调用:如果你注册了 SDK 内置的 MCP 服务器,CLI 会通过
mcp_message请求来调用它们。
这套双向协议是整个 SDK 最核心的设计,理解了它,后面看权限系统、Hook、MCP 就会非常顺畅。
5.3 工具权限系统:PermissionMode + can_use_tool#
源码里对「工具权限」做了比较完整的建模,核心包括:
- PermissionMode:整体权限模式(
default/acceptEdits/plan/bypassPermissions); - PermissionResult:权限回调的返回结果(允许 / 拒绝);
- PermissionUpdate:可以在回调里动态修改权限规则;
- can_use_tool 回调:由业务方提供的异步函数,真正做「要不要放行某个工具」的决策。
关键类型片段大致是:
PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]
@dataclass
class PermissionResultAllow:
behavior: Literal["allow"] = "allow"
updated_input: dict[str, Any] | None = None
updated_permissions: list[PermissionUpdate] | None = None
@dataclass
class PermissionResultDeny:
behavior: Literal["deny"] = "deny"
message: str = ""
interrupt: bool = False
PermissionResult = PermissionResultAllow | PermissionResultDeny
CanUseTool = Callable[[str, dict[str, Any], ToolPermissionContext], Awaitable[PermissionResult]]python当 CLI 需要调用某个工具时,会通过控制协议发来一条 can_use_tool 请求,Query._handle_control_request 的逻辑大概是:
permission_request: SDKControlPermissionRequest = request_data
context = ToolPermissionContext(
signal=None,
suggestions=permission_request.get("permission_suggestions", []) or [],
)
response = await self.can_use_tool(
permission_request["tool_name"],
permission_request["input"],
context,
)
if isinstance(response, PermissionResultAllow):
# behavior = allow,必要时可以修改输入 / 权限
elif isinstance(response, PermissionResultDeny):
# behavior = deny,可以附带 message、interrupt 标记python这样一来:
- 权限决策完全交给业务方,SDK 只负责把请求转发给
can_use_tool; PermissionUpdate.to_dict()会把 Python dataclass 转成 CLI 协议需要的结构,保持和 TypeScript SDK 一致;- 通过
permission_suggestions,CLI 还能给出「建议的规则更新」,由业务方决定是否采纳。
六、传输层与子进程 CLI:SubprocessCLITransport#
_internal/transport/subprocess_cli.py 负责与 Claude Code CLI 的进程通信,是 SDK 和 CLI 之间的「物理通道」。
6.1 CLI 启动与查找策略#
传输层的核心职责包括:
- 查找 CLI 可执行文件路径:
- 优先使用
ClaudeAgentOptions.cli_path显式指定的路径; - 否则尝试使用打包随 SDK 附带的 CLI(二进制文件);
- 再退回到
shutil.which("claude-code")之类的系统 PATH 查询;
- 优先使用
- 根据
ClaudeAgentOptions构造命令行参数:- 模型、system prompt、tools、MCP 服务器、权限模式等都会被翻译成
--xxx参数;
- 模型、system prompt、tools、MCP 服务器、权限模式等都会被翻译成
- 用 anyio 启动子进程:
- 打开 stdin/stdout/stderr 三个管道;
- 把当前工作目录(
cwd)和环境变量(如 API key)传给 CLI。
这一切对使用者来说都是透明的——你只需要提供 ClaudeAgentOptions,SDK 会负责把它翻译成真正的 CLI 命令行。
6.2 命令行长度与临时文件#
一个工程上非常实用的细节是:当 prompt 或配置太长导致命令行超长时,SDK 会自动写入临时文件再传给 CLI。伪代码大概是:
if total_length > _CMD_LENGTH_LIMIT:
# 把 prompt 写入临时文件
temp_file = tempfile.NamedTemporaryFile(...)
temp_file.write(prompt.encode())
cmd.extend(["--prompt-file", temp_file.name])
else:
cmd.extend(["--prompt", prompt])python这样可以避免「命令行参数太长被系统拒绝」这种在生产环境中经常踩坑的问题。
6.3 SDK 启动 CLI 时到底传了什么?#
从 _internal/transport/subprocess_cli.py 可以看到,SDK 启动子进程时其实做了两件事:
-
构建命令行参数
一开始会有一个固定前缀:
pythoncmd = [self._cli_path, "--output-format", "stream-json", "--verbose"]然后根据
ClaudeAgentOptions把配置翻译成 CLI 的--xxx参数,例如:--system-prompt(系统提示词)--tools/--allowedTools/--disallowedTools(工具白/黑名单)--model/--fallback-model--max-turns/--max-budget-usd--permission-mode(权限模式)--mcp-config(MCP 服务器 JSON 配置)--settings(合并了你传入的 settings + sandbox 配置)--agents/--add-dir/--plugin-dir等其他高级选项
最后,根据是否是流式输入决定:
- 流式模式:追加
--input-format stream-json,后面通过 stdin 持续写入 JSON; - 非流式模式:追加
--print -- <prompt>,直接把当前 prompt 放在命令行结尾。
-
构建环境变量并传给子进程
在
connect()里,SDK 会显式构造一份env传给anyio.open_process:
pythonprocess_env = { **os.environ, # 继承当前进程环境(包括 ANTHROPIC_API_KEY 等) **self._options.env, # 你通过 ClaudeAgentOptions.env 额外传入的变量 "CLAUDE_CODE_ENTRYPOINT": "sdk-py", "CLAUDE_AGENT_SDK_VERSION": __version__, }然后这样启动 CLI:
pythonself._process = await anyio.open_process( cmd, stdin=PIPE, stdout=PIPE, stderr=stderr_dest, cwd=self._cwd, env=process_env, user=self._options.user, )这就回答了「CLI 怎么知道环境变量(比如 API Key)」这个问题:
- 系统级的环境变量(例如
ANTHROPIC_API_KEY、HTTP_PROXY)——通过os.environ继承给子进程; - 你在 Python 里额外指定的变量(
ClaudeAgentOptions.env={...})——覆盖或补充在子进程上; - SDK 自己注入的标识(entrypoint、SDK 版本)——方便 CLI 做一些埋点或兼容处理。
换句话说,只要你在启动 Python 进程前把
ANTHROPIC_API_KEY设置好,或者通过ClaudeAgentOptions.env显式传进去,CLI 这边就能读取到,不需要你在代码里手动拼任何os.environ相关逻辑。 - 系统级的环境变量(例如
6.3 消息流:JSON Lines 协议#
SubprocessCLITransport.read_messages() 会从 CLI 的 stdout 中按行读取数据,每一行都是一条 JSON:
- 每一行代表一条事件(用户消息、助手消息、系统消息、控制响应等);
- 这一层只保证「字节流 → JSON dict」的转换,不关心具体语义;
- 语义层的解析交给上层的
message_parser.parse_message。
6.4 其他实现细节(补充)#
- CLI 查找顺序:优先
ClaudeAgentOptions.cli_path→ 打包的_bundled/claude→ 系统 PATH(含多个常见路径),找不到抛CLINotFoundError。 system_prompt:None时传空字符串;若为 preset 且带append,会用--append-system-prompt。- 额外 flags:
--permission-prompt-tool、--continue、--resume <id>、--include-partial-messages、--fork-session、--setting-sources(即便为空字符串也会传)。 - MCP 配置:
mcp_servers为 dict 时会去掉 SDK 内置服务器配置里的instance再序列化成{"mcpServers": ...}传给--mcp-config。 - 插件与扩展:仅支持
type="local"插件并用--plugin-dir;extra_args直接透传为--flag或--flag value。 - 输出格式:
output_format.type == "json_schema"时追加--json-schema <schema_json>。 - 命令行过长优化:只在包含
--agents且长度超限时,把 agents JSON 写临时文件并改为--agents @/tmp/xxx.json。 - 版本检查:默认执行
claude -v,低于2.0.0仅警告,可通过CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK跳过。 - stderr 处理:仅当有
options.stderr回调或extra_args含debug-to-stderr才会 pipe;未提供回调但启用 debug 时写入debug_stderr。
七、消息解析:message_parser.py#
message_parser.parse_message 的职责是把 CLI 输出的原始 JSON 行,转换为上面介绍过的 Message + ContentBlock 体系:
def parse_message(data: dict[str, Any]) -> Message:
message_type = data.get("type")
match message_type:
case "user":
return UserMessage(...)
case "assistant":
return AssistantMessage(...)
case "system":
return SystemMessage(...)
case "result":
return ResultMessage(...)
case "stream_event":
return StreamEvent(...)python几个重点:
- 使用
match-case进行类型分派,语义非常直接; - 对于
assistant消息,会继续解析内部的 content blocks(text / thinking / tool_use / tool_result); - 一旦遇到未知的结构,会抛出
MessageParseError,上层可以统一处理。
这层属于「粘合层」,但它让 ClaudeSDKClient / query() 不需要关心底层 JSON 细节,只要处理干净的 Python 类型即可。
八、MCP 工具集成#
SDK 提供了两种 MCP 服务器接入方式:
6.1 外部 MCP 服务器#
options = ClaudeAgentOptions(
mcp_servers={
"my_server": {
"type": "stdio",
"command": "python",
"args": ["my_mcp_server.py"],
}
}
)python6.2 SDK 内置 MCP 服务器(亮点!)#
SDK 允许你在 Python 进程内定义 MCP 工具,无需启动独立进程:
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool("add", "Add two numbers", {"a": float, "b": float})
async def add(args):
return {"content": [{"type": "text", "text": f"Result: {args['a'] + args['b']}"}]}
@tool("multiply", "Multiply two numbers", {"a": float, "b": float})
async def multiply(args):
return {"content": [{"type": "text", "text": f"Result: {args['a'] * args['b']}"}]}
# 创建服务器
calculator = create_sdk_mcp_server("calculator", tools=[add, multiply])
# 使用
options = ClaudeAgentOptions(
mcp_servers={"calc": calculator},
allowed_tools=["add", "multiply"],
)python优势:
- 更好的性能(无 IPC 开销)
- 更简单的部署(单进程)
- 更容易调试(同一进程)
- 可以直接访问应用状态
从 Query._handle_sdk_mcp_request 的实现还能看到一层细节:SDK 现在是手动路由 JSONRPC 消息到 MCP Server 的:
async def _handle_sdk_mcp_request(self, server_name: str, message: dict[str, Any]) -> dict[str, Any]:
method = message.get("method")
if method == "initialize":
# 返回初始化结果(只声明 tools 能力)
elif method == "tools/list":
# 调用 server.request_handlers[ListToolsRequest]
elif method == "tools/call":
# 调用 server.request_handlers[CallToolRequest]
elif method == "notifications/initialized":
# 简单返回成功
else:
# 其他方法暂不支持python原因是当前 Python MCP SDK 还没有像 TypeScript 那样的通用 Transport 抽象,只能按方法名手工分发。等 MCP SDK 补上这层,Query 这里也可以相应收敛成更通用的实现。
九、Hook 系统#
SDK 支持在关键节点注册 Hook,拦截或修改行为:
7.1 支持的 Hook 事件#
HookEvent = Literal[
"PreToolUse", # 工具调用前
"PostToolUse", # 工具调用后
"UserPromptSubmit", # 用户提交 prompt 前
"Stop", # 停止时
"SubagentStop", # 子 Agent 停止时
"PreCompact", # 压缩上下文前
]python7.2 Hook 配置#
@dataclass
class HookMatcher:
matcher: str | None = None # 匹配规则,如 "Bash" 或 "Write|Edit"
hooks: list[HookCallback] = field(default_factory=list)
timeout: float | None = Nonepython7.3 使用场景#
- PreToolUse:拦截危险工具调用、修改工具输入
- PostToolUse:记录工具执行日志、处理工具结果
- UserPromptSubmit:过滤敏感 prompt、添加上下文
结合源码可以看到,Hook 的输入/输出类型其实非常细致:
class PreToolUseHookInput(BaseHookInput):
hook_event_name: Literal["PreToolUse"]
tool_name: str
tool_input: dict[str, Any]
class SyncHookJSONOutput(TypedDict):
continue_: NotRequired[bool] # 是否继续(注意是 continue_,避免关键字)
suppressOutput: NotRequired[bool] # 隐藏输出
stopReason: NotRequired[str] # 停止原因
decision: NotRequired[Literal["block"]]
hookSpecificOutput: NotRequired[HookSpecificOutput]
class PreToolUseHookSpecificOutput(TypedDict):
hookEventName: Literal["PreToolUse"]
permissionDecision: NotRequired[Literal["allow", "deny", "ask"]]
permissionDecisionReason: NotRequired[str]
updatedInput: NotRequired[dict[str, Any]] # 可以直接修改工具输入python注意:
- Python 里用的是
continue_/async_,Query会在发回 CLI 前通过_convert_hook_output_for_cli把它们转换成continue/async; PreToolUse不光能「拦」,还可以通过updatedInput改写调用参数,通过permissionDecision给出细粒度决策;- 这一层结合前面的
can_use_tool,构成了一个「静态规则 + 动态 Hook + 人工决策」的权限闭环。
十、Sandbox 沙箱与安全性#
在权限和 Hook 之上,SDK 还提供了一层 bash 沙箱配置,用来隔离文件系统和网络:
class SandboxSettings(TypedDict, total=False):
enabled: bool
autoAllowBashIfSandboxed: bool
excludedCommands: list[str]
allowUnsandboxedCommands: bool
network: SandboxNetworkConfig
ignoreViolations: SandboxIgnoreViolations
enableWeakerNestedSandbox: boolpython其中:
enabled:是否启用沙箱;excludedCommands:哪些命令应当在沙箱外运行(如git/docker);allowUnsandboxedCommands:是否允许通过「dangerouslyDisableSandbox」直接跳过沙箱;network:可以放行哪些 Unix Socket、本地绑定、代理端口等;ignoreViolations:某些路径/主机的违规可以忽略;
结合前面的权限规则(Read/Edit/WebFetch),可以做出一套比较严谨的「文件 & 网络」防护策略。源码里也专门强调:真正的读写/网络限制主要来自权限规则,沙箱设置更多是执行环境层面的补充。
十、错误体系#
class ClaudeSDKError(Exception): pass
class CLINotFoundError(ClaudeSDKError): pass # CLI 找不到
class CLIConnectionError(ClaudeSDKError): pass # 连接失败
class CLIJSONDecodeError(ClaudeSDKError): pass # JSON 解析失败
class ProcessError(ClaudeSDKError): pass # 进程错误
class MessageParseError(ClaudeSDKError): pass # 消息解析失败python十一、快速上手示例#
9.1 安装#
pip install claude-agent-sdkbash或从源码安装:
cd claude-agent-sdk-python
python -m venv .venv
source .venv/bin/activate
pip install -e .bash9.2 最简单的查询#
import asyncio
from claude_agent_sdk import query
async def main():
async for message in query(prompt="用一句话解释什么是 Claude Agent SDK"):
print(message)
asyncio.run(main())python11.3 带配置的查询#
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(
system_prompt="你是一名专业的 Python 开发者",
model="claude-3-5-sonnet-latest",
max_turns=5,
)
async for message in query(
prompt="写一个快速排序的 Python 实现",
options=options
):
print(message)
asyncio.run(main())python11.4 实际输出长什么样?#
运行最简单的查询后,你会看到类似这样的消息流(这里只是结构示意):
# 1. 系统消息:会话开始
SystemMessage(subtype="init", data={"session_id": "..."})
# 2. 用户消息
UserMessage(content="用一句话解释什么是 Claude Agent SDK")
# 3. 助手消息(可能包含多个内容块)
AssistantMessage(
content=[
TextBlock(text="Claude Agent SDK 是一个 Python 客户端库,..."),
],
model="claude-3-5-sonnet-latest",
)
# 4. 结果消息:包含费用和统计
ResultMessage(
subtype="result",
duration_ms=1234,
is_error=False,
total_cost_usd=0.003,
usage={...},
)python在流式场景下(prompt 是 AsyncIterable),InternalClient 会走一条稍微复杂一点的路径:
- 使用
SubprocessCLITransport启动 CLI,并把--input-format设为stream-json; - 创建
Query,调用query.start()在后台持续读取消息; - 通过
query.initialize()完成 Hook / MCP 的初始化握手; - 用
query.stream_input(prompt)把用户输入流源源不断写入 CLI; - 当有 SDK MCP 服务器或 Hooks 时,会在关闭 stdin 前等待第一个 result,确保双向控制协议有机会完成;
- 上层的
async for message in query.receive_messages()则以「消息 dict →parse_message→ 强类型 Message」的形式对外暴露。
十二、关键设计亮点总结#
- 分层清晰:公共 API / 内部实现 / 传输层 三层分离,
_internal前缀明确标识内部模块 - 双向控制协议:不是简单的 RPC,而是双向的消息流 + 控制请求/响应机制
- SDK MCP 服务器:支持在 Python 进程内定义工具,零 IPC 开销
- Hook 系统:可在关键节点拦截和修改行为
- 类型安全:大量使用
dataclass、TypedDict、Literal,IDE 补全友好 - anyio 异步:底层使用 anyio,支持 asyncio/trio 两种运行时
12.1 为什么用 anyio?#
SDK 底层使用 anyio ↗ 而不是直接用 asyncio,主要有几个原因:
- 运行时无关:同一套代码可以在
asyncio或trio上运行; - 更好的取消语义:anyio 的
CancelScope比原生 asyncio 的取消更易于组合和控制; - 统一的进程与 IO API:
anyio.open_process等封装了跨平台的子进程管理;
对大多数使用者来说,你只需要像示例那样用 asyncio.run() 跑即可,内部的运行时细节都由 SDK 处理。
十三、总结与下期预告#
这一篇我们做了这些事:
- 从架构角度说明了:SDK 是如何通过子进程与 Claude Code CLI 通信的
- 从 API 角度介绍了:两种使用模式(
query()vsClaudeSDKClient) - 从类型角度梳理了:核心类型系统(Message、ContentBlock、ClaudeAgentOptions)
- 从协议角度解释了:控制协议的设计和 Query 类的作用
- 从实践角度展示了:MCP 工具集成、Hook 系统、错误体系
如果你现在已经把示例在本地跑通,非常建议顺手打开:
src/claude_agent_sdk/types.pysrc/claude_agent_sdk/_internal/query.pytests/test_types.py粗略扫一眼字段和测试用例,下一篇你会读得更快。
在下一期中,主要围绕控制协议和内部 Client 展开,重点包括:
- 深入
_internal/query.py,看控制协议的完整生命周期; - 详细拆解
Query._read_messages和_handle_control_request的实现; - 理解 SDK 如何处理工具权限请求、Hook 回调、MCP 消息路由;
- 结合
src/claude_agent_sdk/client.py看ClaudeSDKClient如何把这些能力对外封成一个易用的 API。