导航栏:首页 / AI教程目录 / AI创客项目开发教程目录 / 第1篇 基础知识篇 / AID101-1-3-05 – 并行多工具调用(parallel tool calls)实例
本节内容简介
在前几节的课程中,我们已经掌握了 Tool Calls 的基本原理、Follow-up 机制的实现方法。细心的朋友可能已经发现:前面几节课程中的示例程序都只处理单个 Tool Call——要么打开 LED,要么调节亮度,每次只执行一个操作。
但在实际的智能家居场景中,用户常常会说一些组合指令,例如:
- “请打开客厅的灯,并把亮度调到最亮”
- “关闭所有设备”
- “打开空调并设置温度为26度”
这些指令包含多个动作,需要 ESP32 一次性执行多个工具调用。本节课程将讲解如何让 ESP32 处理 AI 平台在一次响应中返回的多个 Tool Calls,并依次执行所有操作,最后通过 Follow-up 获取整合后的自然语言反馈。
请注意:不同人工智能大模型的并行工具调用能力有所不同,有些人工智能大模型可以很好的一次性处理多个工具调用,但是有些人工智能大模型则不具备一次性处理多个工具调用的能力,甚至有些人工智能大模型完全不具备工具调用的能力。因此,您在实际操作中,需要留意使用的大模型是否具备一次性处理多个工具调用的能力
什么是多 Tool Calls?
核心概念
多 Tool Calls 是指 AI 平台在一次响应中返回多个工具调用指令,ESP32 需要依次解析并执行每一个指令。

为什么需要多 Tool Calls?
| 场景 | 单 Tool Call 处理 | 多 Tool Calls 处理 |
|---|---|---|
| “打开LED并调到最亮” | 只能打开LED或只能调亮度 | 先打开LED,再调到最亮 |
| “关闭所有灯和风扇” | 只能关闭一种设备 | 依次关闭灯和风扇 |
| “把客厅灯打开,卧室灯关闭” | 只能控制一个灯 | 同时控制客厅和卧室灯 |
多 Tool Calls 让 AI 助手能够处理更复杂、更自然的用户指令,不再局限于单一操作。
🔁 多 Tool Calls 完整流程
整个流程分为五个步骤:
- 第一次请求:ESP32 发送用户指令 + 可用工具列表
- AI 返回多个 Tool Calls:AI 返回包含多个 Tool Calls 的响应信息
- 依次执行多个 Tool Calls:ESP32 解析 AI 返回的多个工具调用,依次执行所有操作
- 第二次请求(Follow-up):ESP32 将所有 Tool Calls 的执行结果一起发送给 AI
- 获取整合的自然语言反馈:AI 根据所有执行结果生成一段整合后的人话回复
⚠️ 关键理解:与上一节不同,本节的第二次请求中,
messages数组里会包含多个role: "tool"消息(每个 Tool Call 对应一个),以及一个包含多个 tool_calls 的role: "assistant"消息。
在下面的内容中,我们将详细讲解这五个阶段的详细内容。
第一阶段:第一次请求(与上一节相同)
第一次请求与上一节课程的 tool_calls_follow_up_1.ino 一致。ESP32 向 AI 平台发送用户指令和工具定义,AI 分析后返回需要调用的函数和参数。
请求 JSON
{
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。"
},
{
"role": "user",
"content": "请打开LED并将亮度调节到最大"
}
],
"tools": [
{ /* controlLEDOnOFF 工具定义 */ },
{ /* controlLEDBrightness 工具定义 */ }
],
"tool_choice": "auto",
"temperature": 0.1
}与上一节的关键区别:用户指令变成了组合指令 "请打开LED并将亮度调节到最大",这句话同时包含”打开”和”调节亮度”两个动作。
第二阶段:AI 返回多个 Tool Calls
当 AI 收到组合指令后,会分析出需要调用多个工具来完成用户的请求。
多 Tool Calls 响应示例
{
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": "",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"action\": \"on\"}",
"name": "controlLEDOnOFF"
},
"id": "call_abaee57e921a4021829447",
"index": 0,
"type": "function"
},
{
"function": {
"arguments": "{\"level\": 5}",
"name": "controlLEDBrightness"
},
"id": "call_f21c48f98191408f8e5854",
"index": 1,
"type": "function"
}
]
}
}
]
}响应解析
与单 Tool Call 相比,多 Tool Calls 的响应有以下特点:
| 字段 | 单 Tool Call | 多 Tool Calls |
|---|---|---|
tool_calls 数组长度 | 1 | 2 或更多 |
每个元素的 index | 0 | 0, 1, 2… 依次递增 |
每个元素的 id | 一个唯一 ID | 每个 Tool Call 有独立的唯一 ID |
关键点:
tool_calls是一个数组,可以包含任意数量的工具调用- 每个工具调用都有独立的
id,后续 Follow-up 时需要一一对应 - 工具调用会按照
index顺序排列,ESP32 应该按顺序依次执行
第三阶段:ESP32 依次执行多个 Tool Calls
ESP32 收到包含多个 Tool Calls 的响应后,需要遍历 tool_calls 数组,依次执行每一个工具调用。
执行流程
检测到 2 个工具调用
[工具调用 1/2]
函数名: controlLEDOnOFF
Tool Call ID: call_abaee57e921a4021829447
参数: {"action": "on"}
>>> 执行Tool Call 1...
执行动作: on
控制LED开关: on
- LED已打开
[工具调用 2/2]
函数名: controlLEDBrightness
Tool Call ID: call_f21c48f98191408f8e5854
参数: {"level": 5}
>>> 执行Tool Call 2...
执行亮度调节: 5
控制LED亮度: 级别 5
- 亮度已设置为最亮级别
>>> 所有Tool Calls执行完成为什么需要按顺序执行?
在这个例子中,如果先执行亮度调节再打开 LED,可能会出现逻辑问题:
- 先调亮度:
controlLEDBrightness(5)发现 LED 是关闭的,会自动打开 LED 并设置亮度 - 再开灯:
controlLEDOnOFF("on")再次打开 LED(虽然已经是打开状态)
虽然这个例子中顺序影响不大,但在更复杂的场景中(如”先关闭旧设备,再打开新设备”),执行顺序至关重要。因此 ESP32 应该严格按照 AI 返回的 index 顺序执行。
第四阶段:构造第二次请求(多 Tool Calls Follow-up)⭐
这是本节课程的核心内容。与上一节的单 Tool Call Follow-up 相比,多 Tool Calls 的第二次请求有以下变化:
第二次请求 JSON 示例
{
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "请打开LED并将亮度调节到最大"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_abaee57e921a4021829447",
"type": "function",
"function": {
"name": "controlLEDOnOFF",
"arguments": "{\"action\": \"on\"}"
}
},
{
"id": "call_f21c48f98191408f8e5854",
"type": "function",
"function": {
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abaee57e921a4021829447",
"content": "LED已成功打开"
},
{
"role": "tool",
"tool_call_id": "call_f21c48f98191408f8e5854",
"content": "LED亮度已成功调节到最亮级别"
}
],
"temperature": 0.1
}与单 Tool Call Follow-up 的关键区别
| 部分 | 单 Tool Call | 多 Tool Calls |
|---|---|---|
assistant 的 tool_calls | 包含 1 个对象 | 包含多个对象 |
tool 消息数量 | 1 条 | 多条(每条对应一个 Tool Call) |
tool_call_id | 一个 ID | 多个不同的 ID,必须与 assistant.tool_calls 一一对应 |
messages 数组的结构
第二次请求的 messages 数组包含多条消息:
| 顺序 | role | 内容 |
|---|---|---|
| 1 | system | 设定 AI 角色 |
| 2 | user | 用户原始指令 |
| 3 | assistant | AI 的第一次响应,包含所有 Tool Calls |
| 4 | tool | 第一个 Tool Call 的执行结果 |
| 5 | tool | 第二个 Tool Call 的执行结果 |
| … | tool | 更多 Tool Call 的执行结果… |
💡 为什么
tool消息要紧跟在assistant消息后面? 因为tool消息是对assistant消息中tool_calls的回应。AI 平台需要看到完整的上下文:”AI 要求做什么 → ESP32 执行的结果是什么”。
第五阶段:解析 AI 的整合自然语言反馈
AI 收到包含多个执行结果的第二次请求后,会分析所有信息,生成一段整合后的自然语言回复。
第二次响应示例
{
"choices": [
{
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "已为您打开LED,并将亮度调节到最大级别,现在灯光最亮!"
}
}
]
}注意: AI 的回复将两个操作的结果整合成一句话,而不是分别回复”LED已打开”和”亮度已调到最大”。这就是多 Tool Calls Follow-up 的价值——AI 能够理解完整的操作上下文,给出更自然、更人性化的反馈。
代码解析:关键变化点
本节课程的示例程序是在上一节课示例程序基础上修改而来,主要改动是将单 Tool Call 处理扩展为多 Tool Calls 处理。
注意
本程序需要配合my_info.h代码使用,否则程序将无法正常编译。
请点击以下链接前往该代码页面,复制下载该代码:
http://ai.taichi-maker.com/index.php/homepage/ai-tutorial-index/ai-maker-project-tutorial-index/my_info_h-code-description/
/*
* ESP32 AI平台调用 Follow-up Tool Calls 示例 - 多工具调用版
*
* 功能描述:
* 本程序专门为ESP32-S3-DevKitC-1开发板设计,演示如何实现LLM模型的多Tool Calls及Follow-up功能。
* 程序支持AI平台在一次响应中调用多个工具(如同时打开LED并调节亮度),
* ESP32会依次执行所有工具调用,然后将所有执行结果通过Follow-up请求发送给LLM,
* 获取整合后的自然语言反馈。
*
* Follow-up Tool Calls流程:
* 1. 第一次请求:发送用户指令,获取Tool Calls响应(可能包含多个工具调用)
* 2. 执行Tool Calls:依次控制LED的开关或亮度
* 3. 第二次请求:将所有Tool Calls响应和执行结果发送给LLM
* 4. 获取自然语言反馈:LLM根据所有执行结果生成自然语言回复
*
* 本程序支持的指令类型:
* - 打开/关闭LED(如"打开LED"、"关闭LED")
* - 调节LED亮度(如"把LED调到亮度最大"、"将LED亮度调到中等")
* - 组合指令(如"请打开LED并将亮度调节到最大")
*
* 作者:Taichi-Maker
* 作者官网:http://ai.taichi-maker.com
* 创建日期:2026年06月27日
* 版本:1.1.3
*
* 硬件要求:
* - ESP32-S3-DevKitC-1开发板(内置一颗WS2812 LED,数据引脚连接到GPIO 48)
*
* 所需软件库:
* - FastLED库
* - ArduinoJson库(版本7.0.0或更高)--用于处理JSON数据
*
* 配置说明:
* 1. 请先通过Arduino IDE的库管理器安装FastLED、ArduinoJson库。
* 2. 在my_info.h文件中填写您的Wi-Fi名称(ssid)和密码(password)。
* 3. 在my_info.h文件中将ai_api_key替换为您从AI平台获取的有效API密钥(切勿泄露!)。
* 4. 确保开发板已正确连接到电脑,并选择正确的开发板型号与端口。
*
* 使用方法:
* 1. 将本程序上传至ESP32-S3-DevKitC-1开发板。
* 2. 打开串口监视器(波特率设置为115200)。
* 3. 程序会自动连接Wi-Fi并发送用户指令给AI平台。
* 4. 观察第一次Tool Calls响应(可能包含多个工具调用)和LED控制执行过程。
* 5. 观察第二次请求和LLM生成的自然语言反馈。
*
* 注意事项:
* - 本例使用client.setInsecure()跳过了SSL证书验证,仅适用于测试环境;
* 在生产环境中应使用有效证书以确保通信安全。
* - 亮度级别为1-5级,其中1为最暗,5为最亮。
*
* 兼容性说明:
* 本程序专为ESP32-S3-DevKitC-1开发板设计,已在ESP32-S3-DevKitC-1开发板上测试通过。
*
* 许可证:MIT License
*
* 程序源:
* 本程序源自太极创客团队精心开发的《AI创客项目开发教程》。该教程专为热爱科技创新、热衷于动手实践
* 的创客爱好者与初学者量身打造,是一套完全免费、开源且注重实战的AIoT(人工智能物联网)入门学习资源。
*
* 通过本教程,您可以系统地掌握从项目构思、方案设计、软硬件选型,到实际搭建、系统集成与调试优化的
* 整个开发流程。
* 您可以通过以下链接获得更多关于本教程的详细信息:
* http://ai.taichi-maker.com/index.php/homepage/ai-tutorial/
*/
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "my_info.h"
// 引入FastLED库
#include <FastLED.h>
// LED配置 - ESP32-S3-DevKitC-1开发板内置WS2812 LED连接到GPIO 48
#define DATA_PIN 48
#define NUM_LEDS 1
// FastLED LED数组
CRGB leds[NUM_LEDS];
/*
* -------------- 用户指令配置 --------------
*
* 在此处修改用户指令,整个程序将使用这个指令进行第一次和第二次LLM调用
*
* 示例指令:
* - "请打开LED并将亮度调节到最大"
* - "打开LED"
* - "关闭LED"
* - "把LED调到中等亮度"
* - "将LED亮度调到最暗"
*
* 注意:修改此处的指令后,程序会自动在所有相关位置使用新指令,
* 无需在其他地方进行修改
*/
String userCommand = "请打开LED并将亮度调节到最大";
/*
* -------------- 第一次请求:带有两个Tool Call的JSON请求体模板--------------
*
* 此处使用C++11的原始字符串字面量(Raw String Literal)语法R"rawliteral(...)rawliteral"
* 来定义一个多行JSON字符串模板,避免手动转义双引号和换行符。
*
* 该JSON符合OpenAI API兼容格式,包含以下关键字段:
* - model: 指定要调用的AI模型名称
* - messages: 对话历史,包含系统角色和用户输入
* - tools: 定义两个独立的LED控制工具函数:
* 1. controlLEDOnOFF - 控制LED开关
* 2. controlLEDBrightness - 控制LED亮度
* - tool_choice: 设置为"auto"让模型自动决定是否使用工具
*
* 注意:
* - 模板中的{{USER_COMMAND}}占位符将在运行时被userCommand变量的值替换
* - 这种方式使第一次请求的结构更加直观,便于学习者理解
*/
const char* ai_payload_first_template = R"rawliteral({
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。"
},
{
"role": "user",
"content": "{{USER_COMMAND}}"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "controlLEDOnOFF",
"description": "控制LED的打开或关闭",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "LED控制动作",
"enum": ["on", "off"]
}
},
"required": ["action"]
}
}
},
{
"type": "function",
"function": {
"name": "controlLEDBrightness",
"description": "控制LED的亮度级别",
"parameters": {
"type": "object",
"properties": {
"level": {
"type": "integer",
"description": "亮度级别,1-5级,其中1为最暗,5为最亮",
"minimum": 1,
"maximum": 5
}
},
"required": ["level"]
}
}
}
],
"tool_choice": "auto",
"temperature": 0.1
})rawliteral";
/*
* -------------- 第二次请求:Follow-up Tool Calls的JSON请求体模板--------------
*
* 此处使用C++11的原始字符串字面量(Raw String Literal)语法R"rawliteral(...)rawliteral"
* 来定义一个多行JSON字符串模板,包含以下关键字段:
* - model: 指定要调用的AI模型名称
* - messages: 完整的对话历史,包括:
* 1. 系统角色消息
* 2. 用户原始消息
* 3. 助手第一次响应(包含Tool Calls)
* 4. Tool Calls执行结果(可能包含多个)
*
* 注意:
* - 模板中的占位符将在运行时被实际值替换
* - {{USER_COMMAND}}占位符将被userCommand变量的值替换
* - {{ASSISTANT_TOOL_CALLS}}占位符将被完整的assistant tool_calls JSON数组替换
* - {{TOOL_RESULTS}}占位符将被所有tool角色消息替换
* - 这种方式使第二次请求的结构更加直观,便于学习者理解多Tool Calls Follow-up的实现
*/
const char* ai_payload_second_template = R"rawliteral({
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "{{USER_COMMAND}}"
},
{
"role": "assistant",
"tool_calls": {{ASSISTANT_TOOL_CALLS}}
},
{{TOOL_RESULTS}}
],
"temperature": 0.1
})rawliteral";
// 创建一个安全的Wi-Fi客户端(用于HTTPS连接)
WiFiClientSecure client;
// 创建HTTP客户端对象,用于发送请求
HTTPClient https;
// 全局变量,用于存储第一次Tool Calls响应
String firstToolCallResponse = "";
// 定义最大支持的工具调用数量
#define MAX_TOOL_CALLS 5
// 存储多个Tool Call信息的结构体数组
struct ToolCallInfo {
String id;
String name;
String arguments;
String result;
};
ToolCallInfo toolCalls[MAX_TOOL_CALLS];
int toolCallCount = 0;
void setup() {
Serial.begin(115200);
delay(1000);
// 初始化FastLED
FastLED.addLeds<WS2812, DATA_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(128);
leds[0] = CRGB::White;
FastLED.show();
Serial.println("FastLED initialized successfully!");
// WiFi connection (unchanged)
Serial.print("Connecting WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
client.setInsecure();
client.setTimeout(15);
Serial.println();
Serial.println("---------- AI Platform Config ----------");
Serial.print("AI Platform API Endpoint: ");
Serial.print(ai_host);
Serial.println(ai_endpoint);
Serial.println();
Serial.print("一切就绪,等待几秒钟,便于您观察板上LED状态.");
delay(5000);
Serial.println("========== 多Tool Calls Follow-up 示例开始 ==========");
callAIPlatformFirst();
callAIPlatformSecond();
Serial.println("\n========== 多Tool Calls Follow-up 示例结束 ==========");
}
// 主循环留空,因为本例只需发送一次请求
void loop() {
delay(10);
}
/*
* -------------- LED开关控制函数 --------------
*
* 该函数根据AI模型返回的参数控制LED的开关状态
*
* 参数:
* - action: 字符串,"on"表示打开LED,"off"表示关闭LED
*
* 返回值:
* - String: 执行结果的描述,用于发送给LLM生成自然语言反馈
*/
String controlLEDOnOFF(String action) {
Serial.print("控制LED开关: ");
Serial.println(action);
String resultDescription = "";
if (action == "on") {
leds[0] = CRGB::White;
FastLED.show();
resultDescription = "LED已成功打开";
} else if (action == "off") {
leds[0] = CRGB::Black;
FastLED.show();
resultDescription = "LED已成功关闭";
} else {
resultDescription = "LED控制失败:无效的控制指令";
}
return resultDescription;
}
/*
* -------------- LED亮度控制函数 --------------
*
* 该函数根据AI模型返回的参数控制LED的亮度级别
*
* 参数:
* - level: 整数,亮度级别,1-5级,其中1为最暗,5为最亮
*
* 返回值:
* - String: 执行结果的描述,用于发送给LLM生成自然语言反馈
*/
String controlLEDBrightness(int level) {
Serial.print("控制LED亮度: 级别 ");
Serial.println(level);
String resultDescription = "";
if (level >= 1 && level <= 5) {
uint8_t brightness = map(level, 1, 5, 50, 255);
FastLED.setBrightness(brightness);
leds[0] = CRGB::White;
FastLED.show();
String levelDescription;
switch (level) {
case 1: levelDescription = "最暗"; break;
case 2: levelDescription = "较暗"; break;
case 3: levelDescription = "中等"; break;
case 4: levelDescription = "较亮"; break;
case 5: levelDescription = "最亮"; break;
}
resultDescription = "LED亮度已成功调节到" + levelDescription + "级别";
} else {
resultDescription = "亮度调节失败:无效的亮度级别";
}
return resultDescription;
}
/*
* -------------- 第一次向AI平台发送HTTPS请求的函数 --------------
*
* 该函数完成以下步骤:
* 1. 构造完整的HTTPS请求URL(基于my_info.h中定义的主机、端口和路径)
* 2. 设置请求头:Content-Type为application/json,Authorization为Bearer + API密钥
* 3. 发送POST请求,携带包含Tool Call定义的JSON请求体
* 4. 接收并打印HTTP响应状态码和响应内容
* 5. 解析响应中的所有Tool Call参数并依次执行相应操作
* 6. 存储所有Tool Calls响应信息,用于第二次请求
*/
void callAIPlatformFirst() {
Serial.println("\n>>> 第一次请求:发送用户指令,获取Tool Calls响应...");
// 构造Authorization请求头:格式为"Bearer <your-api-key>"
String auth = "Bearer ";
auth += ai_api_key;
// 构造第一次请求的JSON体
String firstPayload = buildFirstPayload();
Serial.println("----- 第一次请求JSON -----");
Serial.println(firstPayload);
Serial.println("----- 请求JSON结束 -----");
// 初始化HTTPS连接(使用client、主机名、端口、API路径)
if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
// 设置超时时间(毫秒)- 增加超时时间以处理更复杂的请求
https.setTimeout(20000); // 20秒超时
https.setConnectTimeout(10000); // 10秒连接超时
Serial.println("HTTPS连接已初始化,发送第一次请求...");
// 添加必要的HTTP请求头
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", auth);
// 发送POST请求,并获取HTTP状态码
int httpCode = https.POST(firstPayload);
if (httpCode > 0) {
// 打印HTTP响应状态码(如200表示成功)
Serial.printf("HTTP响应状态码: %d\n", httpCode);
// 如果响应成功(HTTP 200 OK)
if (httpCode == HTTP_CODE_OK) {
// 获取完整的响应字符串
String resp = https.getString();
Serial.println("----- 第一次响应 -----");
Serial.println(resp); // 打印AI返回的完整JSON
Serial.println("----- 响应结束 -----");
// 存储第一次响应,用于第二次请求
firstToolCallResponse = resp;
// 解析Tool Call响应并执行所有操作
parseToolCallsResponse(resp);
}
} else {
// 打印HTTP错误信息(如连接失败、超时等)
Serial.printf("HTTP错误: %s\n", https.errorToString(httpCode).c_str());
Serial.printf("错误代码: %d\n", httpCode);
}
// 结束本次HTTP会话,释放资源
https.end();
} else {
// 如果无法建立HTTPS连接,打印错误提示
Serial.println("无法连接到服务器");
}
}
/*
* -------------- 第二次向AI平台发送HTTPS请求的函数 --------------
*
* 该函数完成以下步骤:
* 1. 构造包含所有Tool Calls响应和执行结果的JSON请求体
* 2. 设置请求头:Content-Type为application/json,Authorization为Bearer + API密钥
* 3. 发送POST请求,携带包含对话历史和执行结果的JSON请求体
* 4. 接收并打印HTTP响应状态码和响应内容
* 5. 解析并显示LLM生成的自然语言反馈
*/
void callAIPlatformSecond() {
Serial.println("\n>>> 第二次请求:发送Tool Calls响应和执行结果,获取自然语言反馈...");
// 构造Authorization请求头:格式为"Bearer <your-api-key>"
String auth = "Bearer ";
auth += ai_api_key;
// 构造第二次请求的JSON体
String secondPayload = buildSecondPayload();
Serial.println("----- 第二次请求JSON -----");
Serial.println(secondPayload);
Serial.println("----- 请求JSON结束 -----");
// 初始化HTTPS连接(使用client、主机名、端口、API路径)
if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
// 设置超时时间(毫秒)
https.setTimeout(20000); // 20秒超时
https.setConnectTimeout(10000); // 10秒连接超时
Serial.println("HTTPS连接已初始化,发送第二次请求...");
// 添加必要的HTTP请求头
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", auth);
// 发送POST请求,并获取HTTP状态码
int httpCode = https.POST(secondPayload);
if (httpCode > 0) {
// 打印HTTP响应状态码(如200表示成功)
Serial.printf("HTTP响应状态码: %d\n", httpCode);
// 如果响应成功(HTTP 200 OK)
if (httpCode == HTTP_CODE_OK) {
// 获取完整的响应字符串
String resp = https.getString();
Serial.println("----- 第二次响应 -----");
Serial.println(resp); // 打印AI返回的完整JSON
Serial.println("----- 响应结束 -----");
// 解析并显示自然语言反馈
parseSecondResponse(resp);
}
} else {
// 打印HTTP错误信息(如连接失败、超时等)
Serial.printf("HTTP错误: %s\n", https.errorToString(httpCode).c_str());
Serial.printf("错误代码: %d\n", httpCode);
}
// 结束本次HTTP会话,释放资源
https.end();
} else {
// 如果无法建立HTTPS连接,打印错误提示
Serial.println("无法连接到服务器");
}
}
/*
* -------------- 构造第一次请求的JSON体 --------------
*
* 该函数完成以下步骤:
* 1. 使用预定义的JSON模板(ai_payload_first_template)
* 2. 替换模板中的占位符为实际值
* 3. 返回完整的第一次请求JSON体
*
* 返回值:
* - String: 构造好的第一次请求JSON体
*/
String buildFirstPayload() {
// 从模板开始构建请求体
String payload = ai_payload_first_template;
// 替换模板中的占位符
payload.replace("{{USER_COMMAND}}", userCommand);
return payload;
}
/*
* -------------- 构造第二次请求的JSON体 --------------
*
* 该函数完成以下步骤:
* 1. 使用预定义的JSON模板(ai_payload_second_template)
* 2. 构造assistant的tool_calls数组JSON字符串
* 3. 构造所有tool角色消息的JSON字符串
* 4. 替换模板中的占位符为实际值
* 5. 返回完整的第二次请求JSON体
*
* 返回值:
* - String: 构造好的第二次请求JSON体
*/
String buildSecondPayload() {
// 从模板开始构建请求体
String payload = ai_payload_second_template;
// 构造assistant的tool_calls数组JSON字符串
String assistantToolCalls = buildAssistantToolCallsJson();
// 构造所有tool角色消息的JSON字符串
String toolResults = buildToolResultsJson();
// 替换模板中的占位符
payload.replace("{{USER_COMMAND}}", userCommand);
payload.replace("{{ASSISTANT_TOOL_CALLS}}", assistantToolCalls);
payload.replace("{{TOOL_RESULTS}}", toolResults);
return payload;
}
/*
* -------------- 构造assistant的tool_calls数组JSON字符串 --------------
*
* 根据存储的所有Tool Call信息,构造完整的tool_calls数组JSON字符串
*
* 返回值:
* - String: 格式如 [{"id":"xxx","type":"function","function":{"name":"xxx","arguments":"xxx"}}, ...]
*/
String buildAssistantToolCallsJson() {
String json = "[";
for (int i = 0; i < toolCallCount; i++) {
if (i > 0) {
json += ",";
}
// 转义arguments中的双引号
String escapedArgs = toolCalls[i].arguments;
escapedArgs.replace("\"", "\\\"");
json += "{";
json += "\"id\":\"" + toolCalls[i].id + "\",";
json += "\"type\":\"function\",";
json += "\"function\":{";
json += "\"name\":\"" + toolCalls[i].name + "\",";
json += "\"arguments\":\"" + escapedArgs + "\"";
json += "}";
json += "}";
}
json += "]";
return json;
}
/*
* -------------- 构造所有tool角色消息的JSON字符串 --------------
*
* 根据存储的所有Tool Call执行结果,构造所有tool角色消息的JSON字符串
* 多个tool消息之间用逗号分隔
*
* 返回值:
* - String: 格式如 {"role":"tool","tool_call_id":"xxx","content":"xxx"}, {...}
*/
String buildToolResultsJson() {
String json = "";
for (int i = 0; i < toolCallCount; i++) {
if (i > 0) {
json += ",";
}
json += "{";
json += "\"role\":\"tool\",";
json += "\"tool_call_id\":\"" + toolCalls[i].id + "\",";
json += "\"content\":\"" + toolCalls[i].result + "\"";
json += "}";
}
return json;
}
/*
* -------------- 解析第一次Tool Calls响应的函数(多工具调用版)--------------
*
* 该函数完成以下步骤:
* 1. 使用ArduinoJson库解析JSON响应
* 2. 检查响应中是否包含tool_calls字段
* 3. 遍历所有tool_calls,提取每个工具调用的函数名称和参数
* 4. 依次调用相应的控制函数执行操作
* 5. 存储所有Tool Calls信息用于第二次请求
*/
void parseToolCallsResponse(String jsonResponse) {
// 解析JSON响应
DynamicJsonDocument doc(8192);
DeserializationError error = deserializeJson(doc, jsonResponse);
if (error) {
Serial.print("JSON解析失败: ");
Serial.println(error.c_str());
return;
}
// 检查是否有tool_calls
if (doc.containsKey("choices") && doc["choices"].size() > 0) {
JsonObject choice = doc["choices"][0];
JsonObject message = choice["message"];
if (message.containsKey("tool_calls") && message["tool_calls"].size() > 0) {
// 获取tool_calls数组
JsonArray toolCallsArray = message["tool_calls"];
toolCallCount = toolCallsArray.size();
Serial.print("检测到 ");
Serial.print(toolCallCount);
Serial.println(" 个工具调用");
// 限制最大数量
if (toolCallCount > MAX_TOOL_CALLS) {
Serial.print("工具调用数量超过最大值,只处理前 ");
Serial.print(MAX_TOOL_CALLS);
Serial.println(" 个");
toolCallCount = MAX_TOOL_CALLS;
}
// 遍历所有tool_calls
for (int i = 0; i < toolCallCount; i++) {
JsonObject toolCall = toolCallsArray[i];
JsonObject function = toolCall["function"];
String functionName = function["name"].as<String>();
// 存储Tool Call信息
toolCalls[i].id = toolCall["id"].as<String>();
toolCalls[i].name = functionName;
toolCalls[i].arguments = function["arguments"].as<String>();
Serial.print("\n[工具调用 ");
Serial.print(i + 1);
Serial.print("/");
Serial.print(toolCallCount);
Serial.println("]");
Serial.print("函数名: ");
Serial.println(functionName);
Serial.print("Tool Call ID: ");
Serial.println(toolCalls[i].id);
Serial.print("参数: ");
Serial.println(toolCalls[i].arguments);
// 执行对应的操作
Serial.print(">>> 第二步:执行Tool Call ");
Serial.print(i + 1);
Serial.println("...");
if (functionName == "controlLEDOnOFF") {
// 解析LED开关函数参数
DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, toolCalls[i].arguments);
if (argsError) {
Serial.print("参数解析失败: ");
Serial.println(argsError.c_str());
toolCalls[i].result = "LED控制失败:参数解析错误";
continue;
}
String action = argsDoc["action"].as<String>();
Serial.print("执行动作: ");
Serial.println(action);
// 调用LED开关控制函数并保存结果
toolCalls[i].result = controlLEDOnOFF(action);
} else if (functionName == "controlLEDBrightness") {
// 解析LED亮度函数参数
DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, toolCalls[i].arguments);
if (argsError) {
Serial.print("参数解析失败: ");
Serial.println(argsError.c_str());
toolCalls[i].result = "亮度调节失败:参数解析错误";
continue;
}
int level = argsDoc["level"].as<int>();
Serial.print("执行亮度调节: ");
Serial.println(level);
// 调用LED亮度控制函数并保存结果
toolCalls[i].result = controlLEDBrightness(level);
} else {
Serial.print("未知函数: ");
Serial.println(functionName);
toolCalls[i].result = "未知工具:" + functionName;
}
}
Serial.println("\n>>> 所有Tool Calls执行完成");
} else {
// 没有tool_calls,可能是普通文本响应
Serial.println("未检测到tool_calls");
if (message.containsKey("content")) {
String content = message["content"].as<String>();
Serial.print("模型回复: ");
Serial.println(content);
}
}
}
}
/*
* -------------- 解析第二次响应的函数--------------
*
* 该函数完成以下步骤:
* 1. 使用ArduinoJson库解析JSON响应
* 2. 提取LLM生成的自然语言反馈
* 3. 显示反馈内容
*/
void parseSecondResponse(String jsonResponse) {
// 解析JSON响应
DynamicJsonDocument doc(4096);
DeserializationError error = deserializeJson(doc, jsonResponse);
if (error) {
Serial.print("第二次响应JSON解析失败: ");
Serial.println(error.c_str());
return;
}
// 提取并显示自然语言反馈
if (doc.containsKey("choices") && doc["choices"].size() > 0) {
JsonObject choice = doc["choices"][0];
JsonObject message = choice["message"];
if (message.containsKey("content") && !message["content"].as<String>().isEmpty()) {
String content = message["content"].as<String>();
Serial.println(">>> LLM自然语言反馈:");
Serial.println(content);
} else {
Serial.println("LLM没有提供自然语言反馈");
}
} else {
Serial.println("无法解析第二次响应");
}
}
/*
【附录:本程序运行结果】
========== 多Tool Calls Follow-up 示例开始 ==========
>>> 第一步:发送第一次请求,获取Tool Calls响应...
>>> 第一次请求:发送用户指令,获取Tool Calls响应...
----- 第一次请求JSON -----
{
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。"
},
{
"role": "user",
"content": "请打开LED并将亮度调节到最大"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "controlLEDOnOFF",
"description": "控制LED的打开或关闭",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "LED控制动作",
"enum": ["on", "off"]
}
},
"required": ["action"]
}
}
},
{
"type": "function",
"function": {
"name": "controlLEDBrightness",
"description": "控制LED的亮度级别",
"parameters": {
"type": "object",
"properties": {
"level": {
"type": "integer",
"description": "亮度级别,1-5级,其中1为最暗,5为最亮",
"minimum": 1,
"maximum": 5
}
},
"required": ["level"]
}
}
}
],
"tool_choice": "auto",
"temperature": 0.1
}
----- 请求JSON结束 -----
HTTPS连接已初始化,发送第一次请求...
HTTP响应状态码: 200
----- 第一次响应 -----
{
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": "",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"action\": \"on\"}",
"name": "controlLEDOnOFF"
},
"id": "call_abaee57e921a4021829447",
"index": 0,
"type": "function"
},
{
"function": {
"arguments": "{\"level\": 5}",
"name": "controlLEDBrightness"
},
"id": "call_f21c48f98191408f8e5854",
"index": 1,
"type": "function"
}
]
}
}
],
"created": 1777905141,
"id": "chatcmpl-225c67f3-df54-914a-8571-36cfbea54c09",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 43,
"prompt_tokens": 278,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 321
}
}
----- 响应结束 -----
检测到 2 个工具调用
[工具调用 1/2]
函数名: controlLEDOnOFF
Tool Call ID: call_abaee57e921a4021829447
参数: {"action": "on"}
>>> 第二步:执行Tool Call 1...
执行动作: on
控制LED开关: on
- LED已打开
[工具调用 2/2]
函数名: controlLEDBrightness
Tool Call ID: call_f21c48f98191408f8e5854
参数: {"level": 5}
>>> 第二步:执行Tool Call 2...
执行亮度调节: 5
控制LED亮度: 级别 5
- 亮度已设置为最亮级别
>>> 所有Tool Calls执行完成
>>> 第三步:发送第二次请求,获取自然语言反馈...
>>> 第二次请求:发送Tool Calls响应和执行结果,获取自然语言反馈...
----- 第二次请求JSON -----
{
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "请打开LED并将亮度调节到最大"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_abaee57e921a4021829447",
"type": "function",
"function": {
"name": "controlLEDOnOFF",
"arguments": "{\"action\": \"on\"}"
}
},
{
"id": "call_f21c48f98191408f8e5854",
"type": "function",
"function": {
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abaee57e921a4021829447",
"content": "LED已成功打开"
},
{
"role": "tool",
"tool_call_id": "call_f21c48f98191408f8e5854",
"content": "LED亮度已成功调节到最亮级别"
}
],
"temperature": 0.1
}
----- 请求JSON结束 -----
HTTPS连接已初始化,发送第二次请求...
HTTP响应状态码: 200
----- 第二次响应 -----
{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "已为您打开LED,并将亮度调节到最大级别,现在灯光最亮!",
"role": "assistant"
}
}
],
"created": 1777905142,
"id": "chatcmpl-bcc15c17-bf0a-9089-865c-6caf79bac3d9",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 17,
"prompt_tokens": 124,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 141
}
}
----- 响应结束 -----
>>> LLM自然语言反馈:
已为您打开LED,并将亮度调节到最大级别,现在灯光最亮!
========== 多Tool Calls Follow-up 示例结束 ==========
*/下面逐段讲解新增和修改的部分。
1. 全局变量:使用结构体数组存储多个 Tool Call 信息(第223-231行)
上一节使用四个独立的 String 变量存储单个 Tool Call 的信息:
// 上一节的单 Tool Call 存储方式
String firstToolCallId = "";
String firstToolCallName = "";
String firstToolCallArguments = "";本节改为使用结构体数组,可以存储多个 Tool Call 的信息:
// 定义最大支持的工具调用数量
#define MAX_TOOL_CALLS 5
// 存储多个Tool Call信息的结构体数组
struct ToolCallInfo {
String id; // Tool Call 的唯一标识
String name; // 调用的函数名称
String arguments; // 调用的参数(JSON字符串)
String result; // 执行结果描述
};
ToolCallInfo toolCalls[MAX_TOOL_CALLS];
int toolCallCount = 0;| 变量/常量 | 用途 |
|---|---|
MAX_TOOL_CALLS | 定义最多支持多少个工具调用(这里设为5个) |
ToolCallInfo | 结构体,封装单个 Tool Call 的所有信息 |
toolCalls[] | 结构体数组,存储所有 Tool Call 的信息, 如:调用工具函数的名称,参数等 |
toolCallCount | 实际检测到的 Tool Call 数量 |
为什么需要
MAX_TOOL_CALLS限制?因为 ESP32 的内存有限,预先分配固定大小的数组可以避免动态内存分配带来的风险。
2. 第二次请求模板的变化(第191-209行)
上一节的第二次请求模板使用固定的单个 tool_calls 和单个 tool 消息:
// 上一节的模板(单Tool Call)
const char* ai_payload_second_template = R"rawliteral({
...
"tool_calls": [
{
"id": "{{TOOL_CALL_ID}}",
...
}
]
...
{
"role": "tool",
"tool_call_id": "{{TOOL_CALL_ID}}",
...
}
})rawliteral";本节改为使用占位符替换整个数组,因为 Tool Calls 的数量是动态的:
const char* ai_payload_second_template = R"rawliteral({
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "{{USER_COMMAND}}"
},
{
"role": "assistant",
"tool_calls": {{ASSISTANT_TOOL_CALLS}}
},
{{TOOL_RESULTS}}
],
"temperature": 0.1
})rawliteral";模板中有三个占位符:
| 占位符 | 替换内容 |
|---|---|
{{USER_COMMAND}} | 用户原始指令 |
{{ASSISTANT_TOOL_CALLS}} | 完整的 tool_calls JSON 数组 |
{{TOOL_RESULTS}} | 所有 tool 角色消息的 JSON |
💡 设计思路:不再为每个 Tool Call 单独设置占位符,而是将整个数组作为一块内容插入。这样无论 AI 返回多少个 Tool Calls,模板都能适应。
3. 构造 assistant 的 tool_calls 数组:buildAssistantToolCallsJson()(第545-569行)⭐
这是全新增加的函数,用于根据存储的所有 Tool Call 信息,构造 assistant 消息中的 tool_calls 数组 JSON 字符串:
String buildAssistantToolCallsJson() {
String json = "[";
for (int i = 0; i < toolCallCount; i++) {
if (i > 0) {
json += ",";
}
// 转义arguments中的双引号
String escapedArgs = toolCalls[i].arguments;
escapedArgs.replace("\"", "\\\"");
json += "{";
json += "\"id\":\"" + toolCalls[i].id + "\",";
json += "\"type\":\"function\",";
json += "\"function\":{";
json += "\"name\":\"" + toolCalls[i].name + "\",";
json += "\"arguments\":\"" + escapedArgs + "\"";
json += "}";
json += "}";
}
json += "]";
return json;
}函数逻辑解析:
- 遍历所有 Tool Call:使用
for循环遍历toolCalls数组 - 处理逗号分隔:第一个元素前不加逗号,后续元素前加逗号
- 转义双引号:
arguments字段的值(如{"level": 5})包含双引号,需要转义为\",否则破坏 JSON 格式 - 拼接 JSON 字符串:手动构造每个 Tool Call 对象的 JSON
输出示例:
[
{
"id": "call_abaee57e921a4021829447",
"type": "function",
"function": {
"name": "controlLEDOnOFF",
"arguments": "{\"action\": \"on\"}"
}
},
{
"id": "call_f21c48f98191408f8e5854",
"type": "function",
"function": {
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}
}
]4. 构造所有 tool 角色消息:buildToolResultsJson()(第580-596行)⭐
这是全新增加的函数,用于构造所有 tool 角色消息的 JSON 字符串:
String buildToolResultsJson() {
String json = "";
for (int i = 0; i < toolCallCount; i++) {
if (i > 0) {
json += ",";
}
json += "{";
json += "\"role\":\"tool\",";
json += "\"tool_call_id\":\"" + toolCalls[i].id + "\",";
json += "\"content\":\"" + toolCalls[i].result + "\"";
json += "}";
}
return json;
}函数逻辑解析:
- 遍历所有 Tool Call:使用
for循环遍历toolCalls数组 - 处理逗号分隔:多个
tool消息之间用逗号分隔 - 一一对应关系:每个
tool消息的tool_call_id必须与assistant.tool_calls中对应元素的id一致
输出示例:
{
"role": "tool",
"tool_call_id": "call_abaee57e921a4021829447",
"content": "LED已成功打开"
},
{
"role": "tool",
"tool_call_id": "call_f21c48f98191408f8e5854",
"content": "LED亮度已成功调节到最亮级别"
}⚠️ 关键注意:tool_call_id 必须与 assistant.tool_calls 中的 id 严格一一对应。如果顺序错乱或 ID 不匹配,AI 平台可能无法正确理解对话上下文。
5. 构造第二次请求体:buildSecondPayload()(第519-535行)
与上一节相比,本节的 buildSecondPayload() 函数不再解析第一次响应的 JSON,而是直接使用已存储的 Tool Call 信息:
String buildSecondPayload() {
// 从模板开始构建请求体
String payload = ai_payload_second_template;
// 构造assistant的tool_calls数组JSON字符串
String assistantToolCalls = buildAssistantToolCallsJson();
// 构造所有tool角色消息的JSON字符串
String toolResults = buildToolResultsJson();
// 替换模板中的占位符
payload.replace("{{USER_COMMAND}}", userCommand);
payload.replace("{{ASSISTANT_TOOL_CALLS}}", assistantToolCalls);
payload.replace("{{TOOL_RESULTS}}", toolResults);
return payload;
}与上一节的区别:
| 步骤 | 上一节(单Tool Call) | 本节(多Tool Calls) |
|---|---|---|
| 解析第一次响应 | 需要再次解析 JSON | 不需要,信息已存储在结构体数组中 |
| 构造 assistant.tool_calls | 从解析的 JSON 中提取 | 从 toolCalls[] 数组构造 |
| 构造 tool 消息 | 构造单条消息 | 构造多条消息 |
| 转义双引号 | 对单个 arguments 转义 | 在 buildAssistantToolCallsJson() 中对每个 arguments 转义 |
6. 解析多个 Tool Calls:parseToolCallsResponse()(第608-726行)⭐
这是本节改动最大的函数。上一节的解析函数只处理单个 Tool Call,本节改为遍历整个 tool_calls 数组:
void parseToolCallsResponse(String jsonResponse) {
// 解析JSON响应
DynamicJsonDocument doc(8192);
DeserializationError error = deserializeJson(doc, jsonResponse);
if (error) {
Serial.print("JSON解析失败: ");
Serial.println(error.c_str());
return;
}
// 检查是否有tool_calls
if (doc.containsKey("choices") && doc["choices"].size() > 0) {
JsonObject choice = doc["choices"][0];
JsonObject message = choice["message"];
if (message.containsKey("tool_calls") && message["tool_calls"].size() > 0) {
// 获取tool_calls数组
JsonArray toolCallsArray = message["tool_calls"];
toolCallCount = toolCallsArray.size();
Serial.print("检测到 ");
Serial.print(toolCallCount);
Serial.println(" 个工具调用");
// 限制最大数量
if (toolCallCount > MAX_TOOL_CALLS) {
Serial.print("工具调用数量超过最大值,只处理前 ");
Serial.print(MAX_TOOL_CALLS);
Serial.println(" 个");
toolCallCount = MAX_TOOL_CALLS;
}
// 遍历所有tool_calls
for (int i = 0; i < toolCallCount; i++) {
JsonObject toolCall = toolCallsArray[i];
JsonObject function = toolCall["function"];
String functionName = function["name"].as<String>();
// 存储Tool Call信息
toolCalls[i].id = toolCall["id"].as<String>();
toolCalls[i].name = functionName;
toolCalls[i].arguments = function["arguments"].as<String>();
Serial.print("\n[工具调用 ");
Serial.print(i + 1);
Serial.print("/");
Serial.print(toolCallCount);
Serial.println("]");
Serial.print("函数名: ");
Serial.println(functionName);
Serial.print("Tool Call ID: ");
Serial.println(toolCalls[i].id);
Serial.print("参数: ");
Serial.println(toolCalls[i].arguments);
// 执行对应的操作
Serial.print(">>> 第二步:执行Tool Call ");
Serial.print(i + 1);
Serial.println("...");
if (functionName == "controlLEDOnOFF") {
// 解析LED开关函数参数
DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, toolCalls[i].arguments);
if (argsError) {
Serial.print("参数解析失败: ");
Serial.println(argsError.c_str());
toolCalls[i].result = "LED控制失败:参数解析错误";
continue;
}
String action = argsDoc["action"].as<String>();
Serial.print("执行动作: ");
Serial.println(action);
// 调用LED开关控制函数并保存结果
toolCalls[i].result = controlLEDOnOFF(action);
} else if (functionName == "controlLEDBrightness") {
// 解析LED亮度函数参数
DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, toolCalls[i].arguments);
if (argsError) {
Serial.print("参数解析失败: ");
Serial.println(argsError.c_str());
toolCalls[i].result = "亮度调节失败:参数解析错误";
continue;
}
int level = argsDoc["level"].as<int>();
Serial.print("执行亮度调节: ");
Serial.println(level);
// 调用LED亮度控制函数并保存结果
toolCalls[i].result = controlLEDBrightness(level);
} else {
Serial.print("未知函数: ");
Serial.println(functionName);
toolCalls[i].result = "未知工具:" + functionName;
}
}
Serial.println("\n>>> 所有Tool Calls执行完成");
} else {
// 没有tool_calls,可能是普通文本响应
Serial.println("未检测到tool_calls");
if (message.containsKey("content")) {
String content = message["content"].as<String>();
Serial.print("模型回复: ");
Serial.println(content);
}
}
}
}与上一节的关键区别:
| 部分 | 上一节(单Tool Call) | 本节(多Tool Calls) |
|---|---|---|
| 获取 Tool Call | toolCalls[0] 直接取第一个 | 使用 for 循环遍历整个数组 |
| 存储信息 | 存到独立的全局变量 | 存到 toolCalls[i] 结构体数组 |
| 执行操作 | 执行一次 | 依次执行多次 |
| 错误处理 | 失败直接返回 | 使用 continue 跳过当前,继续执行下一个 |
| 数量检查 | 不检查 | 检查是否超过 MAX_TOOL_CALLS |
关键代码解析:
获取 Tool Calls 数量:
JsonArray toolCallsArray = message["tool_calls"];
toolCallCount = toolCallsArray.size();使用 size() 方法获取数组长度,知道有多少个工具调用需要处理。
数量限制保护:
if (toolCallCount > MAX_TOOL_CALLS) {
toolCallCount = MAX_TOOL_CALLS;
}防止 AI 返回过多 Tool Calls 导致数组越界。
遍历执行:
for (int i = 0; i < toolCallCount; i++) {
JsonObject toolCall = toolCallsArray[i];
// ...提取信息...
// ...执行操作...
toolCalls[i].result = controlLEDOnOFF(action);
}使用 for 循环依次处理每个 Tool Call,并将执行结果保存到对应的结构体中。
错误处理:
if (argsError) {
toolCalls[i].result = "LED控制失败:参数解析错误";
continue; // 跳过当前,继续执行下一个Tool Call
}使用 continue 而不是 return,确保即使某个 Tool Call 执行失败,其他 Tool Call 仍能继续执行。
完整交互流程示例
下面以用户指令 “请打开LED并将亮度调节到最大” 为例,展示 ESP32、AI 平台和硬件之间的完整交互流程。
第一步:ESP32 发送第一次请求
POST /compatible-mode/v1/chat/completions
{
"model": "qwen-flash",
"messages": [
{"role": "system", "content": "你是一个智能家居助手..."},
{"role": "user", "content": "请打开LED并将亮度调节到最大"}
],
"tools": [ /* 两个LED控制工具 */ ],
"tool_choice": "auto",
"temperature": 0.1
}第二步:AI 返回多个 Tool Calls
{
"choices": [
{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"tool_calls": [
{
"id": "call_abaee57e921a4021829447",
"function": {
"name": "controlLEDOnOFF",
"arguments": "{\"action\": \"on\"}"
}
},
{
"id": "call_f21c48f98191408f8e5854",
"function": {
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}
}
]
}
}
]
}第三步:ESP32 依次执行所有 Tool Calls
- Tool Call 1:
controlLEDOnOFF("on")→ LED 打开 - Tool Call 2:
controlLEDBrightness(5)→ 亮度调到最亮
第四步:ESP32 发送第二次请求(Follow-up)
POST /compatible-mode/v1/chat/completions
{
"model": "qwen-flash",
"messages": [
{"role": "system", "content": "你是一个智能家居助手..."},
{"role": "user", "content": "请打开LED并将亮度调节到最大"},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_abaee57e921a4021829447",
"function": {"name": "controlLEDOnOFF", "arguments": "{\"action\": \"on\"}"}
},
{
"id": "call_f21c48f98191408f8e5854",
"function": {"name": "controlLEDBrightness", "arguments": "{\"level\": 5}"}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abaee57e921a4021829447",
"content": "LED已成功打开"
},
{
"role": "tool",
"tool_call_id": "call_f21c48f98191408f8e5854",
"content": "LED亮度已成功调节到最亮级别"
}
],
"temperature": 0.1
}第五步:AI 返回整合后的自然语言反馈
{
"choices": [
{
"finish_reason": "stop",
"message": {
"content": "已为您打开LED,并将亮度调节到最大级别,现在灯光最亮!"
}
}
]
}单 Tool Call vs 多 Tool Calls 对比总结
| 对比项 | 单 Tool Call | 多 Tool Calls |
|---|---|---|
| 适用指令 | “打开LED” | “打开LED并调到最亮” |
响应中的 tool_calls | 1 个元素 | 多个元素 |
| 存储方式 | 独立全局变量 | 结构体数组 |
| 执行方式 | 执行一次 | for 循环依次执行 |
Follow-up 的 tool 消息 | 1 条 | 多条 |
| AI 反馈特点 | 针对单个操作 | 整合多个操作的结果 |
扩展思考
1. 如何处理更多工具?
本示例只定义了两个工具(开关和亮度),但实际项目中可能需要更多工具:
controlLEDColor:控制 LED 颜色controlLEDWaver:控制 LED 闪烁效果readTemperature:读取温度传感器readHumidity:读取湿度传感器
只需要在 tools 数组中继续添加工具定义,并在 parseToolCallsResponse() 的 for 循环中增加对应的处理分支即可。
2. 如何处理 Tool Call 之间的依赖关系?
有些场景下,Tool Call 之间存在依赖关系。例如:
- “把灯打开并设置为红色”:先执行
controlLEDOnOFF("on"),再执行controlLEDColor("red") - “如果温度高于30度就打开风扇”:先执行
readTemperature(),根据结果决定是否执行controlFan("on")
对于简单的顺序依赖,AI 平台通常会自动按正确顺序排列 Tool Calls。对于条件依赖,则可能需要在 ESP32 端增加逻辑判断。
3. 内存优化建议
ESP32 的内存有限,当 Tool Calls 数量很多时,需要注意:
- 控制
MAX_TOOL_CALLS的大小:根据实际需求设置,不要过大 - 减小 JSON 文档容量:
DynamicJsonDocument的容量参数(如4096)应根据实际响应大小调整 - 及时释放资源:使用
https.end()关闭连接,避免内存泄漏
总结
本节课程讲解了 ESP32 处理多 Tool Calls 的完整流程:
- 使用结构体数组存储多个 Tool Call 信息:替代独立变量,支持动态数量
- 遍历
tool_calls数组依次执行:使用for循环处理每个工具调用 - 构造包含多个
tool消息的 Follow-up 请求:确保每个 Tool Call 都有对应的执行结果 - 获取 AI 整合后的自然语言反馈:让 AI “既会做事,也会说话”
理解了这个流程后,您就可以构建更复杂的 AIoT 应用:同时控制多个设备、执行多种操作,让 AI 助手真正成为智能管家。
基础知识篇目录
第〇章 (AID101-1-0) 序言
第一章 (AID101-1-1) 准备工作
第二章 (AID101-1-2) AI大模型API交互基础
- AID101-1-2-01 – 使用ESP32调用AI大模型API的基本操作
- AID101-1-2-02 – System Role详解
- AID101-1-2-03 – User Role 详解
- AID101-1-2-04 – Assistant Role详解
- AID101-1-2-05 -大模型API交互基础知识总结及练习
第三章 (AID101-1-3) AI大模型工具调用(tools call)
- AID101-1-3-01 – AI大模型工具调用(tools call)基础
- AID101-1-3-02 – 单工具定义实例
- AID101-1-3-03 – 多工具定义实例
- AID101-1-3-04 – 后续工具调用(follow-up tools call)实例
- AID101-1-3-05 – 并行多工具调用(parallel tool calls)实例
- AID101-1-3-06 – 应用开发实例: 可用自然语言控制的人工智能LED
附录