从零构建大模型可调用的Skill:基于Function Calling的完整指南

1. 引言:什么是Skill,为什么需要Function Calling?

大语言模型(LLM)本身是一个静态的、基于训练数据的概率模型。它能够回答通用问题、生成文本,但无法主动获取实时信息(如天气、股票价格),也无法执行业务操作(如发送邮件、创建订单)。为了让大模型"动手做事",我们需要一种机制:模型输出结构化的指令,由外部程序执行这些指令,并将结果反馈给模型 。这种可被模型调用的外部能力单元,在本文中称为 Skill

OpenAI 的 Function Calling(函数调用)是目前最成熟、最广泛使用的Skill实现方式。它允许你在调用Chat Completions API时,传入一组可用的函数定义(JSON Schema),模型会判断何时需要调用某个函数,并以结构化JSON返回函数名和参数。你的应用程序收到这个指令后,执行对应的函数,再将结果传回模型,模型最终生成面向用户的自然语言回答。

本文将以Python为例,逐步教你如何创建一个可供大模型调用的Skill。你会学到从定义最简单的函数,到处理复杂参数、错误重试、并行调用等完整工程实践。


2. 核心概念:模型输出指令,你执行代码

在开始编码前,必须理解一个核心原则:大模型不执行任何代码,它只输出"调用意图"。整个过程分为四个阶段:

  1. 用户输入 → 你发送给模型的请求中包含用户消息和可用的函数定义。

  2. 模型决策 → 模型返回一个function_call对象(也可能返回普通文本)。

  3. 你执行 → 你的代码解析function_call,调用本地或远程函数,获得结果。

  4. 反馈模型 → 你把函数执行结果作为新的消息(角色为function)发回模型,模型据此生成最终回答。

这个循环保证了模型的可控性和安全性------所有实际的系统操作都由你编写的代码完成,模型只负责理解意图和生成回复。


3. 步骤一:定义函数(以Python为例)

3.1 最简单的函数

假设我们要做一个查询天气的Skill。首先,编写一个普通的Python函数:

python

复制代码
def get_current_weather(city: str) -> dict:
    """
    模拟从天气API获取温度。
    实际项目中可替换为requests调用真实服务。
    """
    # 这里模拟数据
    weather_data = {
        "北京": {"temperature": 22, "unit": "celsius", "condition": "晴"},
        "上海": {"temperature": 25, "unit": "celsius", "condition": "多云"},
        "深圳": {"temperature": 28, "unit": "celsius", "condition": "雨"},
    }
    return weather_data.get(city, {"temperature": "unknown", "condition": "未知"})

这个函数接受一个字符串参数city,返回一个字典。关键点是:函数的输入和输出都必须是可JSON序列化的,因为模型只理解JSON,并且你需要在网络传输中传递这些数据。

3.2 函数签名的重要性

为了让模型正确调用,函数应该做到:

  • 单一职责 :一个函数只做一件事,比如get_current_weather不要同时发邮件。

  • 参数类型明确 :使用类型注解(str, int, float, bool, List, Dict),这有助于生成Schema。

  • 提供清晰的文档字符串 :模型可能会读取函数描述(通过Schema中的description字段),但并非直接解析docstring,不过对你的维护者很有用。

3.3 支持其他语言(Node.js示例)

如果你使用Node.js,等价函数为:

javascript

复制代码
async function getCurrentWeather(city) {
    const weatherMap = {
        "北京": { temperature: 22, unit: "celsius", condition: "晴" },
        "上海": { temperature: 25, unit: "celsius", condition: "多云" }
    };
    return weatherMap[city] || { temperature: "unknown", condition: "未知" };
}

函数本身不依赖OpenAI SDK,任何语言都可以实现。


4. 步骤二:生成JSON Schema

OpenAI API要求你提供每个函数的JSON Schema,它描述了函数名、参数类型、是否必填、参数描述等。模型会根据这个Schema决定如何填充参数。

4.1 手动编写Schema

对于上面的get_current_weather,Schema如下:

json

复制代码
{
  "name": "get_current_weather",
  "description": "获取指定城市的当前天气信息",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "城市名称,例如:北京、上海"
      }
    },
    "required": ["city"],
    "additionalProperties": false
  }
}
  • name:必须与你的函数名完全一致(模型返回时会用它来标识)。

  • description:非常重要!模型据此判断何时调用该函数。写清楚函数的能力边界。

  • parameters:遵循JSON Schema规范。type: "object"表示参数是一个对象。

  • properties:每个参数的名称、类型、描述。支持string, number, integer, boolean, array, object

  • required:列出必须提供的参数名。

  • additionalProperties: false:禁止模型传入未定义的参数,提高严谨性。

4.2 自动生成Schema

手动写Schema容易出错,尤其当函数参数复杂时。推荐使用工具从函数签名自动生成。Python中可以使用pydanticinspect模块。OpenAI官方提供了一个openai库的辅助函数format_function_definitions,但更通用的是使用pydantic创建参数模型。

使用Pydantic(推荐)

python

复制代码
from pydantic import BaseModel, Field
from typing import List, Optional

class WeatherParams(BaseModel):
    city: str = Field(..., description="城市名称,例如:北京、上海")
    unit: Optional[str] = Field("celsius", description="温度单位,celsius或fahrenheit")

# 生成JSON Schema
params_schema = WeatherParams.model_json_schema()
print(params_schema)
# 然后手动组装完整的function定义

使用inspect模块(无需额外依赖)

python

复制代码
import inspect
import json

def get_function_schema(func):
    sig = inspect.signature(func)
    params = {}
    required = []
    for name, param in sig.parameters.items():
        param_type = "string"  # 简化处理,实际可映射
        params[name] = {"type": param_type, "description": f"参数 {name}"}
        if param.default == inspect.Parameter.empty:
            required.append(name)
    return {
        "name": func.__name__,
        "description": func.__doc__ or "",
        "parameters": {
            "type": "object",
            "properties": params,
            "required": required,
        },
    }

但这种方式无法获取参数的详细描述。更好的实践是维护一个FUNCTION_DEFINITIONS列表,由开发者手工编写或通过装饰器生成。

4.3 处理复杂类型

  • 数组"type": "array", "items": {"type": "string"}

  • 枚举"enum": ["low", "medium", "high"]

  • 嵌套对象"type": "object", "properties": {...}

  • 任意JSON"type": "object", "additionalProperties": true

示例:一个发送邮件的函数,参数包含收件人列表和可选附件。

json

复制代码
{
  "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": "邮件正文"},
      "attachments": {
        "type": "array",
        "items": {"type": "string"},
        "description": "附件URL列表(可选)"
      }
    },
    "required": ["to", "subject", "body"]
  }
}

5. 步骤三:在API请求中传入functions参数

当你调用OpenAI Chat Completions API时,在请求体中增加functions数组,每个元素就是上面定义的Schema。还可以设置function_call参数来控制模型是否强制调用某个函数。

5.1 基本请求示例

python

复制代码
import openai

openai.api_key = "your-api-key"

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",  # 支持function calling的模型
    messages=[
        {"role": "user", "content": "北京今天天气怎么样?"}
    ],
    functions=[
        {
            "name": "get_current_weather",
            "description": "获取指定城市的当前天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"]
            }
        }
    ],
    function_call="auto"  # 默认auto,模型自主决定是否调用
)

5.2 强制调用某个函数

如果你希望模型必须调用某个函数(比如在特定流程中),可以设置:

python

复制代码
function_call={"name": "get_current_weather"}

这样即使对话上下文不需要,模型也会返回一个function_call。但需要确保提供的参数合理(模型会尽力填充)。

5.3 多个函数的处理

你可以传入多个函数定义,模型会根据用户输入选择最合适的函数,也可能不调用任何函数直接回复文本。

python

复制代码
functions = [
    get_weather_schema,
    send_email_schema,
    search_web_schema
]

模型内部会进行语义匹配。为了提升准确性,务必为每个函数写清晰、详细的description,并在参数描述中包含示例值。


6. 步骤四:解析模型返回并执行函数

模型返回的响应是一个JSON对象。你需要检查response["choices"][0]["message"]中是否包含function_call字段。

6.1 解析function_call

python

复制代码
message = response["choices"][0]["message"]

if message.get("function_call"):
    function_name = message["function_call"]["name"]
    arguments_str = message["function_call"]["arguments"]  # 这是一个JSON字符串
    arguments = json.loads(arguments_str)
    print(f"模型要求调用函数: {function_name}, 参数: {arguments}")
else:
    # 模型直接回复文本
    print(message["content"])

6.2 执行本地函数

你需要维护一个从函数名到实际函数的映射字典:

python

复制代码
available_functions = {
    "get_current_weather": get_current_weather,
    "send_email": send_email,
}

if function_name in available_functions:
    func = available_functions[function_name]
    result = func(**arguments)  # 解包参数
else:
    result = {"error": f"未知函数: {function_name}"}

注意 :模型可能生成错误的参数(例如类型不匹配、缺少必填参数)。建议在调用前使用JSON Schema校验库(如jsonschema)对arguments进行验证,若不通过则向模型返回错误信息。

6.3 将结果返回给模型

函数执行完成后,你需要构造一条function角色的消息,添加到对话历史中,然后再次调用API让模型生成最终回答。

python

复制代码
# 追加函数执行结果
messages.append(message)  # 原模型的function_call消息
messages.append({
    "role": "function",
    "name": function_name,
    "content": json.dumps(result, ensure_ascii=False),  # 必须是字符串
})

# 第二次调用,这次不再传入functions(也可传入,但通常不需要)
second_response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=messages,
    # 不再需要functions,因为函数已经执行完毕
)
final_answer = second_response["choices"][0]["message"]["content"]
print(final_answer)

最终模型会基于函数返回的数据生成自然语言,例如:"北京今天天气晴,温度22摄氏度。"


7. 完整代码示例:一个可交互的对话循环

下面是一个完整的Python脚本,实现了带有Function Calling的对话循环,支持连续多轮调用。

python

复制代码
import openai
import json
import sys

openai.api_key = "your-api-key"

# ----- 定义函数实现 -----
def get_current_weather(city: str) -> dict:
    # 模拟实现
    weather_db = {
        "北京": {"temp": 22, "unit": "celsius", "condition": "晴"},
        "上海": {"temp": 25, "unit": "celsius", "condition": "多云"},
    }
    return weather_db.get(city, {"temp": "未知", "condition": "无数据"})

def send_email(to: str, subject: str, body: str) -> dict:
    # 模拟发送邮件
    print(f"[模拟发送] 收件人: {to}, 主题: {subject}, 正文: {body}")
    return {"success": True, "message_id": "mock-12345"}

# ----- 函数定义(Schema)-----
functions = [
    {
        "name": "get_current_weather",
        "description": "获取指定城市的天气情况",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "城市名,如北京"}
            },
            "required": ["city"]
        }
    },
    {
        "name": "send_email",
        "description": "发送电子邮件",
        "parameters": {
            "type": "object",
            "properties": {
                "to": {"type": "string", "format": "email", "description": "收件人邮箱"},
                "subject": {"type": "string", "description": "邮件主题"},
                "body": {"type": "string", "description": "邮件正文"}
            },
            "required": ["to", "subject", "body"]
        }
    }
]

# ----- 函数映射表 -----
available_funcs = {
    "get_current_weather": get_current_weather,
    "send_email": send_email,
}

def run_conversation(user_input):
    messages = [{"role": "user", "content": user_input}]
    
    # 首次请求
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
        functions=functions,
        function_call="auto"
    )
    
    message = response["choices"][0]["message"]
    messages.append(message)
    
    # 循环处理可能的多次函数调用
    while message.get("function_call"):
        func_name = message["function_call"]["name"]
        args = json.loads(message["function_call"]["arguments"])
        
        if func_name in available_funcs:
            result = available_funcs[func_name](**args)
        else:
            result = {"error": f"Function {func_name} not found"}
        
        messages.append({
            "role": "function",
            "name": func_name,
            "content": json.dumps(result, ensure_ascii=False)
        })
        
        # 再次请求模型,看是否还需要更多函数调用
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            messages=messages,
            functions=functions,
            function_call="auto"
        )
        message = response["choices"][0]["message"]
        messages.append(message)
    
    return message["content"]

if __name__ == "__main__":
    while True:
        query = input("你: ")
        if query.lower() in ["exit", "quit"]:
            break
        answer = run_conversation(query)
        print(f"AI: {answer}")

这个例子展示了处理连续多次函数调用的能力(比如模型先查天气,再根据结果发邮件)。注意,while循环可能无限进行,实际生产环境应设置最大迭代次数(如5次)。


8. 高级话题

8.1 异步Skill与长时间任务

有些Skill执行时间较长(如调用第三方API、生成报告),如果同步等待,会导致用户长时间无响应。解决方案:

  • 异步执行 :使用asyncio或后台任务队列(Celery)。模型第一次返回function_call后,立即返回一个"任务已提交"的中间消息,稍后通过Webhook或轮询获取结果。

  • 流式处理:OpenAI支持流式响应,但函数调用在流式模式下处理较复杂,建议非流式。

示例:启动一个后台线程执行函数,主线程返回占位符。

python

复制代码
import threading

def long_running_task(arg):
    # 耗时操作
    time.sleep(10)
    return {"result": "完成"}

def handle_function_call(func_name, args):
    if func_name == "long_task":
        thread = threading.Thread(target=long_running_task, args=(args,))
        thread.start()
        return {"status": "processing", "message": "任务已启动,稍后查询"}
    # ...

但更好的方式是引入任务ID和状态查询函数。

8.2 处理函数调用错误

模型可能生成无效参数,或函数执行抛出异常。必须优雅处理,将错误信息返回给模型,让模型可以修正。

python

复制代码
try:
    result = func(**args)
except TypeError as e:
    result = {"error": f"参数错误: {e}"}
except Exception as e:
    result = {"error": f"执行失败: {str(e)}"}

然后在function消息中返回result,模型通常会道歉并尝试重新调用或询问用户。

8.3 并行函数调用

OpenAI从2023年11月起支持并行函数调用 (parallel function calling)。模型可以在一次响应中返回多个function_call(放在一个数组里)。这适用于可以并发执行且相互独立的操作。

请求中需要设置parallel_tool_calls: true(默认开启)。模型返回的message中会包含tool_calls字段,每个元素是一个函数调用。

python

复制代码
# 响应示例
{
  "choices": [{
    "message": {
      "role": "assistant",
      "tool_calls": [
        {"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"北京\"}"}},
        {"id": "call_2", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"上海\"}"}}
      ]
    }
  }]
}

你的代码需要并发执行这些函数,然后将结果以tool消息(每个结果对应一个tool_call_id)返回。

python

复制代码
import concurrent.futures

tool_calls = message["tool_calls"]
with concurrent.futures.ThreadPoolExecutor() as executor:
    futures = []
    for tc in tool_calls:
        func_name = tc["function"]["name"]
        args = json.loads(tc["function"]["arguments"])
        futures.append(executor.submit(available_funcs[func_name], **args))
    results = [f.result() for f in futures]

# 构造多个tool消息
for tc, res in zip(tool_calls, results):
    messages.append({
        "role": "tool",
        "tool_call_id": tc["id"],
        "content": json.dumps(res)
    })

8.4 安全与权限控制

Skill可以访问敏感数据或执行写操作,必须严格限制。

  • 输入校验:对模型生成的参数进行白名单校验。例如城市名只能从列表中选,避免SQL注入(如果参数用于数据库查询)。

  • 操作权限:在函数内部检查调用者的身份(如通过API Key中的user_id)。不要依赖模型来判断权限。

  • 速率限制:对每个函数/每个用户设置调用频率上限。

  • 审计日志:记录每次函数调用的参数、结果和调用者,便于追溯。

8.5 跨模型兼容性

OpenAI的Function Calling格式是事实标准,但其他模型(如Anthropic Claude 3的Tool Use、Google Gemini的Function Calling)格式略有差异。若需多模型兼容,可以抽象一层适配器,将各自的工具定义转换为标准格式。


9. 最佳实践总结

  1. 函数粒度适中 :太细(如add_one)浪费模型调用次数;太粗(如process_order包含多个步骤)降低灵活性。推荐每个函数对应一个独立的业务操作。

  2. 描述要具体 :模型的决策高度依赖description。写清楚"何时使用"、"参数含义"、"返回值格式"。例如:"当用户询问某城市天气时调用此函数,不要用于查询历史天气。"

  3. 处理边缘情况:模型可能传入不存在的城市、负数价格等。函数应返回明确的错误信息,并允许模型重新尝试。

  4. 控制token消耗 :每个函数定义都会占用输入token。如果函数很多(>20),可考虑动态选择最相关的几个函数通过functions参数传入,或者使用工具检索(如Semantic Kernel)。

  5. 测试覆盖 :编写单元测试,模拟模型返回的各种function_call,确保你的执行逻辑正确。

  6. 从简单开始:先实现一个只调用一次函数的流程,再逐步添加多轮、并行等高级特性。


10. 结语

通过Function Calling为LLM构建可调用的Skill,是当前将大模型集成到业务系统的最主流、最可靠的方法。它保持了模型与执行环境的清晰边界,既发挥了模型的意图理解优势,又保证了业务操作的确定性和安全性。

本文从定义最简单的一个天气查询函数开始,逐步深入到复杂参数、错误处理、并行调用和生产级安全考量,为你提供了一套完整的工程实践指南。现在,你可以动手为自己的业务场景创建第一个Skill了------无论是查询数据库、发送通知,还是控制物联网设备,大模型都能成为你的智能调度中枢。

记住核心四步:定义函数 → 生成Schema → 请求API → 执行并反馈。熟练之后,你将能构建出真正"会动手"的AI应用。

相关推荐
py有趣2 小时前
力扣热门100题之螺旋矩阵
算法·leetcode
陈天伟教授2 小时前
六种人工智能模型
人工智能
清空mega2 小时前
动手学深度学习——边界框
人工智能
资深流水灯工程师2 小时前
FREERTOS整体架构
架构
永霖光电_UVLED2 小时前
美国能源部(DOE)发布“关键矿产与材料加速器”资助机会
人工智能
xiaoyaohou112 小时前
003、轻量化改进(一):网络剪枝原理与实战
算法·机器学习·剪枝
舒一笑2 小时前
技术圈爆火新词:Harness 工程,OpenAI 和 Anthropic 都在卷这个!
人工智能·程序员·设计
我是章汕呐2 小时前
政策评估的“黄金标准”:DID模型从原理到Stata实操
大数据·人工智能·经验分享·算法·回归
迷途小书童的Note2 小时前
Anthropic 把“AI团队管理“变成了一键服务:Claude Managed Agents解读
人工智能