Tool Use vs Function Calling:LLM工具调用架构深度对比与工程选型

3 阅读1分钟

"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 CallingJSON 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

五、最佳实践总结

  1. 工具描述是最重要的工程:详细、准确的工具描述比调用架构本身更重要,花 80% 的时间在工具描述上
  2. 默认启用并行调用:现代 LLM 都支持并行工具调用,利用这一特性可大幅降低 Agent 延迟
  3. 构建统一抽象层:避免被单一模型厂商锁定,提前设计跨模型的工具调用抽象
  4. 工具数量控制在 20 个以内:工具太多会降低模型的选择准确率,复杂场景用工具路由而非堆砌工具
  5. 完善的错误处理:工具执行失败要给模型清晰的错误信息,让模型能够自我纠正

Function Calling 和 Tool Use 在本质上解决同一个问题,差异主要在 API 风格上。选择哪种方案,最终取决于你使用的模型和生态系统。