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": "[email protected]",
        "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":"[email protected]","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:** [email protected] (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,最终生成完整的回答。

相关推荐
艺杯羹几秒前
JDBC 初认识、速了解
java·数据库·jdbc
陵易居士1 分钟前
Spring如何解决项目中的循环依赖问题?
java·后端·spring
鲜枣课堂1 分钟前
发力“5G-A x AI融智创新”,中国移动推出重要行动计划!打造“杭州Mobile AI第一城”!
人工智能·5g
铁弹神侯9 分钟前
Maven相关名词及相关配置
java·maven
爱的叹息10 分钟前
AI应用开发平台 和 通用自动化工作流工具 的详细对比,涵盖定义、核心功能、典型工具、适用场景及优缺点分析
运维·人工智能·自动化
Aska_Lv14 分钟前
RocketMQ---core原理
后端
Dm_dotnet17 分钟前
使用CAMEL创建第一个Agent Society
人工智能
AronTing19 分钟前
10-Spring Cloud Alibaba 之 Dubbo 深度剖析与实战
后端·面试·架构
会飞的皮卡丘EI22 分钟前
关于Blade框架对数字类型的null值转为-1问题
java·spring boot
没逻辑23 分钟前
⏰ Redis 在支付系统中作为延迟任务队列的实践
redis·后端