跳到主要内容

深入 — 消息解析、跨后端任务、边界与横向对比

本章给要读源码/排障的人。三块:消息解析怎么保证前向兼容、任务管理为什么要自造轮子、这个库的边界在哪。

1. 消息解析:parse_message

1.1 职责

把 CLI 的原始 JSON dict 变成带类型的消息对象(AssistantMessageResultMessage 等),给你干净的 Python 类型(message_parser.py:35)。

1.2 精妙处:未知类型不崩,直接跳过

最外层 match message_type 的兜底分支(message_parser.py:358-362)对不认识的消息类型只 log debug 后返回 None,不抛错。理由写在注释:"Forward-compatible: 新版 CLI 不该让老版 SDK 崩"。InternalClient 收到 None 就跳过(_internal/client.py:224-226)。这让 CLI 能先加新消息类型,SDK 慢慢跟。

1.3 内层却很严格

认识的类型,缺必填字段会抛 MessageParseError(如 message_parser.py:128-131)。松于未知、严于已知——这是刻意的取舍。

1.4 内容块的分派

assistant 消息的 content 逐块 match block["type"],分派成 TextBlock/ThinkingBlock/ToolUseBlock/ToolResultBlock/ServerToolUseBlock/ServerToolResultBlock(message_parser.py:150-190)。其中 server 工具块(web_searchweb_fetch 等,types.py:953 ServerToolName)是 API 服务端替模型执行的,你无需返回结果。

2. 跨 async 后端的任务管理:_task_compat

2.1 它要解决的小问题

Query 要管一堆后台任务(读循环、stream_input、控制请求 handler),而且要能从任何任务上下文取消它们——包括 async 生成器的 finalizer(Python 可能在另一个任务里跑它)。

2.2 为什么不能用 anyio TaskGroup

注释讲得很清楚(_task_compat.py:1-16):anyio 的 TaskGroup cancel scope 有任务亲和性——从不同任务退出它,要么抛 RuntimeError: Attempted to exit cancel scope in a different task,要么在 asyncio 后端 busy-spin。所以不能用。

2.3 解法:spawn_detached

spawn_detached(_task_compat.py:135)用 sniffio 探测当前后端,分派到对应原语,返回统一的 TaskHandle:

后端底层说明
asyncioloop.create_task()直接,自动继承 contextvars
triotrio.lowlevel.spawn_system_task包一层自己的 CancelScope,并显式传 context= 继承 contextvars(_task_compat.py:165-168
其它关掉协程后抛 RuntimeError

这是个典型的"库要同时支持 asyncio 和 trio"的抽象层。trio 那边还额外处理了"系统任务不能抛异常否则崩 trio"——把异常存到 handle 上,.wait() 时再抛(_task_compat.py:106-132)。

3. 错误类型

一棵浅继承树(_errors.py),都继承 ClaudeSDKError:

异常何时抛
CLIConnectionError连不上 CLI
CLINotFoundError找不到 claude 二进制(CLIConnectionError 的子类)
ProcessError子进程非零退出,带 exit_code / stderr
CLIJSONDecodeErrorJSON 解不动或超缓冲上限
MessageParseError认识的消息类型缺必填字段

4. 边界与局限(诚实)

  • 它不是 agent 本体。 真正的 agent 循环、工具执行、模型调用全在 claude CLI 里。这个 SDK 只是驱动器。想懂"agent 怎么决策",得去看 CLI(不在本仓库)。
  • 依赖捆绑 CLI 版本。 最低要求 2.0.0(subprocess_cli.py:31 MINIMUM_CLAUDE_CODE_VERSION),低于此只 warn 不拦。SDK 和 CLI 的协议要对齐,版本错配可能出怪问题。
  • 进程内 MCP 只支持部分方法。 _handle_sdk_mcp_request 手动路由,只实现了 initialize/tools/list/tools/call/notifications/initialized(query.py:585-714);resources、prompts 等还没有,代码里两处 TODO 明说等上游 MCP SDK 加 Transport 抽象再补。
  • ClaudeSDKClient 不能跨 async 上下文复用。 见 01 章第 4 节(client.py:58-64)。
  • 镜像是"至多一次"。 SessionStore.append 失败重试有限、超时不重试,可能丢批次(报 MirrorErrorMessage)。本地转录才是权威副本。
  • 插件只支持本地。 plugins 目前只认 type="local",其它抛 ValueErrorsubprocess_cli.py:356-361)。

5. 横向对比(ai-agent-reference 货架)

这个 SDK 在"agent 框架"里的取舍很独特:

维度claude-agent-sdk 的取舍
agent 循环放哪外包给独立 CLI 子进程,SDK 只做驱动+协议。多数框架(如 LangGraph、Letta)把循环实现在库内。
工具协议押注 MCP;进程内工具也套 MCP 的 JSONRPC 语义
并发模型同时兼容 asyncio 和 trio(自造 spawn_detached),多数库只绑 asyncio
状态持久化本地 JSONL + 可插拔 SessionStore 镜像,而非直接绑某数据库

"把重活外包给子进程"这个决定是理解整个代码库的钥匙:它解释了为什么核心是一条控制协议、为什么配置要翻译成命令行 flag、为什么进程内工具要靠反向请求。

6. 代码地图

主题文件路径符号名
消息解析src/claude_agent_sdk/_internal/message_parser.pyparse_message
内容块类型src/claude_agent_sdk/types.pyTextBlock ToolUseBlock ServerToolUseBlock ContentBlock
结果消息同上ResultMessage DeferredToolUse
跨后端任务src/claude_agent_sdk/_internal/_task_compat.pyspawn_detached TaskHandle
错误类型src/claude_agent_sdk/_errors.pyClaudeSDKError CLINotFoundError ProcessError CLIJSONDecodeError
版本下限src/claude_agent_sdk/_internal/transport/subprocess_cli.pyMINIMUM_CLAUDE_CODE_VERSION