AI学习-2-LLM工具调用

在这篇文章中,我将分享如何通过大语言模型(LLM)调用工具(Tool)来实现增强功能。工具调用是通过 Function Call (函数调用)机制实现的,我会以一个实际的例子为基础,展示如何处理模型生成的 Function Call 请求,并完成工具调用。

什么是 Function Call?

Function Call 是一种机制,允许大语言模型(LLM)在生成文本时请求调用外部工具或函数来完成特定任务。它是 LLM 实现工具调用的一种方式(另一种方式是通过提示词)。通过 Function Call,模型可以:

  1. 扩展能力:请求调用外部API或工具,获取模型本身无法生成的数据(比如天气查询,新闻查询等)。
  2. 提高准确性:获取实时数据或执行复杂计算(比如代码解释器等)。

Function Call 的核心流程

  1. 用户输入提示词:用户提供问题或任务。
  2. 模型解析意图:模型理解用户的需求。
  3. 选择工具:模型根据用户需求,从预定义的工具列表中选择合适的工具。
  4. 生成调用请求:模型生成符合工具规范的调用请求(如JSON格式)。
  5. 调用工具并获取结果:外部系统执行工具调用并返回结果。
  6. 生成最终响应:模型根据工具返回的结果生成最终的回答。

实现步骤

1. 接收用户请求

在上一篇文章中,我分享了如何使用 Ollama 搭建本地 LLM:AI学习-1-Ollama+OpenUI+NeutrinoProxy本地搭建Deepseek-R1。不过,由于 Deepseek-R1 暂时不支持 Function Call,这次我们将使用支持 Function Call 的模型 llama3.2:3b-instruct-q8_0。(其他支持function call的模型亦可)

用户通过 REST API 提交请求,包含模型名称、消息历史、工具列表等参数。以下是一个示例:

arduino 复制代码
curl -X POST http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
  "model": "llama3.2:3b-instruct-q8_0",
  "messages": [
    {
      "role": "user",
      "content": "Can you provide a random user profile?"
    }
  ],
  "stream": false,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_random_user",
        "description": "Get a random user profile",
        "parameters": {
          "type": "object",
          "properties": {
            "gender": {
              "type": "string",
              "description": "The gender of the user, e.g. male or female"
            },
            "nationality": {
              "type": "string",
              "description": "The nationality of the user, e.g. US or GB"
            }
          },
          "required": ["gender", "nationality"]
        }
      }
    }
  ]
}'

在这个请求中:

  • model:指定使用的模型名称。
  • messages:消息历史,包含用户的输入。
  • tools :定义了可以调用的工具,这里是 get_random_user

以下是大模型响应:

json 复制代码
{
    "model": "llama3.2:3b-instruct-q8_0",
    "message": {
        "role": "assistant",
        "content": "",
        "tool_calls": [
            {
                "function": {
                    "name": "get_random_user",
                    "arguments": {
                        "gender": "male",
                        "nationality": "US"
                    }
                }
            }
        ]
    },
    "done_reason": "stop",
    "done": true,
    "total_duration": 9396042300,
    "load_duration": 5451294100,
    "prompt_eval_count": 197,
    "prompt_eval_duration": 2618327400,
    "eval_count": 19,
    "eval_duration": 1325169100
}

可以看到,LLM 并没有直接返回结果,而是返回了 tool_calls。这是模型在理解用户需求后,请求调用工具的动作,这个过程被称为 Function Call

2. 处理 Function Call

当模型生成 Function Call 请求时,后端会解析请求并调用相应的工具,以调用 get_random_user 为例。(这是一个demo代码,实际上的处理不应该这么不优雅)

typescript 复制代码
/**
 * Handles the Function Call requests returned by the Large Language Model (LLM).
 * 
 * @param model     The name of the model being used.
 * @param messages  The current message history in the conversation.
 * @param response  The response from the LLM, which may contain tool calls.
 * @param tools     The list of available tools that the model can call.
 * @param depth     The current recursion depth to prevent infinite loops.
 * @return          A Mono<JsonNode> containing the response with tool call results.
 */
private Mono<JsonNode> handleFunctionCall(String model, List<Map<String, Object>> messages, JsonNode response,
                                          List<Map<String, Object>> tools, int depth) {
    // Limit the recursion depth to prevent infinite loops.
    if (depth >= 5) {
        log.warn("Maximum recursion depth reached. Stopping function call handling.");
        return Mono.just(response);
    }
​
    // Extract tool call information from the LLM's response.
    JsonNode toolCalls = response.path("message").path("tool_calls");
    if (!toolCalls.isArray() || toolCalls.isEmpty()) {
        // If there are no tool calls, return the original response.
        return Mono.just(response);
    }
​
    // List to store the results of each tool call.
    List<Mono<Map<String, Object>>> functionResults = new ArrayList<>();
​
    // Iterate through all tool calls in the response.
    for (JsonNode toolCall : toolCalls) {
        // Extract the tool name and its arguments.
        String functionName = toolCall.path("function").path("name").asText();
        JsonNode arguments = toolCall.path("function").path("arguments");
​
        // Handle the tool call based on the tool name.
        switch (functionName) {
            case "get_random_user":
                // Call the Random User API and store the result in the functionResults list.
                functionResults.add(callRandomUserApi(arguments)
                        .map(result -> Map.of(
                                "role", "tool", // The role of the tool in the conversation.
                                "content", result.toString() // The result of the tool call.
                        )));
                break;
            default:
                // Handle unknown tools by returning a default "not implemented" message.
                log.warn("Unknown function call: {}", functionName);
                functionResults.add(Mono.just(Map.of(
                        "role", "tool",
                        "content", "Function " + functionName + " not implemented"
                )));
        }
    }
​
    // Execute all tool calls and merge the results into the message history.
    return Flux.fromIterable(functionResults)
            .flatMap(mono -> mono) // Execute each tool call.
            .collectList() // Collect all tool call results.
            .flatMap(results -> {
                // Add the tool call results to the message history.
                updatedMessages.addAll(results);
                // Recursively call the chat method to continue the conversation.
                return chat(model, updatedMessages, null, null, null, null, depth + 1);
            });
}
scss 复制代码
/**
 * Calls the Random User API to retrieve random user information.
 * 
 * @param arguments A JSON object containing the tool call parameters:
 *                  - gender: The gender of the user (e.g., male or female).
 *                  - nationality: The nationality of the user (e.g., US, GB).
 * @return          A Mono<JsonNode> containing the API response.
 */
private Mono<JsonNode> callRandomUserApi(JsonNode arguments) {
    // Extract the gender and nationality parameters from the tool call arguments.
    String gender = arguments.path("gender").asText();
    String nationality = arguments.path("nationality").asText();
​
    // Construct the API URL with the provided parameters.
    String apiUrl = String.format("https://randomuser.me/api/?gender=%s&nat=%s", gender, nationality);
    log.info("Calling Random User API: {}", apiUrl);
​
    // Use WebClient to call the API and retrieve the response.
    return webClient.get()
            .uri(apiUrl) // Set the API URL.
            .retrieve() // Execute the HTTP GET request.
            .bodyToMono(JsonNode.class) // Parse the response body as a JsonNode.
            .doOnSuccess(response -> log.info("Received response from Random User API: {}", response)) // Log success.
            .doOnError(e -> log.error("Failed to call Random User API: {}", e.getMessage())); // Log errors.
}

后端会调用 Random User API 获取响应结果,并将结果作为工具调用的输出,传递回 LLM 以生成最终的回答。

perl 复制代码
{
    "results": [{
        "gender": "female",
        "name": {
            "title": "Miss",
            "first": "Kristen",
            "last": "Craig"
        },
        "location": {
            "street": {
                "number": 13,
                "name": "Sunset St"
            },
            "city": "Durham",
            "state": "Indiana",
            "country": "United States",
            "postcode": 81495,
            "coordinates": {
                "latitude": "0.5842",
                "longitude": "14.1016"
            },
            "timezone": {
                "offset": "-9:00",
                "description": "Alaska"
            }
        },
        "email": "kristen.craig@example.com",
        "login": {
            "uuid": "2510c44c-007e-43b1-aef3-3eb92a2688e6",
            "username": "whiteduck316",
            "password": "truck1",
            "salt": "acB6mSfQ",
            "md5": "cf3465e7e494892eeaf360e838b8e318",
            "sha1": "1405c37e150de7ca7d8bd6c572699080308e43f8",
            "sha256": "29b6f4333cbaee3539bce982f4d34152f134d1c6978d5cf01541268fdf8cb3e1"
        },
        "dob": {
            "date": "1996-04-27T16:07:21.155Z",
            "age": 28
        },
        "registered": {
            "date": "2016-09-28T00:45:26.421Z",
            "age": 8
        },
        "phone": "(361) 555-9395",
        "cell": "(601) 916-5945",
        "id": {
            "name": "SSN",
            "value": "640-45-0428"
        },
        "picture": {
            "large": "https://randomuser.me/api/portraits/women/56.jpg",
            "medium": "https://randomuser.me/api/portraits/med/women/56.jpg",
            "thumbnail": "https://randomuser.me/api/portraits/thumb/women/56.jpg"
        },
        "nat": "US"
    }],
    "info": {
        "seed": "ad194c849e4367b3",
        "results": 1,
        "page": 1,
        "version": "1.4"
    }
}

3. 再次调用大模型

python 复制代码
{
  "model": "llama3.2:3b-instruct-q8_0",
  "messages": [
    {
      "role": "user",
      "content": "Can you provide a random user profile?"
    },
    {
      "role":"tool",
      "content": "{"results":[{"gender":"female","name":{"title":"Miss","first":"Kristen","last":"Craig"},"location":{"street":{"number":13,"name":"Sunset St"},"city":"Durham","state":"Indiana","country":"United States","postcode":81495,"coordinates":{"latitude":"0.5842","longitude":"14.1016"},"timezone":{"offset":"-9:00","description":"Alaska"}},"email":"kristen.craig@example.com","login":{"uuid":"2510c44c-007e-43b1-aef3-3eb92a2688e6","username":"whiteduck316","password":"truck1","salt":"acB6mSfQ","md5":"cf3465e7e494892eeaf360e838b8e318","sha1":"1405c37e150de7ca7d8bd6c572699080308e43f8","sha256":"29b6f4333cbaee3539bce982f4d34152f134d1c6978d5cf01541268fdf8cb3e1"},"dob":{"date":"1996-04-27T16:07:21.155Z","age":28},"registered":{"date":"2016-09-28T00:45:26.421Z","age":8},"phone":"(361) 555-9395","cell":"(601) 916-5945","id":{"name":"SSN","value":"640-45-0428"},"picture":{"large":"https://randomuser.me/api/portraits/women/56.jpg","medium":"https://randomuser.me/api/portraits/med/women/56.jpg","thumbnail":"https://randomuser.me/api/portraits/thumb/women/56.jpg"},"nat":"US"}],"info":{"seed":"ad194c849e4367b3","results":1,"page":1,"version":"1.4"}}"
    }
  ],
  "stream": false
}

得到结果如下:

swift 复制代码
{
    "model": "llama3.2:3b-instruct-q8_0",
    "message": {
        "role": "assistant",
        "content": "Here is a formatted user profile based on the provided tool call response:\n\n**User Information:**\n\n* **Name:** Kristen Craig\n* **Title:** Miss\n* **Gender:** Female\n* **Age:** 28 (as of April 27, 1996)\n* **Location:**\n\t+ Street: Sunset St\n\t+ City: Durham\n\t+ State: Indiana\n\t+ Country: United States\n\t+ Postcode: 81495\n\t+ Coordinates: Latitude: 0.5842, Longitude: 14.1016\n\t+ Timezone: -9:00 (Alaska)\n* **Email:** kristen.craig@example.com (whiteduck316)\n* **Login Information:**\n\t+ Username: whiteduck316\n\t+ Password: truck1\n\n**Personal Details:**\n\n* Date of Birth: April 27, 1996\n* Registered on: September 28, 2016 (age 8 at the time)\n\n**Contact Information:**\n\n* Phone: (361) 555-9395\n* Cell: (601) 916-5945\n\n**Social Media/Profile Picture:**\n\n* Picture: Available in large, medium, and thumbnail formats.\n\nNote that this user profile is randomly generated from the provided tool call response."
    },
    "done_reason": "stop",
    "done": true,
    "total_duration": 28186676000,
    "load_duration": 4196755600,
    "prompt_eval_count": 479,
    "prompt_eval_duration": 3975156900,
    "eval_count": 269,
    "eval_duration": 20012577700
}

总结

以上就是最简易的tool调用的过程,总结来说,Function Call 的本质是让 LLM 理解用户需求,生成需要调用的函数和参数。服务端根据模型的请求执行函数,并将结果返回给 LLM,最终生成完整的回答。

相关推荐
BD_Marathon1 小时前
【Flink】部署模式
java·数据库·flink
技术与健康2 小时前
LLM实践系列:利用LLM重构数据科学流程03- LLM驱动的数据探索与清洗
大数据·人工智能·重构
张小九992 小时前
Foldseek快速蛋白质结构比对
人工智能
鼠鼠我捏,要死了捏3 小时前
深入解析Java NIO多路复用原理与性能优化实践指南
java·性能优化·nio
ningqw3 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友3 小时前
vi编辑器命令常用操作整理(持续更新)
后端
云卓SKYDROID4 小时前
无人机延时模块技术难点解析
人工智能·无人机·高科技·云卓科技·延迟摄像
superlls4 小时前
(Redis)主从哨兵模式与集群模式
java·开发语言·redis
神齐的小马4 小时前
机器学习 [白板推导](十三)[条件随机场]
人工智能·机器学习
荼蘼4 小时前
CUDA安装,pytorch库安装
人工智能·pytorch·python