"Tool Use"和"Function Calling"在大模型圈经常被混用,但它们代表了不同的设计哲学。本文深入对比两种架构,帮你在构建 AI Agent 时做出正确的工程选择。
一、概念厘清:两者的本质区别
1.1 Function Calling(函数调用)
Function Calling 是 OpenAI 在 GPT-4 API 中引入的机制,核心特征:
- 结构化输出:LLM 输出特定的 JSON 格式,指定要调用的函数名和参数
- 单轮完成:一次 API 调用中,模型决定调用什么函数、传什么参数
- 外部执行:函数的实际执行由调用方(你的代码)完成,结果再传回模型
# Function Calling 示例
import openai
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
}
]
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
tools=tools,
tool_choice="auto"
)
# 模型返回工具调用请求
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name) # "get_weather"
print(tool_call.function.arguments) # '{"city": "北京", "unit": "celsius"}'
1.2 Tool Use(工具使用)
Tool Use 是 Anthropic 在 Claude API 中的对应实现,设计理念有所不同:
- 更强的语义解耦:工具描述更自然语言化,减少对 JSON Schema 的依赖
- 并行工具调用:原生支持在单次响应中请求多个工具调用
- 强调安全性:在设计层面更注重对工具使用的安全控制
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "获取指定城市的实时天气信息,包括温度、湿度和天气状况",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称(中文或英文)"
}
},
"required": ["city"]
}
}
]
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "北京和上海今天哪个城市更热?"}]
)
# Claude 可能同时请求两个城市的天气(并行工具调用)
for content in response.content:
if content.type == "tool_use":
print(f"工具: {content.name}, 参数: {content.input}")
二、关键差异深度对比
2.1 并行工具调用能力
# OpenAI 并行工具调用(GPT-4o 支持)
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "比较北京和上海的天气"}],
tools=tools,
parallel_tool_calls=True # 显式开启并行
)
# 可能返回多个工具调用
for tool_call in response.choices[0].message.tool_calls:
print(tool_call.function.name, tool_call.function.arguments)
# 输出:
# get_weather {"city": "北京"}
# get_weather {"city": "上海"}
# Claude 默认支持并行(无需特殊配置)
# 模型自动判断哪些工具可以并行调用
2.2 工具描述质量的影响
# 差的工具描述(函数式思维)
bad_tool = {
"name": "search",
"description": "search(query: str) -> list",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"}
}
}
}
# 好的工具描述(语义化思维)
good_tool = {
"name": "web_search",
"description": """搜索互联网获取最新信息。
适用场景:
- 需要获取实时信息(新闻、股价、天气)
- 需要查找具体事实但不确定准确性
- 用户询问近期事件
不适用场景:
- 模型已知的通用知识
- 需要深度分析而非信息查找的任务
最佳实践:查询应简洁明确,避免超长查询语句。""",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索查询词,应简洁明确。例如:'2026年诺贝尔物理学奖得主'"
},
"max_results": {
"type": "integer",
"description": "返回结果数量,默认5,最多20",
"default": 5
}
},
"required": ["query"]
}
}
2.3 错误处理机制
# 工具调用错误处理的标准模式
async def execute_with_error_handling(tool_call, tool_registry):
tool_name = tool_call.function.name
try:
args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError as e:
# 参数解析失败
return {
"tool_call_id": tool_call.id,
"role": "tool",
"content": f"参数解析失败:{e}。请检查参数格式。"
}
if tool_name not in tool_registry:
return {
"tool_call_id": tool_call.id,
"role": "tool",
"content": f"工具 '{tool_name}' 不存在。可用工具:{list(tool_registry.keys())}"
}
try:
result = await tool_registry[tool_name](**args)
return {
"tool_call_id": tool_call.id,
"role": "tool",
"content": json.dumps(result, ensure_ascii=False)
}
except TypeError as e:
# 参数类型错误
return {
"tool_call_id": tool_call.id,
"role": "tool",
"content": f"参数错误:{e}。请提供正确的参数类型。"
}
except Exception as e:
# 工具执行失败
return {
"tool_call_id": tool_call.id,
"role": "tool",
"content": f"工具执行失败:{str(e)}"
}
三、统一抽象层设计
3.1 跨模型的工具调用适配器
实际工程中,我们往往需要同时支持多个模型。设计一个统一的抽象层:
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Dict, Any, Optional, Callable
import json
@dataclass
class ToolDefinition:
"""跨平台的工具定义格式"""
name: str
description: str
parameters: Dict[str, Any] # JSON Schema 格式
handler: Callable # 实际执行函数
@dataclass
class ToolCallRequest:
"""工具调用请求"""
call_id: str
tool_name: str
arguments: Dict[str, Any]
class UnifiedToolCallAdapter(ABC):
"""跨模型工具调用适配器基类"""
@abstractmethod
def format_tools(self, tools: List[ToolDefinition]) -> List[Dict]:
"""将统一工具定义转换为平台特定格式"""
pass
@abstractmethod
def parse_tool_calls(self, response) -> List[ToolCallRequest]:
"""从模型响应中解析工具调用请求"""
pass
@abstractmethod
def format_tool_results(self, results: List[Dict]) -> List[Dict]:
"""将工具结果格式化为平台特定的消息格式"""
pass
class OpenAIToolAdapter(UnifiedToolCallAdapter):
"""OpenAI Function Calling 适配器"""
def format_tools(self, tools: List[ToolDefinition]) -> List[Dict]:
return [
{
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": t.parameters
}
}
for t in tools
]
def parse_tool_calls(self, response) -> List[ToolCallRequest]:
message = response.choices[0].message
if not message.tool_calls:
return []
return [
ToolCallRequest(
call_id=tc.id,
tool_name=tc.function.name,
arguments=json.loads(tc.function.arguments)
)
for tc in message.tool_calls
]
def format_tool_results(self, results: List[Dict]) -> List[Dict]:
return [
{
"role": "tool",
"tool_call_id": r["call_id"],
"content": json.dumps(r["result"], ensure_ascii=False)
}
for r in results
]
class AnthropicToolAdapter(UnifiedToolCallAdapter):
"""Anthropic Tool Use 适配器"""
def format_tools(self, tools: List[ToolDefinition]) -> List[Dict]:
return [
{
"name": t.name,
"description": t.description,
"input_schema": t.parameters
}
for t in tools
]
def parse_tool_calls(self, response) -> List[ToolCallRequest]:
tool_calls = []
for content in response.content:
if content.type == "tool_use":
tool_calls.append(ToolCallRequest(
call_id=content.id,
tool_name=content.name,
arguments=content.input
))
return tool_calls
def format_tool_results(self, results: List[Dict]) -> List[Dict]:
return [
{
"type": "tool_result",
"tool_use_id": r["call_id"],
"content": json.dumps(r["result"], ensure_ascii=False)
}
for r in results
]
3.2 完整的 Agent 执行循环
class ToolCallingAgent:
"""
通用工具调用 Agent,支持 OpenAI 和 Anthropic
"""
def __init__(
self,
model_client,
adapter: UnifiedToolCallAdapter,
tools: List[ToolDefinition],
max_iterations: int = 10
):
self.client = model_client
self.adapter = adapter
self.tools = {t.name: t for t in tools}
self.formatted_tools = adapter.format_tools(tools)
self.max_iterations = max_iterations
async def run(self, user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
for iteration in range(self.max_iterations):
# 调用模型
response = await self._call_model(messages)
# 解析工具调用请求
tool_calls = self.adapter.parse_tool_calls(response)
if not tool_calls:
# 没有工具调用,提取最终答案
return self._extract_final_answer(response)
# 并行执行所有工具调用
tool_results = await asyncio.gather(*[
self._execute_tool(tc) for tc in tool_calls
])
# 将工具结果添加到消息历史
assistant_message = self._extract_assistant_message(response)
messages.append(assistant_message)
formatted_results = self.adapter.format_tool_results(tool_results)
messages.extend(formatted_results)
return "已达到最大迭代次数,任务可能未完成。"
async def _execute_tool(self, tool_call: ToolCallRequest) -> Dict:
tool = self.tools.get(tool_call.tool_name)
if not tool:
return {"call_id": tool_call.call_id, "result": {"error": f"工具不存在: {tool_call.tool_name}"}}
try:
if asyncio.iscoroutinefunction(tool.handler):
result = await tool.handler(**tool_call.arguments)
else:
result = tool.handler(**tool_call.arguments)
return {"call_id": tool_call.call_id, "result": result}
except Exception as e:
return {"call_id": tool_call.call_id, "result": {"error": str(e)}}
四、工程选型指南
4.1 选型决策矩阵
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 纯 OpenAI 生态 | Function Calling | 原生支持,文档完善 |
| 纯 Anthropic 生态 | Tool Use | 原生支持,并行调用自然 |
| 多模型混用 | 统一抽象层 | 灵活切换,避免锁定 |
| 需要严格 Schema 验证 | Function Calling | JSON Schema 支持更完善 |
| 复杂多步骤 Agent | 任意(配合框架) | 取决于框架支持 |
| 开源模型部署 | 取决于模型 | Qwen/LLaMA 支持 FC 格式 |
4.2 常见踩坑与解决方案
# 坑 1:工具参数中使用 Python 保留字段名
# 错误
bad_params = {
"type": "object",
"properties": {
"type": {"type": "string"}, # "type" 与 JSON Schema 保留字冲突
}
}
# 解决:避免使用 "type", "$schema" 等 JSON Schema 保留字
# 坑 2:工具结果过长导致上下文超限
def truncate_tool_result(result: str, max_tokens: int = 2000) -> str:
"""截断过长的工具结果"""
# 粗略估算:1 token ≈ 4 字符(英文)/ 2 字符(中文)
max_chars = max_tokens * 3
if len(result) > max_chars:
return result[:max_chars] + f"\n...[结果已截断,原始长度 {len(result)} 字符]"
return result
# 坑 3:工具调用死循环
class LoopDetector:
def __init__(self, max_same_calls: int = 3):
self.call_history = []
self.max_same = max_same_calls
def check_loop(self, tool_name: str, arguments: Dict) -> bool:
call_key = f"{tool_name}:{json.dumps(arguments, sort_keys=True)}"
self.call_history.append(call_key)
# 如果最近 N 次调用都是同一个工具+参数,视为死循环
recent = self.call_history[-self.max_same:]
if len(recent) == self.max_same and len(set(recent)) == 1:
return True # 检测到死循环
return False
五、最佳实践总结
- 工具描述是最重要的工程:详细、准确的工具描述比调用架构本身更重要,花 80% 的时间在工具描述上
- 默认启用并行调用:现代 LLM 都支持并行工具调用,利用这一特性可大幅降低 Agent 延迟
- 构建统一抽象层:避免被单一模型厂商锁定,提前设计跨模型的工具调用抽象
- 工具数量控制在 20 个以内:工具太多会降低模型的选择准确率,复杂场景用工具路由而非堆砌工具
- 完善的错误处理:工具执行失败要给模型清晰的错误信息,让模型能够自我纠正
Function Calling 和 Tool Use 在本质上解决同一个问题,差异主要在 API 风格上。选择哪种方案,最终取决于你使用的模型和生态系统。