第 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。失败的是这一条:
# 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:
# 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 1Agent 读完测试,读完实现,改了一行,跑了一次测试,然后说:
修复完成。测试通过。
你打开 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 节选):
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;后面的每一章,都是在这段循环周围加东西。
你回到电脑前,看到它的工具调用记录:
[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:
cd code
python3 tiny/v0_loop.py --scripted输出(节选,--scripted 是替身,不是模型真实输出):
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 行;这里省略了文件头注释):
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跑。
运行前你需要三样东西:
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 兼容接口),再加:
export ANTHROPIC_BASE_URL="https://api.deepseek.com/anthropic"
export ANTHROPIC_MODEL="deepseek-flash"SDK 启动的是 Claude Code CLI,所以 CLI 的登录方式/配置目录也会生效;最稳的是像上面这样显式设环境变量。
注意 TASK 比 20 行循环多了两句话:
Read README.md first. Do not edit tests; fix the implementation.这两句属于领域层,不是 Harness。它们让模型第一次就走在正确的路上;但真正兜底的是后面的权限规则——第 12 章会把“不许改测试”变成代码强制的约束。
这一次,工具调用变成了这样(这是一条典型的工具序列,真实顺序会随模型和版本变化):
[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 变成了:
--- 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 | 它有什么 |
|---|---|---|
| A | 20 行循环 | 读、改、跑;通用提示;没有权限、没有完成检查 |
| B | 20 行循环 + 两道约束 | A + tests/ 只读 + “测试通过且测试文件未变”才算完成 |
| C | tiny | 工具、权限、上下文、压缩、会话、子 Agent |
| D | SDK | 15 行配置 + 内置工具 + 权限 + Hook + 会话 |
| E | Claude Code | 产品形态的同一套 Harness |
任务:
fix-due-date:修好days_left的 off-by-one;add-json-output:给 CLI 加--json,补测试和 README;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 章。
路线大致是这样:
第 1–2 章 定位:Harness 六问;Claude Code / SDK / 裸 API 的关系
第 3–9 章 亲手造:循环 → 工具 → 权限 → 上下文 → 压缩 → 会话/子 Agent → 组装
第 10–16 章 换引擎:同一个 Agent,用 SDK 重写;工具、权限、上下文、长任务、控制、边界
第 17–21 章 工程化:评测、成本、时延、可靠性、安全
第 22–25 章 实战:coding / research / support 三个业务 Agent + 设计清单读完你会有四样东西:
- 一个能修测试、能加功能的 coding agent;
- 一张“Harness 控制矩阵”,能诊断任何 Agent 的行为;
- 一套评测方法,能证明你的改动到底有没有变好;
- 一份把方法迁移到你自己的业务的设计清单。
这本书不教你“提示词大全”,也不带你读 CLI 源码。它教你造 Harness:先用手写一遍,知道每个零件为什么存在;再用 SDK 换掉手写的部分,知道哪些不用自己写、哪些必须自己配。
1.8 走之前,留三个问题
这一章不总结了,留三个问题给你:
-
你的 Agent 现在靠什么判断“做完了”?这个信号能被伪造吗? 如果答案是“测试通过”,那它就可能改测试;如果答案是“模型说完成了”,那它就可能提前停。
-
你的 Agent 能改哪些文件、跑哪些命令?这些限制是提示词说的,还是代码强制的? 提示词是请求,权限才是约束。
-
同一个任务交给 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为准。