大模型 Function Call

Function call(在MCP场景也叫 tool call)提供了一种让大模型与我们的代码或者外部服务进行交互的方法,这种方式强大且灵活。
本文主要讲解如何让大模型与我们的自定义代码进行交互,以实现获取数据或者采取行动。
如下是三个Function call示例,依次是获取天气预报、发送邮件和搜索本地知识库。

获取天气预报:

from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current temperature for a given location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and country e.g. Bogotá, Colombia"
                }
            },
            "required": [
                "location"
            ],
            "additionalProperties": False
        },
        "strict": True
    }
}]

completion = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "What is the weather like in Paris today?"}],
    tools=tools
)

print(completion.choices[0].message.tool_calls)

运行结果:

[{
    "id": "call_12345xyz",
    "type": "function",
    "function": {
        "name": "get_weather",
        "arguments": "{\"location\":\"Paris, France\"}"
    }
}]

发送电子邮件:

from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "send_email",
        "description": "Send an email to a given recipient with a subject and message.",
        "parameters": {
            "type": "object",
            "properties": {
                "to": {
                    "type": "string",
                    "description": "The recipient email address."
                },
                "subject": {
                    "type": "string",
                    "description": "Email subject line."
                },
                "body": {
                    "type": "string",
                    "description": "Body of the email message."
                }
            },
            "required": [
                "to",
                "subject",
                "body"
            ],
            "additionalProperties": False
        },
        "strict": True
    }
}]

completion = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "Can you send an email to ilan@example.com and katia@example.com saying hi?"}],
    tools=tools
)

print(completion.choices[0].message.tool_calls)

运行结果:

[
    {
        "id": "call_9876abc",
        "type": "function",
        "function": {
            "name": "send_email",
            "arguments": "{\"to\":\"ilan@example.com\",\"subject\":\"Hello!\",\"body\":\"Just wanted to say hi\"}"
        }
    },
    {
        "id": "call_9876abc",
        "type": "function",
        "function": {
            "name": "send_email",
            "arguments": "{\"to\":\"katia@example.com\",\"subject\":\"Hello!\",\"body\":\"Just wanted to say hi\"}"
        }
    }
]

搜索本地知识库:

from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "search_knowledge_base",
        "description": "Query a knowledge base to retrieve relevant info on a topic.",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The user question or search query."
                },
                "options": {
                    "type": "object",
                    "properties": {
                        "num_results": {
                            "type": "number",
                            "description": "Number of top results to return."
                        },
                        "domain_filter": {
                            "type": [
                                "string",
                                "null"
                            ],
                            "description": "Optional domain to narrow the search (e.g. 'finance', 'medical'). Pass null if not needed."
                        },
                        "sort_by": {
                            "type": [
                                "string",
                                "null"
                            ],
                            "enum": [
                                "relevance",
                                "date",
                                "popularity",
                                "alphabetical"
                            ],
                            "description": "How to sort results. Pass null if not needed."
                        }
                    },
                    "required": [
                        "num_results",
                        "domain_filter",
                        "sort_by"
                    ],
                    "additionalProperties": False
                }
            },
            "required": [
                "query",
                "options"
            ],
            "additionalProperties": False
        },
        "strict": True
    }
}]

completion = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "Can you find information about ChatGPT in the AI knowledge base?"}],
    tools=tools
)

print(completion.choices[0].message.tool_calls)

运行结果:

[{
    "id": "call_4567xyz",
    "type": "function",
    "function": {
        "name": "search_knowledge_base",
        "arguments": "{\"query\":\"What is ChatGPT?\",\"options\":{\"num_results\":3,\"domain_filter\":null,\"sort_by\":\"relevance\"}}"
    }
}]

从示例中可以看出,大模型对 Function call 的输入和输出都是数组,支持多个候选Function,已经执行多个Function。
对于输入,每个Function的信息是数组的中的一个元素,主要信息以JSON格式存储于function字段,其中的key含义:

  • name,函数名称
  • description,函数作用描述
  • parameters,函数入参,其中包括入参的类型(Object,string等),如果是Object则需要列举出其属性并放在 properties 中,必填属性放在 required 字段
  • strict

对于输出结果,存放在 completion.choices[0].message.tool_calls 中。其中包括要调用的方法名称,以及组装好的入参存放在 arguments 字段。

概述

借助 Function call,我们可以授权大模型执行我们的自定义代码。基于系统提示词(system prompt)和用户提示词,大模型可以自助决策要调用哪些函数,而不仅仅是生成文本或音频。
然后你可以完成函数调用并将执行结果再次输入给大模型,大模型会将这些函数执行结果连同之前的输入整合后梳理给你一个最终结果。
在这里插入图片描述

Function call有两种主要的应用场景:

场景明细
读取数据获取最新数据以整合进入大模型的输出结果(RAG)。用于搜索本地知识库和通过API(比如获取天气数据)获取领域数据
采取行动执行动作,比如提交表单,调用API,更改原因状态数据,或者执行类Agent的工作流

示例

我们来详细看下获取天气预报这个例子。首先我们定义一个获取天气预报的函数:

import requests

def get_weather(latitude, longitude):
    response = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m")
    data = response.json()
    return data['current']['temperature_2m']

跟前文中的示例不同,本函数的入参不是地名location,而是经纬度。但是,大模型可以自动的将很多城市名称转换成经纬度。

函数调用步骤

第一步,将函数定义信息连同提示词一起输入给大模型:

from openai import OpenAI
import json

client = OpenAI()

tools = [{
    "type": "function",
    "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
    }
}]

messages = [{"role": "user", "content": "What's the weather like in Paris today?"}]

completion = client.chat.completions.create(
    model="gpt-4.1",
    messages=messages,
    tools=tools,
)

第二步,大模型决定要调用的函数并返回函数名称和入参,具体数据存在 completion.choices[0].message.tool_calls 中:

[{
    "id": "call_12345xyz",
    "type": "function",
    "function": {
      "name": "get_weather",
      "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
    }
}]

第三步,执行函数调用,解析大模型的返回结果并手动执行函数调用:

tool_call = completion.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)

result = get_weather(args["latitude"], args["longitude"])

第四步,将函数执行结果输入给大模型,大模型会把执行结果整合进最终的输出结果:

messages.append(completion.choices[0].message)  # append model's function call message
messages.append({                               # append result message
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": str(result)
})

completion_2 = client.chat.completions.create(
    model="gpt-4.1",
    messages=messages,
    tools=tools,
)

第五步,大模型给出最终结果,结果存放在 completion_2.choices[0].message.content中:

"The current temperature in Paris is 14°C (57.2°F)."

函数定义的最佳实践

  • 编写清晰详细的函数名称、入参描述和函数功能描述
    • 显式的描述函数功能,以及每个入参的含义包括格式,以及函数返回值的含义
    • 使用系统提示词去描述何时该使用以及何时不该使用函数,一般要精确告诉大模型该做什么
    • 包括样例和边界case,特别是处理可能重复出现的失败case(注意:添加示例可能降低推理模型的性能)
  • 使用软件工程最佳实践
    • 确保函数职责明确且符合直觉(符合POLA原则
    • 使用枚举和对象来避免非法调用(比如,toggle_light(on: bool, off: bool) 就可能出现非法调用)
    • 通过实习生测试,把输入给大模型的函数信息展示给一个实习生或者正常人,他们能正常使用这个函数吗?如果不能,关于这个函数他们会问你什么问题?把这些问题的答案添加到提示词中
  • 给大模型减负,能用代码的尽量用代码
    • 不要把你已经知道的参数输入给大模型,比如如果基于之前的菜单你已经可以知道 order_id 了,就不要使用 order_id 参数了,而应该使用无参的 submit_refund() 然后使用代码传入 order_id 参数
    • 合并严格串行的函数,比如,如果你在调用 query_location() 之后总是会调用 mark_location(),那么你应该把 mark_location() 的代码合并到 query_location() 里面
  • 为了更高的准确性,保持函数数量在低位水平
    • 评估不同函数数量时的性能
    • 尽量在任何时候函数数量都不超过20个,虽然这只是一个参考建议
  • 使用 OpenAI 资源

Token消耗

本质上,Function call会以某种方式被注入到系统提示词中,也就是说Function call是模型上下文的一部分并且算入输入Token。如果遇到Token数量限制,建议降低函数数量或者精简函数入参描述信息。
如果函数数量较多,同样建议使用微调来降低Token消耗量。

处理函数调用

当大模型返回一个函数调用,我们必须手动去执行这个函数调用然后返回结果。鉴于大模型返回的函数可能包含0个、1个或者多个,所以最佳实践是多个处理。
大模型返回结果tool_calls是个数组,每个元素包含一个id(后续在提交函数返回值时会用到这个id)和一个 function字段,该字段值包括 name 和 json 格式的入参。
一个典型的包含多个函数的大模型返回值如下:

[
    {
        "id": "call_12345xyz",
        "type": "function",
        "function": {
            "name": "get_weather",
            "arguments": "{\"location\":\"Paris, France\"}"
        }
    },
    {
        "id": "call_67890abc",
        "type": "function",
        "function": {
            "name": "get_weather",
            "arguments": "{\"location\":\"Bogotá, Colombia\"}"
        }
    },
    {
        "id": "call_99999def",
        "type": "function",
        "function": {
            "name": "send_email",
            "arguments": "{\"to\":\"bob@email.com\",\"body\":\"Hi bob\"}"
        }
    }
]

依次执行函数并累计记录执行结果:

for tool_call in completion.choices[0].message.tool_calls:
    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    result = call_function(name, args)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": str(result)
    })

上面的代码中包含一个伪代码 call_function 来路由转发每个函数调用,其可能的实现可以是:

def call_function(name, args):
    if name == "get_weather":
        return get_weather(**args)
    if name == "send_email":
        return send_email(**args)

格式化结果

函数执行结果必须是string类型,但具体格式可以由我们自定义(JSON,错误码,纯文本等)。大模型会根据实际需要来翻译这个字符串结果。
如果函数没有返回值(比如 send_email()),那只需简单的返回一个成功或者失败的字符串即可(比如 “success”)。

将函数返回值整合进大模型输出

在将函数执行结果添加到 messages 参数中后,我们就能把这些结果重新输入给大模型,从而得到一个最终的输出结果:

completion = client.chat.completions.create(
    model="gpt-4.1",
    messages=messages,
    tools=tools,
)

最终输入结果示例:

"It's about 15°C in Paris, 18°C in Bogotá, and I've sent that email to Bob."

其他配置

工具选择

大模型默认会自助决定何时使用以及使用多少个函数。同时我们也可以通过 tool_choice 参数来强制指定这些行为。

  • Auto,默认行为,调用0个、1个或者多个函数,tool_choice:"auto"
  • Required,调用1个或者多个函数,tool_choice:"required"
  • Forced Function,指定调用某个函数,tool_choice: {"type": "function", "function": {"name": "get_weather"}}
    在这里插入图片描述

我们也可以将 tool_choice 设置为none来模拟无函数。

并行函数调用

大模型可能决定再一次对话中调用多个函数。我们可以通过将 parallel_tool_calls 设置为 false 来避免这个问题,这样可以保证最多只调用一个函数。
注意:截至当前,如果大模型在一次对话调用多个函数,那么这些函数调用的严格模式将被禁用。

严格模式

strict 设置为 true 会保证函数调用严格遵循函数说明,而不是尽量保证。建议使用开启严格模式。
本质上,严格模式底层的工作原理是利用了结构化输出能力,所以使用严格模式需要遵循如下条件:

  • paramters粒粒面的每个对象的 additionalProperties 都必须设置成 false
  • properties 里面的所有字段都必须是 required
    对于非必填字段,我们可以给添加 null 类型:

开启严格模式:

{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Retrieves current weather for the given location.",
        "strict": true,
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and country e.g. Bogotá, Colombia"
                },
                "units": {
                    "type": ["string", "null"],
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Units the temperature will be returned in."
                }
            },
            "required": ["location", "units"],
            "additionalProperties": false
        }
    }
}

关闭严格模式:

{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Retrieves current weather for the given location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and country e.g. Bogotá, Colombia"
                },
                "units": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Units the temperature will be returned in."
                }
            },
            "required": ["location"],
        }
    }
}

尽管我们推荐开启严格模式,但是它还是有一些限制的:

  • 不支持JSON的一些特性(参加支持的格式
  • 在第一次请求时格式会经历额外的处理并被缓存。如果每次请求的格式都不相同,会增加响应延迟
  • 格式处于性能原因被缓存,不适用于 zero data retention

流式输出

流式输出用于需要实时展示函数参数生成过程的场景。
打开流式输出的方法很简单,仅需将 stream 参数设置成 true 即可,然后我们就可以在 delta 对象中得到流式数据块。

from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current temperature for a given location.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and country e.g. Bogotá, Colombia"
                }
            },
            "required": ["location"],
            "additionalProperties": False
        },
        "strict": True
    }
}]

stream = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "What's the weather like in Paris today?"}],
    tools=tools,
    stream=True
)

for chunk in stream:
    delta = chunk.choices[0].delta
    print(delta.tool_calls)

上述代码执行结果如下:

[{"index": 0, "id": "call_DdmO9pD3xa9XTPNJ32zg2hcA", "function": {"arguments": "", "name": "get_weather"}, "type": "function"}]
[{"index": 0, "id": null, "function": {"arguments": "{\"", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": "location", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": "\":\"", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": "Paris", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": ",", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": " France", "name": null}, "type": null}]
[{"index": 0, "id": null, "function": {"arguments": "\"}", "name": null}, "type": null}]
null

可以看出,输出结果并未被聚合好之后存入 content 字段,而是以 arguments 字段的形式陆续给出的。
当一个或多个函数被调用时,delta.tool_calls 里面包含如下字段:

字段作用
index表示当前被调用的函数
id函数调用id
functionfunction call delta(包括名称和参数)
typetool_call 的类型(原因是 function)

上面的很多字段只有在第一个delta元素中被赋值,包括 id、function.name和type。
下面的代码展示了如何把 delta 聚合成一个最终的 tool_call 对象:

final_tool_calls = {}

for chunk in stream:
    for tool_call in chunk.choices[0].delta.tool_calls or []:
        index = tool_call.index

        if index not in final_tool_calls:
            final_tool_calls[index] = tool_call

        final_tool_calls[index].function.arguments += tool_call.function.arguments

最终聚合的结果是:

{
    "index": 0,
    "id": "call_RzfkBpJgzeR0S242qfvjadNe",
    "function": {
        "name": "get_weather",
        "arguments": "{\"location\":\"Paris, France\"}"
    }
}

参考文献

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值