avatar


8.SDK

简介

Claude Code的本质是"大模型+工具调用循环+有限上下文窗口",前面各章的扩展手段,都没有离开Claude Code自己的进程。上一章《7.Headless》的Headless模式(非交互模式)把人移出了循环,但它仍然是从进程外整只调用CLI:字符串进、字符串出,控制粒度到命令行参数为止。要把工具调用循环(agent loop)嵌进自有应用,还缺三样东西:进程内的自定义工具、类型化的消息、代码级的权限决策。补齐这三样的,就是本章的主角Claude Agent SDK——工具调用循环的库形态。

先交代一句更名:Agent SDK原名Claude Code SDK,2025年9月更名,以反映它从"Claude Code的编程接口"演变为通用的Agent运行时。本文统一称Agent SDK,或简称SDK。

本文,我们从定位与选型讲起,依次讨论安装与两种调用模式、配置选项、权限的程序化控制、进程内自定义工具、Hooks(钩子)与消息流,以及会话管理。贯穿全章的示例场景是同一个:一个嵌在内部Web服务里的代码审查应用。

Agent SDK是什么

从控制粒度看,使用Claude Code有三层递进:

  • 交互CLI
    人守着终端,逐轮下指令、答权限提示,控制手段是配置与斜杠命令。
  • Headless
    claude -p把人移出循环,输入输出变成文本与JSON,控制手段是命令行参数。但每次调用都要启动一个完整的CLI进程,Claude Code对我们的应用而言是个黑盒。
  • Agent SDK
    循环本身以库的形态进入我们的进程:工具是我们的函数,权限决策是我们的回调,消息是类型化对象,会话是可编程的资源。

还有一个容易混淆的对象:Anthropic Client SDK,即直接调用Claude API的官方SDK。Client SDK只负责把请求发给模型、把回复取回来,工具调用循环要我们自己实现——定义工具、解析模型的调用请求、执行、把结果拼回对话、再次请求,循环往复。Agent SDK则把循环连同权限系统、会话管理一起接管,我们提供的只是工具和策略。一句话:Client SDK给我们模型,Agent SDK给我们循环。

Headless与SDK都能无人值守地运行,怎么选:

维度 Headless(claude -p Agent SDK
运行形态 进程外调用完整CLI 进程内库,嵌入自有应用
输入输出 字符串与JSON文本 类型化消息对象
自定义工具 需在进程外另起工具服务器 进程内函数即工具
权限决策 启动前用参数预声明 运行时由代码回调逐次决定
多轮会话 session_id串联多次进程调用 会话常驻进程内,可编程管理
部署依赖 只需CLI本身 需要Node 18+或Python 3.10+运行时
典型场景 CI/CD单步任务、shell脚本、一次性任务 Web服务、内部工具、需要细粒度控制的长期服务

判据可以压缩成一句话:字符串进出够用,就留在Headless;需要进程内控制,就上SDK。

安装与两种模式

两个语言版本的包:

语言 包名 运行时要求 安装命令
TypeScript @anthropic-ai/claude-agent-sdk Node 18+ npm install @anthropic-ai/claude-agent-sdk
Python claude-agent-sdk Python 3.10+ pip install claude-agent-sdk

TypeScript包附带可选的原生Claude Code二进制,不需要单独安装Claude Code。认证走ANTHROPIC_API_KEY环境变量。

SDK提供两种调用模式:

  • query()
    单轮查询。默认每次调用都开一个全新会话(可用resume选项恢复历史会话,见后文"会话管理"一节),返回异步生成器,Claude每生成一条消息就即时产出一条。适合一次性任务。
  • ClaudeSDKClient
    连续会话,Python独有的类。支持多轮追问,还提供set_permission_mode()set_model()等方法在会话中途改变配置。TypeScript没有对应的类,等价能力由query()的流式输入模式承担:以异步可迭代对象作为prompt持续送入消息,并在返回对象上调用setPermissionMode()setModel()。适合交互式应用。

命名约定:同一个配置项,TypeScript用驼峰(如allowedTools),Python用蛇形(如allowed_tools)。本文的表格会并列给出两种写法,只写一种的表示两侧相同。

先看query()的最小用法,让代码审查服务跑起来。Python版示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
options = ClaudeAgentOptions(
allowed_tools=["Read", "Grep", "Glob"],
max_turns=10,
)
async for message in query(prompt="审查src/auth目录,指出安全隐患", options=options):
print(message)

asyncio.run(main())

TypeScript版功能等价。示例代码:

1
2
3
4
5
6
7
8
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
prompt: "审查src/auth目录,指出安全隐患",
options: { allowedTools: ["Read", "Grep", "Glob"], maxTurns: 10 },
})) {
console.log(message);
}

循环里拿到的message不是字符串,而是类型化的消息对象,具体类型见后文"Hooks与消息流"一节。

配置选项

Headless模式下,模型、工具、权限这些控制项都是命令行参数;SDK把它们连同MCP(Model Context Protocol,模型上下文协议)服务器、Hooks回调、Sub-Agent(子智能体)定义一起,收进一个配置对象ClaudeAgentOptions。参数从此不再是启动前定死的静态值,而是可以按用户、按任务动态计算的代码。

字段(Python / TypeScript) 默认值 说明
model CLI默认模型 指定模型
system_prompt / systemPrompt 最小系统提示 传字符串自定义,或选用预设系统提示
allowed_tools / allowedTools 预批准的工具列表,逐项自动放行
disallowed_tools / disallowedTools 禁用工具,裸名与带说明符的模式语义不同,见下文
permission_mode / permissionMode default 权限模式,见下一节
max_turns / maxTurns 不限 最大执行轮数
cwd 进程当前目录 工作目录
thinking TypeScript为{ type: 'adaptive' },Python为None 推理行为,结构化对象:typeadaptive/enabled/disabledenabled时可另设token预算
mcp_servers / mcpServers MCP服务器:外部服务器配置,或进程内服务器对象
hooks 事件到回调函数的映射
can_use_tool / canUseTool 权限决策回调,见下一节
agents 用代码定义Sub-Agent
resume session_id恢复历史会话
setting_sources / settingSources 缺省时等同加载userprojectlocal三级 从文件系统加载哪些层级的配置
skills 自动发现 限定加载的Skill(技能)列表

其中disallowed_tools的两种写法要分清:裸工具名(如"Bash")把该工具从工具集中整个移除;带说明符的模式(如"Bash(rm *)")只阻止匹配的那类调用,工具本身仍在。

四个字段牵涉其他章展开过的机制,SDK只是换了形态:

  • setting_sources / settingSources
    控制从文件系统加载哪些层级的配置("user"/"project"/"local"),决定.claude/目录与CLAUDE.md是否进入会话,记忆的层级体系见《2.Memory》
  • agents
    不需要.claude/agents/目录,直接在代码里给出Sub-Agent定义,委派机制见《4.SubAgent》
  • hooks
    Hook从配置文件里的shell命令变成进程内回调函数,形态差异见本章后文,协议本身见《5.Hooks》
  • mcp_servers / mcpServers
    除了外部MCP服务器配置,还可以直接传进程内服务器对象。外部服务器的配置与认证见《6.MCP》,进程内形态是SDK独有的增量,见本章后文。

权限的程序化控制

交互模式下,权限提示弹给终端前的人;SDK嵌在Web服务里时,终端前没有人。权限决策必须整体交给代码。

SDK评估一次工具调用是否放行,按固定顺序过六道关卡,先到先决:

  1. Hooks——PreToolUse回调最先执行,它的deny是终局,后面的allow救不回来
  2. Deny规则
  3. Ask规则
  4. 权限模式(permission_mode/permissionMode
  5. Allow规则
  6. canUseTool回调——前面都没给出决策时的兜底

其中Deny/Ask/Allow规则就是Tool(specifier)语法的权限规则,语法与优先级见《1.概述》,SDK里通常经setting_sourcessettings.json加载,或由allowed_tools/disallowed_tools给出。

六种权限模式(default/acceptEdits/plan/auto/dontAsk/bypassPermissions)的定义见《1.概述》,SDK侧只有两处差异需要单独交代:auto模式仅TypeScript版支持;plan模式下文件编辑类操作会强制走canUseTool询问。

canUseTool是SDK特有的能力:一个运行时回调,收到工具名与工具输入,返回allow/deny/ask/defer四种决策之一——allow放行、deny拒绝、ask强制发起一次询问、defer不表态。在没有终端的场景里,"询问用户"这个动作也由它接管:我们可以把询问转成Web页面上的一次确认。

以代码审查服务为例,限制写操作只能落在服务自己的工作目录内。示例代码:

1
2
3
4
5
6
7
8
9
10
const options = {
permissionMode: "default",
allowedTools: ["Read", "Grep", "Glob"],
canUseTool: async (toolName, input) => {
if (toolName === "Write" && !String(input.file_path).startsWith("/srv/review/")) {
return { behavior: "deny", message: "只允许写入审查工作目录" };
}
return { behavior: "allow", updatedInput: input };
},
};

Python侧对应字段为can_use_tool,回调逻辑同理。

进程内自定义工具

循环的能力上限等于工具集上限。外部MCP服务器解决了标准化接入,但它是独立进程:要单独部署、走进程间通信(IPC),工具逻辑与应用逻辑之间隔着协议与进程边界。当工具要调用的就是应用自己的函数——查自家数据库、调内部服务——为一个函数起一个服务器进程并不划算。

SDK给出的答案是进程内自定义工具:用普通函数定义工具,打包成进程内MCP服务器,工具调用就是函数调用,没有子进程,也没有IPC开销。

给代码审查服务加一个查询内部工单系统的工具。TypeScript用tool()加zod(TypeScript生态的参数模式校验库)定义参数结构,再用createSdkMcpServer打包。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const getIssue = tool(
"get_issue",
"按编号查询内部工单系统中的issue详情",
{ issue_id: z.string() },
async ({ issue_id }) => {
const issue = await issueDb.find(issue_id); // 我们自己的业务代码
return { content: [{ type: "text", text: JSON.stringify(issue) }] };
}
);

const tracker = createSdkMcpServer("tracker", "1.0.0", [getIssue]);

for await (const message of query({
prompt: "审查ISSUE-42涉及的代码改动",
options: {
mcpServers: { tracker },
allowedTools: ["Read", "Grep", "Glob", "mcp__tracker__get_issue"],
},
})) {
console.log(message);
}

Python用@tool装饰器与create_sdk_mcp_server,功能等价。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions

@tool("get_issue", "按编号查询内部工单系统中的issue详情", {"issue_id": str})
async def get_issue(args):
issue = await issue_db.find(args["issue_id"]) # 我们自己的业务代码
return {"content": [{"type": "text", "text": str(issue)}]}

tracker = create_sdk_mcp_server("tracker", "1.0.0", [get_issue])

options = ClaudeAgentOptions(
mcp_servers={"tracker": tracker},
allowed_tools=["Read", "Grep", "Glob", "mcp__tracker__get_issue"],
)

三点说明:

  • 命名
    进程内工具同样遵循mcp__<server>__<tool>命名,把全名加进allowed_tools/allowedTools即预批准,权限规则里也按这个名字写。
  • 返回格式
    工具函数直接返回MCP工具结果格式(content数组),与外部服务器的返回一致,模型侧无感知。
  • 与外部服务器的分工
    外部MCP服务器适合跨团队复用、独立演进的能力;进程内工具适合与应用共生的业务逻辑。外部形态的配置、传输与认证见《6.MCP》

注意:工具的描述是写给模型看的使用说明,参数是模型生成的输入。不要信任模型传入的任何参数,工具内部必须自行校验。

Hooks与消息流

CLI里,Hook是settings.json中的一条配置加一个shell命令,数据经stdin/stdout以JSON交换;SDK里,Hook是注册在hooks选项上的进程内异步函数——事件数据从函数参数进,决策从返回值出,可以直接读写应用自己的状态,也可以打断点调试。返回值里的关键字段与CLI协议一致:PreToolUse可返回permissionDecisionupdatedInputPostToolUse可返回additionalContextupdatedToolOutputUserPromptSubmit可返回additionalContext等,完整协议见《5.Hooks》,这里只讲SDK侧的差异。

第一个差异是事件覆盖面,两个语言版本并不对等:

事件 Python TypeScript
PreToolUse 支持 支持
PostToolUse 支持 支持
PostToolUseFailure 支持 支持
UserPromptSubmit 支持 支持
Stop 支持 支持
SubagentStart / SubagentStop 支持 支持
PermissionRequest 支持 支持
Notification 支持 支持
PreCompact 支持 支持
PostToolBatch 不支持 支持
SessionStart / SessionEnd 不支持 支持
MessageDisplay 不支持 支持

第二个差异是matcher的写法要点:多个工具做精确列表用管道分隔(如Write|Edit);用正则时以^$包裹;单个工具名不含其他字符时按精确匹配处理;MCP工具(含进程内工具)写全名mcp__<server>__<tool>

消息流是SDK的另一半。上一章的stream-json是进程边界上的文本行协议,SDK把同样的信息升级为进程内的类型化消息对象,由query()的异步生成器逐条产出。消息是两层结构:外层区分"谁在说话",内层区分"说的是什么"。

外层的消息类型:

消息类型(TypeScript / Python) 含义
SDKAssistantMessage / AssistantMessage Claude的一轮输出,content字段是内容块列表
SDKUserMessage / UserMessage 用户侧消息,工具执行结果以这类消息回传
SDKSystemMessage / SystemMessage 系统事件,如会话初始化(内含session_id等信息)
SDKResultMessage / ResultMessage 最终结果,含耗时、开销等统计

文本与工具调用不是独立的消息类型,而是消息内部的内容块:TextBlock是文本;ToolUseBlock是工具调用请求,带工具名、输入与idToolResultBlock是工具执行结果,带contenttool_use_id。遍历时的常见模式是两层分派:先按消息类型,再按内容块类型——文本块累积成最终报告,ToolUseBlockToolResultBlock按id配对,用来展示执行进度、记审计日志。对代码审查服务来说,这意味着页面可以实时渲染"Claude正在读哪个文件、跑到第几步",而不是挂一个加载动画等最终字符串。

会话管理

每个会话都有唯一的session_id,消息流开头的系统初始化消息里就带着它,应用可以自行记录、与业务数据关联。

恢复与分叉:resume选项传入session_id,新调用就在原有上下文上继续,不必在prompt里重复交代背景;在恢复的同时设置fork_session(TypeScript为forkSession),则从原会话复制出一个独立分支,原会话不受影响。对代码审查服务,这直接对应产品功能:用户点开旧审查继续追问用resume,"基于同一份分析尝试两种修复思路"用fork。

经验

  • allowedTools不制约bypassPermissions
    两者同时设置时,所有工具仍会被自动批准。要硬性移除能力,用disallowed_tools/disallowedTools,并分清裸名与带说明符模式的语义差别。
  • 显式指定settingSources时记得带上"project"
    否则项目级.claude/settings.json不会被加载,在CLI里调好的权限规则与Hooks到了SDK里会"神秘失效"。
  • Hook的matcher只匹配工具名,不匹配文件路径
    想按路径过滤(比如只拦截对.env的写入),要在回调内部检查tool_input里的文件路径字段。
  • updatedInput必须伴随permissionDecision
    修改工具输入时,返回值要放在hookSpecificOutput内,且同时给出permissionDecisionallowask),否则修改不会生效。
  • Hook没触发时的排查顺序
    先查事件名大小写(是PreToolUse不是preToolUse),再查matcher是否精确匹配到工具名,最后确认回调注册在正确的事件下。
  • 权限评估顺序常被误解
    Hooks与Deny规则在前,canUseTool只是最后的兜底,不要指望Allow规则能越过前面的deny,完整的六道关卡见前文"权限的程序化控制"一节。
  • Sub-Agent继承主会话的权限模式
    权限模式会从主会话向Sub-Agent级联,且无法逐个覆盖,机制见《4.SubAgent》。给主会话开bypassPermissions之前,先想清楚它派出去的Sub-Agent也会带着同样的权限。
  • 上手路径
    query()allowed_tools起步;需要多轮交互再引入ClaudeSDKClient(TypeScript则启用流式输入);需要自定义权限时,用canUseTool回调或PreToolUse Hook。

回望本章,我们在代码里定义了工具、回调、Sub-Agent与Hook;加上前几章在.claude/目录里攒下的CLAUDE.md、Skills与MCP配置,这些定制资产已经散落在各个项目里,团队之间的共享还停留在拷贝文件。把它们打包成可分发、可版本化的单元,是《9.Plugins》要解决的问题。

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/128080
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板