AI创客项目开发教程 –AID101-1-3-02 – 单工具定义实例

导航栏:首页 / 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 48
  • NUM_LEDS 1:开发板上只有 1 颗 WS2812 LED
  • AvantLumi 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) {

解析成功后,程序按照以下层级结构检查响应内容:

  1. 检查根对象是否包含 choices 数组,且数组不为空
  2. 获取 choices 数组的第一个元素
  3. 从第一个 choice 中获取 message 对象
  4. 检查 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 功能控制硬件的完整流程:

  1. 定义工具:在 JSON 请求中通过 tools 字段声明 ESP32 上可用的函数
  2. 发送请求:将用户指令和工具定义一起发送给 AI 平台
  3. 解析响应:使用 ArduinoJson 库解析 AI 返回的结构化指令
  4. 执行操作:根据解析结果调用相应的函数控制硬件

理解了这个流程后,就可以进一步扩展:定义更多的工具函数(如调节亮度、改变颜色、读取传感器等),让 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
  }
}