Chapter 04

让 Claude 动手干活

大模型光会说话是不够的——"帮我查一下今天上海的天气"需要调 API,"计算 2.3% 年化 10 年复利"需要动计算器。Tool Use 把模型接到真实世界:模型决定调什么、你执行、结果回塞给模型。一切 Agent 的根基都是这个循环。

核心流程:两次 API 调用

第 1 次调用                                第 2 次调用
───────────                                ───────────
你 → Claude:  "今天上海天气"             你 → Claude: (同前述 + 工具结果)
              + tools=[get_weather]                   messages: [
Claude →:     "我要调 get_weather                      { role: user, content: "..." },
              input: {city: '上海'}"                   { role: assistant, content: [tool_use] },
              stop_reason: tool_use                    { role: user, content: [tool_result] }
                                                      ]
你:          (执行工具,拿到结果)      Claude →:  "今天上海 18 度,多云"

关键点:Claude 不会自己执行工具——你拿着 tool_use 去执行,然后把 tool_result 塞回去,Claude 才给最终答案。

定义 tool schema

Schema 用 JSON Schema 格式描述输入:

const tools: Anthropic.Tool[] = [
  {
    name: "get_weather",
    description: "获取指定城市的当前天气",
    input_schema: {
      type: "object",
      properties: {
        city: {
          type: "string",
          description: "城市的中文名或英文名",
        },
        unit: {
          type: "string",
          enum: ["celsius", "fahrenheit"],
          description: "温度单位",
          default: "celsius",
        },
      },
      required: ["city"],
    },
  },
];
Description 决定一切
Claude 靠 description 判断什么时候调、传什么参数。写得越清楚(含正反示例、边界条件),模型调用越准。参数的 description 也要细:"日期,ISO 格式 YYYY-MM-DD" 比"日期"可靠 10 倍。

第一次调用

const first = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  tools,
  messages: [
    { role: "user", content: "上海今天穿什么?" },
  ],
});

// first.stop_reason === "tool_use"
// first.content 里是:
// [
//   { type: "text", text: "我先查下上海天气。" },   // 可选,Claude 可能先讲话
//   { type: "tool_use", id: "toolu_01AbC...", name: "get_weather",
//     input: { city: "上海", unit: "celsius" } }
// ]

执行工具 + 第二次调用

const toolUse = first.content.find((b) => b.type === "tool_use")!;
const result = await getWeather(toolUse.input.city, toolUse.input.unit);
// result = { temp: 18, desc: "多云", humidity: 60 }

const second = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  tools,
  messages: [
    { role: "user", content: "上海今天穿什么?" },
    { role: "assistant", content: first.content },   // 原样回塞
    {
      role: "user",
      content: [{
        type: "tool_result",
        tool_use_id: toolUse.id,
        content: JSON.stringify(result),   // 或直接传 string
      }],
    },
  ],
});

// second.content[0].text === "上海 18°C 多云,建议穿薄外套..."

完整 agent loop

复杂场景 Claude 可能连续调好几次工具。通用 loop 模板:

const messages: MessageParam[] = [{ role: "user", content: userQuery }];

while (true) {
  const resp = await client.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 2048,
    tools,
    messages,
  });

  messages.push({ role: "assistant", content: resp.content });

  if (resp.stop_reason === "end_turn") break;

  if (resp.stop_reason === "tool_use") {
    const results = [];
    for (const block of resp.content) {
      if (block.type === "tool_use") {
        const result = await runTool(block.name, block.input);
        results.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: JSON.stringify(result),
        });
      }
    }
    messages.push({ role: "user", content: results });
    continue;
  }

  break;   // max_tokens 等其他情况
}

这就是所有 Agent 框架的最小内核——LangGraph、OpenAI Agents、Mastra 的本质就是这个 loop 加编排。

并行工具调用

Claude 一次可能返回多个 tool_use block(它判断可以并行):

first.content = [
  { type: "tool_use", id: "toolu_1", name: "get_weather", input: { city: "上海" } },
  { type: "tool_use", id: "toolu_2", name: "get_weather", input: { city: "北京" } },
];

// 并行执行
const results = await Promise.all(
  first.content.filter((b) => b.type === "tool_use").map(async (b) => ({
    type: "tool_result",
    tool_use_id: b.id,
    content: JSON.stringify(await runTool(b.name, b.input)),
  }))
);

// 一次性把所有结果回塞
messages.push({ role: "user", content: results });
提示模型并行
system prompt 里加一句 "When multiple independent tool calls can be made, invoke them in parallel." —— 有明显效果。

tool_choice 控制

{"type": "auto"}
默认。Claude 自主决定是否用工具
{"type": "any"}
必须调用某个工具,不允许直接文本回答
{"type": "tool", "name": "extract_entity"}
强制调某个工具——JSON 抽取场景的黄金姿势
{"type": "none"}
本次调用禁用所有工具,只允许文本回答

JSON 抽取的正解

想让 Claude 返回"这段文本抽取出的 {name, age, email}"?别让它回 JSON 字符串——用强制 tool call:

const tools = [{
  name: "record_person",
  description: "从文本中抽取人物信息",
  input_schema: {
    type: "object",
    properties: {
      name: { type: "string" },
      age: { type: "integer", minimum: 0, maximum: 150 },
      email: { type: "string", format: "email" },
    },
    required: ["name", "age"],
  },
}];

const resp = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 256,
  tools,
  tool_choice: { type: "tool", name: "record_person" },   // ← 强制
  messages: [{ role: "user", content: "张三 30 岁,邮箱 z@a.com" }],
});

const data = resp.content[0].input;
// data === { name: "张三", age: 30, email: "z@a.com" }  ← 直接结构化对象!

返回的 input 已经是对象,schema 保证字段类型——不用写 JSON 解析,不用防御异常

错误处理:告诉 Claude 工具失败了

{
  type: "tool_result",
  tool_use_id: "toolu_...",
  content: "Error: API rate limit exceeded, try again in 30s",
  is_error: true,   // ← 关键
}

Claude 看到 is_error: true 会决定要不要重试、换策略、或向用户解释——比闷不吭声让模型自己懵要好得多。

流式 + Tool Use

流式响应时,tool_use 的 input 通过 input_json_delta 分片到来(见第 3 章)。SDK 自动累加,用 stream.on("inputJson", ...) 或等 finalMessage

和 Prompt Caching 搭配

工具定义是 不变 的重复 prompt ——完美契合 Prompt Caching:

tools: [
  { name: "get_weather", ... },
  { name: "search_web", ... },
  {
    name: "query_db",
    ...,
    cache_control: { type: "ephemeral" },   // 打在最后一个工具
  },
],

所有之前的工具定义自动被缓存,复用时 90% 折扣。

内置服务端工具

Anthropic 有几个"不用你自己实现"的服务端工具:

声明方式和普通 tool 一样,执行由 Anthropic 侧代劳,返回标准 tool_result——你写的应用逻辑其实还是那个 loop。

常见坑

漏掉 assistant 消息
第二次调用必须把第一次的 resp.content 作为 assistant role 塞回 messages,少了 tool_result 就对不上
tool_use_id 对不上
tool_result.tool_use_id 必须精确匹配 Claude 给的 id,拼错 400
遗漏的 tool_result
如果 Claude 返了 3 个 tool_use,你得在下一条 user 消息里给全 3 个 tool_result(可以并排)
description 太短
"查天气" 不如 "获取指定城市今日温度湿度。city 必填,中英文均可。不支持历史或预报。" Claude 更能判断边界
工具过多
超过 20 个工具时 Claude 有选型偏差,考虑分层 agent 或 routing

本章小结