核心流程:两次 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"], }, }, ];
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 控制
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 有几个"不用你自己实现"的服务端工具:
computer_20250124— Computer Use(第 9 章)text_editor_20250728— 文件读写(用于 coding agent)bash_20250124— 执行 shellweb_search_20250305— 网络搜索(Anthropic 托管)
声明方式和普通 tool 一样,执行由 Anthropic 侧代劳,返回标准 tool_result——你写的应用逻辑其实还是那个 loop。
常见坑
resp.content 作为 assistant role 塞回 messages,少了 tool_result 就对不上tool_result.tool_use_id 必须精确匹配 Claude 给的 id,拼错 400本章小结
- Tool Use = 两次(或多次)API 调用,模型告诉你调谁,你执行并回塞
stop_reason=tool_use是循环继续的信号;end_turn才是终点- Claude 可能并行返回多个 tool_use——同时执行、同时回塞
tool_choice强制调用某工具是最可靠的 JSON 抽取方式is_error: true让模型理解工具失败,能 gracefully 降级