导航栏:首页 / AI教程目录 / AI创客项目开发教程目录 / 第1篇 基础知识篇 / AID101-1-3-02 – 单工具定义实例
本节内容简介
本节将详细讲解如何让 ESP32-S3-DevKitC-1 开发板连接 Wi-Fi 后,通过 AI 平台的 Tool Calls(工具调用)功能,根据用户的自然语言指令控制开发板上的板载 LED。
整个程序可以分为四个阶段:初始化与准备、系统启动、发送 Tool Calls 请求和解析响应并执行操作。接下来,我们将逐步分析每个阶段的具体工作。
注意
本节课程示例程序需要使用一款名叫AvantLumi的第三方库。该库可以大大简化LED控制的代码,让我们可以更好的专注于课程内容学习。
您可以通过以下本站网页了解如何获取该库的详细信息:
AvantLumi库下载页
本节代码 – 单工具定义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平台调用 Tool Calls 示例
*
* 功能描述:
* 本程序专门为ESP32-S3-DevKitC-1开发板设计,演示如何通过LLM模型的Tool Calls功能
* 控制开发板上内置的WS2812 LED。程序通过HTTPS协议向AI平台发送带有工具定义的请求,
* AI模型根据用户指令分析出需要调用的工具和参数,ESP32解析响应后执行相应的LED控制操作。
*
* 作者:Taichi-Maker
* 作者官网:http://ai.taichi-maker.com
* 创建日期:2026年04月17日
* 版本:1.0.5
*
* 硬件要求:
* - ESP32-S3-DevKitC-1开发板(内置一颗WS2812 LED,数据引脚连接到GPIO 48)
*
* 所需软件库:
* - AvantLumi库(用于控制WS2812 LED)
* - FastLED库(AvantLumi库的依赖库,必须安装)
* - ArduinoJson库(版本7.0.0或更高)——用于处理JSON数据
*
* 配置说明:
* 1. 请先通过Arduino IDE的库管理器安装FastLED、ArduinoJson和AvantLumi库。
* 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. AI模型通过Tool Calls功能分析指令并返回控制参数。
* 5. ESP32解析响应并控制LED的点亮和熄灭。
*
* 注意事项:
* - 本例使用client.setInsecure()跳过了SSL证书验证,仅适用于测试环境;
* 在生产环境中应使用有效证书以确保通信安全。
* - 用户指令为"打开LED"或"关闭LED",可根据需要修改。
*
* 兼容性说明:
* 本程序专为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"
// 引入AvantLumi库
#include "AvantLumi.h"
// LED配置 - ESP32-S3-DevKitC-1开发板内置WS2812 LED连接到GPIO 48
#define DATA_PIN 48
#define NUM_LEDS 1
// 创建AvantLumi实例
AvantLumi myLumi(DATA_PIN, NUM_LEDS);
/*
* -------------- 带有Tool Calls的JSON请求体 --------------
*
* 此处使用C++11的原始字符串字面量(Raw String Literal)语法R"rawliteral(...)rawliteral"
* 来定义一个多行JSON字符串,避免手动转义双引号和换行符。
*
* 该JSON符合OpenAI API兼容格式,包含以下关键字段:
* - model: 指定要调用的AI模型名称
* - messages: 对话历史,包含系统角色和用户输入
* - tools: 定义可用的工具函数列表,此处定义了一个控制LED开关的函数
* - tool_choice: 设置为"auto"让模型自动决定是否使用工具
* - temperature: 控制生成文本的随机性
* - max_tokens: 限制模型最多生成的token数量
*/
const char* ai_payload = R"rawliteral({
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以帮助用户控制设备。"
},
{
"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"]
}
}
}
],
"tool_choice": "auto",
"temperature": 0.1
})rawliteral";
// 创建一个安全的Wi-Fi客户端(用于HTTPS连接)
WiFiClientSecure client;
// 创建HTTP客户端对象,用于发送请求
HTTPClient https;
void setup() {
// 初始化串口通信,波特率为115200
Serial.begin(115200);
delay(1000); // 短暂等待串口稳定
// 初始化LED控制器
if (myLumi.begin()) {
Serial.println("AvantLumi initialized successfully!");
} else {
Serial.println("AvantLumi initialization failed! Halting.");
while (true); // 停止执行
}
// 利用Avant_Lumi库设置板上LED的初始状态,便于观察
myLumi.setRGB(255, 255, 255); // 设置LED颜色初始参数为白色
myLumi.setSwitch(false); // 设置LED初始状态为关闭,便于观察
myLumi.setFade(false); // 设置LED初始闪烁状态为关闭,便于观察
// 执行LED状态初始设置
for(int i=0; i<1024; i++){
myLumi.update();
}
// 开始连接Wi-Fi
Serial.print("Connecting WiFi");
WiFi.begin(ssid, password);
// 循环等待直到成功连接
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// 成功连接后换行
Serial.println();
// 【重要】跳过SSL证书验证(仅用于开发测试!)
// 在真实产品中应加载有效证书以防止中间人攻击
client.setInsecure();
// 设置SSL连接超时时间
client.setTimeout(15); // 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(10000);
Serial.print("开始调用AI大模型");
// 调用函数,向AI平台发送请求
callAIPlatform();
}
// 主循环留空,因为本例只需发送一次请求
void loop() {
}
/*
* -------------- 向AI平台发送HTTPS请求的核心函数 --------------
*
* 该函数完成以下步骤:
* 1. 构造完整的HTTPS请求URL(基于my_info.h中定义的主机、端口和路径)
* 2. 设置请求头:Content-Type为application/json,Authorization为Bearer + API密钥
* 3. 发送POST请求,携带包含Tool Calls定义的JSON请求体
* 4. 接收并打印HTTP响应状态码和响应内容
* 5. 解析响应中的Tool Calls参数并执行相应操作
*/
void callAIPlatform() {
Serial.println("\n>>> Calling AI Platform API...");
// 构造Authorization请求头:格式为"Bearer <your-api-key>"
String auth = "Bearer ";
auth += ai_api_key;
// 初始化HTTPS连接(使用client、主机名、端口、API路径)
if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
// 设置超时时间(毫秒)- 增加超时时间以处理更复杂的请求
https.setTimeout(20000); // 20秒超时
https.setConnectTimeout(10000); // 10秒连接超时
Serial.println("HTTPS connection initialized, sending request...");
// 添加必要的HTTP请求头
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", auth);
// 发送POST请求,并获取HTTP状态码
int httpCode = https.POST(ai_payload);
if (httpCode > 0) {
// 打印HTTP响应状态码(如200表示成功)
Serial.printf("HTTP Response code: %d\n", httpCode);
// 如果响应成功(HTTP 200 OK)
if (httpCode == HTTP_CODE_OK) {
// 获取完整的响应字符串
String resp = https.getString();
Serial.println("----- Response -----");
Serial.println(resp); // 打印AI返回的完整JSON
Serial.println("----- End -----");
// 解析Tool Calls响应
parseToolCallsResponse(resp);
}
} else {
// 打印HTTP错误信息(如连接失败、超时等)
Serial.printf("HTTP error: %s\n", https.errorToString(httpCode).c_str());
Serial.printf("Error code: %d\n", httpCode);
}
// 结束本次HTTP会话,释放资源
https.end();
} else {
// 如果无法建立HTTPS连接,打印错误提示
Serial.println("Unable to connect to server");
}
}
/*
* -------------- 解析Tool Calls响应的函数 --------------
*
* 该函数完成以下步骤:
* 1. 使用ArduinoJson库解析JSON响应
* 2. 检查响应中是否包含tool_calls字段
* 3. 提取函数名称和参数
* 4. 调用相应的控制函数执行操作
*/
void parseToolCallsResponse(String jsonResponse) {
// 解析JSON响应
DynamicJsonDocument doc(4096);
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) {
JsonObject toolCall = message["tool_calls"][0];
JsonObject function = toolCall["function"];
String functionName = function["name"].as<String>();
Serial.print("检测到工具调用: ");
Serial.println(functionName);
if (functionName == "controlLEDOnOFF") {
// 解析函数参数
String arguments = function["arguments"].as<String>();
// 解析参数JSON
DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, arguments);
if (argsError) {
Serial.print("参数解析失败: ");
Serial.println(argsError.c_str());
return;
}
String action = argsDoc["action"].as<String>();
Serial.print("执行动作: ");
Serial.println(action);
// 调用LED控制函数
controlLEDOnOFF(action);
} else {
Serial.print("未知函数: ");
Serial.println(functionName);
}
} else {
// 没有tool_calls,可能是普通文本响应
Serial.println("未检测到tool_calls");
if (message.containsKey("content")) {
String content = message["content"].as<String>();
Serial.print("模型回复: ");
Serial.println(content);
}
}
}
}
/*
* -------------- LED控制函数 --------------
*
* 该函数根据AI模型返回的参数控制LED的开关状态
*
* 参数:
* - action: 字符串,"on"表示打开LED,"off"表示关闭LED
*/
void controlLEDOnOFF(String action) {
Serial.print("控制LED: ");
Serial.println(action);
if (action == "on") {
myLumi.setSwitch(true);
Serial.println("通过controlLEDOnOFF函数,LED已打开");
} else if (action == "off") {
// 关闭LED
myLumi.setSwitch(false);
Serial.println("通过controlLEDOnOFF函数,LED已关闭");
} else {
Serial.println("无效的LED控制指令");
}
}
/*
* ========================================================================
* 【附录1】本程序实际发送的HTTP请求内容(供参考)
* ========================================================================
*
* 假设my_info.h中定义如下(仅为示例):
* const char* ai_host = "dashscope.aliyuncs.com";
* const uint16_t ai_port = 443;
* const char* ai_endpoint = "/compatible-mode/v1/chat/completions";
* const char* ai_api_key = "sk-xxxxxxxxxxxxxxxxxxxxxxxx";
*
* 则程序实际发出的完整HTTP请求如下:
*
* POST /compatible-mode/v1/chat/completions HTTP/1.1
* Host: dashscope.aliyuncs.com
* Content-Type: application/json
* Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxx
* Content-Length: [自动计算]
*
* {
* "model": "Qwen/Qwen3-VL-8B-Instruct",
* "messages": [
* {
* "role": "system",
* "content": "你是一个智能家居助手,可以帮助用户控制设备"
* },
* {
* "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"]
* }
* }
* }
* ],
* "tool_choice": "auto",
* "temperature": 0.1
* }
*
* ========================================================================
* 【附录2】本程序通过百炼平台收到的HTTP响应内容(供参考)
* ========================================================================
{
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": "",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"action\": \"on\"}",
"name": "controlLEDOnOFF"
},
"id": "call_79fb27a337534b0fa58147",
"index": 0,
"type": "function"
}
]
}
}
],
"created": 1776425906,
"id": "chatcmpl-7fc1186d-a768-95c1-845c-5f9d32e89951",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 21,
"prompt_tokens": 177,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 198
}
}
* ========================================================================
*/代码详解
阶段一:初始化与准备
在程序正式开始运行之前,需要先做好一些准备工作。这一阶段主要完成三件事:引入必要的库文件、定义 LED 配置,以及定义 Tool Calls 请求数据。
步骤1:包含必要的库(第54-58行)
#include <WiFi.h> // Wi-Fi连接功能
#include <WiFiClientSecure.h> // 安全的HTTPS客户端
#include <HTTPClient.h> // HTTP请求功能
#include <ArduinoJson.h> // JSON解析库
#include "my_info.h" // 用户配置信息(WiFi密码、API密钥等)程序的开始处使用 #include 指令引入了五个库文件。与第2章的简单请求示例相比,这里新增了 ArduinoJson.h 库:
WiFi.h:让 ESP32 能够连接无线网络WiFiClientSecure.h:让 ESP32 能够通过加密方式(HTTPS)与服务器安全通信HTTPClient.h:让 ESP32 能够按照 HTTP 协议发送网络请求ArduinoJson.h:新增,用于解析 AI 平台返回的 JSON 格式响应,提取 Tool Calls 中的函数名和参数my_info.h:这是用户自定义的文件,用来存放 Wi-Fi 名称、密码以及 AI 平台的 API 密钥
步骤2:引入 LED 控制库并配置(第61-68行)
#include "AvantLumi.h"
#define DATA_PIN 48
#define NUM_LEDS 1
AvantLumi myLumi(DATA_PIN, NUM_LEDS);本程序使用 AvantLumi 库来控制 ESP32-S3-DevKitC-1 开发板上的板载 WS2812 LED。该 LED 的数据引脚连接到 GPIO 48。
DATA_PIN 48:定义 LED 的数据引脚为 GPIO 48NUM_LEDS 1:开发板上只有 1 颗 WS2812 LEDAvantLumi myLumi(DATA_PIN, NUM_LEDS):创建 AvantLumi 实例,用于控制 LED
注意
本节课程示例程序需要使用一款名叫AvantLumi的第三方库。该库可以大大简化LED控制的代码,让我们可以更好的专注于课程内容学习。
您可以通过以下本站网页了解如何获取该库的详细信息:
AvantLumi库下载页
步骤3:定义 Tool Calls 请求的 JSON 体(第84-118行)
const char* ai_payload = R"rawliteral({
"model": "qwen-flash",
"messages": [...],
"tools": [...],
"tool_choice": "auto",
"temperature": 0.1
})rawliteral";接下来,程序定义了发送给 AI 平台的请求内容。与第2章的简单请求相比,这里的 JSON 请求体新增了 tools 和 tool_choice 字段,用于启用 Tool Calls 功能。
请求内容包含六个关键参数:
model:指定要使用的 AI 模型名称,这里是”qwen-flash”messages:对话内容,包含系统提示和用户问题tools:新增,声明 ESP32 上可用的工具函数列表tool_choice:新增,设置为"auto"让 AI 自动决定是否调用工具temperature:控制 AI 输出的随机性,这里设为 0.1 以确保输出稳定可靠max_tokens:限制 AI 回答的最大长度
其中 tools 数组中定义了一个名为 controlLEDOnOFF 的函数:
{
"type": "function",
"function": {
"name": "controlLEDOnOFF",
"description": "控制LED的打开或关闭",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "LED控制动作",
"enum": ["on", "off"]
}
},
"required": ["action"]
}
}
}这段 JSON 告诉 AI 平台:ESP32 上有一个名为 controlLEDOnOFF 的函数,它可以控制 LED 的开关,需要一个名为 action 的字符串参数,取值只能是 "on" 或 "off"。
步骤4:创建客户端对象(第120-124行)
WiFiClientSecure client; // 安全的HTTPS客户端
HTTPClient https; // HTTP客户端对象为了让 ESP32 能够与 AI 服务器通信,程序创建了两个核心对象。这两个对象与第2章的示例完全相同:
client(WiFiClientSecure 类型):负责建立和管理与服务器的安全连接https(HTTPClient 类型):负责按照 HTTP 协议发送请求和接收响应
阶段二:系统启动(setup函数)
ESP32 程序中的 setup() 函数会在开发板启动时执行一次。接下来看看这个函数中做了些什么。
步骤5:初始化串口(第128-129行)
Serial.begin(115200);
delay(1000); // 等待串口稳定首先启动串口通信,波特率设为 115200。串口是 ESP32 与电脑之间最常用的通信方式,通过它可以在电脑的串口监视器中查看程序运行的信息。
这里等待 1 秒是为了确保串口完全启动,避免后续的打印信息丢失。
在连接 Wi-Fi 和调用 AI 平台之前,程序需要先初始化 LED 控制器,确保 LED 处于可控状态。
步骤6:初始化 LED (第132-137行)
if (myLumi.begin()) {
Serial.println("AvantLumi initialized successfully!");
} else {
Serial.println("AvantLumi initialization failed! Halting.");
while (true); // 停止执行
}首先调用 myLumi.begin() 初始化 AvantLumi 库。如果初始化失败,程序会进入死循环停止执行,因为 LED 控制是本程序的核心功能。
步骤7:设置 LED 初始状态(第136-142行)
步骤7:设置 LED 初始状态(第140-147行)
myLumi.setRGB(255, 255, 255); // 设置LED颜色初始参数为白色
myLumi.setSwitch(false); // 设置LED初始状态为关闭,便于观察
myLumi.setWaver(false); // 设置LED初始闪烁状态为关闭,便于观察
for(int i=0; i<1024; i++){
myLumi.update();
}然后设置 LED 的初始状态:
setRGB(255, 255, 255):将 LED 颜色设置为白色(即RGB值 255 255 255)setSwitch(false):将 LED 开关状态设置为关闭,便于观察setWaver(false):关闭 LED 的闪烁效果,便于观察
最后通过循环调用 update() 方法 1024 次,确保初始设置生效。可能有的朋友看到这里会感到有些费解,为何这里的update()函数要用一个for循环来调用1024次。这是因为AvantLumi库为了确保LED在状态变化过程中有一个平滑的过渡(比如LED从打开到关闭,有一个从亮到暗的渐变过程),因此需要我们多次调用update()函数,才能实现LED从打开到关闭。因此这里使用for循环多次调用update()函数,实现真正的效果变化。如果您看完这段话感觉不太理解,只要记住一点,就是,只有通过这里的for循环,才能确保 LED 在程序启动后处于关闭状态,这样才能更好的观察 AI 是如何控制 LED 的变化效果。
步骤8:连接Wi-Fi(第150-160行)
Serial.print("Connecting WiFi");
WiFi.begin(ssid, password); // 从my_info.h读取WiFi名称和密码
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(); // 连接成功后换行然后让 ESP32 连接到 Wi-Fi 网络。程序会不断检查连接状态,直到成功连接为止。连接过程中会在串口监视器打印圆点来显示进度,连接成功后换行显示。
Wi-Fi 连接是后续访问 AI 服务器的前提,只有成功连接网络,才能与互联网上的 AI 平台进行通信。
步骤9:配置HTTPS客户端(第164-167行)
client.setInsecure(); // 跳过SSL证书验证(仅用于测试!)
client.setTimeout(15); // 设置15秒超时在建立安全连接之前,需要对客户端进行一些配置。
setInsecure() 方法会跳过 SSL 证书验证,这样做的好处是可以简化开发流程。需要注意的是,在正式产品中应该使用有效的 SSL 证书来确保通信安全。
setTimeout(15) 设置了 15 秒的超时时间,如果在规定时间内无法完成与AI平台的通讯操作,程序就会放弃等待。这可以避免程序因为网络通讯问题而无限期卡住。
步骤10:打印配置信息并调用AI平台(第170-181行)
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(10000);
Serial.print("开始调用AI大模型");
callAIPlatform(); // 发送AI请求准备工作完成后,程序会先在串口监视器上打印 AI 平台的地址信息。然后等待 10 秒钟,让用户有时间观察 LED 的初始状态(关闭状态)。最后调用 callAIPlatform() 函数来发送真正的请求。
阶段三:发送 Tool Calls 请求(callAIPlatform函数)
callAIPlatform() 函数封装了与 AI 服务器通信的完整流程。与第2章的示例相比,这个函数在成功获取响应后增加了 Tool Calls 解析的步骤。
步骤11:构造Authorization请求头(第205-206行)
String auth = "Bearer ";
auth += ai_api_key; // 从my_info.h读取API密钥首先需要构造身份验证信息。AI 服务器需要确认请求者的身份,这就需要使用 API 密钥。
“Bearer” 是 HTTP 协议中常用的认证方式,后面的 API 密钥就是我们的”通行证”。最终构造出的认证头类似:"Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxx"。
步骤12:初始化HTTPS连接(第209行)
if (https.begin(client, ai_host, ai_port, ai_endpoint)) {然后调用 begin() 方法初始化 HTTPS 连接。这个方法需要传入服务器的主机名、端口号和 API 路径等参数。
如果初始化成功,begin() 会返回 true,程序就会继续执行后续操作;否则会跳过这段代码并报错。
步骤13:设置超时时间(第212-213行)
https.setTimeout(20000); // 20秒超时
https.setConnectTimeout(10000); // 10秒连接超时为了提高程序的稳定性,需要设置超时时间。
setTimeout(20000) 设置整个请求的超时时间为 20 秒;setConnectTimeout(10000) 设置建立连接的超时时间为 10 秒。这样即使网络不稳定,程序也不会无限等待。
步骤14:添加HTTP请求头(第218-219行)
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", auth);接下来添加 HTTP 请求头。请求头告诉服务器一些必要的信息:
Content-Type: application/json:告诉服务器,我们发送的数据是 JSON 格式Authorization: auth:告诉服务器我们的身份(API 密钥)
这是 HTTP 协议的标准做法,所有网络请求都需要包含这些信息。
步骤15:发送POST请求(第222行)
int httpCode = https.POST(ai_payload);一切准备就绪后,调用 POST() 方法将请求发送到 AI 服务器。参数 ai_payload 就是我们在步骤3中定义的包含 Tool Calls 的 JSON 请求体。
服务器返回的 HTTP 状态码会被保存到 httpCode 变量中。常见的状态码有:200 表示成功,404 表示找不到资源,500 表示服务器错误等。
步骤16:处理响应并解析 Tool Calls(第224-243行)
if (httpCode > 0) {
Serial.printf("HTTP Response code: %d\n", httpCode);
if (httpCode == HTTP_CODE_OK) {
String resp = https.getString();
Serial.println("----- Response -----");
Serial.println(resp);
Serial.println("----- End -----");
// 解析Tool Calls响应
parseToolCallsResponse(resp);
}
} else {
Serial.printf("HTTP error: %s\n", https.errorToString(httpCode).c_str());
Serial.printf("Error code: %d\n", httpCode);
}收到服务器响应后,需要判断请求是否成功。
首先检查 httpCode 是否大于 0:如果大于 0,说明请求已经成功发送并收到了响应;如果小于等于 0,说明请求失败(比如无法连接服务器)。
如果状态码是 200(HTTP_CODE_OK),说明请求成功。这时调用 getString() 方法读取服务器的响应内容,并在串口监视器中打印出来。
与本教程第2章的示例程序不同,本示例程序在打印完响应内容后,程序调用了 parseToolCallsResponse(resp) 函数来解析 AI 返回的 Tool Calls 指令。这是本程序的核心逻辑,负责将 AI 的结构化指令转换为实际的 LED 控制操作。
步骤17:结束HTTP会话(第246行)
https.end();最后调用 end() 方法关闭与服务器的连接,释放占用的资源。这是良好的编程习惯,可以避免内存泄漏等问题。
阶段四:解析 Tool Calls 并执行操作
这是本程序最核心的部分,负责将 AI 返回的结构化指令解析出来,并调用相应的函数控制 LED。
步骤18:解析 Tool Calls 响应(parseToolCallsResponse函数,第262-271行)
void parseToolCallsResponse(String jsonResponse) {
DynamicJsonDocument doc(4096);
DeserializationError error = deserializeJson(doc, jsonResponse);
if (error) {
Serial.print("JSON解析失败: ");
Serial.println(error.c_str());
return;
}首先使用 ArduinoJson 库的 DynamicJsonDocument 创建一个 JSON 文档对象,容量为 4096 字节。然后调用 deserializeJson() 将服务器返回的 JSON 字符串解析为可操作的数据结构。
如果解析失败(比如 JSON 格式不正确),函数会打印错误信息并直接返回。
步骤19:检查是否包含 tool_calls(第274-278行)
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) {解析成功后,程序按照以下层级结构检查响应内容:
- 检查根对象是否包含
choices数组,且数组不为空 - 获取
choices数组的第一个元素 - 从第一个 choice 中获取
message对象 - 检查
message中是否包含tool_calls数组,且数组不为空
这种层级结构是 OpenAI 兼容 API 的标准响应格式。如果 AI 决定调用工具,tool_calls 数组中就会包含具体的工具调用指令。
步骤20:提取函数名称和参数(第279-288行)
JsonObject toolCall = message["tool_calls"][0];
JsonObject function = toolCall["function"];
String functionName = function["name"].as<String>();
Serial.print("检测到工具调用: ");
Serial.println(functionName);
if (functionName == "controlLEDOnOFF") {
String arguments = function["arguments"].as<String>();如果检测到 tool_calls,程序会提取第一个工具调用的信息:
functionName:获取 AI 要求调用的函数名称arguments:获取函数调用所需的参数(JSON 格式的字符串)
然后检查函数名称是否为 controlLEDOnOFF。如果是,就继续解析参数;如果不是,会打印”未知函数”的提示。
步骤21:解析参数并执行操作(第291-305行)
DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, arguments);
if (argsError) {
Serial.print("参数解析失败: ");
Serial.println(argsError.c_str());
return;
}
String action = argsDoc["action"].as<String>();
Serial.print("执行动作: ");
Serial.println(action);
controlLEDOnOFF(action);由于 arguments 是一个 JSON 格式的字符串(例如 {"action": "on"}),需要再次使用 ArduinoJson 进行解析。
解析成功后,从参数对象中提取 action 字段的值("on" 或 "off"),然后调用 controlLEDOnOFF(action) 函数来实际控制 LED。
步骤22:处理普通文本响应(第316-318行)
} else {
Serial.println("未检测到tool_calls");
if (message.containsKey("content")) {
String content = message["content"].as<String>();
Serial.print("模型回复: ");
Serial.println(content);
}
}如果 AI 没有返回 tool_calls(比如用户的问题不需要控制 LED),程序会检查 message 中是否包含 content 字段。如果有,就打印 AI 的普通文本回复。
步骤23:LED 控制函数(controlLEDOnOFF函数,第329-343行)
void controlLEDOnOFF(String action) {
Serial.print("控制LED: ");
Serial.println(action);
if (action == "on") {
myLumi.setSwitch(true);
Serial.println("通过controlLEDOnOFF函数,LED已打开");
} else if (action == "off") {
myLumi.setSwitch(false);
Serial.println("通过controlLEDOnOFF函数,LED已关闭");
} else {
Serial.println("无效的LED控制指令");
}
}这是实际执行 LED 控制的函数。根据传入的 action 参数:
"on":调用myLumi.setSwitch(true)打开 LED"off":调用myLumi.setSwitch(false)关闭 LED- 其他值:打印错误提示
注意:真正执行 GPIO 操作的是 ESP32,而不是 AI 模型。AI 模型只是根据用户的自然语言指令,决定应该调用哪个函数、传入什么参数。
主循环 Loop()
void loop() {
}ESP32 程序中的 loop() 函数会在 setup() 执行完后反复运行。
本示例程序仅仅在启动过程中,也就是在setup函数中需要执行一次AI平台工具调用,对LED进行操作。完成这一任务后,ESP32开发板也就完成了使命,可以休息了,因此本程序的loop()函数是空的。
不过这里我想要多提一下,如果我们未来使用AvantLumi库开发LED控制项目,那么就必须在loop函数调用update() 方法。update()是 AvantLumi 库的核心,负责处理 LED 的亮度过渡和调色板混合。即使只是简单地开关 LED,也需要在循环中持续调用 update(),以确保库内部的时序逻辑正常工作。关于这一点,我们后续教程中还会接触到。
总结
这个程序展示了 ESP32 通过 Tool Calls 功能控制硬件的完整流程:
- 定义工具:在 JSON 请求中通过
tools字段声明 ESP32 上可用的函数 - 发送请求:将用户指令和工具定义一起发送给 AI 平台
- 解析响应:使用 ArduinoJson 库解析 AI 返回的结构化指令
- 执行操作:根据解析结果调用相应的函数控制硬件
理解了这个流程后,就可以进一步扩展:定义更多的工具函数(如调节亮度、改变颜色、读取传感器等),让 AI 能够控制更复杂的物联网设备。
附录:本程序实际发送的 HTTP 请求内容(供参考)
假设 my_info.h 中定义如下(仅为示例):
const char* ai_host = "dashscope.aliyuncs.com";
const uint16_t ai_port = 443;
const char* ai_endpoint = "/compatible-mode/v1/chat/completions";
const char* ai_api_key = "sk-xxxxxxxxxxxxxxxxxxxxxxxx";则程序实际发出的完整 HTTP 请求如下:
POST /compatible-mode/v1/chat/completions HTTP/1.1
Host: dashscope.aliyuncs.com
Content-Type: application/json
Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxx
Content-Length: [自动计算]
{
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以帮助用户控制设备。"
},
{
"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"]
}
}
}
],
"tool_choice": "auto",
"temperature": 0.1
}附录:本程序通过百炼平台收到的 HTTP 响应内容(供参考)
请注意,以下响应内容可能与实际响应内容存在差异,这里提供的信息仅供参考。
{
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": "",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"action\": \"on\"}",
"name": "controlLEDOnOFF"
},
"id": "call_79fb27a337534b0fa58147",
"index": 0,
"type": "function"
}
]
}
}
],
"created": 1776425906,
"id": "chatcmpl-7fc1186d-a768-95c1-845c-5f9d32e89951",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 21,
"prompt_tokens": 177,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 198
}
}