教程Claude Code系列第 7 / 8 篇

Claude Agent SDK 进阶:工具太多时的工具搜索、把改过的文件退回去、显示任务进度

做应用迟早碰到的三件事,本站用 Python 逐样实测。工具搜索通过中转站默认是关的,打开之后如果像前几篇那样写了 tools=[],它照样不生效,而且没有任何提示。文件检查点能把编辑工具改过的文件退回去,用 Bash 改的退不回来。任务清单在新模型上默认不开,开了它也不一定用,用了轮数是原来的三倍多。

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

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

本页目录(6 节)
  1. 一、工具搜索
  2. 二、文件检查点
  3. 三、todo 跟踪
  4. 三样放在一起
  5. 这一趟花了多少
  6. 这次实测没有覆盖到的

这篇教程解决三个问题,都是把智能体做成应用之后迟早会碰到的:

  1. 工具越加越多,几十上百个,全塞进上下文又贵又容易选错,怎么办?
  2. 它把文件改坏了,怎么退回去?
  3. 任务要跑一两分钟,怎么给用户显示“做到第几步了”?

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 没有实测。

来源

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

相关文章