Claude的Tool Use API:把「工单」写对,模型才会真的动手

你如果把大模型想成「特别会聊天的顾问」,那 Tool Use 就是给他发一张标准化工单:工单上写清楚有哪些活可以派、每件活需要什么材料、以及这次到底必须派活还是看着办

很多工程问题不是模型「不会用工具」,而是工单格式不对:tool_choice 写成了字符串但 API 要对象、或者 tool_result 的顺序踩了雷,直接被 400 打回。

读完这篇,你会把 Claude 侧的工具链路从「凭感觉调」升级到「按协议调」:tools 怎么组、tool_choice 四种模式各干什么、input_schema 和 OpenAI 的 parameters 有什么心态差异;最后有一段 Go 代码,用标准库 HTTP 把两轮对话跑通,方便你对照抓包、写单测、接网关。

官方总览见 Tool use with Claude;工具定义细则见 Define tools;回传结果见 Handle tool calls。Messages 请求体字段以 Messages API 为准。

本文可能需要你已经阅读了前几篇文章:

  • relref 第 12 篇解决“Function Calling 是什么”
  • 第 13 篇解决“一次调用流程怎么跑通”

这篇会默认你已经知道 tool_use -> tool_result 的基本闭环,不再重复流程入门,而是聚焦字段级细节。我们只做一件事:把 Claude 里最容易写错、也最影响稳定性的参数协议拆透。

一、Claude 的「工具」在协议里长什么样

和「单独搞一个 function 通道」不同,Claude 把工具调用嵌在消息的多段内容里:assistant 的消息里可以出现 tool_use 块;你执行完以后,用 user 消息里的 tool_result 块把结果贴回去。官方在 Handle tool calls 里把这种差异讲得很直白:没有单独的 tool 角色,全靠 content 数组里的类型字段协作。

12-什么是Function Calling:让大模型学会调用工具中我们已经知道一次完整的客户端工具调用流程,对于 Claude 的工具调用是相同的:

sequenceDiagram
    participant App as 你的服务
    participant API as Anthropic Messages
    participant Model as Claude

    App->>API: POST /v1/messages + tools + messages
    API->>Model: 注入工具定义与对话
    Model-->>API: content 含 tool_use
    API-->>App: stop_reason = tool_use
    App->>App: 本地执行 name + input
    App->>API: 追加 assistant 原样 + user(tool_result)
    API->>Model: 带上工具结果继续推理
    Model-->>App: 自然语言最终答复

客户端工具(自己实现逻辑)走上面这条回路;服务端工具(例如网页里提到的 web_search)由 Anthropic 执行,响应里往往已经内嵌结果,你不必自己拼 tool_result。两类工具可以混在同一个 tools 数组里,但心智模型要分开,详见 Tool use overview

Tool Use 本质是「模型产出结构化工单(tool_use)→ 你在沙箱外干活 → 用 tool_result 交卷」。安全边界清晰,代价是你要严格遵守消息块顺序。

二、tools 参数:每一项都是一张「能力说明书」

请求体顶层的 tools 是一个数组,元素通常是客户端自定义工具,每个对象至少包含:

字段作用
name模型在 tool_use.name 里会引用的标识符
description用自然语言告诉模型「这是什么、何时用、何时别用」——官方强调这是性能杠杆最大的字段
input_schemaJSON Schema 描述 input 对象的结构,也就是模型生成参数时的「模具」

name 的合法性在文档里写死为正则 ^[a-zA-Z0-9_-]{1,64}$(见 Define tools)。别在这里玩花活:中文名、带点号的伪命名空间,都可能直接校验失败。

description 不要写成一句「查询数据库」就完事。官方建议写满「做什么、何时做、每个参数如何影响行为、有哪些限制」——你写得越像给人类工程师的交接文档,模型越不容易瞎调用。好坏对比在同一个文档里就有对照示例。

input_schema 在协议层面就是一块 JSON Schema(文档指向 draft/2020-12 系能力,见 Messages API 内 Tool 类型说明)。工程上最常见的骨架是:

1{
2  "type": "object",
3  "properties": {
4    "location": { "type": "string", "description": "城市,例如 上海" }
5  },
6  "required": ["location"]
7}

Anthropic 还在持续扩展工具定义能力,例如严格校验、缓存和延迟加载。它们的可用范围可能受模型、API 版本和工具类型影响,这篇不把动态字段抄成一张容易过期的清单。需要时直接查 Strict tool useTool reference 和官方 SDK 类型,再针对当前模型验证。

如果你习惯了 OpenAI 的 functions[].parameters,可以把 input_schema 当成同一角色的「Anthropic 方言」:名字不同,干的活一样——都是约束模型吐出来的 JSON 形状。

老墨说: input_schema 负责「格式」,description 负责「语义与边界」;只写 schema 不写清楚说明,模型会合法地调用错工具。

三、tool_choice:四种模式,管的是「态度」不是「工具实现」

tool_choice 描述的是模型这一轮对工具的策略,常见四种(见 Messages API - ToolChoice 与 Cookbook Tool choice):

  • {"type":"auto"}(默认):模型自己决定要不要调用、调用几个。适合大多数对话式产品。
  • {"type":"none"}:明确禁止工具。适合「这段链路绝对不能碰工具」的降级路径,或和「带 tools 定义但临时关掉」组合使用。
  • {"type":"any"}:强制至少使用你提供的某一工具(具体哪个由模型选)。适合「必须把自然语言编译成结构化动作」的流水线。
  • {"type":"tool","name":"某个工具名"}:强制使用指定工具。适合你已经在外层路由好了意图、只差填参数的场景。

这四种类型都还可以带一个布尔字段 disable_parallel_tool_use。把它设为 true 时,相当于告诉模型:「这一轮最多(或恰好)给我一张工单」——在 auto 下是「至多一个 tool_use」,在 any / tool 下是「恰好一个」。并行多工具时的行为细节见官方 Parallel tool use

需要留意的限制:若你启用了 Extended thinking(扩展思考),tool_choice、工具调用和思考块的组合规则会比普通请求更严格。这里不要凭经验猜支持矩阵,直接以 Extended thinking 指南Messages API 当前说明为准。工程上我一般会把「长链推理」和「强制指定工具」拆成两阶段:先让模型产出判断或计划,再在下一轮用 tool_choice 约束具体工具。

tool_choice 解决的是产品策略(要不要硬派活),不是替你执行工具;执行权始终在你手里。

四、tool_usetool_result:两个最容易翻车的细节

stop_reasontool_use 时,content 里会出现形如官方示例的块(摘自 Handle tool calls):

1{
2  "type": "tool_use",
3  "id": "toolu_01A09q90qw90lq917835lq9",
4  "name": "get_weather",
5  "input": { "location": "San Francisco, CA", "unit": "celsius" }
6}

你回传结果时,用 tool_result,其中 tool_use_id 必须对上上面的 idcontent 可以是字符串,也可以是嵌套的 text / image / document 块数组。

翻车点一:顺序。tool_resultuser 消息里,所有 tool_result 必须排在前面,后面才能跟普通 text。文档里的反例一贴就懂:先写一句「这是结果:」再贴 tool_result,会直接 400。

翻车点二:历史拼接。 第二轮请求要把上一轮 assistant 的完整 content 原样塞回 messages,再追加你的 user(tool_result)。漏抄一段 tool_use,后面就对不上号。

老墨说:tool_use.id 当成分布式链路里的 trace_id——丢了就没法对账。

五、完整示例:用 Go 标准库走通一轮工具闭环

下面这段代码刻意只用 net/http + encoding/json,不绑特定 SDK 版本,方便你对照官方 JSON。密钥和模型分别从 ANTHROPIC_API_KEYANTHROPIC_MODEL 读取,模型名从 Models 列表 选择,避免文章里的固定别名过期后误导读者。

工具实现为一个极简的 get_weather:根据 location 返回固定字符串,重点演示协议,不接真实气象站。

  1package main
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"fmt"
  7	"io"
  8	"log"
  9	"net/http"
 10	"os"
 11	"strings"
 12)
 13
 14const anthropicAPI = "https://api.anthropic.com/v1/messages"
 15
 16// --- 请求体(只声明本示例用到的字段)---
 17
 18type messageCreateRequest struct {
 19	Model       string          `json:"model"`
 20	MaxTokens   int             `json:"max_tokens"`
 21	Tools       []toolDef       `json:"tools,omitempty"`
 22	ToolChoice  json.RawMessage `json:"tool_choice,omitempty"`
 23	Messages    []message       `json:"messages"`
 24	System      string          `json:"system,omitempty"`
 25}
 26
 27type toolDef struct {
 28	Name        string         `json:"name"`
 29	Description string         `json:"description"`
 30	InputSchema map[string]any `json:"input_schema"`
 31}
 32
 33type message struct {
 34	Role    string `json:"role"`
 35	Content any    `json:"content"` // string 或 []contentBlock
 36}
 37
 38type contentBlock struct {
 39	Type string `json:"type"`
 40
 41	// text
 42	Text string `json:"text,omitempty"`
 43
 44	// tool_use
 45	ID    string         `json:"id,omitempty"`
 46	Name  string         `json:"name,omitempty"`
 47	Input map[string]any `json:"input,omitempty"`
 48
 49	// tool_result
 50	ToolUseID string `json:"tool_use_id,omitempty"`
 51	Content   any    `json:"content,omitempty"` // string 或嵌套块
 52	IsError   bool   `json:"is_error,omitempty"`
 53}
 54
 55// --- 响应体(只解析本示例关心的字段)---
 56
 57type messageResponse struct {
 58	ID         string          `json:"id"`
 59	Role       string          `json:"role"`
 60	Content    []contentBlock  `json:"content"`
 61	StopReason string          `json:"stop_reason"`
 62	Usage      json.RawMessage `json:"usage"`
 63}
 64
 65func main() {
 66	apiKey := strings.TrimSpace(os.Getenv("ANTHROPIC_API_KEY"))
 67	if apiKey == "" {
 68		log.Fatal("请设置环境变量 ANTHROPIC_API_KEY")
 69	}
 70	model := strings.TrimSpace(os.Getenv("ANTHROPIC_MODEL"))
 71	if model == "" {
 72		log.Fatal("请设置环境变量 ANTHROPIC_MODEL,值以 Anthropic 当前 Models 文档为准")
 73	}
 74
 75	client := &http.Client{}
 76
 77	tools := []toolDef{
 78		{
 79			Name: "get_weather",
 80			Description: "根据城市名返回一句模拟天气描述。仅用于演示 Tool Use;" +
 81				"当用户询问某地天气、气温、是否下雨时使用;不要用于与天气无关的问题。",
 82			InputSchema: map[string]any{
 83				"type": "object",
 84				"properties": map[string]any{
 85					"location": map[string]any{
 86						"type":        "string",
 87						"description": "城市或地区,例如 上海、San Francisco, CA",
 88					},
 89					"unit": map[string]any{
 90						"type": "string",
 91						"enum": []string{"celsius", "fahrenheit"},
 92					},
 93				},
 94				"required": []string{"location"},
 95			},
 96		},
 97	}
 98
 99	// 第一轮:教学示例用指定工具,避免 auto 模式下模型直接自然语言回答
100	toolChoiceWeather, _ := json.Marshal(map[string]any{
101		"type": "tool",
102		"name": "get_weather",
103	})
104
105	firstUser := message{
106		Role: "user",
107		Content: []contentBlock{
108			{Type: "text", Text: "帮我看看上海明天适合跑步吗?从天气角度简单说说。"},
109		},
110	}
111
112	firstReq := messageCreateRequest{
113		Model:      model,
114		MaxTokens:  1024,
115		Tools:      tools,
116		ToolChoice: toolChoiceWeather,
117		Messages:   []message{firstUser},
118	}
119
120	firstResp, err := callMessages(client, apiKey, firstReq)
121	if err != nil {
122		log.Fatal(err)
123	}
124
125	log.Printf("第一轮 stop_reason=%s", firstResp.StopReason)
126
127	if firstResp.StopReason != "tool_use" {
128		log.Fatalf("预期 stop_reason=tool_use,实际为 %s;请检查提示语或模型行为", firstResp.StopReason)
129	}
130
131	// 解析 tool_use 块并执行本地工具
132	var toolUses []contentBlock
133	for _, b := range firstResp.Content {
134		if b.Type == "tool_use" {
135			toolUses = append(toolUses, b)
136		}
137	}
138	if len(toolUses) == 0 {
139		log.Fatal("响应中未找到 tool_use 块")
140	}
141
142	toolResults := make([]contentBlock, 0, len(toolUses))
143	for _, tu := range toolUses {
144		out, execErr := runTool(tu.Name, tu.Input)
145		tb := contentBlock{
146			Type:      "tool_result",
147			ToolUseID: tu.ID,
148		}
149		if execErr != nil {
150			tb.Content = execErr.Error()
151			tb.IsError = true
152		} else {
153			tb.Content = out
154		}
155		toolResults = append(toolResults, tb)
156	}
157
158	// 第二轮:assistant 原样带回 + user 仅含 tool_result(本示例无额外 user 文本)
159	secondReq := messageCreateRequest{
160		Model:     model,
161		MaxTokens: 1024,
162		Tools:     tools,
163		Messages: []message{
164			firstUser,
165			{
166				Role:    "assistant",
167				Content: firstResp.Content,
168			},
169			{
170				Role:    "user",
171				Content: toolResults,
172			},
173		},
174	}
175
176	secondResp, err := callMessages(client, apiKey, secondReq)
177	if err != nil {
178		log.Fatal(err)
179	}
180
181	log.Printf("第二轮 stop_reason=%s", secondResp.StopReason)
182
183	for _, b := range secondResp.Content {
184		if b.Type == "text" {
185			fmt.Println("--- Claude 最终回复 ---")
186			fmt.Println(b.Text)
187		}
188	}
189}
190
191func callMessages(client *http.Client, apiKey string, body messageCreateRequest) (*messageResponse, error) {
192	payload, err := json.Marshal(body)
193	if err != nil {
194		return nil, err
195	}
196
197	req, err := http.NewRequest(http.MethodPost, anthropicAPI, bytes.NewReader(payload))
198	if err != nil {
199		return nil, err
200	}
201	req.Header.Set("x-api-key", apiKey)
202	req.Header.Set("anthropic-version", "2023-06-01")
203	req.Header.Set("content-type", "application/json")
204
205	resp, err := client.Do(req)
206	if err != nil {
207		return nil, err
208	}
209	defer resp.Body.Close()
210
211	raw, _ := io.ReadAll(resp.Body)
212	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
213		return nil, fmt.Errorf("API %s: %s", resp.Status, string(raw))
214	}
215
216	var out messageResponse
217	if err := json.Unmarshal(raw, &out); err != nil {
218		return nil, err
219	}
220	return &out, nil
221}
222
223func runTool(name string, input map[string]any) (string, error) {
224	switch name {
225	case "get_weather":
226		loc, _ := input["location"].(string)
227		if strings.TrimSpace(loc) == "" {
228			return "", fmt.Errorf("缺少 location")
229		}
230		unit, _ := input["unit"].(string)
231		if unit == "" {
232			unit = "celsius"
233		}
234		// 假数据,仅演示协议
235		return fmt.Sprintf("【模拟】%s:气温约 22%s,微风,降水概率低,适合户外慢跑。",
236			loc, unitLabel(unit)), nil
237	default:
238		return "", fmt.Errorf("未知工具: %s", name)
239	}
240}
241
242func unitLabel(unit string) string {
243	switch unit {
244	case "fahrenheit":
245		return "°F"
246	default:
247		return "°C"
248	}
249}

把上面保存为 main.go 后执行:

1export ANTHROPIC_API_KEY="你的密钥"
2export ANTHROPIC_MODEL="你当前可用的模型 ID"
3go run main.go

若你想把 tool_choice 换成其他策略,只需把 toolChoiceWeather 改成例如:

1toolChoiceAuto, _ := json.Marshal(map[string]any{"type": "auto"})
2toolChoiceAny, _ := json.Marshal(map[string]any{"type": "any"})
3toolChoiceNamed, _ := json.Marshal(map[string]any{"type": "tool", "name": "get_weather"})

并写回 messageCreateRequest.ToolChoice 即可,语义与官方 Cookbook Tool choice 一致。

生产环境更推荐使用官方维护的 anthropic-sdk-go,省掉手写结构体与枚举字段的心智负担;但当你要接公司统一网关、做审计日志、或在边缘环境裁剪依赖时,上面这种「裸 HTTP + 明确 JSON」仍然是最稳的对照系。

老墨总结

Claude 的 Tool Use 不是魔法,而是一套把自然语言编译成结构化动作的协议:tools 负责声明能力边界,tool_choice 负责声明调用策略,input_schema 负责把参数形状钉死。真正决定体验上限的,往往是你愿不愿意把 description 当成产品文档来写。

这套协议最适合已经想清楚「哪些事必须落库、哪些事只能沙箱外执行」的团队;若你还在探索期,先用 auto 把闭环跑通,再按需切到 any / tool 做硬编排。遇到 400,优先查两类问题:tool_result 是否排在文本前面、历史里的 tool_use 是否被完整回放。

严格模式、并行工具、服务端工具计费,都可以在你跑通最小闭环后再逐项加。第一版先把工具定义、调用策略、结果回传和错误处理测稳,再决定要不要打开这些进阶能力。

完整示例代码见 Github

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读