Appearance
Function Calling vs Tool Use 实战对比
OpenAI 把它叫 Function Calling,Anthropic 叫 Tool Use,本质都是 让 LLM 决定调用什么工具,再把结果喂回去做下一轮推理。但两家的接口设计差异挺大,迁移时踩坑的人很多。这篇直接把字段、消息流、代码摆出来。
一句话总结
- OpenAI:tool 是 messages 数组里多一种 role;适合短链调用。
- Anthropic:tool 是 message content block 的一种类型;适合 Agent 多轮规划。
一、最小例子的对比
需求:让模型根据天气决定回复用户问的问题。一个工具:get_weather(city)。
OpenAI
ts
// 1. 第一次调用
const res = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: '北京今天能出门跑步吗?' }],
tools: [
{
type: 'function',
function: {
name: 'get_weather',
description: '查询指定城市的实时天气',
parameters: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
},
},
],
});
const toolCall = res.choices[0].message.tool_calls?.[0];
// toolCall = {
// id: 'call_abc',
// type: 'function',
// function: { name: 'get_weather', arguments: '{"city":"北京"}' }
// }
// 2. 自己跑工具
const args = JSON.parse(toolCall.function.arguments);
const weather = await getWeather(args.city);
// 3. 把结果回喂
const res2 = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'user', content: '北京今天能出门跑步吗?' },
res.choices[0].message,
{
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(weather),
},
],
tools,
});
console.log(res2.choices[0].message.content);
// "北京今天 PM2.5 偏高,建议改成室内……"Anthropic
ts
// 1. 第一次调用
const res = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
messages: [{ role: 'user', content: '北京今天能出门跑步吗?' }],
tools: [
{
name: 'get_weather',
description: '查询指定城市的实时天气',
input_schema: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
},
],
});
const toolUse = res.content.find((c) => c.type === 'tool_use');
// toolUse = {
// type: 'tool_use',
// id: 'toolu_abc',
// name: 'get_weather',
// input: { city: '北京' } // 已解析为对象
// }
// 2. 自己跑工具
const weather = await getWeather(toolUse.input.city);
// 3. 把结果回喂(注意 role 是 user,不是 tool)
const res2 = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
messages: [
{ role: 'user', content: '北京今天能出门跑步吗?' },
{ role: 'assistant', content: res.content },
{
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: toolUse.id,
content: JSON.stringify(weather),
},
],
},
],
tools,
});二、关键字段对照
| 概念 | OpenAI | Anthropic |
|---|---|---|
| 工具声明 | tools[].function.parameters | tools[].input_schema |
| 调用结果(模型→你) | message.tool_calls[] | content[] 中 tool_use block |
| 工具结果(你→模型) | role: "tool" 消息 | role: "user" + tool_result block |
| ID 字段 | tool_call_id | tool_use_id |
| 参数解析 | 字符串,要 JSON.parse | 已经是对象 |
| 多工具并行 | 同一 message 多个 tool_calls | 同一 message 多个 tool_use block |
| 强制调用 | tool_choice: { type: "function", function: { name } } | tool_choice: { type: "tool", name } |
| 强制不调用 | tool_choice: "none" | tool_choice: { type: "none" } |
| 必须调用某个 | tool_choice: "required" | tool_choice: { type: "any" } |
三、流式情况下的差异(这是真正的难点)
非流式很简单,流式才是 Agent UI 真正要面对的。两家的流式 Tool Use 信号完全不同。
OpenAI 流式
每个 chunk 的 delta.tool_calls[] 是累加片段:
delta: { tool_calls: [{ index: 0, id: "call_abc", function: { name: "get_weather" } }] }
delta: { tool_calls: [{ index: 0, function: { arguments: '{\"ci' } }] }
delta: { tool_calls: [{ index: 0, function: { arguments: 'ty\":\"' } }] }
delta: { tool_calls: [{ index: 0, function: { arguments: '北京\"}' } }] }前端要按 index 字段累加 arguments 字符串,结束后再 JSON.parse。
Anthropic 流式
event: content_block_start
data: { content_block: { type: "tool_use", id: "toolu_abc", name: "get_weather", input: {} } }
event: content_block_delta
data: { delta: { type: "input_json_delta", partial_json: "{\"ci" } }
event: content_block_delta
data: { delta: { type: "input_json_delta", partial_json: "ty\":\"北京\"}" } }
event: content_block_stoppartial_json 也是累加字符串。同样在 content_block_stop 后 JSON.parse。
前端 UI 体验差异:
- OpenAI 流式工具调用时,模型不会同时输出文字。
- Anthropic 流式时,模型可以"先想,再决定调工具"——
content[]里会先有textblock,然后才出tool_useblock。所以 Claude 的 Agent 看起来更"会说话"。
四、什么时候用哪家
OpenAI 更适合
- 短任务、函数固定、不需要 LLM 解释自己在干啥;
- 已经在用 OpenAI 全家桶(Embedding、Whisper 等);
- 需要并行调多个工具且不在乎 Agent 风格——OpenAI 在简单 function call 上历史最久,最稳。
Anthropic 更适合
- 多轮 Agent,需要 LLM 写"我下一步打算 X,因为 Y";
- 工具有失败/重试场景——Claude 在收到
tool_result中的错误信息后,自我纠正能力明显更强; - 复杂 schema(嵌套、union 类型)——Anthropic 对 input_schema 的遵守度肉眼可见更好。
DeepSeek
DeepSeek 用的是 OpenAI 兼容协议,所以字段跟 OpenAI 完全一样,但工具能力比 GPT-4o 弱一些,简单场景没问题,复杂 Agent 还是用 Claude 或 GPT-4o。
五、迁移:写一层适配
如果要在产品里同时支持两家,建一层抽象:
ts
type Tool = {
name: string;
description: string;
parameters: object; // JSON Schema
run: (args: object) => Promise<any>;
};
async function chatWithTools(
provider: 'openai' | 'anthropic',
messages: Message[],
tools: Tool[],
) {
if (provider === 'openai') {
return runOpenAI(messages, tools);
}
return runAnthropic(messages, tools);
}runOpenAI / runAnthropic 内部各自管 multi-turn 循环(调工具 → 回喂结果 → 再调,直到模型不再 request tool)。
业务方只跟 chatWithTools 打交道,永远不用碰各家字段差异。
六、踩坑清单
- OpenAI 的
arguments是字符串——一定要JSON.parse,且要 try/catch(模型偶尔产 invalid JSON,需要重试)。 - Anthropic 的
tool_result必须是数组里的 block,不是字符串——很多人按 OpenAI 习惯写content: 'xxx'会被拒。 - 多工具并行时,OpenAI 同一轮所有 tool_calls 必须全部回喂结果才能再发下一轮,缺一个就报错。Anthropic 同理。
- 错误回喂:工具失败时把错误信息写进
tool_result.content里,别在客户端兜底——交给 LLM 自己决定要不要重试或换路径。 - schema 不要太宽松——
additionalProperties: true会让模型胡乱塞参数,写明required和enum。 - strict mode:OpenAI 2024 后加了
tools[].function.strict: true,开了之后保证输出严格符合 schema。代价是降一点质量,但生产环境强烈推荐。
