大模型只会聊天?那是你还没用 Function Calling。本文从原理到实战,带你搞懂 Function Calling 的核心机制,并用 Java 实现完整的调用流程。


一、前言

你有没有遇到过这样的场景:

  • 用户问「北京今天天气怎么样?」,大模型只能回答「我无法获取实时数据」

  • 你想让 AI 帮你查数据库、调接口、发邮件,但它只会生成文本

  • 你费劲写了一堆 Prompt 让模型输出 JSON,结果格式千奇百怪

Function Calling 就是来解决这些问题的。

它让大模型从一个「只会说话的嘴」,变成了「能指挥工具的手」。


二、Function Calling 原理与工作流

2.1 什么是 Function Calling?

Function Calling(函数调用)是 OpenAI 在 2023 年 6 月推出的一项能力,允许大模型在对话过程中识别用户意图,并输出结构化的函数调用请求

核心要点:

  • 模型不执行函数,它只告诉你:「我想调用哪个函数,传什么参数」

  • 实际执行权在你的代码,你拿到模型的输出后,自己决定要不要执行、怎么执行

  • 执行完后,把结果喂回模型,模型再生成最终的自然语言回答

💡 一句话总结:大模型负责「想」,你的代码负责「做」。

2.2 为什么需要 Function Calling?

在没有 Function Calling 之前,我们要让模型调用工具,通常有两种方式:

方式一:Prompt 硬解析

请以如下 JSON 格式输出你的需求:
{"function": "xxx", "params": {"city": "北京"}}

问题:模型输出的 JSON 经常格式不对、多一个逗号、少一个引号,解析起来噩梦一般。

方式二:用 LangChain 等框架

框架帮你做了工具绑定和解析,但引入了额外的抽象层,调试困难,且强依赖框架。

Function Calling 的优势:

  • 原生支持,API 层面就定义好了工具 schema

  • 结构化输出,模型返回标准 JSON,不需要你正则匹配

  • 多工具并行,一次请求可以调用多个函数

  • 模型自主决策,它会自己判断要不要调用、调哪个

2.3 工作流全景

整个 Function Calling 的流程可以分为 6 步

┌─────────────┐
│  用户提问     │  "北京天气怎么样?"
└──────┬──────┘
       ▼
┌─────────────┐
│  LLM 分析意图 │  模型判断:需要调用 getWeather 函数
└──────┬──────┘
       ▼
┌─────────────────────┐
│  模型输出函数调用请求   │  {"name": "getWeather", "arguments": {"city": "北京"}}
└──────┬──────────────┘
       ▼
┌─────────────┐
│  应用层执行函数 │  你调用天气 API,拿到结果
└──────┬──────┘
       ▼
┌─────────────────┐
│  结果喂回模型     │  {"temp": "28°C", "condition": "晴"}
└──────┬──────────┘
       ▼
┌───────────────────┐
│  模型生成最终回答    │  "北京今天 28°C,天气晴朗,适合出行。"
└───────────────────┘

注意,第 3 步和第 5 步之间发生了两次 API 调用

调用次数方向目的
第 1 次你 → OpenAI发送用户消息 + 工具定义,模型决定是否调用工具
第 2 次你 → OpenAI把工具执行结果喂回,模型生成最终回答

三、OpenAI Function Calling API 的工作流

3.1 定义工具(Tool Schema)

首先,你需要告诉模型「你有哪些工具可以用」。工具用 JSON Schema 描述:

{
  "type": "function",
  "function": {
    "name": "getWeather",
    "description": "查询指定城市的天气信息",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "城市名称,如:北京、上海"
        }
      },
      "required": ["city"]
    }
  }
}

几个关键字段:

  • name:函数名,模型会用这个名字来调用

  • description:函数描述,非常重要——模型靠它理解什么时候该用这个工具

  • parameters:参数的 JSON Schema,定义类型、描述、是否必填

⚠️ description 写得好不好,直接影响模型的调用准确率。把它当成给新同事写的 API 文档。

3.2 Java 完整实现

下面是用 OpenAI Java SDK 实现的完整流程:

import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.chat.completions.*;
import com.openai.models.*;
​
import java.util.*;
​
public class FunctionCallingDemo {
​
    private static final OpenAIClient client = OpenAIOkHttpClient.builder()
            .apiKey(System.getenv("OPENAI_API_KEY"))
            .build();
​
    public static void main(String[] args) {
​
        // ====== 第一步:定义工具 ======
        List<ChatCompletionTool> tools = List.of(
            ChatCompletionTool.builder()
                .type(ChatCompletionTool.Type.FUNCTION)
                .function(FunctionDefinition.builder()
                    .name("getWeather")
                    .description("查询指定城市的天气信息")
                    .parameters(JsonObjectSchema.builder()
                        .type(JsonObjectSchema.Type.OBJECT)
                        .addProperty("city", JsonStringSchema.builder()
                            .type(JsonStringSchema.Type.STRING)
                            .description("城市名称")
                            .build())
                        .required(List.of("city"))
                        .build())
                    .build())
                .build()
        );
​
        // ====== 第二步:构建对话消息 ======
        List<ChatCompletionMessageParam> messages = new ArrayList<>();
        messages.add(ChatCompletionMessageParam.ofChatCompletionUserMessageParam(
            UserMessage.builder().content("北京今天天气怎么样?").build()
        ));
​
        // ====== 第三步:第一次调用 —— 让模型决定是否调用工具 ======
        ChatCompletion completion = client.chatCompletions().create(
            ChatCompletionCreateParams.builder()
                .model("gpt-4")
                .messages(messages)
                .tools(tools)
                .build()
        );
​
        ChatCompletionMessage message = completion.choices().get(0).message();
​
        // ====== 第四步:检查模型是否要调用工具 ======
        if (message.toolCalls().isPresent()) {
            for (ToolCall toolCall : message.toolCalls().get()) {
                String funcName = toolCall.function().name();
                String argumentsJson = toolCall.function().arguments();
​
                System.out.println("模型请求调用: " + funcName);
                System.out.println("参数: " + argumentsJson);
​
                // ====== 第五步:执行本地函数 ======
                String result = executeFunction(funcName, argumentsJson);
                System.out.println("执行结果: " + result);
​
                // 将模型消息和工具结果加入对话历史
                messages.add(ChatCompletionMessageParam.ofChatCompletionAssistantMessageParam(message));
                messages.add(ChatCompletionMessageParam.ofChatCompletionToolMessageParam(
                    ToolMessage.builder()
                        .toolCallId(toolCall.id())
                        .content(result)
                        .build()
                ));
            }
​
            // ====== 第六步:第二次调用 —— 模型生成最终回答 ======
            ChatCompletion finalCompletion = client.chatCompletions().create(
                ChatCompletionCreateParams.builder()
                    .model("gpt-4")
                    .messages(messages)
                    .tools(tools)
                    .build()
            );
​
            System.out.println("最终回答: " +
                finalCompletion.choices().get(0).message().content());
        } else {
            // 模型直接回答,无需调用工具
            System.out.println("直接回答: " + message.content());
        }
    }
​
    // ====== 函数执行器 ======
    private static String executeFunction(String name, String argumentsJson) {
        // 实际项目中用 Jackson/Gson 解析
        switch (name) {
            case "getWeather":
                // 这里模拟调用天气 API
                return """
                    {"city": "北京", "temperature": "28°C", "condition": "晴", "humidity": "45%%"}
                    """;
            default:
                return "{\"error\": \"未知函数: " + name + "\"}";
        }
    }
}

3.3 运行结果

模型请求调用: getWeather
参数: {"city":"北京"}
执行结果: {"city": "北京", "temperature": "28°C", "condition": "晴", "humidity": "45%"}
最终回答: 北京今天天气晴朗,气温 28°C,湿度 45%,非常适合户外活动。

3.4 并行工具调用(Parallel Function Calling)

当用户的问题需要调用多个工具时,模型可以一次返回多个工具调用请求

// 用户问:"北京和上海今天天气分别怎么样?"
// 模型可能一次返回两个 tool_calls:
// ToolCall 1: getWeather(city="北京")
// ToolCall 2: getWeather(city="上海")
​
if (message.toolCalls().isPresent()) {
    for (ToolCall toolCall : message.toolCalls().get()) {
        // 逐个执行,结果都加到 messages 里
        String result = executeFunction(
            toolCall.function().name(),
            toolCall.function().arguments()
        );
        messages.add(/* tool message */);
    }
    // 最后一次调用,模型综合所有结果生成回答
}

四、Function Calling 与传统 API 调用的区别

很多人会问:「这不就是封装了一层 API 调用吗?我自己写 if/else 也能做到。」

来,我们对比一下:

4.1 架构对比

传统方式:

用户输入 → 你写正则/NLU 解析意图 → if-else 路由 → 调用 API → 拼接回答

Function Calling 方式:

用户输入 → 模型理解意图 + 输出结构化调用 → 你执行 → 模型生成回答

4.2 详细对比

维度传统 API 调用Function Calling
意图识别你写规则/正则/NLU 模型大模型原生能力,零代码
参数提取你自己解析,容易出错模型直接输出 JSON,结构化
多轮对话你维护上下文状态机模型自动理解上下文
新增工具改路由逻辑、加 if-else写一个 JSON Schema 就行
多工具协同复杂的编排逻辑模型自动决定调用顺序和组合
错误处理你写所有边界情况模型会根据描述合理使用
开发成本高,每个意图都要写代码低,定义 schema 即可
灵活性固定逻辑,难以扩展模型可处理未预见的表达方式

4.3 举个实际例子

假设你要做一个智能客服,支持查订单、查物流、退款。

传统方式:

// 你得写一堆规则
if (input.contains("订单") && input.contains("查")) {
    return handleOrderQuery(parseOrderId(input));
} else if (input.contains("物流") || input.contains("快递")) {
    return handleLogisticsQuery(parseTrackingNumber(input));
} else if (input.contains("退款") || input.contains("退货")) {
    return handleRefund(parseOrderId(input));
} else {
    return "抱歉,我没听懂";
}

用户说「我上周买的那个东西到哪了?」——你的规则匹配不上。

Function Calling 方式:

你只需要定义三个工具的 schema,然后把用户原话丢给模型。模型会自动判断:

{
  "name": "queryLogistics",
  "arguments": {"order_id": "用户上周的订单", "time_range": "last_week"}
}

用户换个说法「快递走到哪了」「我的包裹呢」,模型都能正确理解。

4.4 本质区别

传统方式中,你既是架构师又是工人——你得理解用户意图、提取参数、路由到正确的函数。

Function Calling 中,模型是架构师,你是工人——模型理解意图、提取参数、决定调什么,你只负责执行。

🎯 这不是「更好的正则表达式」,而是范式转变:从「代码驱动」到「意图驱动」。


五、最佳实践与踩坑指南

5.1 工具描述要写好

// ❌ 差的描述
.description("查天气")
​
// ✅ 好的描述
.description("查询指定城市的当前天气信息,包括温度、天气状况、湿度。当用户询问某个城市的天气时调用此函数。")

模型靠 description 决定什么时候调用,写得越清楚,调用越准确。

5.2 参数定义要精确

// ❌ 模糊的参数
.addProperty("date", JsonStringSchema.builder()
    .type(JsonStringSchema.Type.STRING)
    .build())
​
// ✅ 精确的参数
.addProperty("date", JsonStringSchema.builder()
    .type(JsonStringSchema.Type.STRING)
    .description("查询日期,格式为 yyyy-MM-dd,如 2024-01-15。默认为今天。")
    .build())

5.3 错误处理不能省

private static String executeFunction(String name, String argsJson) {
    try {
        switch (name) {
            case "getWeather":
                Map<String, Object> args = parseJson(argsJson);
                String city = (String) args.get("city");
                if (city == null || city.isBlank()) {
                    return "{\"error\": \"缺少必填参数: city\"}";
                }
                return weatherService.query(city);
            default:
                return "{\"error\": \"未注册的函数: " + name + "\"}";
        }
    } catch (Exception e) {
        return "{\"error\": \"执行异常: " + e.getMessage() + "\"}";
    }
}

5.4 tool_choice 参数

// auto —— 模型自己决定(默认)
.toolChoice(ChatCompletionToolChoice.AUTO)
​
// required —— 强制模型调用至少一个工具
.toolChoice(ChatCompletionToolChoice.REQUIRED)
​
// 指定某个工具 —— 强制调用特定函数
.toolChoice(ChatCompletionNamedToolChoice.builder()
    .type(ChatCompletionNamedToolChoice.Type.FUNCTION)
    .function(FunctionName.builder().name("getWeather").build())
    .build())

六、总结

你以前的做法现在的做法
写正则解析用户输入模型自己理解意图
if-else 路由到不同函数定义 schema,模型自动选择
自己拼接回答模型生成自然语言回答
加功能改代码加一个 JSON Schema

Function Calling 不是银弹,它适合:

  • ✅ 需要让 AI 调用外部工具(API、数据库、文件系统)

  • ✅ 需要结构化输出(JSON 而非自由文本)

  • ✅ 用户意图多样,规则难以穷举

不太适合:

  • ❌ 简单的关键词匹配(杀鸡用牛刀)

  • ❌ 对延迟极其敏感的场景(两次 API 调用)

  • ❌ 需要 100% 确定性的逻辑(模型有概率出错)


如果觉得有帮助,点个 👍 收藏一下,有问题评论区见!

更多推荐