Claude Agent SDK 进阶:工具太多时的工具搜索、把改过的文件退回去、显示任务进度
做应用迟早碰到的三件事,本站用 Python 逐样实测。工具搜索通过中转站默认是关的,打开之后如果像前几篇那样写了 tools=[],它照样不生效,而且没有任何提示。文件检查点能把编辑工具改过的文件退回去,用 Bash 改的退不回来。任务清单在新模型上默认不开,开了它也不一定用,用了轮数是原来的三倍多。
本站已实测,测试日期:2026-10-13。
这篇教程解决三个问题,都是把智能体做成应用之后迟早会碰到的:
- 工具越加越多,几十上百个,全塞进上下文又贵又容易选错,怎么办?
- 它把文件改坏了,怎么退回去?
- 任务要跑一两分钟,怎么给用户显示“做到第几步了”?
SDK 对这三件事各有一个现成的机制。三个都有默认状态和你想的不一样的地方。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-13 |
| 版本 | claude-agent-sdk 0.2.165 |
| 系统 | Ubuntu 24.04,Python 3.12.3 |
| 模型 | Sonnet 5.5 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
一、工具搜索
它解决什么
每个工具的名字、说明、参数格式都要放进上下文,模型才知道有这个工具。工具一多,文档给的数字是:50 个工具的定义能占 1 万到 2 万 token;同时加载的工具超过三五十个,模型选对工具的准确率会下降。
工具搜索的做法是先不把定义放进去。模型手上只有一份概要,要用的时候先搜,搜到的几个(默认最多 5 个)才加载进来。代价是每搜一次多一个来回。
文档给的分界线:不到 10 个工具,全部预先加载通常更快。
实测用的 40 个工具
一个仓库管理的服务器:10 个城市的仓,每个仓 4 种查询,一共 40 个工具。它们是在循环里生成的:
from claude_agent_sdk import create_sdk_mcp_server, tool
CITIES = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安", "南京", "重庆"]
KINDS = [("stock", "库存件数"), ("sales", "昨日销量"), ("returns", "昨日退货数"), ("staff", "在岗人数")]
def make(kind, label, i, city):
@tool(f"{kind}_wh{i:02d}", f"查询{city}仓的{label}。只管{city}仓,别的仓不要用它。", {"sku": str})
async def handler(args):
...
return handler
TOOLS = [make(k, label, i, c) for k, label in KINDS for i, c in enumerate(CITIES)]
server = create_sdk_mcp_server(name="wms", version="1.0.0", tools=TOOLS)
问题是“成都仓里 SKU-88 还有多少库存?”。40 个里只有 stock_wh05 是对的。
五种配置的结果
| 内置工具怎么设 | ENABLE_TOOL_SEARCH |
启动时的工具数 | 发生了什么 | 轮数 | 用时 | 写缓存的 token | 花费估算 |
|---|---|---|---|---|---|---|---|
tools=[] |
不设 | 40 | 直接调对了工具 | 2 | 9.5 秒 | 4859 | 0.0196 美元 |
tools=[] |
true |
40 | 同上,没有搜索 | 2 | 7.1 秒 | 4952 | 0.0198 美元 |
tools=[] |
false |
40 | 同上 | 2 | 9.2 秒 | 4859 | 0.0196 美元 |
tools=["ToolSearch"] |
true |
41 | 先搜索,再调对了工具 | 3 | 13.6 秒 | 3257 | 0.0182 美元 |
不写 tools,内置工具全留 |
true |
62 | 先搜索,再调对了工具 | 3 | 15.2 秒 | 15661 | 0.0613 美元 |
五次都答对了:成都仓有 135 件。
读出来的三件事
一、通过中转站,默认是关的。 文档写明:ANTHROPIC_BASE_URL 指向非官方地址时,SDK 默认关掉工具搜索,因为多数代理不会转发它需要的那种内容块。第一行就是这个状态,40 个工具的定义全部预先加载。
要打开,在选项里设环境变量:
ClaudeAgentOptions(..., env={"ENABLE_TOOL_SEARCH": "true"})
文档提醒,设成 true 之后如果代理不支持,请求会直接失败。本站用的这家中转站是支持的,第四、五行都正常跑完了。
二、tools=[] 会让它白开。 这是最容易踩的。第二行明明设了 true,结果和没设一样:没有搜索,40 个定义照样全部加载,也没有任何提示。
对比第四行就明白了:搜索本身是一个叫 ToolSearch 的内置工具。前几篇为了省钱一直写 tools=[],把内置工具全去掉了,ToolSearch 也在里面。没有它,模型没法搜,SDK 就退回到全部预先加载。
所以要同时满足两条:
ClaudeAgentOptions(
tools=["ToolSearch"], # 内置工具里至少留下它
allowed_tools=["mcp__wms", "ToolSearch"],
env={"ENABLE_TOOL_SEARCH": "true"},
...
)
三、40 个小工具,省得很有限。 第四行比第一行:写缓存的 token 从 4859 降到 3257,花费从 0.0196 降到 0.0182 美元,省了 7%;用时从 9.5 秒涨到 13.6 秒,因为多了搜索那一个来回。
这 40 个工具的说明都只有一句话,定义很短。换成参数复杂、说明很长的工具,或者数量再翻几倍,差距才会拉开。本站没有测更大的规模。
第五行是另一个教训:不去掉内置工具的话,光是它们的定义就让写缓存的 token 到了一万五,花费是第四行的三倍多。用不上的内置工具,该去掉还是要去掉。
它是怎么搜的
第四行里那次搜索的输入:
ToolSearch {"query": "stock 成都 Chengdu", "max_results": 10}
它拿“库存”“成都”这样的词去搜。能不能搜到,靠的是工具的名字和说明里有没有这些词。用了工具搜索,说明就不只是给模型读的,还是给搜索用的。 每个工具的说明里要有能把它和别的工具区分开的词。
其他取值
ENABLE_TOOL_SEARCH |
行为 |
|---|---|
| 不设 | 默认开。但接口地址不是官方的时候退回到预先加载 |
true |
强制开 |
auto |
工具定义加起来到了上下文窗口的 10% 才开,不到就预先加载 |
auto:5 |
同上,门槛换成 5% |
false |
关 |
工具数量会变的应用,auto 比较省心。本站没有试 auto。
二、文件检查点
它是什么
打开之后,SDK 在智能体用写文件、编辑文件的工具改动文件之前,先留一份备份。之后可以让文件回到某个时间点的样子:它新建的文件删掉,它改过的文件恢复原样。
文档强调的一点:退回去的只是磁盘上的文件,对话不会跟着退。 对话历史还在,模型还记得自己“改过”。
怎么用
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, UserMessage, ResultMessage
options = ClaudeAgentOptions(
cwd=PROJ,
enable_file_checkpointing=True, # 一、打开
permission_mode="acceptEdits",
extra_args={"replay-user-messages": None}, # 二、让消息流里带上用户消息的 uuid
)
checkpoint = None
async with ClaudeSDKClient(options) as client:
await client.query("把 config.json 里的 port 改成 9090,再新建一个 CHANGELOG.md……")
async for m in client.receive_response():
# 三、第一条用户消息的 uuid 就是“动手之前”的检查点
if isinstance(m, UserMessage) and m.uuid and not checkpoint:
checkpoint = m.uuid
# 四、退回去
await client.rewind_files(checkpoint)
第二步那一行容易漏。不加它,消息流里没有 uuid,就没有东西可以退。
实测:退得回来
项目里有一个 config.json(端口 8080)和一个 notes.md。让它改端口并新建一个文件,每一步之后看磁盘:
[开始之前] {"port":8080,"debug":false} | config.json | notes.md
[调用] Read config.json
[调用] Edit config.json
[调用] Write CHANGELOG.md
[它改完之后] {"port":9090,"debug":false} | CHANGELOG.md | config.json | notes.md
[退回之后] {"port":8080,"debug":false} | config.json | notes.md
端口变回了 8080,新建的 CHANGELOG.md 被删掉了。rewind_files 没有返回值,成不成功要自己去看文件。
实测:用 Bash 改的,退不回来
同样的改动,这次让它用 Bash 做(sed -i 改端口,echo > 建文件):
[开始之前] {"port":8080,"debug":false} | config.json | notes.md
[调用] Bash sed -i 's/8080/9090/' config.json && echo 改了端口 > CHANGELOG.md
[它改完之后] {"port":9090,"debug":false} | CHANGELOG.md | config.json | notes.md
[退回之后] {"port":9090,"debug":false} | CHANGELOG.md | config.json | notes.md
什么都没退。没有报错,没有警告。
文档写明了范围:只跟踪写文件、编辑文件、编辑笔记本这三个工具做的改动。通过 Bash 做的不跟踪,子代理做的改动也不跟踪。
这决定了它能当什么用:
- 如果你的智能体只有读写文件的工具、没有 Bash,检查点是一张可靠的后悔药。
- 如果给了 Bash,它就只管得住一部分。
rm、mv、sed -i、任何脚本对文件的改动,都在它视线之外。这时候要靠别的:让它在 git 仓库里干活、把工作目录整个复制一份、或者用沙箱限制它能写的范围。
文档里还有的
本站没有实测的部分:
- 多个检查点。 每条用户消息的 uuid 都是一个检查点。多轮对话里可以把它们都记下来,退到中间某一步,比如“保留第一轮的重构,撤掉第二轮加的测试”。
- 事后再退。 记下会话 ID 和检查点,之后用
resume恢复会话、发一个空提示,再调rewind_files。 - 用命令行退。 有会话 ID 和检查点的话,不写代码也能退:
CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true claude -p --resume <会话ID> --rewind-files <检查点>
- 退了之后要告诉模型。 对话没有退,模型以为文件还是改过的样子。接着聊之前说一句“刚才的改动已经撤销了”。
三、todo 跟踪
它是什么,为什么默认没有
Claude Code 有一组任务工具:TaskCreate、TaskUpdate、TaskGet、TaskList。开着的时候,Claude 做多步任务会自己记一份清单,每做一步更新状态。这些更新在消息流里是一次次工具调用,你的程序可以拿来画进度。
但在新模型上它们默认是关的。文档的解释:新模型不靠写清单也能跟住多步任务,而工具的定义和提醒要占上下文。默认开着的只有较早的那几代模型。
本站在 Sonnet 5.5 上确认了:启动信息的工具清单里,任务类的只有一个 TaskStop。
打开的办法(三选一):
ClaudeAgentOptions(..., env={"CLAUDE_CODE_ENABLE_TODO_TOOLS": "1"})
或者在 allowed_tools 里点名其中一个,或者在 tools 列表里列出来。
实测:开了不一定用,用了要多花钱
同一个三步任务(新建 README、新建 hello.py、运行它),三种情况:
| 情况 | 任务工具在不在 | Claude 用了吗 | 轮数 | 用时 | 花费估算 |
|---|---|---|---|---|---|
| 默认 | 不在 | - | 4 | 17.0 秒 | 0.0877 美元 |
| 打开,提示里只说“先列出任务清单再动手” | 在 | 没用 | 4 | 10.6 秒 | 0.0967 美元 |
| 打开,提示里明确要求用任务工具登记和更新 | 在 | 用了 | 13 | 24.3 秒 | 0.1267 美元 |
第二行:工具给了,提示里也说了“列清单”,它还是没有调用任务工具,直接干完了。新模型确实不太依赖这个。
第三行:明确要求之后,消息流里是这样的:
5.3s ☐ 新任务:新建 README.md
5.7s ☐ 新任务:新建 hello.py
6.4s ☐ 新任务:运行 hello.py
9.5s ▶ 任务 1 → in_progress
10.0s [调用] Write README.md
13.5s ☑ 任务 1 → completed
13.9s ▶ 任务 2 → in_progress
14.2s [调用] Write hello.py
17.0s ☑ 任务 2 → completed
17.4s ▶ 任务 3 → in_progress
17.7s [调用] Bash python3 hello.py
20.4s ☑ 任务 3 → completed
24.3s [结束] 轮数=13
这就是能直接画成进度条的数据。代价也清楚:轮数从 4 涨到 13,花费多了四成多,时间多了七秒。每一次登记、每一次改状态都是一轮。
怎么读这些调用
ICON = {"pending": "☐", "in_progress": "▶", "completed": "☑", "deleted": "✗"}
async for m in query(prompt=..., options=options):
if isinstance(m, AssistantMessage):
for b in m.content:
if not isinstance(b, ToolUseBlock):
continue
if b.name == "TaskCreate":
print("☐ 新任务:", b.input.get("subject"))
elif b.name == "TaskUpdate":
print(ICON.get(b.input.get("status")), "任务", b.input.get("taskId"), "→", b.input.get("status"))
实测看到的字段:
TaskCreate的输入有subject(标题)、description(说明)、activeForm(进行中时显示的说法)。TaskUpdate的输入有taskId和status。状态是pending、in_progress、completed,删除一个任务是把状态设成deleted。
该不该开
文档的建议很明确:只有你的程序要读这些调用的时候才开,比如记日志或者画自己的进度显示。
想显示进度又不想付这个钱,有一个便宜的替代:直接用普通的工具调用当进度。第四篇讲过,每次工具调用开始时消息流里都有信号,显示“正在读文件…”“正在运行测试…”不需要任何额外的轮数。它没有“一共几步、做到第几步”,但对多数界面够用了。
真要用任务清单,把要求写进系统提示(“多步任务先用任务工具登记,每步更新状态”),不要指望它自己想起来。
三样放在一起
| 机制 | 默认状态 | 要注意的 |
|---|---|---|
| 工具搜索 | 直连官方时开;经过中转站时关 | 手动打开后,内置工具里要留着 ToolSearch |
| 文件检查点 | 关 | 只管编辑类工具的改动,Bash 和子代理的不管;要加 replay-user-messages 才拿得到检查点 |
| 任务清单 | 新模型上关 | 开了模型不一定用;用了轮数成倍增加 |
这一趟花了多少
约 0.6 美元。工具搜索五次约 0.14 美元;检查点两次约 0.16 美元;任务清单三次约 0.31 美元。后两样用的是带全部内置工具的配置,所以每次都比前几篇的订单助手贵。这是 SDK 按官方价格估算的数字,不是中转站实际扣的金额。
这次实测没有覆盖到的
- 工具搜索只测了 40 个很小的工具。 更多、更复杂的工具能省多少,没有数据。
auto模式没有试。 - 只在酷站AI 一家中转站上验证了工具搜索能用。 别家转不转发那种内容块,要各自试;不支持的话文档说请求会直接失败。
- 检查点只测了“改完立刻退”。 多个检查点、事后恢复会话再退、命令行的
--rewind-files都没有试。 - 子代理的改动不被跟踪,是文档的说法,没有验证。
- 任务清单只在 Sonnet 5.5 上测过,每种情况只跑了一次。别的模型用不用、用得多频繁,没有比较。
- TypeScript 没有实测。