Agent 开发入门(二):让模型学会用工具 — Function Calling

模型不只能说,还能做。Function Calling 让模型告诉你它想调什么函数,你来执行。

上一篇,我们跑通了第一次大模型 API 调用。模型能回答问题,能聊天,但它只能"说"——它不知道今天天气怎么样,不能帮你查数据库,不能发邮件。

这篇,我们给它装上"手"。

先聊聊历史:从 functions 到 tools

在正式动手之前,先花两分钟理清一个容易混淆的概念。

2023 年 6 月,OpenAI 在 Chat Completions API 中首次引入了 functions 参数和 function_call 参数,让模型可以声明"我想调用某个函数"。这是大模型工具调用的起点。

functions 有个明显的局限:它只支持函数调用这一种工具类型。OpenAI 很快意识到,模型需要的"工具"不只是函数——未来可能还有代码解释器、文件检索等。

于是 2023 年 11 月,OpenAI 发布了新的 tools 参数和 tool_choice 参数,替代了原来的 functionsfunction_calltools 是一个数组,每个元素有 type 字段,目前 type 只有 "function" 一种,但这个设计为未来扩展留了口子。同时,模型返回的字段也从 function_call 变成了 tool_calls(注意是复数),支持一次返回多个工具调用。

旧的 functions 参数随后被标记为 deprecated。

时间 变化 关键参数
2023.06 首次引入函数调用 functions + function_call
2023.11 升级为工具调用 tools + tool_choice + tool_calls
之后 functions 被废弃 旧参数仍可用但不推荐

现在几乎所有平台(包括百炼、DeepSeek、智谱等)都直接实现了 tools 版本的接口。所以我们这篇文章直接用 tools,不再提旧的 functions

参考:OpenAI Function calling and other API updates (2023.06)百炼 Function Calling 文档

Function Calling 的本质

概念理清了,再说说它到底在干什么。

模型不是真的在调用你的函数。

Function Calling 的实际流程是这样的:

  1. 你告诉模型:“我有这些工具,每个工具能干什么,需要什么参数”
  2. 用户提问,模型判断需不需要用工具
  3. 如果需要,模型返回:“我想调用 get_weather 这个函数,参数是 {"city": "北京"}
  4. 拿到这个信息,去真正执行函数,拿到结果
  5. 你把结果塞回 messages,再发一次请求
  6. 模型根据结果生成最终回复

模型做的只是"决策"——它告诉你该调什么、传什么参数。真正的执行在你这边。

这个设计很合理:模型是语言模型,不是执行引擎。它擅长理解意图、生成结构化输出,但它没有权限访问你的数据库、调用你的内部 API。

用一张表对比一下:

普通对话 Function Calling
用户问 → 模型答 → 结束 用户问 → 模型决策 → 你执行 → 模型总结
模型只能用训练数据 模型可以用实时数据
一轮结束 可能多轮

完整流程:查天气示例

我们做一个"查天气"的例子,用模拟数据,重点是走通整个流程。

第一步:定义工具

工具用 JSON Schema 描述,告诉模型这个函数叫什么、干什么、需要什么参数:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "查询指定城市的当前天气",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "城市名称,如:北京、上海"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "温度单位,默认 celsius"
        }
      },
      "required": ["city"]
    }
  }
}

description 字段非常重要——模型就是靠这个判断什么时候该用这个工具。写得越清楚,模型判断越准。

第二步:发送请求,模型返回 tool_calls

把工具定义加到请求里,发给模型:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class FunctionCallingDemo {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        String requestBody = """
                {
                    "model": "qwen-plus",
                    "messages": [
                        {"role": "user", "content": "北京今天天气怎么样?"}
                    ],
                    "tools": [
                        {
                            "type": "function",
                            "function": {
                                "name": "get_weather",
                                "description": "查询指定城市的当前天气",
                                "parameters": {
                                    "type": "object",
                                    "properties": {
                                        "city": {
                                            "type": "string",
                                            "description": "城市名称,如:北京、上海"
                                        },
                                        "unit": {
                                            "type": "string",
                                            "enum": ["celsius", "fahrenheit"],
                                            "description": "温度单位,默认 celsius"
                                        }
                                    },
                                    "required": ["city"]
                                }
                            }
                        }
                    ],
                    "tool_choice": "auto"
                }
                """;

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(
                    "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"))
                .header("Authorization", "Bearer YOUR_BAILIAN_API_KEY")
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                .build();

        HttpResponse<String> response = client.send(request,
                HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());
    }
}

模型返回的不是普通文本,而是 tool_calls

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\": \"北京\", \"unit\": \"celsius\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

注意几个关键字段:

  • finish_reasontool_calls,不是 stop,说明模型没有直接回答,而是要调工具
  • tool_calls[0].id:这个调用的唯一 ID,后面回传结果时要用
  • tool_calls[0].function.arguments:参数是 JSON 字符串,不是对象,需要你自己解析

第三步:执行函数,回传结果

你拿到 tool_calls 后,执行对应的函数,然后把结果以 role: tool 的消息加回 messages,再发第二轮请求:

// 第二轮:把工具执行结果回传给模型
String secondRequest = """
        {
            "model": "qwen-plus",
            "messages": [
                {"role": "user", "content": "北京今天天气怎么样?"},
                {
                    "role": "assistant",
                    "content": null,
                    "tool_calls": [
                        {
                            "id": "call_abc123",
                            "type": "function",
                            "function": {
                                "name": "get_weather",
                                "arguments": "{\\"city\\": \\"北京\\", \\"unit\\": \\"celsius\\"}"
                            }
                        }
                    ]
                },
                {
                    "role": "tool",
                    "tool_call_id": "call_abc123",
                    "content": "北京今天晴,气温 22°C,湿度 45%,东南风 3 级"
                }
            ],
            "tools": [...]
        }
        """;

模型拿到工具结果后,生成最终回复:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "北京今天天气不错,晴天,气温 22°C,湿度适中,东南风 3 级,适合出行。"
      },
      "finish_reason": "stop"
    }
  ]
}

整个流程走完了。

完整可运行示例

把三步串起来,一个完整的 Java 程序:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class WeatherAgentDemo {

    static final String API_URL =
        "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
    static final String API_KEY = "YOUR_BAILIAN_API_KEY";

    // 模拟天气查询,实际项目里换成真实 API 调用
    static String getWeather(String city) {
        return "{\"city\": \"" + city + "\", \"temperature\": 22, "
             + "\"unit\": \"celsius\", \"condition\": \"晴\", "
             + "\"humidity\": \"45%\", \"wind\": \"东南风 3 级\"}";
    }

    static String toolsJson() {
        return """
                [
                    {
                        "type": "function",
                        "function": {
                            "name": "get_weather",
                            "description": "查询指定城市的当前天气",
                            "parameters": {
                                "type": "object",
                                "properties": {
                                    "city": {
                                        "type": "string",
                                        "description": "城市名称,如:北京、上海"
                                    }
                                },
                                "required": ["city"]
                            }
                        }
                    }
                ]
                """;
    }

    static String sendRequest(HttpClient client, String body) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(API_URL))
                .header("Authorization", "Bearer " + API_KEY)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();
        return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }

    static String extract(String json, String key) {
        String search = "\"" + key + "\":";
        int start = json.indexOf(search);
        if (start == -1) return "";
        start += search.length();
        while (start < json.length() && json.charAt(start) == ' ') start++;
        char first = json.charAt(start);
        if (first == '"') {
            int end = json.indexOf('"', start + 1);
            return json.substring(start + 1, end);
        }
        int end = start;
        while (end < json.length() && json.charAt(end) != ',' && json.charAt(end) != '}') end++;
        return json.substring(start, end).trim();
    }

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        // ---- 第一轮:发送用户问题 + 工具定义 ----
        String firstBody = """
                {
                    "model": "qwen-plus",
                    "messages": [
                        {"role": "user", "content": "北京今天天气怎么样?"}
                    ],
                    "tools": %s,
                    "tool_choice": "auto"
                }
                """.formatted(toolsJson());

        String firstResponse = sendRequest(client, firstBody);
        System.out.println("=== 第一轮响应 ===");
        System.out.println(firstResponse);

        if (!firstResponse.contains("\"finish_reason\":\"tool_calls\"")
                && !firstResponse.contains("\"finish_reason\": \"tool_calls\"")) {
            System.out.println("模型直接回答,无需调用工具");
            return;
        }

        String toolCallId = extract(firstResponse, "id");
        String city = "北京"; // 简化处理,实际应解析 arguments

        String weatherResult = getWeather(city);
        System.out.println("\n=== 工具执行结果 ===");
        System.out.println(weatherResult);

        // ---- 第二轮:把工具结果回传给模型 ----
        String secondBody = """
                {
                    "model": "qwen-plus",
                    "messages": [
                        {"role": "user", "content": "北京今天天气怎么样?"},
                        {
                            "role": "assistant",
                            "content": null,
                            "tool_calls": [
                                {
                                    "id": "%s",
                                    "type": "function",
                                    "function": {
                                        "name": "get_weather",
                                        "arguments": "{\\"city\\": \\"%s\\"}"
                                    }
                                }
                            ]
                        },
                        {
                            "role": "tool",
                            "tool_call_id": "%s",
                            "content": "%s"
                        }
                    ],
                    "tools": %s
                }
                """.formatted(
                    toolCallId, city, toolCallId,
                    weatherResult.replace("\"", "\\\"").replace("\n", ""),
                    toolsJson()
                );

        String secondResponse = sendRequest(client, secondBody);
        System.out.println("\n=== 最终回复 ===");
        System.out.println(secondResponse);
    }
}

这个示例为了零依赖,JSON 解析用了简单字符串查找。实际项目里建议引入 Jackson 或 Gson,解析会更可靠。

tool_choice 参数

tool_choice 控制模型是否必须调用工具:

含义
"auto" 模型自己判断要不要用工具(默认)
"none" 禁止调用工具,强制直接回答
"required" 必须调用至少一个工具
{"type": "function", "function": {"name": "get_weather"}} 强制调用指定函数

大多数场景用 auto 就够了。required 适合你明确知道需要工具的场景,比如数据提取任务。指定函数名适合测试或强制走某个分支。

常见坑

坑 1:模型不调用工具

最常见的原因是 description 写得不够清楚。模型是靠描述判断要不要用工具的。

// 差
"description": "天气"

// 好
"description": "查询指定城市的实时天气,包括温度、天气状况、湿度和风力"

另一个原因是问题本身不需要工具。"今天天气怎么样"会触发工具,"天气预报是什么意思"不会。

坑 2:arguments 解析失败

arguments 是字符串,不是 JSON 对象。模型偶尔会生成格式不规范的 JSON,比如多余的逗号、单引号代替双引号。

建议加 try-catch,解析失败时记录原始 arguments,方便排查。

坑 3:第二轮忘记带 assistant 消息

第二轮请求的 messages 里,必须包含第一轮模型返回的 assistant 消息(带 tool_calls 的那条)。很多人只加了 tool 消息,忘了 assistant 消息,会报错或得到奇怪的结果。

正确的 messages 顺序:

user → assistant(tool_calls) → tool → [assistant 最终回复]

坑 4:多次工具调用

模型可能在一次响应里返回多个 tool_calls,比如同时查北京和上海的天气。这时候你需要遍历 tool_calls 数组,每个都执行,每个都回传一条 role: tool 消息,tool_call_id 分别对应。

[
    {"role": "user", "content": "..."},
    {"role": "assistant", "tool_calls": [{"id": "call_1", ...}, {"id": "call_2", ...}]},
    {"role": "tool", "tool_call_id": "call_1", "content": "北京天气..."},
    {"role": "tool", "tool_call_id": "call_2", "content": "上海天气..."}
]

这和 Agent 有什么关系

Function Calling 就是 Agent 的"手"。

没有 Function Calling,模型只能在语言空间里打转,它能分析、能建议,但做不了任何实际的事。

有了 Function Calling,模型可以:

  • 查数据库,拿到实时数据再回答
  • 调内部 API,执行业务操作
  • 搜索网页,获取最新信息
  • 写文件、发邮件、触发工作流

Agent 的核心循环就是:模型决策 → 工具执行 → 结果反馈 → 模型再决策,一直循环直到任务完成。Function Calling 是这个循环的基础设施。

你现在已经掌握了这个循环的完整实现方式。

动手试试

  1. 把完整示例跑一遍,观察两轮请求和响应的结构
  2. 加一个 get_time 工具,让模型能回答"现在几点了"
  3. 试试 tool_choice: "required",看模型在不需要工具的问题上会怎么处理
  4. 故意把 description 写得很模糊,观察模型是否还能正确判断

下一篇,我们给模型装上记忆——对话上下文管理。模型本身没有记忆,但我们可以用 messages 数组模拟出"记住了"的效果,同时处理上下文窗口超限的问题。


觉得有用的话点个收藏,后续系列持续更新。

本文 API 格式基于百炼 OpenAI 兼容接口文档百炼 Function Calling 文档,如有变动请以官方最新文档为准。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐