AID101-1-3-06 – 应用开发实例: 可用自然语言控制的人工智能LED

导航栏:首页 / AI教程目录 / AI创客项目开发教程目录 / 第1篇 基础知识篇 / AID101-1-3-06 – 应用开发实例: 可用自然语言控制的人工智能LED

本节内容简介

恭喜您完成了本章的学习内容!在前五节课程中,我们从Tool Calls的基础概念出发,逐步掌握了单工具调用、多工具定义、Follow-up机制以及并行多工具调用等核心技术。本节将对本章所学知识进行系统总结,并通过一个综合性的实践项目——可用自然语言控制的人工智能LED系统,帮助您将巩固所学知识。

一、本章知识重点回顾

1.1 Tool Calls核心概念

Tool Calls(工具调用)是现代大语言模型的一项重要功能,它让AI不仅能”说话”,还能主动调用预定义的工具为我们”办事”。在物联网场景中,Tool Calls让AI模型能够理解用户的自然语言指令,并生成结构化的控制命令,由ESP32执行实际的硬件操作。

核心流程

AI_API_Tool_Calls_WorkFlow_Chart

关键理解:实际执行函数的是ESP32,而不是AI模型。AI模型仅仅起到将自然语言指令转化为结构化函数调用指令的作用。

1.2 工具定义的JSON Schema

工具定义使用JSON Schema格式,核心字段包括:

字段作用
name函数的唯一标识名
description功能描述,帮助AI理解工具用途
parameters参数规范,定义函数接受哪些参数
type参数类型(string/integer/number/boolean等)
enum枚举值限制,限定参数只能取特定值
minimum/maximum数值范围约束
required必填参数列表

1.3 从单工具到多工具的演进

本章展示了工具调用的逐步演进过程:

课程核心内容技术要点
3-01Tool Calls基础理解工具调用的概念、JSON请求结构、AI响应格式
3-02单工具定义实例定义controlLEDOnOFF工具,实现LED开关控制
3-03多工具定义实例同时定义开关和亮度两个工具,AI自动选择最合适的工具
3-04后续工具调用实例执行完工具后,将结果发回AI获取自然语言反馈
3-05并行多工具调用处理AI一次返回的多个Tool Calls,按优先级执行

1.4 Follow-up机制的核心要点

Follow-up机制让AI”既会做事,也会说话”,其核心在于构造完整的对话历史:

  1. 第一次请求:发送用户指令 + 可用工具列表
  2. 执行Tool Calls:解析AI的工具调用指令,执行硬件操作
  3. 第二次请求:将完整的对话历史(包括assistanttool_callstool的执行结果)再次发送给AI
  4. 获取自然语言反馈:AI根据执行结果生成友好的人话回复

第二次请求的关键结构

{
  "messages": [
    {"role": "system", "content": "..."},
    {"role": "user", "content": "..."},
    {"role": "assistant", "tool_calls": [...]},
    {"role": "tool", "tool_call_id": "...", "content": "..."}
  ]
}

1.5 多Tool Calls处理要点

当AI在一次响应中返回多个工具调用时,ESP32需要:

  1. 使用结构体数组存储:定义ToolCallInfo结构体,存储每个Tool Call的idnameargumentsresult
  2. 遍历执行:使用for循环依次解析并执行每个工具调用
  3. 保持ID对应关系:每个tool消息的tool_call_id必须与assistant.tool_calls中的id严格一一对应
  4. 按优先级排序:根据业务逻辑确定执行顺序(如先开关、再亮度、再颜色)

二、综合练习项目: 可用自然语言控制的人工智能LED

2.1 练习目标

通过本练习,您将综合运用本章所学的全部知识,创建一个功能完善的智能LED控制系统。该系统可以实现以下功能:

  1. LED开关控制:打开或关闭LED
  2. LED亮度调节:支持1-5级亮度调节
  3. LED颜色控制:支持通过颜色名称设置LED颜色(如红色、蓝色、浅蓝色等)
  4. LED状态查询:查询当前LED的开关状态、亮度级别和RGB颜色值
  5. 组合指令处理:支持同时执行多个操作(如”打开LED并将颜色设置为红色”)
  6. 自然语言反馈:通过Follow-up机制获取AI生成的友好回复

2.2 练习目的

借鉴本章所学的知识,编写一个完整的Arduino程序,搭建基于ESP32的智能LED控制系统:

硬件要求

  • ESP32-S3-DevKitC-1开发板(内置WS2812 LED,数据引脚GPIO 48)

功能要求:

  1. 支持通过串口监视器接收用户输入的自然语言指令,实现LED控制功能
  2. 程序启动后连接Wi-Fi,等待用户通过串口监视器输入指令
  3. 定义四个工具函数:controlLEDOnOFFcontrolLEDBrightnesscontrolLEDColorgetLEDStatus
  4. 实现多Tool Calls的解析和执行,按优先级排序(开关 > 亮度 > 颜色 > 状态查询)
  5. 实现Follow-up机制,获取AI的自然语言反馈
  6. 建议使用AvantLumi库实现LED控制,也可以使用如FastLED库等其它库实现LED控制

三、代码讲解

以下是本练习的参考实现代码

注意

本程序需要配合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 示例 - 智能LED多工具调用版
 * 
 * 功能描述:
 * 本程序专门为ESP32-S3-DevKitC-1开发板设计,演示如何实现LLM模型的多Tool Calls及Follow-up功能。
 * 程序支持AI平台在一次响应中调用多个工具(如同时打开LED、调节亮度并设置颜色),
 * ESP32会按照优先级依次执行所有工具调用,然后将所有执行结果通过Follow-up请求发送给LLM,
 * 获取整合后的自然语言反馈。
 * 
 * 支持的四个工具及执行优先级:
 * 1. controlLEDOnOFF    - LED开关控制(最高优先级)
 * 2. controlLEDBrightness - LED亮度控制
 * 3. controlLEDColor    - LED颜色控制
 * 4. getLEDStatus       - LED状态查询(最低优先级)
 * 
 * Follow-up Tool Calls流程:
 * 1. 用户通过串口监视器输入指令
 * 2. 第一次请求:发送用户指令,获取Tool Calls响应(可能包含多个工具调用)
 * 3. 执行Tool Calls:按照优先级排序后依次执行
 * 4. 第二次请求:将所有Tool Calls响应和执行结果发送给LLM
 * 5. 获取自然语言反馈:LLM根据所有执行结果生成自然语言回复
 * 
 * 本程序支持的指令类型:
 * - 打开/关闭LED(如"打开LED"、"关闭LED")
 * - 调节LED亮度(如"把LED调到亮度最大"、"将LED亮度调到中等")
 * - 调节LED颜色(如"请将LED调节为浅蓝色"、"把LED变成红色")
 * - 查询LED状态(如"告诉我当前LED状态"、"LED是否开着")
 * - 组合指令(如"请打开LED并将亮度调节到最大,颜色设置为红色")
 * 
 * 作者:Taichi-Maker
 * 作者官网:http://ai.taichi-maker.com
 * 创建日期:2026年06月28日
 * 版本:1.1.1
 * 
 * 硬件要求:
 * - ESP32-S3-DevKitC-1开发板(内置一颗WS2812 LED,数据引脚连接到GPIO 48)
 * 
 * 所需软件库:
 * - FastLED库(用于控制WS2812 LED)
 * - 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,然后等待用户输入指令。
 * 4. 在串口监视器中输入LED控制指令。
 * 5. 观察第一次Tool Calls响应(可能包含多个工具调用)和LED控制执行过程。
 * 6. 观察第二次请求和LLM生成的自然语言反馈。
 * 7. 程序会继续等待新的用户指令,可以重复测试。
 * 
 * 注意事项:
 * - 本例使用client.setInsecure()跳过了SSL证书验证,仅适用于测试环境;
 *   在生产环境中应使用有效证书以确保通信安全。
 * - 亮度级别为1-5级,其中1为最暗,5为最亮。
 * - 颜色控制支持常见的颜色名称,如红色、蓝色、绿色、黄色、紫色、浅蓝色等。
 * - 多工具调用时按照 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"

// 引入原生 FastLED 库
#include <FastLED.h>

// LED配置 - ESP32-S3-DevKitC-1开发板内置WS2812 LED连接到GPIO 48
#define DATA_PIN 48
#define NUM_LEDS 1

// 创建 FastLED 灯珠数组
CRGB leds[NUM_LEDS];

// ---------------- FastLED 状态追踪变量 ----------------
bool ledSwitchState = false;                 // 当前开关状态:false关,true开
int ledBrightnessLevel = 1;                  // 当前亮度级别:1~5级
uint8_t ledR = 255, ledG = 255, ledB = 255;  // 当前设定的RGB颜色值

// 将当前设定的状态变量物理应用到 FastLED 硬件
void applyLedState() {
  if (!ledSwitchState) {
    leds[0] = CRGB::Black;
    FastLED.setBrightness(0);
  } else {
    leds[0] = CRGB(ledR, ledG, ledB);
    // 将1~5级亮度线性映射为 FastLED 支持的 0~255 区间
    uint8_t fastledBright = map(ledBrightnessLevel, 1, 5, 51, 255);
    FastLED.setBrightness(fastledBright);
  }
  FastLED.show();
}

// 用户指令变量,将从串口读取
String userCommand = "";

// 指令接收状态标志
bool commandReceived = false;

/* * -------------- 第一次请求:带有四个独立Tool Call的JSON请求体模板--------------
 */
const char* ai_payload_first_template = R"rawliteral({
  "model": "qwen-flash",
  "messages": [
    {
      "role": "system",
      "content": "你是一个智能家居助手,可以控制LED的开关、亮度和颜色,也可以查询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"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "controlLEDColor",
        "description": "控制LED的颜色",
        "parameters": {
          "type": "object",
          "properties": {
            "color": {
              "type": "string",
              "description": "LED颜色名称,如红色、蓝色、绿色、黄色、紫色、浅蓝色等"
            }
          },
          "required": ["color"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "getLEDStatus",
        "description": "查询LED的当前工作状态,包括开关状态、亮度级别和RGB颜色值",
        "parameters": {
          "type": "object",
          "properties": {},
          "required": []
        }
      }
    }
  ],
  "tool_choice": "auto",
  "temperature": 0.1
})rawliteral";

/* * -------------- 第二次请求:Follow-up Tool Calls的JSON请求体模板--------------
 */
const char* ai_payload_second_template = R"rawliteral({
  "model": "qwen-flash",
  "messages": [
    {
      "role": "system",
      "content": "你是一个智能家居助手,可以帮助用户控制LED的开关、亮度和颜色,也可以查询LED状态。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
    },
    {
      "role": "user",
      "content": "{{USER_COMMAND}}"
    },
    {
      "role": "assistant",
      "tool_calls": {{ASSISTANT_TOOL_CALLS}}
    },
    {{TOOL_RESULTS}}
  ],
  "temperature": 0.1
})rawliteral";

/* * -------------- 颜色代码请求:获取HEX颜色代码的JSON请求体模板--------------
 */
const char* ai_payload_color_code_template = R"rawliteral({
  "model": "qwen-flash",
  "messages": [
    {
      "role": "system",
      "content": "你是一个颜色转换助手。请将用户提供的颜色名称转换为HEX颜色代码。只返回HEX颜色代码,格式为#RRGGBB,不要返回任何其他文字。"
    },
    {
      "role": "user",
      "content": "{{COLOR_NAME}}"
    }
  ],
  "temperature": 0.1
})rawliteral";

WiFiClientSecure client;
HTTPClient https;

String firstToolCallResponse = "";

#define MAX_TOOL_CALLS 5

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 控制器 (WS2812 规格,GRB色彩顺序)
  FastLED.addLeds<WS2812, DATA_PIN, GRB>(leds, NUM_LEDS);
  Serial.println("FastLED initialized successfully!");

  // 设置LED初始状态为白色、关闭、1级亮度,便于观察
  ledR = 255; ledG = 255; ledB = 255;
  ledSwitchState = false;
  ledBrightnessLevel = 1;
  applyLedState();

  Serial.print("Connecting WiFi");
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("WiFi连接成功!");

  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.println("========== 智能LED多Tool Calls Follow-up 示例 (FastLED版) ==========");
  Serial.println("程序已就绪,等待用户输入指令...");
  Serial.println();
  Serial.println("支持的指令示例:");
  Serial.println("- 打开LED");
  Serial.println("- 关闭LED");
  Serial.println("- LED亮度调节为最大");
  Serial.println("- 请打开LED并将亮度调节到最大,颜色设置为红色");
  Serial.println();
  Serial.print("请输入您的指令: ");
}

void loop() {
  
  if (Serial.available()) {
    userCommand = Serial.readStringUntil('\n');
    userCommand.trim(); 
    
    if (userCommand.length() > 0) {
      commandReceived = true;
      Serial.println(userCommand);
      
      toolCallCount = 0;
      
      Serial.println("\n>>> 第一步:发送第一次请求,获取Tool Calls响应...");
      callAIPlatformFirst();
      
      delay(3000);
      
      Serial.println("\n>>> 第三步:发送第二次请求,获取自然语言反馈...");
      callAIPlatformSecond();
      
      Serial.println("\n========== 本次指令处理完成 ==========");
      Serial.println();
      Serial.print("请输入您的指令: ");
      commandReceived = false;
    }
  }
  
  delay(10);
}

/* * -------------- LED开关控制函数 --------------
 */
String controlLEDOnOFF(String action) {
  Serial.print("控制LED开关: ");
  Serial.println(action);
  
  String resultDescription = "";
  if (action == "on") {
    ledSwitchState = true;
    applyLedState();
    Serial.println(" - LED已打开");
    resultDescription = "LED已成功打开";
  } else if (action == "off") {
    ledSwitchState = false;
    applyLedState();
    Serial.println(" - LED已关闭");
    resultDescription = "LED已成功关闭";
  } else {
    Serial.println(" - 无效的LED控制指令");
    resultDescription = "LED控制失败:无效的控制指令";
  }
  
  return resultDescription;
}

/* * -------------- LED亮度控制函数 --------------
 */
String controlLEDBrightness(int level) {
  Serial.print("控制LED亮度: 级别 ");
  Serial.println(level);
  
  String resultDescription = "";
  if (level >= 1 && level <= 5) {
    if (!ledSwitchState) {
      ledSwitchState = true;
      ledR = 255; ledG = 255; ledB = 255; // 自动打开时默认设为白色
      Serial.println(" - LED已自动打开");
    }
    
    // 更新亮度级别并同步硬件
    ledBrightnessLevel = level;
    applyLedState();

    String levelDescription;
    switch (level) {
      case 1: levelDescription = "最暗"; break;
      case 2: levelDescription = "较暗"; break;
      case 3: levelDescription = "中等"; break;
      case 4: levelDescription = "较亮"; break;
      case 5: levelDescription = "最亮"; break;
      default: levelDescription = "未知"; break;
    }
    
    Serial.print(" - 亮度已设置为");
    Serial.print(levelDescription);
    Serial.println("级别");
    
    resultDescription = "LED亮度已成功调节到" + levelDescription + "级别";
  } else {
    Serial.println(" - 无效的亮度级别,必须在1-5之间");
    resultDescription = "亮度调节失败:无效的亮度级别,必须在1-5之间";
  }
  
  return resultDescription;
}

/* -------------- LED颜色控制函数 -------------- */
String controlLEDColor(String colorName) {
  Serial.print("设置LED颜色: ");
  Serial.println(colorName);

  String hexColorCode = callAIPlatformColorCode(colorName);
  
  if (hexColorCode.length() > 0) {
    uint8_t r, g, b;
    hexToRGB(hexColorCode, r, g, b);
    
    if (!ledSwitchState) {
      ledSwitchState = true;
      Serial.println(" - LED已自动打开");
    }
    
    // 设置LED颜色并同步硬件
    ledR = r; ledG = g; ledB = b;
    applyLedState();

    Serial.print(" - LED颜色已设置为");
    Serial.print(colorName);
    Serial.print(" (HEX: ");
    Serial.print(hexColorCode);
    Serial.print(", RGB: ");
    Serial.print(r);
    Serial.print(", ");
    Serial.print(g);
    Serial.print(", ");
    Serial.print(b);
    Serial.println(")");
    
    return "LED颜色已成功设置为" + colorName + " (HEX: " + hexColorCode + ")";
  } else {
    Serial.println(" - 获取颜色代码失败");
    return "颜色设置失败:无法获取" + colorName + "的HEX颜色代码";
  }
}

/* -------------- LED状态查询函数 --------------*/
String getLEDStatus() {
  Serial.println("查询LED状态...");
  
  String statusJson = "{\"switch\":\"" + String(ledSwitchState ? "on" : "off") + "\",\"bright\":" + String(ledBrightnessLevel) + ",\"rgb\":{\"r\":" + String(ledR) + ",\"g\":" + String(ledG) + ",\"b\":" + String(ledB) + "}}";
  
  Serial.println("LED状态JSON: " + statusJson);
  
  String statusDescription = parseLEDStatusJson(statusJson);
  
  return statusDescription;
}

/* -------------- 解析LED状态JSON并生成描述的函数 --------------*/
String parseLEDStatusJson(String statusJson) {
  DynamicJsonDocument doc(2048);
  DeserializationError error = deserializeJson(doc, statusJson);
  if (error) {
    Serial.print("LED状态JSON解析失败: ");
    Serial.println(error.c_str());
    return "LED状态查询失败:无法解析状态数据";
  }
  
  String switchState = doc["switch"] | "unknown";
  int brightness = doc["bright"] | 0;
  int r = doc["rgb"]["r"] | 0;
  int g = doc["rgb"]["g"] | 0;
  int b = doc["rgb"]["b"] | 0;

  String switchDescription;
  if (switchState == "on") {
    switchDescription = "打开";
  } else if (switchState == "off") {
    switchDescription = "关闭";
  } else {
    switchDescription = "未知";
  }
  
  String statusDescription = "LED工作状态:当前LED处于" + switchDescription + "状态,亮度为" + String(brightness) + ",LED的rgb颜色值(" + String(r) + "," + String(g) + "," + String(b) + "),请根据用户问题\"" + userCommand + "\",结合LED状态报告,生成一段简短但友好的LED状态汇报。";
  Serial.println("状态描述: " + statusDescription);
  
  return statusDescription;
}

/* * -------------- 向AI平台请求颜色代码的函数 --------------
 */
String callAIPlatformColorCode(String colorName) {
  Serial.println("\n>>> 请求颜色代码: " + colorName);
  String auth = "Bearer ";
  auth += ai_api_key;

  String colorPayload = ai_payload_color_code_template;
  colorPayload.replace("{{COLOR_NAME}}", colorName);
  
  Serial.println("----- 颜色代码请求JSON -----");
  Serial.println(colorPayload);
  Serial.println("----- 请求JSON结束 -----");

  if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
    https.setTimeout(20000);
    https.setConnectTimeout(10000); 

    Serial.println("HTTPS连接已初始化,发送颜色代码请求...");

    https.addHeader("Content-Type", "application/json");
    https.addHeader("Authorization", auth);

    int httpCode = https.POST(colorPayload);
    if (httpCode > 0) {
      Serial.printf("HTTP响应状态码: %d\n", httpCode);
      if (httpCode == HTTP_CODE_OK) {
        String resp = https.getString();
        Serial.println("----- 颜色代码响应 -----");
        Serial.println(resp);
        Serial.println("----- 响应结束 -----");
        
        String hexColorCode = parseColorCodeResponse(resp);
        https.end();
        return hexColorCode;
      }
    } else {
      Serial.printf("HTTP错误: %s\n", https.errorToString(httpCode).c_str());
      Serial.printf("错误代码: %d\n", httpCode);
    }
    https.end();
  } else {
    Serial.println("无法连接到服务器");
  }
  
  return "";
}

/* * -------------- 解析颜色代码响应的函数 --------------
 */
String parseColorCodeResponse(String response) {
  DynamicJsonDocument doc(1024);
  DeserializationError error = deserializeJson(doc, response);
  if (error) {
    Serial.print("颜色代码响应JSON解析失败: ");
    Serial.println(error.c_str());
    return "";
  }
  
  if (doc.containsKey("choices") && doc["choices"].size() > 0) {
    String content = doc["choices"][0]["message"]["content"];
    content.trim();
    
    if (content.startsWith("#") && content.length() == 7) {
      Serial.print("提取到HEX颜色代码: ");
      Serial.println(content);
      return content;
    } else {
      Serial.print("无效的HEX颜色代码: ");
      Serial.println(content);
      return "";
    }
  }
  
  Serial.println("响应中未找到颜色代码");
  return "";
}

/* * -------------- HEX颜色代码转RGB函数 --------------
 */
void hexToRGB(String hexColor, uint8_t& r, uint8_t& g, uint8_t& b) {
  if (hexColor.startsWith("#")) {
    hexColor = hexColor.substring(1);
  }
  r = (uint8_t)strtol(hexColor.substring(0, 2).c_str(), NULL, 16);
  g = (uint8_t)strtol(hexColor.substring(2, 4).c_str(), NULL, 16);
  b = (uint8_t)strtol(hexColor.substring(4, 6).c_str(), NULL, 16);
}

/* * -------------- 第一次向AI平台发送HTTPS请求的函数 --------------
 */
void callAIPlatformFirst() {
  Serial.println("\n>>> 第一次请求:发送用户指令,获取Tool Calls响应...");
  String auth = "Bearer ";
  auth += ai_api_key;

  String firstPayload = buildFirstPayload();
  Serial.println("----- 第一次请求JSON -----");
  Serial.println(firstPayload);
  Serial.println("----- 请求JSON结束 -----");

  if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
    https.setTimeout(20000);
    https.setConnectTimeout(10000); 

    Serial.println("HTTPS连接已初始化,发送第一次请求...");

    https.addHeader("Content-Type", "application/json");
    https.addHeader("Authorization", auth);

    int httpCode = https.POST(firstPayload);
    if (httpCode > 0) {
      Serial.printf("HTTP响应状态码: %d\n", httpCode);
      if (httpCode == HTTP_CODE_OK) {
        String resp = https.getString();
        Serial.println("----- 第一次响应 -----");
        Serial.println(resp);  
        Serial.println("----- 响应结束 -----");
        firstToolCallResponse = resp;
        parseToolCallsResponse(resp);
      }
    } else {
      Serial.printf("HTTP错误: %s\n", https.errorToString(httpCode).c_str());
      Serial.printf("错误代码: %d\n", httpCode);
    }
    https.end();
  } else {
    Serial.println("无法连接到服务器");
  }
}

/* * -------------- 第二次向AI平台发送HTTPS请求的函数 --------------
 */
void callAIPlatformSecond() {
  Serial.println("\n>>> 第二次请求:发送Tool Calls响应和执行结果,获取自然语言反馈...");
  String auth = "Bearer ";
  auth += ai_api_key;

  String secondPayload = buildSecondPayload();
  Serial.println("----- 第二次请求JSON -----");
  Serial.println(secondPayload);
  Serial.println("----- 请求JSON结束 -----");

  if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
    https.setTimeout(20000);
    https.setConnectTimeout(10000); 

    Serial.println("HTTPS连接已初始化,发送第二次请求...");

    https.addHeader("Content-Type", "application/json");
    https.addHeader("Authorization", auth);

    int httpCode = https.POST(secondPayload);
    if (httpCode > 0) {
      Serial.printf("HTTP响应状态码: %d\n", httpCode);
      if (httpCode == HTTP_CODE_OK) {
        String resp = https.getString();
        Serial.println("----- 第二次响应 -----");
        Serial.println(resp);  
        Serial.println("----- 响应结束 -----");
        parseSecondResponse(resp);
      }
    } else {
      Serial.printf("HTTP错误: %s\n", https.errorToString(httpCode).c_str());
      Serial.printf("错误代码: %d\n", httpCode);
    }
    https.end();
  } else {
    Serial.println("无法连接到服务器");
  }
}

String buildFirstPayload() {
  String payload = ai_payload_first_template;
  payload.replace("{{USER_COMMAND}}", userCommand);
  return payload;
}

String buildSecondPayload() {
  String payload = ai_payload_second_template;
  String assistantToolCalls = buildAssistantToolCallsJson();
  String toolResults = buildToolResultsJson();
  
  String escapedUserCommand = userCommand;
  escapedUserCommand.replace("\\", "\\\\");
  escapedUserCommand.replace("\"", "\\\"");
  escapedUserCommand.replace("\n", "\\n");
  escapedUserCommand.replace("\r", "\\r");
  escapedUserCommand.replace("\t", "\\t");

  payload.replace("{{USER_COMMAND}}", escapedUserCommand);
  payload.replace("{{ASSISTANT_TOOL_CALLS}}", assistantToolCalls);
  payload.replace("{{TOOL_RESULTS}}", toolResults);
  
  return payload;
}

String buildAssistantToolCallsJson() {
  String json = "[";
  for (int i = 0; i < toolCallCount; i++) {
    if (i > 0) json += ",";
    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 += "]";
  return json;
}

String buildToolResultsJson() {
  String json = "";
  for (int i = 0; i < toolCallCount; i++) {
    if (i > 0) json += ",";
    
    String escapedContent = toolCalls[i].result;
    escapedContent.replace("\\", "\\\\"); // 必须先转义反斜杠本身
    escapedContent.replace("\"", "\\\""); // 转义双引号

    json += "{";
    json += "\"role\":\"tool\",";
    json += "\"tool_call_id\":\"" + toolCalls[i].id + "\",";
    json += "\"content\":\"" + escapedContent + "\""; // 替换原有的 toolCalls[i].result
    json += "}";
  }
  return json;
}
/* 
 * -------------- 获取工具优先级 --------------
 * 
 * 优先级定义:
 * 1. controlLEDOnOFF - LED开关控制(最高优先级)
 * 2. controlLEDBrightness - LED亮度控制
 * 3. controlLEDColor - LED颜色控制
 * 4. getLEDStatus - LED状态查询(最低优先级)
 */
int getToolPriority(String toolName) {
  if (toolName == "controlLEDOnOFF") return 1;
  else if (toolName == "controlLEDBrightness") return 2;
  else if (toolName == "controlLEDColor") return 3;
  else if (toolName == "getLEDStatus") return 4; 
  return 99;
}

/* 
 * -------------- 按优先级排序工具调用 --------------
 */
void sortToolCallsByPriority() {
  for (int i = 0; i < toolCallCount - 1; i++) {
    for (int j = 0; j < toolCallCount - i - 1; j++) {
      int priorityA = getToolPriority(toolCalls[j].name);
      int priorityB = getToolPriority(toolCalls[j + 1].name);
      if (priorityA > priorityB) {
        ToolCallInfo temp = toolCalls[j];
        toolCalls[j] = toolCalls[j + 1];
        toolCalls[j + 1] = temp;
      }
    }
  }
  
  Serial.println("\n>>> 工具调用已按优先级排序(开关 > 亮度 > 颜色 > 状态)");
  for (int i = 0; i < toolCallCount; i++) {
    Serial.print("  [");
    Serial.print(i + 1);
    Serial.print("] ");
    Serial.print(toolCalls[i].name);
    Serial.print(" (优先级: ");
    Serial.print(getToolPriority(toolCalls[i].name));
    Serial.println(")");
  }
}

void parseToolCallsResponse(String jsonResponse) {
  DynamicJsonDocument doc(8192);
  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("tool_calls") && message["tool_calls"].size() > 0) {
      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;
      }
      
      for (int i = 0; i < toolCallCount; i++) {
        JsonObject toolCall = toolCallsArray[i];
        JsonObject function = toolCall["function"];
        String functionName = function["name"].as<String>();
        
        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);
      }
      
      sortToolCallsByPriority();

      for (int i = 0; i < toolCallCount; i++) {
        String functionName = toolCalls[i].name;
        Serial.print("\n>>> 第二步:执行Tool Call ");
        Serial.print(i + 1);
        Serial.print("/");
        Serial.print(toolCallCount);
        Serial.println("...");

        if (functionName == "controlLEDOnOFF") {
          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);
          toolCalls[i].result = controlLEDOnOFF(action);

        } else if (functionName == "controlLEDBrightness") {
          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);
          toolCalls[i].result = controlLEDBrightness(level);

        } else if (functionName == "controlLEDColor") {
          DynamicJsonDocument argsDoc(512);
          DeserializationError argsError = deserializeJson(argsDoc, toolCalls[i].arguments);
          if (argsError) {
            Serial.print("参数解析失败: ");
            Serial.println(argsError.c_str());
            toolCalls[i].result = "颜色设置失败:参数解析错误";
            continue;
          }
          String color = argsDoc["color"].as<String>();
          Serial.print("执行颜色设置: ");
          Serial.println(color);
          toolCalls[i].result = controlLEDColor(color);

        } else if (functionName == "getLEDStatus") {
          Serial.println("执行LED状态查询...");
          toolCalls[i].result = getLEDStatus();

        } else {
          Serial.print("未知函数: ");
          Serial.println(functionName);
          toolCalls[i].result = "未知工具:" + functionName;
        }
      }
      
      Serial.println("\n>>> 所有Tool Calls执行完成");
    } else {
      Serial.println("未检测到tool_calls");
      if (message.containsKey("content")) {
        String content = message["content"].as<String>();
        Serial.print("模型回复: ");
        Serial.println(content);
      }
    }
  }
}

void parseSecondResponse(String jsonResponse) {
  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("无法解析第二次响应");
  }
}

3.1 代码整体架构

本程序在架构上延续了上一节课程里示例程序的并行多工具调用框架,但进行了以下重要扩展:

  1. 从固定指令到交互式输入:通过loop()函数中的Serial.available()检测用户输入,实现循环接收指令
  2. 新增颜色控制工具:增加了controlLEDColor工具和对应的颜色转换逻辑
  3. 新增状态查询工具:增加了getLEDStatus工具,支持查询LED当前状态
  4. 工具优先级排序:实现了按优先级排序执行的功能

3.2 关键代码讲解

3.2.1 四个工具的定义(第134-201行)

第一次请求模板中定义了四个工具,这是本程序的核心:

"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"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "controlLEDColor",
        "description": "控制LED的颜色",
        "parameters": {
          "type": "object",
          "properties": {
            "color": {
              "type": "string",
              "description": "LED颜色名称,如红色、蓝色、绿色、黄色、紫色、浅蓝色等"
            }
          },
          "required": ["color"]
        }
      }
    },
    {
      "type": "function",
      "function": {
        "name": "getLEDStatus",
        "description": "查询LED的当前工作状态,包括开关状态、亮度级别和RGB颜色值",
        "parameters": {
          "type": "object",
          "properties": {},
          "required": []
        }
      }
    }
  ]

重点讲解

  • controlLEDOnOFF:使用enum限定参数只能是"on""off",确保AI不会生成无效指令
  • controlLEDBrightness:使用minimum: 1maximum: 5约束亮度级别范围
  • controlLEDColor:参数为字符串类型的颜色名称,AI会将”浅蓝色”、”红色”等自然语言转换为具体的颜色名称
  • getLEDStatus:无参数工具,用于查询LED当前状态

3.2.2 交互式指令处理(第307-335行)

与前几节程序在setup()中固定发送指令不同,本程序在loop()中实现交互式输入:

// 主循环,处理串口输入和LED更新
void loop() {
  
  if (Serial.available()) {
    userCommand = Serial.readStringUntil('\n');
    userCommand.trim(); 
    
    if (userCommand.length() > 0) {
      commandReceived = true;
      Serial.println(userCommand);
      
      toolCallCount = 0;
      
      Serial.println("\n>>> 第一步:发送第一次请求,获取Tool Calls响应...");
      callAIPlatformFirst();
      
      delay(3000);
      
      Serial.println("\n>>> 第三步:发送第二次请求,获取自然语言反馈...");
      callAIPlatformSecond();
      
      Serial.println("\n========== 本次指令处理完成 ==========");
      Serial.println();
      Serial.print("请输入您的指令: ");
      commandReceived = false;
    }
  }
  
  delay(10);
}

设计要点

  1. 每次处理新指令前重置toolCallCount,确保不会残留上次的数据
  2. 通过串口获取用户输入信息,并且将用户输入信息作为LED控制指令,通过函数callAIPlatformFirst()发送给AI平台。
  3. 成功调用工具后,使用callAIPlatformSecond()函数通过AI大模型获取自然语言答复
  4. 两次请求之间加入delay(3000),让用户有时间观察LED状态变化

3.2.3 工具优先级排序(第727-761行)

本程序引入了工具优先级的概念,确保多Tool Calls按正确顺序执行:

/* 
 * -------------- 获取工具优先级 --------------
 * 
 * 优先级定义:
 * 1. controlLEDOnOFF - LED开关控制(最高优先级)
 * 2. controlLEDBrightness - LED亮度控制
 * 3. controlLEDColor - LED颜色控制
 * 4. getLEDStatus - LED状态查询(最低优先级)
 */
int getToolPriority(String toolName) {
  if (toolName == "controlLEDOnOFF") return 1;
  else if (toolName == "controlLEDBrightness") return 2;
  else if (toolName == "controlLEDColor") return 3;
  else if (toolName == "getLEDStatus") return 4; 
  return 99;
}

/* 
 * -------------- 按优先级排序工具调用 --------------
 */
void sortToolCallsByPriority() {
  for (int i = 0; i < toolCallCount - 1; i++) {
    for (int j = 0; j < toolCallCount - i - 1; j++) {
      int priorityA = getToolPriority(toolCalls[j].name);
      int priorityB = getToolPriority(toolCalls[j + 1].name);
      if (priorityA > priorityB) {
        ToolCallInfo temp = toolCalls[j];
        toolCalls[j] = toolCalls[j + 1];
        toolCalls[j + 1] = temp;
      }
    }
  }

为什么需要优先级排序?

考虑用户指令”打开LED并将亮度调到最大”:

  • 如果不排序,AI可能先返回亮度调节,再返回开关控制
  • 如果LED初始是关闭状态,先调亮度再开灯可能导致亮度设置不生效
  • 通过优先级排序,确保”开关”操作最先执行,”状态查询”最后执行

3.2.4 颜色控制与AI辅助转换(第404-440行)

颜色控制是本程序的一个亮点,它需要借助AI将自然语言颜色名称转换为HEX颜色代码:

/* -------------- LED颜色控制函数 --------------*/
String controlLEDColor(String colorName) {
  Serial.print("设置LED颜色: ");
  Serial.println(colorName);

  String hexColorCode = callAIPlatformColorCode(colorName);
  
  if (hexColorCode.length() > 0) {
    uint8_t r, g, b;
    hexToRGB(hexColorCode, r, g, b);
    
    if (!ledSwitchState) {
      ledSwitchState = true;
      Serial.println(" - LED已自动打开");
    }
    
    // 设置LED颜色并同步硬件
    ledR = r; ledG = g; ledB = b;
    applyLedState();

    Serial.print(" - LED颜色已设置为");
    Serial.print(colorName);
    Serial.print(" (HEX: ");
    Serial.print(hexColorCode);
    Serial.print(", RGB: ");
    Serial.print(r);
    Serial.print(", ");
    Serial.print(g);
    Serial.print(", ");
    Serial.print(b);
    Serial.println(")");
    
    return "LED颜色已成功设置为" + colorName + " (HEX: " + hexColorCode + ")";
  } else {
    Serial.println(" - 获取颜色代码失败");
    return "颜色设置失败:无法获取" + colorName + "的HEX颜色代码";
  }
}

技术要点

  1. 独立的AI请求:颜色转换使用独立的JSON模板,与主Tool Calls流程分开
  2. 严格的响应校验parseColorCodeResponse()函数检查响应是否为有效的HEX格式(以#开头,长度为7)
  3. 自动开灯:如果LED处于关闭状态,设置颜色时自动打开LED,提升用户体验

3.2.5 LED状态查询与智能反馈(第442 – 453行)

getLEDStatus函数不仅查询LED状态,还生成一段让AI能够理解并转述的状态描述:

/* -------------- LED状态查询函数 --------------*/
String getLEDStatus() {
  Serial.println("查询LED状态...");
  
  String statusJson = "{\"switch\":\"" + String(ledSwitchState ? "on" : "off") + "\",\"bright\":" + String(ledBrightnessLevel) + ",\"rgb\":{\"r\":" + String(ledR) + ",\"g\":" + String(ledG) + ",\"b\":" + String(ledB) + "}}";
  
  Serial.println("LED状态JSON: " + statusJson);
  
  String statusDescription = parseLEDStatusJson(statusJson);
  
  return statusDescription;
}

设计亮点

  1. 将用户问题嵌入状态描述:这样AI在生成回复时,能够结合用户的具体问题(如”LED是否开着”或”LED亮度情况”)给出针对性回答
  2. 指令式提示:在状态描述末尾明确告诉AI”请生成一段简短但友好的LED状态汇报”,引导AI生成合适的回复

3.2.6 第二次请求的构造(第664-681行)

buildSecondPayload()函数是Follow-up机制的核心:

String buildSecondPayload() {
  String payload = ai_payload_second_template;
  String assistantToolCalls = buildAssistantToolCallsJson();
  String toolResults = buildToolResultsJson();
  
  String escapedUserCommand = userCommand;
  escapedUserCommand.replace("\\", "\\\\");
  escapedUserCommand.replace("\"", "\\\"");
  escapedUserCommand.replace("\n", "\\n");
  escapedUserCommand.replace("\r", "\\r");
  escapedUserCommand.replace("\t", "\\t");

  payload.replace("{{USER_COMMAND}}", escapedUserCommand);
  payload.replace("{{ASSISTANT_TOOL_CALLS}}", assistantToolCalls);
  payload.replace("{{TOOL_RESULTS}}", toolResults);
  
  return payload;
}

关键改进

  1. 用户指令转义:对userCommand中的反斜杠、双引号、换行符等进行转义,防止用户输入特殊字符破坏JSON格式
  2. 动态数组构造buildAssistantToolCallsJson()buildToolResultsJson()函数动态构造JSON数组,适应任意数量的Tool Calls

四、本章总结

通过第3章的学习,我们已经掌握了使用ESP32与AI平台进行Tool Calls交互的完整技术栈:

4.1 核心技能清单

✅ 理解Tool Calls工作原理:AI模型负责”理解”和”决策”,ESP32负责”执行”

✅ 定义工具函数:使用JSON Schema精确定义工具的名称、描述和参数约束

✅ 解析AI响应:使用ArduinoJson库解析AI返回的结构化指令

✅ 实现Follow-up机制:通过完整的对话历史获取AI的自然语言反馈

✅ 处理多Tool Calls:使用结构体数组存储、排序和执行多个工具调用

✅ 构建交互式应用:从固定指令升级到循环接收用户输入的交互式系统

4.2 学习路径回顾

3-01 理论基础
  ↓
3-02 单工具实践(开关LED)
  ↓
3-03 多工具扩展(增加亮度控制)
  ↓
3-04 对话增强(Follow-up获取自然语言反馈)
  ↓
3-05 并行处理(多Tool Calls排序执行)
  ↓
3-06 综合实践(智能LED控制系统)← 您在这里

4.3 拓展思考

掌握了本章技术后,您可以尝试以下拓展:

  1. 增加更多设备控制:在tools数组中定义更多工具,如控制继电器、读取温湿度传感器等
  2. 实现条件执行:根据传感器数据决定是否执行某些操作(如”如果温度超过30度,打开风扇”)
  3. 添加语音输入:结合语音识别模块,实现语音控制LED
  4. 接入智能家居平台:将ESP32接入Home Assistant等智能家居平台,实现更丰富的联动

五、结束语

恭喜您完成了《AI创客项目开发教程 – 第1篇 – 基础知识篇》的全部学习!您现在已经具备了使用AI大模型通过Tool Calls控制ESP32硬件的核心能力。这些技能将成为您开发更复杂AIoT项目的基础。在接下来的教程学习中,我们将与您一起搭建丰富有趣的AI与硬件结合的人工智能创库项目,帮助您一起学习如何将人工智能应用到创客项目中。