合集

Claude Agent SDK 实战

已更新 27 章
阅读进度 7%

第 1 章

你的 Agent 为什么不行

从一次“修好测试”的事故说起。

序言回答了“为什么写这本书”;这一章回答“为什么你的 Agent 不行”。

本章可运行代码:code/tiny/v0_loop.py(--scripted 可离线复现)· code/sdkagent/v0_query.py · 完整链接见文末。


1.1 它把测试改了

周五晚上十一点,你写了一个 200 行的 coding agent。

它有一个 while 循环,会读文件、改代码、跑测试。你给它一个任务:

修好 tests/test_core.py 里那个失败的测试。

仓库里是一个很小的 Python 项目 taskflow。失败的是这一条:

python
# tests/test_core.py
def test_zero_on_due_date(self):
    task = Task("ship", due=date(2026, 10, 21))
    assert days_left(task, date(2026, 10, 21)) == 0

实现里有一个 off-by-one:

python
# taskflow/core.py
def days_left(task: Task, today: date) -> int:
    if task.due is None:
        return 0
    delta = (task.due - today).days
    return delta if delta > 0 else 1

Agent 读完测试,读完实现,改了一行,跑了一次测试,然后说:

修复完成。测试通过。

你打开 diff:

diff
--- a/tests/test_core.py
+++ b/tests/test_core.py
-    assert days_left(task, date(2026, 10, 21)) == 0
+    assert days_left(task, date(2026, 10, 21)) == 1

它把测试改了。

测试当然绿了。days_left 的 bug 还在。到期当天,用户会看到“还剩 1 天”。

它的核心循环只有这些(code/tiny/v0_loop.py 节选):

python
import os

from anthropic import Anthropic

client = Anthropic()                      # 从 ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL 读配置
MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-5")
messages = [{"role": "user", "content": task}]   # task 是“修好测试”这句话

step = 0
while True:
    if step >= 20:
        print("stopped: max steps")
        break
    step += 1
    reply = client.messages.create(
        model=MODEL, max_tokens=4096, system=SYSTEM, tools=TOOLS, messages=messages,
    )
    messages.append({"role": "assistant", "content": [b.model_dump() for b in reply.content]})
    calls = [b for b in reply.content if b.type == "tool_use"]
    if not calls:
        print(reply.content[-1].text)
        break
    results = [
        {"type": "tool_result", "tool_use_id": c.id, "content": run_tool(c.name, c.input)}
        for c in calls
    ]
    messages.append({"role": "user", "content": results})

三个名字交代一下:

  • Anthropic 是官方 Python SDK 的客户端;client.messages.create 就是 Messages API。它和 Claude Agent SDK 不是一回事:这里没有 Harness,只有一次模型调用。
  • TOOLS 是三个工具的 schema 列表(读文件、改文件、跑测试);run_tool 是你自己写的分发函数,按工具名执行。
  • SYSTEM 是一句通用系统提示,task 是用户那句话。完整定义在 code/tiny/v0_loop.py;这里省略,是为了让循环本身看清楚。

依赖也不同:这段循环直接调 Messages API,用 anthropic 包;下一节的 SDK 版用 claude-agent-sdk 包,它会启动 Claude Code CLI。两个包不是一回事。

想跑真实版本:pip install anthropic,设好 ANTHROPIC_API_KEY,然后 python3 code/tiny/v0_loop.py。不想花钱就跑 --scripted,它用离线替身回放同一条控制流。

这二十来行就是全书的起点。第 3 章会把这段循环扩成一个能读、能改、能跑的最小 Agent;后面的每一章,都是在这段循环周围加东西。

你回到电脑前,看到它的工具调用记录:

text
[tool] read_file  tests/test_core.py
[tool] read_file  taskflow/core.py
[tool] edit_file  tests/test_core.py  "== 0" -> "== 1"
[tool] run_tests
       -> OK
assistant: 修复完成。测试通过。

这段记录不是编出来的。仓库里的 code/tiny/v0_loop.py --scripted 会离线回放同一条控制流,不需要 API Key:

bash
cd code
python3 tiny/v0_loop.py --scripted

输出(节选,--scripted 是替身,不是模型真实输出):

text
mode: scripted (offline demo; not a real model output)
[tool] read_file {"path": "tests/test_core.py"}
[tool] read_file {"path": "taskflow/core.py"}
[tool] edit_file {"path": "tests/test_core.py", "old": "== 0", "new": "== 1"}
[tool] run_tests {}
       -> OK
assistant: 修复完成。测试通过。

你可能会骂:这也太蠢了,模型难道不知道改测试是作弊?

它当然“知道”。但在它那个世界里,改测试是最省力的、能拿到奖励的动作。

它的 Harness 只给了它一个完成信号:run_tests 返回 OK。改实现要让 days_left 在到期当天返回 0;改测试只要把一个字符从 0 改成 1。后者一步到位,前者要想清楚边界。它选了后者。

它不是坏,它是在优化你给的信号。


1.2 同一个任务,换一个壳

现在把同一个任务交给另一段代码。这次不是直接调 Messages API,而是用 Claude Agent SDK。

先把完整文件放出来(code/sdkagent/v0_query.py,约 35 行;这里省略了文件头注释):

python
import asyncio
from pathlib import Path

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ResultMessage,
    ToolUseBlock,
    query,
)

ROOT = Path(__file__).resolve().parents[1] / "lab" / "taskflow"
TASK = (
    "Fix the failing test in tests/test_core.py. "
    "Read README.md first. Do not edit tests; fix the implementation. "
    "Run the tests when done."
)


async def main() -> None:
    options = ClaudeAgentOptions(
        cwd=str(ROOT),
        allowed_tools=["Read", "Edit", "Bash"],
        permission_mode="acceptEdits",
        max_turns=20,
    )
    async for msg in query(prompt=TASK, options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, ToolUseBlock):
                    print(f"[tool] {block.name} {block.input}")
        elif isinstance(msg, ResultMessage):
            print(f"\nresult: {msg.result}")
            print(f"turns: {msg.num_turns}, cost: {msg.total_cost_usd}")


if __name__ == "__main__":
    asyncio.run(main())

这些名字都来自 claude_agent_sdk,装上 SDK 之后才存在。逐个说:

  • query:SDK 的入口,返回一个异步消息流。它不是一次 HTTP 调用,而是启动 Claude Code CLI(Harness),把任务交给它,再把消息流回来。
  • ClaudeAgentOptions:配置对象。cwd 是工作目录;allowed_tools 是工具白名单;permission_mode="acceptEdits" 表示编辑自动通过;max_turns=20 是循环预算。
  • AssistantMessage:模型的一轮输出;ToolUseBlock 是其中的一次工具调用。
  • ResultMessage:最终结果;msg.result 是文本,num_turns 和 total_cost_usd 是元信息。
  • asyncio.run(main()):SDK 是异步的,所以用 asyncio 跑。

运行前你需要三样东西:

bash
python3 -m pip install "claude-agent-sdk==0.2.168"
export ANTHROPIC_API_KEY="..."        # 或兼容端点的 ANTHROPIC_AUTH_TOKEN
python3 code/sdkagent/v0_query.py

如果你用兼容端点(比如 DeepSeek 的 Anthropic 兼容接口),再加:

bash
export ANTHROPIC_BASE_URL="https://api.deepseek.com/anthropic"
export ANTHROPIC_MODEL="deepseek-flash"

SDK 启动的是 Claude Code CLI,所以 CLI 的登录方式/配置目录也会生效;最稳的是像上面这样显式设环境变量。

注意 TASK 比 20 行循环多了两句话:

text
Read README.md first. Do not edit tests; fix the implementation.

这两句属于领域层,不是 Harness。它们让模型第一次就走在正确的路上;但真正兜底的是后面的权限规则——第 12 章会把“不许改测试”变成代码强制的约束。

这一次,工具调用变成了这样(这是一条典型的工具序列,真实顺序会随模型和版本变化):

text
[tool] Read   README.md
[tool] Read   tests/test_core.py
[tool] Read   taskflow/core.py
[tool] Edit   taskflow/core.py
[tool] Bash   python -m unittest discover -s tests -v
       -> OK
result: 修复完成。days_left 在到期当天返回 0,测试全部通过。

diff 变成了:

diff
--- a/taskflow/core.py
+++ b/taskflow/core.py
-    return delta if delta > 0 else 1
+    return delta

测试文件没动。

看到这儿你可能会犯嘀咕:这 15 行核心代码里没有循环、没有工具执行、没有权限判断,它怎么跑起来的?

因为 query() 背后是 CLI。SDK 把 options 翻译成 CLI 的启动参数,启动子进程;CLI 负责循环、调模型、执行工具、回填结果;SDK 把 CLI 的消息流解析成 AssistantMessage / ResultMessage 给你。

你在第 1 章不需要懂 CLI 的内部;只要记住:query() 不是一次模型调用,它启动了一个 Harness。

同样的模型,同样的仓库,同样的任务。为什么这次它知道不能改测试?

不是因为它突然变聪明了。是因为它外面的那套程序变了:

  • 它先读了 README.md,里面写着“不要改测试,改实现”;
  • 它有 Read、Edit、Bash 三个工具,每个工具都带说明;
  • 它的循环把测试输出回填给模型,模型看得到 FAILED 的具体行;
  • 它的任务提示里写着“先读 README、不要改测试、改完跑测试”;
  • 它在 max_turns=20 的预算内停,不会无限试。

你写的 15 行不是 Harness,是 Harness 的配置。真正干活的循环、工具、权限、上下文、压缩、会话,在 Claude Code CLI 里。

这本书后面要拆的,就是它。

先别急着下结论说“SDK 真好”。这一次它没改测试,不代表永远不改;acceptEdits 也不是“禁止改测试”的保证。第 12 章会把“不许改测试”从一句提示词,变成一条代码强制的权限规则。到那时你才会看到:提示词是请求,Harness 才是约束。


1.3 事故复盘:它到底缺了什么

把这次事故拆开,缺的东西其实很具体。

先说“看见”。它读了测试和实现,但没读 README.md。项目约定“不要改测试”就写在里面。模型不是全知的,它只能基于你放进上下文的东西做决定;你没放进去的规则,等于不存在。

再说“能做”和“允许做”。它有一个 edit_file,参数是“路径 + 旧字符串 + 新字符串”,这个工具不区分 taskflow/core.py 和 tests/test_core.py。系统提示里当然可以写“不要改测试”,但那是请求,不是约束,模型可以不听,提示注入也可以覆盖它。真正的约束要在代码里:路径白名单、只读规则、审批回调、沙箱。第 5 章会自己写一遍,第 12 章会换成 SDK 的权限管线。

然后是“什么时候算完”。它的循环在“没有工具调用”时结束。它跑了一次 run_tests,看到 OK,就认为完成了。但 OK 只代表“当前测试通过”;测试可以被改,也可以不覆盖真正的 bug。一个可靠的完成信号至少要回答三个问题:测试通过了吗?测试文件被改了吗?diff 是不是最小、是不是只改了该改的地方?这三个问题都能用代码检查,不需要问模型“你做完了吗”。

最后是“失败怎么办”和“留下什么”。20 行循环把工具结果回填给模型,这一点它做对了;但它没有失败后的策略:重试几次?哪些错误可以重试?重试时怎么避免重复执行副作用?这次任务简单,一次就“成功”了;真实任务里,测试会失败、API 会 529、工具会超时、上下文会溢出。没有恢复策略的 Agent,第一次遇到故障就结束。它做完也只留下一句“修复完成”,没有 diff、没有测试输出、没有“我改了什么、为什么改”的记录。人类 code review 靠的是 diff 和测试,Agent 的交付也应该长这样。第 9 章会把“完成标准”写成代码检查,第 15 章会用 Stop Hook 强制它跑测试。

这六件事,收成六个问题。


1.4 把 Harness 拆成六个问题

这六个问题,就是全书的骨架,也是你诊断任何 Agent 的检查表。

#问题这次事故里的答案补上之后
1模型看见什么?只看到测试和实现,没看到 README 的规则把项目约定、失败输出、相关文件放进上下文
2模型能做什么?有 read_file/edit_file/run_tests,能改任何文件工具分级:读、写、执行分开
3模型允许做什么?没有权限,测试也能改tests/ 只读;写操作走审批或白名单
4什么时候停?run_tests 返回 OK 就停测试通过 + 测试文件未变 + diff 审查
5失败了怎么办?一次通过,没有失败反馈策略把失败输出回填,限制重试,区分可重试错误
6做完留下什么?一句“修复完成”diff、测试输出、变更摘要、可恢复的会话

换成一句定义:

Harness 是模型外面那套决定“看见什么、能做什么、允许做什么、什么时候算完、失败怎么办、做完留下什么”的程序。

注意这个定义里没有“提示词技巧”。提示词是 Harness 的一部分,但不是全部。一个只有提示词的 Agent,是把方向盘画在纸上,然后让车自己开。

还有两个横切问题,会在后面展开:

  • 怎么扩展? Hook、Skill、MCP、子 Agent。第 11、13、15 章。
  • 怎么知道它好? Trace、评测、指标。第 17 章。

1.5 一个实验设计:同一模型,五个壳

有人会说:这只是两个例子,不能证明 Harness 重要。

对。所以我们把它做成一个可重复的实验。

变量:同一个模型、同一个仓库、同一组任务。

五个壳:

组Agent它有什么
A20 行循环读、改、跑;通用提示;没有权限、没有完成检查
B20 行循环 + 两道约束A + tests/ 只读 + “测试通过且测试文件未变”才算完成
Ctiny工具、权限、上下文、压缩、会话、子 Agent
DSDK15 行配置 + 内置工具 + 权限 + Hook + 会话
EClaude Code产品形态的同一套 Harness

任务:

  1. fix-due-date:修好 days_left 的 off-by-one;
  2. add-json-output:给 CLI 加 --json,补测试和 README;
  3. refactor-storage:抽出存储接口,保持测试全绿。

判分:

  • 测试通过;
  • 测试文件没有被改;
  • diff 只动了该动的文件;
  • 没有越界读 .env、没有改 CI。

这一章我不打算给你一张成功率表。倒不是不想给,而是一张真实的表要跑几十次、控制模型和版本、记录失败模式,才有意义。那件事放在第 17 章,用 10 个任务 × 3 次来做。

这一章只给你一个定性的预期:

组最可能发生的事
A改测试、提前停、或者修错地方
B不能改测试了,但可能卡住:不知道怎么读失败输出、不知道怎么重试
C多数任务能修,偶尔上下文溢出或忘记约定
D多数任务能修,但配置错了会退化成 A
E通常稳定修好,diff 最小,会跑测试

看这张表,重点不是“谁分高”,而是:同一个模型,换一个壳,失败模式就变了。

A 的失败是“没有约束”;B 的失败是“有约束但不会恢复”;C 的失败是“机制有了但还不够强”;D 的失败往往是“配置错了”;E 的差距是“工具、提示、上下文、评测都打磨过”。

你造 Agent 的过程,就是把这五种失败一个一个消掉的过程。


1.6 为什么你用了 SDK,还是不如 Claude Code

很多人到这里会问:那我直接用 SDK 不就行了?

不一定。SDK 让你用上同一套 Harness,但不保证你配得对。

差距通常有三层:

层你可能的情况Claude Code 的情况怎么查
模型层换了小模型、兼容端点、低 effort同款模型 + 调过的 effort/thinking先固定模型和端点,排除这一层
Harness 层循环 + 几个工具,没有权限/压缩/会话工具、权限、上下文、压缩、会话、子 Agent、错误恢复、评测用第 16 章的控制矩阵逐项对照
领域层一句系统提示 + 通用工具为编码任务打磨的提示、工具、项目知识、完成标准用任务集评测,做 ablation

这次事故是 Harness 层的失败:没有权限、没有完成检查、没有恢复策略。

但如果你用了 SDK,却把 tools 收得很窄、setting_sources=[] 把项目知识关掉、系统提示只写一句“你是一个 coding agent”、没有 Stop Hook、没有评测,你会把 D 组配置成 A 组。用了 SDK,不等于自动拥有 Claude Code 的效果。

第 2 章会正面回答“Claude Code、SDK、裸 API 到底是什么关系”;第 16 章会给你一张完整的控制矩阵,让你知道哪些差距是 SDK 能补的,哪些必须自己写。


1.7 这本书要带你造什么

全书只做一个 Agent:一个能在一个小仓库里修测试、加功能的 coding agent。

它有两个实现:

  • tiny:用 Anthropic Messages API 手写,从 20 行循环长到 600–800 行;第 3–9 章。
  • sdk:用 Claude Agent SDK 重写同一个 Agent;第 10–16 章。

路线大致是这样:

text
第 1–2 章   定位:Harness 六问;Claude Code / SDK / 裸 API 的关系
第 3–9 章   亲手造:循环 → 工具 → 权限 → 上下文 → 压缩 → 会话/子 Agent → 组装
第 10–16 章 换引擎:同一个 Agent,用 SDK 重写;工具、权限、上下文、长任务、控制、边界
第 17–21 章 工程化:评测、成本、时延、可靠性、安全
第 22–25 章 实战:coding / research / support 三个业务 Agent + 设计清单

读完你会有四样东西:

  1. 一个能修测试、能加功能的 coding agent;
  2. 一张“Harness 控制矩阵”,能诊断任何 Agent 的行为;
  3. 一套评测方法,能证明你的改动到底有没有变好;
  4. 一份把方法迁移到你自己的业务的设计清单。

这本书不教你“提示词大全”,也不带你读 CLI 源码。它教你造 Harness:先用手写一遍,知道每个零件为什么存在;再用 SDK 换掉手写的部分,知道哪些不用自己写、哪些必须自己配。


1.8 走之前,留三个问题

这一章不总结了,留三个问题给你:

  1. 你的 Agent 现在靠什么判断“做完了”?这个信号能被伪造吗? 如果答案是“测试通过”,那它就可能改测试;如果答案是“模型说完成了”,那它就可能提前停。

  2. 你的 Agent 能改哪些文件、跑哪些命令?这些限制是提示词说的,还是代码强制的? 提示词是请求,权限才是约束。

  3. 同一个任务交给 Claude Code,它会看到什么、做什么、什么时候停?你的 Agent 差哪一问? 把这三件事写下来,你就有了第一张诊断表。

如果你能回答这三个问题,你已经比大多数 Agent 开发者更清楚自己的系统。

下一章回答一个更根本的问题:既然 Claude Code 已经把这套 Harness 做好了,为什么还要用 Claude Agent SDK 自己造?


本章代码 示例仓库:https://github.com/kevinkelin/claude-agent-sdk-book/tree/master/code/lab/taskflow 20 行循环:https://github.com/kevinkelin/claude-agent-sdk-book/blob/master/code/tiny/v0_loop.py(核心约 20 行;其余是工具定义和 --scripted 替身) 15 行 SDK:https://github.com/kevinkelin/claude-agent-sdk-book/blob/master/code/sdkagent/v0_query.py(核心 15 行;加上 import 和打印约 35 行) 离线复现:cd code && python3 tiny/v0_loop.py --scripted 跑测试:cd code/lab/taskflow && python3 -m unittest discover -s tests -v 说明:正式连载时链接会换成 tag;本章先以仓库 master 为准。