在这篇文章中,我将分享如何通过大语言模型(LLM)调用工具(Tool)来实现增强功能。工具调用是通过 Function Call (函数调用)机制实现的,我会以一个实际的例子为基础,展示如何处理模型生成的 Function Call 请求,并完成工具调用。
什么是 Function Call?
Function Call 是一种机制,允许大语言模型(LLM)在生成文本时请求调用外部工具或函数来完成特定任务。它是 LLM 实现工具调用的一种方式(另一种方式是通过提示词)。通过 Function Call,模型可以:
- 扩展能力:请求调用外部API或工具,获取模型本身无法生成的数据(比如天气查询,新闻查询等)。
- 提高准确性:获取实时数据或执行复杂计算(比如代码解释器等)。
Function Call 的核心流程
- 用户输入提示词:用户提供问题或任务。
- 模型解析意图:模型理解用户的需求。
- 选择工具:模型根据用户需求,从预定义的工具列表中选择合适的工具。
- 生成调用请求:模型生成符合工具规范的调用请求(如JSON格式)。
- 调用工具并获取结果:外部系统执行工具调用并返回结果。
- 生成最终响应:模型根据工具返回的结果生成最终的回答。
实现步骤
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,最终生成完整的回答。