什么是 ‘Function Calling’ 的底层协议?解析 OpenAI 与 Anthropic 在工具调用格式上的细微差异

各位同仁,各位对大语言模型(LLM)充满热情的开发者们,大家好。

今天,我们将深入探讨一个极其重要且正在彻底改变LLM应用范式的技术:’Function Calling’,或者更广义地称之为“工具使用”(Tool Use)。我们将剖析其底层协议,并细致比较OpenAI与Anthropic这两大行业领导者在实现这一功能上的细微差异。作为一名编程专家,我希望通过今天的讲座,为大家提供一个严谨、深入且充满实践代码的视角,帮助大家更好地理解和运用这项强大技术。

一、 函数调用:LLM能力的飞跃

在过去,大语言模型的主要能力在于文本生成、理解和推理。它们是语言大师,但却不具备直接执行外部动作的能力。想象一下,你有一位极其聪明的助手,他能理解你所有的指令,并给出详尽的建议,但却不能帮你打开电脑、查询天气,甚至不能帮你发一封邮件。这就是早期LLM的局限性。

“函数调用”机制的引入,彻底打破了这一壁垒。它赋予了LLM与外部世界交互的能力,将LLM从一个“语言模型”升级为一个“智能代理”(Agent)。其核心思想是:LLM在理解用户意图后,如果判断需要借助外部工具(即函数)来完成任务,它会生成一个结构化的、符合预定义协议的函数调用请求。我们的应用程序接收到这个请求后,会实际执行对应的函数,并将函数执行的结果再反馈给LLM,LLM则根据这个结果继续生成响应。

核心价值:

  1. 扩展能力边界: 让LLM能够执行搜索、查询数据库、发送邮件、控制智能设备等任何外部API能做的事情。
  2. 提高准确性: 避免LLM“幻觉”(hallucination),对于事实性、实时性或需要精确计算的任务,将任务委托给可靠的外部工具。
  3. 构建复杂工作流: 允许LLM通过一系列工具调用,逐步完成复杂的多步骤任务。
  4. 增强用户体验: 用户无需离开聊天界面,即可获得集成多功能的服务。

基本工作流程概览:

  1. 工具定义 (Tool Definition): 开发者向LLM描述可用的工具(函数)及其参数,通常以JSON Schema的形式。
  2. 意图识别与工具选择 (Intent Recognition & Tool Selection): 用户提出请求后,LLM根据其内部知识和提供的工具描述,判断是否需要使用工具,以及使用哪个工具。
  3. 生成工具调用 (Generate Tool Call): 如果LLM决定使用工具,它会生成一个结构化的工具调用请求,包含工具名称和参数。
  4. 执行工具 (Execute Tool): 应用程序接收到LLM生成的工具调用请求,解析它,并在后端执行对应的实际函数。
  5. 返回工具结果 (Return Tool Result): 函数执行完毕后,其结果被发送回LLM。
  6. 最终响应 (Final Response): LLM结合工具结果和之前的对话上下文,生成最终的用户响应。

![Function Calling Flowchart Concept]

这整个过程,就像LLM在与我们的应用程序进行一场结构化的对话。它不再是仅仅输出文本,而是输出“指令”,而我们应用程序的任务就是理解并执行这些指令,然后将“执行报告”反馈给它。

二、 函数调用的底层协议:通用原理

无论具体实现如何,函数调用的底层协议都遵循一些核心原则。这些原则定义了LLM与外部应用程序之间进行信息交换的契约。

2.1 工具描述(Tool Definition Schema)

这是协议的第一步:应用程序告诉LLM它有什么能力。工具描述通常是一个包含函数名称、功能描述以及参数定义的对象数组。参数定义至关重要,它通常采用JSON Schema的格式,精确地描述了每个参数的名称、类型、是否必需、枚举值以及详细说明。

为什么是JSON Schema?
JSON Schema是一个强大的工具,用于描述JSON数据的结构。它提供了丰富的数据类型(string, number, boolean, object, array等)、约束(minLength, maxLength, minimum, maximum, enum等)以及描述性元数据(title, description)。LLM通过解析这些Schema,能够准确地理解每个工具的功能以及调用它所需的输入格式。

一个通用工具描述的例子:

[
  {
    "type": "function",
    "function": {
      "name": "get_current_weather",
      "description": "获取指定城市当前的实时天气情况",
      "parameters": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "城市名称,例如:旧金山、北京"
          },
          "unit": {
            "type": "string",
            "enum": ["celsius", "fahrenheit"],
            "description": "温度单位,可选:摄氏度或华氏度"
          }
        },
        "required": ["location"]
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "send_email",
      "description": "发送一封电子邮件给指定收件人",
      "parameters": {
        "type": "object",
        "properties": {
          "to": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            },
            "description": "收件人邮箱地址列表"
          },
          "subject": {
            "type": "string",
            "description": "邮件主题"
          },
          "body": {
            "type": "string",
            "description": "邮件正文"
          }
        },
        "required": ["to", "subject", "body"]
      }
    }
  }
]

LLM会利用这些信息,结合用户输入,决定是否生成一个get_current_weathersend_email的调用。

2.2 LLM生成的工具调用(Tool Call Output)

当LLM判断需要调用工具时,它不会直接执行函数,而是生成一个结构化的消息,其中包含:

  • 工具名称 (Tool Name): LLM决定要调用的函数名称。
  • 参数 (Arguments/Input): 以JSON格式表示的函数参数及其对应的值。

这个结构化的消息是LLM对应用程序发出的“指令”。应用程序必须能够解析这个消息,提取出函数名称和参数,然后执行相应的操作。

一个通用工具调用输出的例子:

{
  "tool_calls": [
    {
      "id": "call_abc123",
      "function": {
        "name": "get_current_weather",
        "arguments": {
          "location": "北京",
          "unit": "celsius"
        }
      }
    }
  ]
}

请注意,这里的arguments字段,在不同的提供商那里,可能是JSON字符串,也可能是直接的JSON对象。这是一个关键的细微差异,我们稍后会详细讨论。

2.3 应用程序返回的工具结果(Tool Result Input)

应用程序执行完函数后,需要将结果反馈给LLM。这个结果也必须是结构化的,通常包含:

  • 关联ID (Tool Call ID): 用于将结果与之前的某个工具调用关联起来。
  • 执行结果 (Result Content): 函数执行的实际输出,可以是成功的数据,也可以是错误信息。

LLM接收到这个结果后,会将其作为新的上下文纳入对话历史,并据此生成最终的用户响应,或者决定进行后续的工具调用。

一个通用工具结果输入的例子:

{
  "role": "tool",
  "tool_call_id": "call_abc123",
  "content": "{"temperature": 25, "unit": "celsius", "description": "晴朗"}"
}

同样,这里的content字段,在不同提供商那里,可以是纯字符串,也可以是包含结构化数据的特定消息类型。

总结来说,函数调用的底层协议就是一套关于如何描述工具、如何请求工具执行以及如何报告工具结果的结构化通信规范。现在,让我们深入了解OpenAI和Anthropic是如何实现这些原则的。

三、 OpenAI的函数调用协议深度解析

OpenAI是最早将函数调用机制(Function Calling)大规模推广的平台之一,其API设计简洁而强大。

3.1 工具定义 (tools 参数)

在OpenAI API中,你通过tools参数向模型提供一个或多个函数定义。每个函数定义都必须是一个object,包含typefunction两个键。type目前固定为"function"function对象则包含namedescriptionparameters

  • name (string, required): 函数的名称,必须是字母、数字或下划线组成的字符串,且不能以数字开头。这是LLM在生成工具调用时会使用的名称。
  • description (string, optional): 函数的详细描述。这对于LLM理解函数用途至关重要,好的描述能提高LLM选择和使用工具的准确性。
  • parameters (object, required): 函数参数的JSON Schema定义。这部分告诉LLM函数接受哪些参数,它们的类型,是否必需,以及它们的含义。

Python代码示例:定义OpenAI工具

import json
from openai import OpenAI

# 假设我们有一个客户端实例
client = OpenAI(api_key="YOUR_OPENAI_API_KEY")

# 定义一个获取当前天气的工具
def get_current_weather(location: str, unit: str = "celsius") -> dict:
    """获取指定城市当前的实时天气情况"""
    # 实际应用中,这里会调用一个外部API或数据库
    if location.lower() == "北京":
        return {"location": "北京", "temperature": 25, "unit": unit, "description": "晴朗"}
    elif location.lower() == "上海":
        return {"location": "上海", "temperature": 28, "unit": unit, "description": "多云"}
    else:
        return {"location": location, "temperature": "未知", "unit": unit, "description": "无法获取"}

# 定义一个发送邮件的工具
def send_email(to: list[str], subject: str, body: str) -> dict:
    """发送一封电子邮件给指定收件人"""
    print(f"Sending email to: {to}, Subject: {subject}, Body: {body}")
    # 实际应用中,这里会调用邮件API
    return {"status": "success", "message": f"Email sent to {', '.join(to)}"}

# 工具函数的映射,方便根据名称查找和执行
available_functions = {
    "get_current_weather": get_current_weather,
    "send_email": send_email,
}

# OpenAI API期望的工具定义格式
openai_tools_definition = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "获取指定城市当前的实时天气情况",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市名称,例如:旧金山、北京"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位,可选:摄氏度或华氏度"
                    }
                },
                "required": ["location"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "send_email",
            "description": "发送一封电子邮件给指定收件人",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "format": "email", # 这是一个提示,LLM不强制验证格式
                            "description": "收件人邮箱地址"
                        },
                        "description": "收件人邮箱地址列表"
                    },
                    "subject": {
                        "type": "string",
                        "description": "邮件主题"
                    },
                    "body": {
                        "type": "string",
                        "description": "邮件正文"
                    }
                },
                "required": ["to", "subject", "body"]
            }
        }
    }
]

print("OpenAI Tools Definition:")
print(json.dumps(openai_tools_definition, indent=2, ensure_ascii=False))

3.2 LLM生成的工具调用 (tool_calls 消息)

当用户输入一个需要工具的请求时,OpenAI模型会返回一个特殊的assistant消息。这个消息的content字段可能是None(如果只有工具调用)或者包含一些文本(如果LLM同时想说些什么),但最重要的是它会包含一个tool_calls数组。

tool_calls数组中的每个元素都是一个object,表示一个工具调用。每个工具调用包含:

  • id (string, required): 模型的内部ID,用于在后续的工具结果消息中引用此调用。
  • type (string, required): 固定为"function"
  • function (object, required): 包含namearguments
    • name (string): LLM决定调用的函数名称。
    • arguments (string): 这是一个JSON格式的字符串,包含了函数参数及其值。应用程序在执行前需要解析这个字符串。

Python代码示例:接收和解析OpenAI工具调用

# 模拟用户请求
user_message_weather = "北京现在天气怎么样?"
user_message_email = "给张三([email protected])发邮件,主题是会议通知,内容是明天上午9点开会。"

# 示例一:获取天气
messages_weather = [
    {"role": "user", "content": user_message_weather}
]

print(f"nUser Query (Weather): {user_message_weather}")
response_weather = client.chat.completions.create(
    model="gpt-4o", # 或 gpt-3.5-turbo 等支持 function calling 的模型
    messages=messages_weather,
    tools=openai_tools_definition,
    tool_choice="auto" # 允许模型自动决定是否调用工具
)

assistant_message_weather = response_weather.choices[0].message
print(f"OpenAI Assistant Response (Weather):n{json.dumps(assistant_message_weather.model_dump(), indent=2, ensure_ascii=False)}")

if assistant_message_weather.tool_calls:
    for tool_call in assistant_message_weather.tool_calls:
        function_name = tool_call.function.name
        # 核心差异:OpenAI的arguments是一个JSON字符串,需要手动解析
        function_args = json.loads(tool_call.function.arguments)
        tool_call_id = tool_call.id

        print(f"n  Detected Tool Call ID: {tool_call_id}")
        print(f"  Function Name: {function_name}")
        print(f"  Function Arguments: {function_args}")

        # 在这里执行实际的函数
        if function_name in available_functions:
            function_to_call = available_functions[function_name]
            function_response = function_to_call(**function_args)
            print(f"  Tool Execution Result: {function_response}")

            # 将工具执行结果添加到消息历史中,以便LLM继续处理
            messages_weather.append(assistant_message_weather) # 先添加LLM的工具调用消息
            messages_weather.append(
                {
                    "tool_call_id": tool_call_id,
                    "role": "tool",
                    "content": json.dumps(function_response, ensure_ascii=False) # 工具结果也通常是JSON字符串
                }
            )

            print("n  Sending Tool Result back to LLM...")
            final_response_weather = client.chat.completions.create(
                model="gpt-4o",
                messages=messages_weather,
                tools=openai_tools_definition,
                tool_choice="auto"
            )
            print(f"  Final LLM Response: {final_response_weather.choices[0].message.content}")
else:
    print("  No tool call detected for weather query.")

# 示例二:发送邮件
messages_email = [
    {"role": "user", "content": user_message_email}
]

print(f"nUser Query (Email): {user_message_email}")
response_email = client.chat.completions.create(
    model="gpt-4o",
    messages=messages_email,
    tools=openai_tools_definition,
    tool_choice="auto"
)

assistant_message_email = response_email.choices[0].message
print(f"OpenAI Assistant Response (Email):n{json.dumps(assistant_message_email.model_dump(), indent=2, ensure_ascii=False)}")

if assistant_message_email.tool_calls:
    for tool_call in assistant_message_email.tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)
        tool_call_id = tool_call.id

        print(f"n  Detected Tool Call ID: {tool_call_id}")
        print(f"  Function Name: {function_name}")
        print(f"  Function Arguments: {function_args}")

        if function_name in available_functions:
            function_to_call = available_functions[function_name]
            function_response = function_to_call(**function_args)
            print(f"  Tool Execution Result: {function_response}")

            messages_email.append(assistant_message_email)
            messages_email.append(
                {
                    "tool_call_id": tool_call_id,
                    "role": "tool",
                    "content": json.dumps(function_response, ensure_ascii=False)
                }
            )

            print("n  Sending Tool Result back to LLM...")
            final_response_email = client.chat.completions.create(
                model="gpt-4o",
                messages=messages_email,
                tools=openai_tools_definition,
                tool_choice="auto"
            )
            print(f"  Final LLM Response: {final_response_email.choices[0].message.content}")
else:
    print("  No tool call detected for email query.")

3.3 提供工具输出 (tool 消息)

在OpenAI API中,工具的执行结果通过一个特殊的tool角色消息发送回模型。

  • role (string, required): 必须是"tool"
  • tool_call_id (string, required): 必须与之前LLM生成的tool_calls中某个工具调用的id匹配,用于关联请求和结果。
  • content (string, required): 工具函数执行后的结果。通常是一个JSON字符串,但也可以是任何文本内容。

关键点: OpenAI采用了一个独立的tool角色来承载工具执行结果,这使得对话历史的结构更加清晰,区分了用户、助手和工具的言论。

3.4 强制工具调用 (tool_choice 参数)

OpenAI提供了tool_choice参数来控制模型的工具调用行为:

  • "none": 模型不会调用任何工具,即使提供了工具定义。
  • "auto" (default): 模型自动决定是否调用工具。
  • {"type": "function", "function": {"name": "your_function_name"}}: 强制模型调用特定的函数。如果模型无法生成符合要求的参数,可能会报错。

用例: 当你明确知道用户意图需要某个特定工具时,可以使用强制调用来提高效率和可靠性。

3.5 并行函数调用

OpenAI的模型能够在一个响应中返回多个tool_calls。这意味着LLM可以一次性识别出多个需要执行的独立任务。应用程序可以选择并行执行这些工具调用,从而提高效率。

代码示例 (已在上述接收和解析部分体现): assistant_message.tool_calls是一个列表,可以遍历执行。

# ... (之前的代码) ...
if assistant_message_weather.tool_calls:
    for tool_call in assistant_message_weather.tool_calls:
        # 每个tool_call都是一个独立的工具调用
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)
        tool_call_id = tool_call.id
        # ... 执行和返回结果 ...

应用程序需要决定是串行执行这些调用,还是并行执行。如果它们是相互独立的,并行执行通常更高效。

3.6 错误处理

在OpenAI的函数调用中,如果工具执行发生错误,应用程序应将错误信息作为tool消息的content返回给LLM。LLM会尝试理解这个错误,并可能采取以下行动:

  • 向用户解释错误。
  • 尝试使用不同的参数重新调用工具(如果其内部推理允许)。
  • 建议用户调整请求。

示例:返回错误信息

# ... (假设get_current_weather函数现在可以模拟错误) ...
def get_current_weather_with_error(location: str, unit: str = "celsius") -> dict:
    if location.lower() == "未知城市":
        raise ValueError(f"无法获取 '{location}' 的天气信息。")
    # ... 正常逻辑 ...

# ... 当发生错误时 ...
try:
    function_response = function_to_call(**function_args)
    content_to_send = json.dumps(function_response, ensure_ascii=False)
except Exception as e:
    error_message = f"Error executing {function_name}: {str(e)}"
    print(f"  Tool Execution Error: {error_message}")
    content_to_send = json.dumps({"error": error_message}, ensure_ascii=False) # 或者直接发送 error_message 字符串

messages_weather.append(
    {
        "tool_call_id": tool_call_id,
        "role": "tool",
        "content": content_to_send
    }
)

LLM接收到包含错误信息的tool消息后,可以据此生成一个友好的错误提示给用户。

四、 Anthropic的工具使用协议深度解析

Anthropic的Claude模型也提供了强大的工具使用(Tool Use)能力,但其协议在消息结构上与OpenAI存在一些有趣的差异,这反映了其对对话流的独特设计理念。

4.1 背景:从XML到结构化JSON

Anthropic的早期模型(如Claude 2.1)主要依赖于在文本响应中嵌入XML标签(如<tool_code><tool_results>)来指示工具使用。这种方式虽然灵活,但解析起来相对繁琐,且容易出错。随着Claude 3系列模型的发布,Anthropic引入了更现代、更结构化的JSON协议,与OpenAI的思路更加接近,但仍保持其独特的风格。我们这里主要讨论基于Claude 3系列模型的结构化JSON协议。

4.2 工具定义 (tools 参数)

与OpenAI类似,Anthropic API通过tools参数接收函数定义。其结构也与OpenAI非常相似,包含namedescriptioninput_schema

  • name (string, required): 函数的名称。
  • description (string, optional): 函数的详细描述。
  • input_schema (object, required): 函数参数的JSON Schema定义。与OpenAI的parameters等效。

Python代码示例:定义Anthropic工具

import json
from anthropic import Anthropic

# 假设我们有一个客户端实例
client_anthropic = Anthropic(api_key="YOUR_ANTHROPIC_API_KEY")

# 沿用之前的工具函数
# def get_current_weather(...)
# def send_email(...)
# available_functions = {...}

# Anthropic API期望的工具定义格式
anthropic_tools_definition = [
    {
        "name": "get_current_weather",
        "description": "获取指定城市当前的实时天气情况",
        "input_schema": { # 这里的键是 input_schema
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "城市名称,例如:旧金山、北京"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "温度单位,可选:摄氏度或华氏度"
                }
            },
            "required": ["location"]
        }
    },
    {
        "name": "send_email",
        "description": "发送一封电子邮件给指定收件人",
        "input_schema": {
            "type": "object",
            "properties": {
                "to": {
                    "type": "array",
                    "items": {
                        "type": "string",
                        "format": "email",
                        "description": "收件人邮箱地址"
                    },
                    "description": "收件人邮箱地址列表"
                },
                "subject": {
                    "type": "string",
                    "description": "邮件主题"
                },
                "body": {
                    "type": "string",
                    "description": "邮件正文"
                }
            },
            "required": ["to", "subject", "body"]
        }
    }
]

print("nAnthropic Tools Definition:")
print(json.dumps(anthropic_tools_definition, indent=2, ensure_ascii=False))

可以看到,工具定义的结构与OpenAI几乎一致,只是根键名从parameters变成了input_schema

4.3 LLM生成的工具调用 (tool_use 块)

Anthropic模型生成工具调用时,其响应是一个assistant角色消息。但与OpenAI不同的是,工具调用并不是独立的消息字段,而是作为content数组中的一个特殊元素类型:tool_use

content数组可以包含多个元素,可以是文本 ({"type": "text", "text": "..."}),也可以是工具调用 ({"type": "tool_use", ...})。

tool_use块包含:

  • type (string, required): 固定为"tool_use"
  • id (string, required): 模型的内部ID,用于在后续的工具结果消息中引用此调用。
  • name (string, required): LLM决定调用的函数名称。
  • input (object, required): 这是一个直接的JSON对象,包含了函数参数及其值。这是与OpenAI的一个重要差异。应用程序无需额外解析JSON字符串。

Python代码示例:接收和解析Anthropic工具调用

# 模拟用户请求
user_message_weather = "北京现在天气怎么样?"
user_message_email = "给张三([email protected])发邮件,主题是会议通知,内容是明天上午9点开会。"

# 示例一:获取天气
messages_weather_anthropic = [
    {"role": "user", "content": user_message_weather}
]

print(f"nUser Query (Weather - Anthropic): {user_message_weather}")
response_weather_anthropic = client_anthropic.messages.create(
    model="claude-3-opus-20240229", # 或 claude-3-sonnet-20240229 等支持 tool use 的模型
    max_tokens=1024,
    messages=messages_weather_anthropic,
    tools=anthropic_tools_definition
)

assistant_message_anthropic = response_weather_anthropic
print(f"Anthropic Assistant Response (Weather):n{json.dumps(assistant_message_anthropic.model_dump(), indent=2, ensure_ascii=False)}")

# 遍历content以查找tool_use块
tool_calls_detected = []
if assistant_message_anthropic.content:
    for content_block in assistant_message_anthropic.content:
        if content_block.type == "tool_use":
            tool_calls_detected.append(content_block)

if tool_calls_detected:
    for tool_call in tool_calls_detected:
        function_name = tool_call.name
        # 核心差异:Anthropic的input直接是JSON对象,无需json.loads()
        function_args = tool_call.input
        tool_call_id = tool_call.id

        print(f"n  Detected Tool Call ID: {tool_call_id}")
        print(f"  Function Name: {function_name}")
        print(f"  Function Arguments: {function_args}")

        # 在这里执行实际的函数
        if function_name in available_functions:
            function_to_call = available_functions[function_name]
            function_response = function_to_call(**function_args)
            print(f"  Tool Execution Result: {function_response}")

            # 将工具执行结果添加到消息历史中,以便LLM继续处理
            messages_weather_anthropic.append(
                {"role": assistant_message_anthropic.role, "content": assistant_message_anthropic.content} # 先添加LLM的工具调用消息
            )
            messages_weather_anthropic.append(
                {
                    "role": "user", # 注意:Anthropic的工具结果是作为user消息的一部分发送
                    "content": [
                        {
                            "type": "tool_result",
                            "tool_use_id": tool_call_id,
                            "content": json.dumps(function_response, ensure_ascii=False) # 工具结果可以是JSON字符串
                        }
                    ]
                }
            )

            print("n  Sending Tool Result back to LLM...")
            final_response_weather_anthropic = client_anthropic.messages.create(
                model="claude-3-opus-20240229",
                max_tokens=1024,
                messages=messages_weather_anthropic,
                tools=anthropic_tools_definition
            )
            print(f"  Final LLM Response: {final_response_weather_anthropic.content[0].text}")
else:
    print("  No tool call detected for weather query (Anthropic).")

# 示例二:发送邮件
messages_email_anthropic = [
    {"role": "user", "content": user_message_email}
]

print(f"nUser Query (Email - Anthropic): {user_message_email}")
response_email_anthropic = client_anthropic.messages.create(
    model="claude-3-opus-20240229",
    max_tokens=1024,
    messages=messages_email_anthropic,
    tools=anthropic_tools_definition
)

assistant_message_email_anthropic = response_email_anthropic
print(f"Anthropic Assistant Response (Email):n{json.dumps(assistant_message_email_anthropic.model_dump(), indent=2, ensure_ascii=False)}")

tool_calls_detected_email = []
if assistant_message_email_anthropic.content:
    for content_block in assistant_message_email_anthropic.content:
        if content_block.type == "tool_use":
            tool_calls_detected_email.append(content_block)

if tool_calls_detected_email:
    for tool_call in tool_calls_detected_email:
        function_name = tool_call.name
        function_args = tool_call.input
        tool_call_id = tool_call.id

        print(f"n  Detected Tool Call ID: {tool_call_id}")
        print(f"  Function Name: {function_name}")
        print(f"  Function Arguments: {function_args}")

        if function_name in available_functions:
            function_to_call = available_functions[function_name]
            function_response = function_to_call(**function_args)
            print(f"  Tool Execution Result: {function_response}")

            messages_email_anthropic.append(
                {"role": assistant_message_email_anthropic.role, "content": assistant_message_email_anthropic.content}
            )
            messages_email_anthropic.append(
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "tool_result",
                            "tool_use_id": tool_call_id,
                            "content": json.dumps(function_response, ensure_ascii=False)
                        }
                    ]
                }
            )

            print("n  Sending Tool Result back to LLM...")
            final_response_email_anthropic = client_anthropic.messages.create(
                model="claude-3-opus-20240229",
                max_tokens=1024,
                messages=messages_email_anthropic,
                tools=anthropic_tools_definition
            )
            print(f"  Final LLM Response: {final_response_email_anthropic.content[0].text}")
else:
    print("  No tool call detected for email query (Anthropic).")

4.4 提供工具输出 (tool_result 块)

这是Anthropic协议中与OpenAI最显著的差异之一:Anthropic将工具的执行结果作为user角色消息的一部分发送回模型。具体来说,它是一个user消息中的content数组里的tool_result类型块。

tool_result块包含:

  • type (string, required): 固定为"tool_result"
  • tool_use_id (string, required): 必须与之前LLM生成的tool_use中某个工具调用的id匹配。
  • content (string | list[ContentBlock], required): 工具函数执行后的结果。可以是纯字符串,也可以是包含其他内容块的数组(例如,如果工具返回了图片或其他复杂结构)。通常,我们会将JSON结果序列化为字符串。

关键点: Anthropic没有独立的tool角色。它将工具执行结果视为“用户提供了新的信息”(即工具执行的结果),这使得对话流保持在userassistant之间交替,对一些人来说,这更符合“聊天”的自然模式。

4.5 强制工具调用

Anthropic的API通常通过在system提示或用户提示中给出明确指令来引导工具使用,而不是像OpenAI那样有专门的tool_choice参数来强制调用。模型会根据提示和提供的工具定义,自主决定是否以及如何调用工具。在某些场景下,可以通过精心设计的提示词来“引导”模型进行特定工具调用,但这不如OpenAI的tool_choice参数那样直接。

4.6 并行工具调用

与OpenAI类似,Anthropic的模型也能够在单个assistant响应的content数组中返回多个tool_use块。这表示模型可能一次性识别出多个可以并行执行的工具调用。

# ... (已在上述接收和解析部分体现) ...
# tool_calls_detected 列表可以包含多个 tool_use 块
if tool_calls_detected:
    for tool_call in tool_calls_detected:
        # ... 逐个处理或并行处理 ...

4.7 错误处理

在Anthropic的工具使用中,如果工具执行发生错误,应用程序应将错误信息作为tool_result块的content返回给LLM。LLM会像处理任何其他工具结果一样处理它,并尝试根据错误信息生成合适的响应。

示例:返回错误信息

# ... 当发生错误时 ...
try:
    function_response = function_to_call(**function_args)
    content_to_send = json.dumps(function_response, ensure_ascii=False)
except Exception as e:
    error_message = f"Error executing {function_name}: {str(e)}"
    print(f"  Tool Execution Error: {error_message}")
    content_to_send = json.dumps({"error": error_message}, ensure_ascii=False) # 或者直接发送 error_message 字符串

messages_weather_anthropic.append(
    {
        "role": "user",
        "content": [
            {
                "type": "tool_result",
                "tool_use_id": tool_call_id,
                "content": content_to_send
            }
        ]
    }
)

Anthropic的模型会读取这个错误信息,并通常会生成一个解释性的响应给用户。

五、 比较分析:OpenAI vs. Anthropic

现在,让我们通过一个表格和详细的讨论,来总结OpenAI和Anthropic在函数调用协议上的细微差异。

特性 OpenAI (GPT Models) Anthropic (Claude 3 Models) 差异解读
工具定义 tools 参数,内部键为 function.parameters tools 参数,内部键为 input_schema 命名差异,但底层都是JSON Schema,功能上几乎等效。OpenAI的type: "function"是强制的,Anthropic省略了顶层type字段,直接列出函数对象。
LLM生成工具调用 assistant 消息中的 tool_calls 数组 assistant 消息的 content 数组中的 tool_use 类型块 核心差异1: OpenAI使用独立的字段,Anthropic将其嵌入到content数组中。
工具参数 tool_calls[i].function.arguments 是一个 JSON字符串 tool_use.input 是一个 JSON对象 核心差异2: Anthropic的input直接就是解析好的JSON对象,开发者无需额外调用json.loads(),简化了客户端逻辑。OpenAI需要额外的解析步骤。
工具调用ID tool_calls[i].id tool_use.id 命名一致,功能相同,用于关联请求和结果。
提供工具结果 tool 角色消息,包含 tool_call_idcontent user 角色消息的 content 数组中的 tool_result 类型块 核心差异3: OpenAI引入了tool这一独立角色,清晰地表示工具的“发声”。Anthropic将工具结果视为“用户提供的信息”,将其置于user角色消息中,保持了user-assistant的对话交替模式。
工具结果内容 tool 消息的 content (字符串) tool_result 块的 content (字符串或内容块数组) 两者都接受字符串,Anthropic的content可以更灵活地支持多种内容块类型(如图像),但通常也用字符串承载JSON结果。
强制工具调用 tool_choice 参数 (none, auto, 特定函数) 主要通过提示词引导,API层面无直接参数强制 OpenAI提供了更直接的机制来控制模型的工具调用决策。Anthropic更依赖于模型的推理能力和提示词工程。
并行调用 tool_calls 数组可以包含多个条目 content 数组可以包含多个 tool_use 两者都支持并行调用,只是表示方式不同。应用程序都可以遍历这些调用并决定如何执行。
错误处理 tool 消息的 content 中包含错误信息 tool_result 块的 content 中包含错误信息 概念一致,都是将错误信息作为工具结果反馈给LLM,LLM进行后续处理。只是消息结构不同。
哲学/架构 将工具视为对话中的一个独立参与者 (tool 角色) 将工具操作视为用户提供信息的扩展 (user 角色中嵌入 tool_result) OpenAI的设计更倾向于将工具执行视为一个明确的、独立的步骤。Anthropic的设计更强调对话的流畅性,将工具的反馈自然地融入用户输入流,可能使其对多模态输入/输出的扩展更为顺滑。
开发体验 需要 json.loads() 解析参数,但 tool 角色清晰 参数直接是JSON对象,省去解析步骤;但需要遍历 content 数组寻找 tool_use 和在 user 消息中构建 tool_result 各有优劣,Anthropic的直接JSON对象减少了一个解析步骤,可能略微降低出错几率。OpenAI的独立tool角色对于追踪工具流可能更直观。

细微差异的深层影响:

  1. JSON字符串 vs. JSON对象: 这是最直接的差异。Anthropic在API层面就替开发者完成了JSON字符串到对象的解析,减少了客户端代码的负担,也降低了因解析错误导致的问题。OpenAI选择将原始JSON字符串传递给客户端,给予开发者更多的控制权(尽管通常都需要json.loads())。
  2. tool角色 vs. user角色嵌入:
    • OpenAI的tool角色:这种设计将工具执行结果视为一个独立的“实体”或“参与者”的“发声”。对话历史清晰地展示了用户、助手、工具之间的交互。这对于调试和理解LLM的决策路径可能更直观。
    • Anthropic在user角色中嵌入tool_result:这种设计将工具结果视为用户为LLM提供了额外的信息,以帮助LLM完成任务。这使得整个对话流始终保持在用户和助手之间的交替,可能更符合“聊天”的自然语境,尤其是在多模态场景下,用户可以提供文本、图像、工具结果等多种信息。

这些差异虽然细微,但在构建复杂的LLM应用时,可能会影响代码的整洁性、调试的便利性以及对未来扩展的考量。两种方法都有效,选择哪种更多取决于个人偏好、项目需求以及对LLM交互哲学的理解。

六、 高级概念与最佳实践

理解了底层协议后,我们还需要掌握一些高级概念和最佳实践,以构建更健壮、更智能的LLM应用。

6.1 优秀的工具Schema设计

清晰、准确的JSON Schema是函数调用成功的基石。

  • 详细的description 为每个函数和每个参数编写清晰的、富有描述性的description。LLM主要依靠这些描述来理解工具的用途和参数的含义。
  • 精确的typeformat 使用string, number, boolean, array, object等准确定义参数类型。对于字符串,可以使用format(如"email", "date", "uri")作为提示。
  • enumdefault 对于有限的选项,使用enum可以极大地提高LLM生成参数的准确性。default值可以减少LLM需要推理的负担。
  • required字段: 明确哪些参数是必需的。
  • 嵌套Schema: 如果参数本身是复杂对象,可以使用嵌套的properties来定义。

6.2 鲁棒的错误处理

工具执行中错误是不可避免的。

  • 区分LLM参数错误和工具执行错误:
    • LLM参数错误: LLM可能生成了不符合Schema的参数(例如,类型错误、缺少必需参数)。这通常在客户端解析时就能捕获。此时,可以将错误信息作为普通用户消息发送回LLM,告知它参数有误,让它尝试纠正。
    • 工具执行错误: LLM生成的参数是正确的,但实际函数执行时出现了问题(例如,外部API宕机、数据库查询失败、权限不足)。此时,应将具体的错误信息作为工具结果反馈给LLM,让LLM决定如何向用户解释或尝试其他策略。
  • 详细的错误信息: 返回给LLM的错误信息应尽可能详细,包含错误类型、错误代码、错误消息等,帮助LLM更好地理解并作出响应。
  • 重试机制: 对于瞬时错误(如网络问题),可以在应用程序层面实现重试逻辑。

6.3 工具编排(Tool Orchestration)

复杂的任务可能需要一系列工具调用,甚至涉及条件判断和循环。

  • 链式调用: 一个工具的结果成为下一个工具的输入。LLM可以在接收到第一个工具结果后,根据需要生成第二个工具调用。
  • 并行调用: 多个独立的工具可以同时执行。
  • 用户确认: 对于敏感操作(如发送邮件、进行支付),在执行工具前向用户征求确认。这通常通过LLM生成一个确认消息,等待用户回复后再执行工具。
  • 回溯与修正: 当工具链中的某个环节出现问题时,LLM能够回溯并尝试不同的工具或参数。

6.4 状态管理

工具通常会改变应用程序的内部状态或查询外部状态。

  • 无状态与有状态: 设计工具时考虑它们是否会修改系统状态。查询工具通常是无状态的,而修改工具(如update_database)是有状态的。
  • 上下文一致性: 确保LLM在生成工具调用时,能够获取到最新的相关上下文信息。这意味着应用程序在每次请求时都需要将完整的对话历史(包括之前的用户消息、助手响应和工具结果)发送给LLM。

6.5 安全性考量

函数调用将LLM的能力扩展到外部系统,因此安全性至关重要。

  • 输入验证: 即使LLM生成了结构化的参数,应用程序仍需对这些参数进行严格的验证,以防LLM“幻觉”出不安全或恶意的数据,或者用户通过提示词注入恶意指令。
  • 权限控制: 确保LLM调用的工具只能访问其所需的最小权限资源。
  • 敏感信息处理: 避免将敏感的API密钥、数据库凭证等直接暴露给LLM。工具函数在后端安全地处理这些信息。
  • 审计与日志: 记录所有工具调用及其结果,便于审计和问题排查。

6.6 可观测性

在生产环境中,理解工具调用的行为至关重要。

  • 日志记录: 记录每一次工具调用的请求、参数、执行结果以及任何错误。
  • 监控: 监控工具的执行时间、成功率、错误率,以及LLM调用工具的频率。
  • 可追溯性: 确保能够轻松追踪从用户请求到最终响应的完整工具调用链。

七、 函数调用的未来展望

函数调用只是LLM迈向通用人工智能的一小步,但却是至关重要的一步。未来,我们可以期待:

  • 更强大的推理能力: LLM将能更智能地选择工具,理解工具的副作用,并进行更复杂的规划。
  • 自我修正: 模型在接收到工具执行错误时,能够更好地诊断问题并自行调整参数或选择其他工具。
  • 多模态工具: 不仅限于文本输入/输出,工具可以处理和生成图像、音频、视频等多种模态的数据。
  • Agentic行为的普及: 更多的应用将基于LLM Agent架构构建,LLM能够自主地进行任务分解、工具选择、执行和结果评估。
  • 标准化与互操作性: 随着不同提供商的API不断演进,可能会出现更统一的工具协议标准,简化跨平台开发。

总结

函数调用机制,无论是OpenAI的tool_calls还是Anthropic的tool_use,都将大语言模型从单纯的文本生成器提升为能与真实世界交互的强大代理。虽然两者在消息结构上存在细微差异,反映了各自的设计哲学,但其核心功能——将LLM的推理能力与外部工具的执行能力相结合——是殊途同归的。深入理解这些协议,并结合最佳实践,是构建下一代智能应用的关键。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注