Claude Agent SDK 入门:用 Python 写第一个会自己读代码、改代码的智能体
Agent SDK 把 Claude Code 的工具、循环和上下文管理做成了一个库,可以放进你自己的 Python 或 TypeScript 程序。本站从安装开始实测:二十行代码跑通第一个智能体,看清一次运行里收到哪些消息、结果有哪些字段、出错怎么判断。要留意:allowed_tools 不是白名单。
本站已实测,测试日期:2026-10-12。
这篇教程解决一个问题:Claude Code 在终端里能读代码、改文件、跑命令,怎么把这套能力放进你自己写的程序里。
答案是 Claude Agent SDK。它不是又一个调模型接口的库:模型接口只负责“你发一段话,它回一段话”,要让模型能动手,工具怎么定义、调用结果怎么喂回去、上下文满了怎么办,都得你自己写。Agent SDK 把 Claude Code 里已经做好的这一整套直接给你:内置的读写文件、搜索、跑命令这些工具,自动循环到任务完成,以及权限、会话、hooks。你写的程序只要给它一句任务,然后读它一路上发回来的消息。
本站用 Python 从安装开始走了一遍。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-12 |
| 版本 | claude-agent-sdk 0.2.165,它自带的 Claude Code 是 2.1.294 |
| 系统 | Ubuntu 24.04,Python 3.12.3 |
| 模型 | Opus 5.5(没有另外指定时的默认值) |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
先分清它和另外几样东西
官方文档的对照,按“你想怎么做”来选:
| 你想 | 用什么 | 你得到的 |
|---|---|---|
| 把 Claude Code 的智能体嵌进自己的 Python 或 TypeScript 程序,在自己管的进程里跑 | Agent SDK | 一个库。它运行 Claude Code 的可执行文件,带着内置工具、权限、会话、hooks |
| 在终端里做日常开发,或者跑一次性的任务 | Claude Code 命令行 | 终端界面 |
| 从自己的代码里直接调模型接口 | Client SDK | 直接访问接口,工具循环自己写 |
| 让 Anthropic 替你托管智能体 | Managed Agents | 托管的运行环境 |
用别的语言(Go、Java 等)想驱动同一套循环,文档的建议是把命令行当子进程来调,也就是 claude -p 加 --output-format json。那种用法见把 Claude Code 接进脚本和 CI。SDK 和它的关系可以这样理解:claude -p 是把结果打印出来让你去解析,SDK 是把同样的东西变成你程序里的对象和函数。
安装
要求 Python 3.10 以上,或者 Node.js 18 以上。Python 这边:
mkdir my-agent && cd my-agent
python3 -m venv .venv
source .venv/bin/activate
pip install claude-agent-sdk
用 uv 的话是 uv init 加 uv add claude-agent-sdk。
装完看一眼版本:
python -c "import claude_agent_sdk; print(claude_agent_sdk.__version__)"
0.2.165
两点实际情况:
- SDK 自带一份 Claude Code。 不用另外安装命令行。我们的虚拟环境里它在
claude_agent_sdk/_bundled/claude,版本是 2.1.294,和机器上另外装的那份命令行(2.1.296)不是同一份。整个虚拟环境约 300MB,大头就是它。 - Ubuntu 和 Debian 上
python3 -m venv可能报错,提示缺ensurepip。装上系统包python3-venv就好。没有管理员权限的办法见安全检查插件那篇里“缺 Python 的依赖”一节。
凭据
官方文档的做法是在运行程序的那个 shell 里设环境变量:
export ANTHROPIC_API_KEY=你的key
文档强调 SDK 不会自动读 .env 文件,key 放在 .env 里的话要自己先加载。
通过中转站用的,设接口地址和凭据两个变量,变量名和命令行完全一样(见Claude Code 接入中转站)。也可以写在代码里,文档给的例子:
options = ClaudeAgentOptions(
env={"ANTHROPIC_BASE_URL": "https://gateway.example.com"},
)
Python 的 env 是在继承的环境变量上叠加;TypeScript 的 env 是整个替换,要自己把 process.env 展开放进去。
我们实测时什么变量都没设,程序也跑通了。原因是这台机器上的 ~/.claude/settings.json 里已经写着接口地址和凭据,SDK 自带的 Claude Code 默认会读用户设置。在开发机上这很省事,但不要依赖它:部署到别的机器上没有这个文件。后面有一个实验就是把设置来源关掉,结果是直接提示没有登录。
还有一条政策上的限制,文档写了两遍:除非事先得到批准,Anthropic 不允许第三方开发者在自己的产品里提供 claude.ai 账号登录。用 SDK 做的产品要用 API key 这类认证方式。
第一个智能体
先放一个有 bug 的文件 utils.py,这是官方快速上手里的例子:
def calculate_average(numbers):
total = 0
for num in numbers:
total += num
return total / len(numbers)
def get_user_name(user):
return user["name"].upper()
空列表会除以零,user 是 None 时会报 TypeError。
然后是智能体本身,agent.py:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage
async def main():
async for message in query(
prompt="检查 utils.py 里会导致崩溃的 bug,发现了就修掉。",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Glob"],
permission_mode="acceptEdits",
),
):
if isinstance(message, AssistantMessage):
for block in message.content:
if hasattr(block, "text"):
print(block.text)
elif hasattr(block, "name"):
print(f"[调用工具] {block.name}")
elif isinstance(message, ResultMessage):
print(f"[结束] {message.subtype}")
asyncio.run(main())
三样东西:
query是入口。它返回一个异步迭代器,Claude 每做一步就吐出一条消息,所以用async for去读。prompt是任务。用哪些工具由它自己判断。options是配置。这里预先批准了三个工具,并把权限模式设成自动接受文件修改。
运行:
python agent.py
[调用工具] Read
[调用工具] Grep
No other code calls these functions, so I can change what they return without breaking anything.
[调用工具] Write
[调用工具] Bash
I found and fixed three ways `utils.py` could crash. I haven't run anything to confirm the fixes,
because the test command needed approval and didn't run.
……
[结束] success
30 秒跑完。文件确实改了:
+ if not numbers:
+ return 0
- return user["name"].upper()
+ if not user:
+ return ""
+ name = user.get("name")
+ if not name:
+ return ""
+ return str(name).upper()
它还主动指出了一个要我们拿主意的地方:遇到非法输入时返回 0 和空字符串能避免崩溃,但会把坏数据掩盖过去,要不要改成抛出异常。
allowed_tools 不是白名单
注意它用了哪些工具:Read、Grep、Write、Bash。我们在 allowed_tools 里只写了 Read、Edit、Glob。
这是最容易误解的一处。allowed_tools 的意思是“这几个工具不用问,直接放行”,不是“只能用这几个”。官方文档的原话:没列出的工具仍然可用,对它们的调用如果需要批准,就交给权限模式去决定。
这一次的经过正好说明了这一点:
- Grep 是只读的,本来就不需要批准。
- Write 是写文件,
acceptEdits模式自动接受。 - Bash 它想跑一条命令验证修复,这需要批准,没人批,所以没跑成。它在回复里如实说了。
真要让它碰不到某个工具,用 disallowed_tools。我们试了禁掉 Bash 再让它运行 date:
options=ClaudeAgentOptions(disallowed_tools=["Bash"])
不行。我这里没有 Bash 工具,所以运行不了 `date` 命令。
权限这一块的完整规则,本系列后面会单独写一篇。
一次运行里发生了什么
query 背后是一个循环。官方文档的描述:
- Claude 收到你的任务,连同系统提示词、工具的定义和对话历史。
- 它判断怎么做:回一段文字,或者要求调用一个或几个工具,或者两样都有。
- SDK 执行这些工具,把结果喂回给它。
- 第 2、3 步反复进行,每一个来回叫一轮,直到它给出一个不带工具调用的回答。
- SDK 发回最后的文字,以及一条带着用量、花费和会话 ID 的结果消息。
这个循环不需要你的代码参与,你只是在旁边看消息。我们把每条消息的类型打印出来,任务是“这个目录里有哪些 .py 文件,各做什么”:
SystemMessage/init {"model": "claude-opus-5-5", "permissionMode": "auto", "cwd": "...", "claude_code_version": "2.1.294", "tools": 23}
AssistantMessage 工具调用: Bash {"command": "find . -name \"*.py\" ..."}
UserMessage ['ToolResultBlock']
AssistantMessage ThinkingBlock
AssistantMessage 工具调用: Bash {"command": "sed -n 40,200p inspect_sdk.py", ...}
UserMessage ['ToolResultBlock']
AssistantMessage 文本: '这个目录里有 3 个 `.py` 文件:……'
ResultMessage: subtype=success is_error=False num_turns=3 duration_ms=24490 total_cost_usd=0.1868
对照着看:
| 消息类型 | 什么时候出现 | 里面有什么 |
|---|---|---|
SystemMessage,子类型 init |
最开始 | 这次会话的元数据:模型、权限模式、工作目录、可用的工具 |
AssistantMessage |
Claude 每产出一块内容 | 一块文字、一次工具调用,或者一段思考 |
UserMessage |
每次工具执行完 | 喂回给 Claude 的工具结果 |
ResultMessage |
循环结束 | 最后的文字、用量、花费、会话 ID |
打开“部分消息”选项后还有第五种 StreamEvent,用来逐字输出,这篇不展开。
你的程序处理哪些,取决于要做什么:只要最后的结果,就只看 ResultMessage;要显示进度,看 AssistantMessage。
init 那一行里有一个值得注意的值:permissionMode 是 auto。我们没有设权限模式,它默认用的就是 auto 模式,由分类器来审每个操作。所以上面这次运行里它直接用了 Bash,没有人拦。写无人值守的程序时,权限模式要明确设,不要用默认值。
结果消息
循环结束时的 ResultMessage 是程序最该认真读的一条。上面那次的完整内容:
subtype=success is_error=False num_turns=3 duration_ms=24490
total_cost_usd=0.18679 stop_reason=end_turn
session_id=44ae9046-239d-48ef-b25e-0c9f7ca2a000
usage: input=3228 cache_write=28000 cache_read=49722 output=699
| 字段 | 含义 |
|---|---|
subtype |
怎么结束的,见下表 |
is_error |
这次是不是以错误结束 |
result |
最后的文字。只在成功时有 |
num_turns |
用了几轮 |
total_cost_usd |
花费的估算,按官方价格算的,不是账单 |
usage |
主循环的 token 用量 |
model_usage |
按模型分的用量,包含子代理 |
session_id |
之后要接着这次对话时用 |
stop_reason |
模型最后一次为什么停下,正常是 end_turn |
subtype 的几种取值,来自官方文档:
subtype |
发生了什么 |
|---|---|
success |
正常完成 |
error_max_turns |
到了轮数上限 |
error_max_budget_usd |
到了花费上限 |
error_during_execution |
循环被一个错误打断 |
文档提醒 Python 里 total_cost_usd、usage、model_usage 的类型是可空的,读之前要判断是不是 None。
出错的时候
两种情况我们都触发了。
到了上限。 把轮数限制成 1,给一个一轮做不完的任务:
options=ClaudeAgentOptions(allowed_tools=["Read", "Glob", "Grep"], max_turns=1)
ResultMessage: subtype=error_max_turns is_error=True num_turns=2 total_cost_usd=0.1373
[抛出异常] ResultError: Claude Code returned an error result: Reached maximum number of turns (1) (exit code: 1)
先收到一条结果消息,然后 query 抛出异常。文档说这是有意的。所以循环外面要包一层:
try:
async for message in query(prompt=..., options=...):
if isinstance(message, ResultMessage):
handle(message) # 这里已经能拿到 subtype、花费和会话 ID
except Exception as error:
log(error)
即使以错误结束,结果消息里的花费和会话 ID 仍然有,可以记账,也可以之后接着跑。
没有凭据。 把设置来源关掉,SDK 就读不到那份带凭据的用户设置了:
options=ClaudeAgentOptions(setting_sources=[])
AssistantMessage 文本: 'Not logged in · Please run /login'
ResultMessage: subtype=success is_error=True num_turns=1 total_cost_usd=0
[抛出异常] ResultError: Claude Code returned an error result: Not logged in · Please run /login (exit code: 1)
这里有一个容易漏掉的细节:subtype 是 success,is_error 却是 True。判断成败要看 is_error,不能只看 subtype 是不是 success。
常用的几个选项
都是 ClaudeAgentOptions 的字段。标了“实测”的是这次跑过的。
| 选项 | 作用 | 备注 |
|---|---|---|
allowed_tools |
这些工具不用问直接放行 | 实测。不是白名单 |
disallowed_tools |
这些工具一律不给 | 实测 |
permission_mode |
权限模式 | 实测。不设时这里默认是 auto |
model |
用哪个模型,别名或完整的名字 | 实测,见下 |
effort |
思考的力度,low 到 max |
实测 |
system_prompt |
系统提示词 | 实测,见下 |
cwd |
工作目录。不设就是程序的当前目录 | 没有实测 |
max_turns |
轮数上限 | 实测 |
max_budget_usd |
花费上限,按估算 | 没有实测 |
setting_sources |
加载哪几处的设置和 CLAUDE.md。传 [] 是全不加载 |
实测 |
env |
给底下的 Claude Code 进程设环境变量 | 没有实测 |
换模型能省多少。 几次一轮就答完的运行:
| 配置 | 问的是什么 | 估算花费 |
|---|---|---|
| 默认(Opus 5.5) | 两个一句话就能答的小问题 | 0.131 和 0.132 美元 |
model="claude-sonnet-5-5", effort="low" |
只回复两个字 | 0.059 美元 |
问题不完全一样,但这种一轮的运行,花费几乎全在固定开销上:每次请求都带着两万多 token 的系统提示词和工具说明,和命令行的情况相同。简单的活指定便宜的模型,差一半以上。
系统提示词。 把项目的约定写进去:
options=ClaudeAgentOptions(
system_prompt="你是这个项目的代码助手。项目约定:所有函数遇到非法输入都抛出 ValueError,不返回默认值。",
)
问它函数返回值有什么要求,并且不许读文件:
关于返回值,我只知道一条相关约定:遇到非法输入时,函数不能返回默认值(比如 None、空值、0),
必须抛出 ValueError。除此之外……我没有拿到任何约定,所以不知道。
前面第一个智能体自作主张返回了 0 和空字符串,就是因为没人告诉它项目的约定。除了系统提示词,SDK 默认也会读项目里的 CLAUDE.md,写法见CLAUDE.md 怎么写。
文档还说明了一处限制:模型接口的 temperature、top_p、max_tokens 在选项里没有对应的字段。要控制的话用 effort 或者花费上限,或者直接去调模型接口。
TypeScript 版本
本站只实测了 Python。TypeScript 的写法是平行的,按官方文档:
npm init -y
npm pkg set type=module
npm install @anthropic-ai/claude-agent-sdk
npm install --save-dev tsx
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Review utils.py for bugs that would cause crashes. Fix any issues you find.",
options: { allowedTools: ["Read", "Edit", "Glob"], permissionMode: "acceptEdits" }
})) {
if (message.type === "result") console.log(`Done: ${message.subtype}`);
}
三处不同:选项名是驼峰写法(allowedTools、maxTurns);消息靠 type 字符串来区分,不用 isinstance;助手消息的内容在 message.message.content 里,多套了一层。
这一趟花了多少
一次修 bug 的运行加七组小实验,合计约 1.1 美元,是 SDK 返回的 total_cost_usd 相加的估算,不是中转站实际扣的金额。
这次实测没有覆盖到的
- TypeScript 没有实测,代码照官方文档给出。
- 直接设环境变量、用
env选项传凭据没有实测。 我们用的是用户设置文件里已有的配置。 max_budget_usd、cwd、fallback_model没有试。- 逐字输出、多轮对话、自定义工具、hooks、子代理都没有涉及,留给本系列后面的几篇。
- 只在 Linux 上测过,每个实验只跑了一次。它第一次运行时用英文回答、后面几次用中文,我们没有去控制这一点。
- 模型的默认值、默认的权限模式是这个版本上看到的,以后可能变。
下一步
这个智能体只会用内置的工具。系列的下一篇讲怎么给它加你自己的工具:用几行 Python 定义一个函数,让它能查你的数据库、调你的接口。