教程Claude Code系列第 1 / 8 篇

Claude Agent SDK 入门:用 Python 写第一个会自己读代码、改代码的智能体

Agent SDK 把 Claude Code 的工具、循环和上下文管理做成了一个库,可以放进你自己的 Python 或 TypeScript 程序。本站从安装开始实测:二十行代码跑通第一个智能体,看清一次运行里收到哪些消息、结果有哪些字段、出错怎么判断。要留意:allowed_tools 不是白名单。

发布 · 约 12 分钟读完 · 经人工审核()· 本站的内容怎么来的

本站已实测,测试日期:2026-10-12。

本页目录(12 节)
  1. 先分清它和另外几样东西
  2. 安装
  3. 凭据
  4. 第一个智能体
  5. 一次运行里发生了什么
  6. 结果消息
  7. 出错的时候
  8. 常用的几个选项
  9. TypeScript 版本
  10. 这一趟花了多少
  11. 这次实测没有覆盖到的
  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 背后是一个循环。官方文档的描述:

  1. Claude 收到你的任务,连同系统提示词、工具的定义和对话历史。
  2. 它判断怎么做:回一段文字,或者要求调用一个或几个工具,或者两样都有。
  3. SDK 执行这些工具,把结果喂回给它。
  4. 第 2、3 步反复进行,每一个来回叫一轮,直到它给出一个不带工具调用的回答。
  5. 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 定义一个函数,让它能查你的数据库、调你的接口。

来源

通过 API 中转站使用这些模型?到 Veridrop 免费检测中转站给你的是不是它声称的模型,或者看看中转站排行榜前 100 名。

相关文章