Function calling是什么:
Function calling provides a powerful and flexible way for OpenAI models to interface with your code or external services.。
个人理解下来就是LLM只有一张嘴,Function calling就是给这张嘴配上一些外部的能力(发邮件, 查db,搜网络等等)。
Function calling的流程:
这里附上一张OPENAI官方的流程图:
流程图中一共5个步骤,分别解析如下:
-
开发者将用户的 "问题" 和 “我这有什么工具” 告诉大模型。
-
大模型根据上一步中的“问题”分析出要调用哪几个工具,将工具调的参数及工具名字返回给开发者。
-
开发者根据参数调用对应的工具。
-
开发者将”调用工具的返回信息“ 和 “之前的所有信息”再次返回给大模型。
-
大模型综合所有信息回答最初的”问题“。
下面是openAI官网中的代码片段,展示了如何从代码层面接入一个function calling,代码中的注释对应着步骤1~5每个步骤。
from openai import OpenAI
import json
client = OpenAI()
tools = [{
"type": "function",
"name": "get_weather",
"description": "Get current temperature for provided coordinates in celsius.",
"parameters": {
"type": "object",
"properties": {
"latitude": {"type": "number"},
"longitude": {"type": "number"}
},
"required": ["latitude", "longitude"],
"additionalProperties": False
},
"strict": True
}]
input_messages = [{"role": "user", "content": "What's the weather like in Paris today?"}]
## 步骤1: 将问题和工具传给大模型(tools中定义了工具的信息)
response = client.responses.create(
model="gpt-4.1",
input=input_messages,
tools=tools,
)
## 步骤2 大模型返回工具调用的信息
# 此时的response为:
#[{
# "type": "function_call",
# "id": "fc_12345xyz",
# "call_id": "call_12345xyz",
# "name": "get_weather",
# "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
#}]
## 步骤3 根据步骤2 中的参数以及工具名称调用工具, latitude为48.8566; longitude为:2.3522
result = get_weather(args["latitude"], args["longitude"])
## 步骤4 将步骤3的执行结果和之前的所有信息再次传递给大模型
input_messages.append({ # append result message
"type": "function_call_output",
"call_id": result.call_id,
"output": str(result)
})
## 步骤5 大模型“综上所述”返回最终结果
response_2 = client.responses.create(
model="gpt-4.1",
input=input_messages,
tools=tools,
)
print(response_2.output_text)
# "The current temperature in Paris is 14°C (57.2°F)."
部分平台内集成的可视化FC流程:(混元)
下图为function call的可视化配置,分为三部分:
- 最上部分是选择“决策模型”,就是将原始问题和工具信息第一次要给的大模型,对应上面的步骤1和步骤2中的模型。
- 中间部分是插件设置,可以添加多个,也就是需要执行的工具,对应步骤3(此处我开发了一个根据姓名查询员工基础信息的插件,hardcode了一个36岁年龄)。
- 最下面是润色大模型,就是对应步骤4和5对应的大模型。
运行效果如下:
可以看到大模型根据我的问题,知道要调用我提供给他的能力“get_info_by_name”,查到用户的年龄之后,再给润色模型生成最终的答案。
Function calling的问题:
在步骤1中每个模型有自己的方式,数据格式,字段名称,sdk暴露出的方法等等。
Openai:
tools = [{
"type": "function",
"name": "get_weather",
"description": "Get current temperature for provided coordinates in celsius.",
"parameters": {
"type": "object",
"properties": {
"latitude": {"type": "number"},
"longitude": {"type": "number"}
},
"required": ["latitude", "longitude"],
"additionalProperties": False
},
"strict": True
}]
input_messages = [{"role": "user", "content": "What's the weather like in Paris today?"}]
response = client.responses.create(
model="gpt-4.1",
input=input_messages,
tools=tools,
)
Germini:
get_product_sku = "get_product_sku"
get_product_sku_func = FunctionDeclaration(
name=get_product_sku,
description="Get the SKU for a product",
# Function parameters are specified in OpenAPI JSON schema format
parameters={
"type": "object",
"properties": {
"product_name": {"type": "string", "description": "Product name"}
},
},
)
# Specify another function declaration and parameters for an API request
get_store_location_func = FunctionDeclaration(
name="get_store_location",
description="Get the location of the closest store",
# Function parameters are specified in JSON schema format
parameters={
"type": "object",
"properties": {"location": {"type": "string", "description": "Location"}},
},
)
# Define a tool that includes the above functions
retail_tool = Tool(
function_declarations=[
get_product_sku_func,
get_store_location_func,
],
)
# Initialize Gemini model
model = GenerativeModel(
model_name="gemini-1.5-flash-001",
generation_config=GenerationConfig(temperature=0),
tools=[retail_tool],
)
# Start a chat session
chat = model.start_chat()
Llama:
api_request_json = {
"model": "llama3.1-70b",
"messages": [
{"role": "user", "content": "What is the weather like in London?"},
],
"functions": [
{
"name": "get_weather_info",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location"]
}
}
],
}
response = llama.run(api_request_json)
在步骤2中每个模型返回的关于调用的指令也不同:
openAI:
[{
"type": "function_call",
"id": "fc_12345xyz",
"call_id": "call_12345xyz",
"name": "get_weather",
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
}]
{
"candidates": [
{
"content": {
"role": "model",
"parts": [
{
"functionCall": {
"name": "get_current_weather",
"args": {
"location": "New Delhi"
}
}
},
{
"functionCall": {
"name": "get_current_weather",
"args": {
"location": "San Francisco"
}
}
}
]
},
}
],
}
{
'role': 'assistant',
'content': None,
'function_call': {
'name': 'get_email_summary',
'arguments': {
'value': 1,
'login': 'mail@mail.com'
}
}
}
所谓_上梁不正下梁歪_,在完整的function calling步骤中,前面的步骤1和2的不同,导致后面的各个步骤都不同,尤其是在给LLM开发对应的外部能力的时候(步骤3中调用的服务),开发者需要对每个模型的function calling都做兼容。
MCP是什么:
2024年11月底,由 Anthropic 推出的一种开放标准,旨在统一大型语言模型(LLM)与外部数据源和工具之间的通信协议。MCP 的主要目的在于解决当前 AI 模型因数据孤岛限制而无法充分发挥潜力的难题,MCP 使得 AI 应用能够安全地访问和操作本地及远程数据,为 AI 应用提供了连接万物的接口。
网上到处都在说什么“开放标准”,像“usb”一样,其实就是在function calling的基础上,解决了“上梁不正下梁歪”的问题,
-
标准了大模型发现工具和使用工具的方式(Tool discovery)。
-
标准了外部能力的开发范式以及大模型调用外部能力的方式(Invocation)。
-
将调用外部工具执行的结果做了统一格式化,最终再喂给大模型进行润色(Response handling)。
-
为调用外部工具之前提供了审批。
MCP咋标准化的?(下面只分析了tools的标准化)
为此我浅看了一下mcp sdk中的内容,答案是他根本没标准化,他只提供了标准化过程中的一些基本方法。(此处我用了“只”,本人才疏学浅无意冒犯)
假设你现在开发一个查询天气的方法(api调用),python中只需要使用sdk/server暴露出的装饰器@mcp.tool()来装饰你的方法,这个方法就被“标准化了”,对应的在sdk/client中的list_tools()方法就能返回这个工具的所有信息,包括作用,名称,参数。 这个标准化的方法就能够轻易的被大模型理解发现并调用。
list_tools返回了工具的信息。
然后通过list_tools拼接到prompt中的方式告诉大模型,我这边有了什么样的工具,每个工具该怎么调用,每个工具是干什么的等等。(下图为mcp sdk中example中的client)
当有了list_tools能拿到工具的信息之后,再要如何告诉大模型呢,这里我看了cline和continuedev的源码,发现有两种方式:
1.通过万能的prompt,将list_tools返回的工具信息拼接到prompt中,要求输出的指令格式。
下图为(Cline)中的prompts,也是通过
sdk/client中的list_tools拿到“标准化”之后的工具的信息,然后通过prompt再传给大模型:
为了验证上面的prmopt是否真的生效,我又本地利用ollama搭建了本地模型服务,cline指向本地服务之后打印出了喂给本地大模型的prmopt。
下图为cline指向本地ollama的服务配置:
下图为我在cline配置完成get_weather的mcp服务之后,询问"what is the weather like in Shenzhen"之后ollama打印出的被投喂的prompt内容:
**2.**做一层不同LLM之间的tools字段的格式映射。
我去看了continuedev中,没用使用prompt来进行tools的标准化,而是使用了更粗暴简单的方式,在不同大模型之间的字段映射,
下面是germini的:
下面是bedrock的:
下面是ollama的:
上面的这些标准化的逻辑,都发生在LLM应用,也就是MCP定义的“host”中,作为mcp的sdk只给你提供了像是“list_tool”或者“call_tool”这样的原子方法。
这再次验证了,mcp的sdk中只提供了标准化过程中的一些原子方法。真正的“标准化”或者说抹平各个模型调用和发现工具的差异性是作为LLM应用开发者的你来完成的。
一点想法
-
mcp提供的sdk并没有帮你去“抹平”差异,它做的是提供了一些工具方法让“开发者”去抹平这个差异。
-
什么场景下某个应用是需要不断换大模型的?
-
越来越多的可视化流程编辑,可以使得function calling做的足够傻瓜化,反而使用mcp接入服务流程上需要大量的开发。
references: