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

导航栏:首页 / AI教程目录 / AI创客项目开发教程目录 / 第1篇 基础知识篇 / AID101-1-3-02 – 多工具定义实例

本节内容简介

本节将讲解如何在 ESP32-S3-DevKitC-1 开发板上实现多个工具定义多个工具调用的方法。本节课所使用的这个程序是在上一节演示程序的基础上进行功能扩展。

与上一节ESP32只能控制 LED 开关不同,本节课程的程序新增了对 LED 亮度的控制功能。AI 模型可以根据用户的自然语言指令,智能判断应该调用哪个工具函数:

  • 当用户说”打开 LED”时,调用 controlLEDOnOFF 函数
  • 当用户说”把 LED 调到亮度最大”时,调用 controlLEDBrightness 函数

本节示例程序

注意

本程序需要配合my_info.h代码使用,否则程序将无法正常编译。
请点击以下链接前往该代码页面,复制下载该代码:

http://ai.taichi-maker.com/index.php/homepage/ai-tutorial-index/ai-maker-project-tutorial-index/my_info_h-code-description/

本节程序在上一节‘AID101-1-3-02 – 单工具定义实例’示例程序的基础上,实现了以下核心进阶:

  1. 多工具定义:在 tools 数组中同时定义了开关控制和亮度调节两个工具
  2. 多工具调用parseToolCallsResponse 函数能够根据 AI 返回的函数名,调用不同的处理逻辑
  3. 参数范围约束:使用 minimum 和 maximum 限制亮度级别在 1-5 之间,确保 AI 生成的参数在合法范围内

理解了这个多工具调用的模式后,就可以进一步扩展:添加更多工具函数(如LED调节颜色、设置LED闪烁模式、读取传感器数据等),构建功能更丰富的 AI 物联网应用。

/* 
 * ESP32 AI平台调用 Tool Calls 示例 - 增强版
 * 
 * 功能描述:
 * 本程序专门为ESP32-S3-DevKitC-1开发板设计,演示如何通过LLM模型的Tool Calls功能
 * 控制开发板上内置的WS2812 LED的开关和亮度。程序通过HTTPS协议向AI平台发送带有两个工具定义
 * 的请求,AI模型根据用户指令分析出需要调用的工具和参数,ESP32解析响应后执行相应的LED控制操作。
 * 
 * 本程序支持两种类型的指令:
 * 1. 打开/关闭LED(如"打开LED"、"关闭LED")
 * 2. 调节LED亮度(如"把LED调到亮度最大"、"将LED亮度调到中等")
 * 
 * 作者:Taichi-Maker
 * 作者官网:http://ai.taichi-maker.com
 * 创建日期:2026年05月03日
 * 版本:2.0.1
 * 
 * 硬件要求:
 * - 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证书验证,仅适用于测试环境;
 *   在生产环境中应使用有效证书以确保通信安全。
 * - 亮度级别为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"

// 引入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: 定义可用的工具函数列表,包含两个函数:
 *   1. controlLEDOnOFF - 控制LED开关
 *   2. controlLEDBrightness - 控制LED亮度
 * - tool_choice: 设置为"auto"让模型自动决定是否使用工具
 */
const char* ai_payload = R"rawliteral({
  "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
})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(true);        // 设置LED状态初始参数为关闭
  myLumi.setFade(false);         // 设置LED闪烁初始参数为关闭
  myLumi.setBright(1);           // 设置LED亮度初始参数为1(低亮度)

  // 执行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() {
  // 必须在循环中调用update()方法,处理LED的亮度过渡和调色板混合
  myLumi.update();
  delay(10); // 短暂延时,避免过度占用CPU
}


/* 
 * -------------- 向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. 根据函数名称调用相应的控制函数执行操作
 * 支持两个函数:controlLEDOnOFF和controlLEDBrightness
 */
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") {
        // 解析LED开关函数参数
        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 if (functionName == "controlLEDBrightness") {
        // 解析LED亮度函数参数
        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;
        }
        
        int level = argsDoc["level"].as<int>();
        Serial.print("执行亮度调节: ");
        Serial.println(level);
        
        // 调用LED亮度控制函数
        controlLEDBrightness(level);
        
      } 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控制指令");
  }
}

/* 
 * -------------- LED亮度控制函数 --------------
 * 
 * 该函数根据AI模型返回的参数控制LED的亮度级别
 * 
 * 参数:
 * - level: 整数,亮度级别,1-5级,其中1为最暗,5为最亮
 */
void controlLEDBrightness(int level) {
  Serial.print("控制LED亮度: 级别 ");
  Serial.println(level);
  
  if (level >= 1 && level <= 5) {
    // 确保LED是打开状态,以便观察亮度变化
    if (!myLumi.getSwitch()) { // 如果LED的状态是关闭的
      myLumi.setSwitch(true);  // 则打开LED
      myLumi.setRGB(255, 255, 255); // 设置LED为白色
    }
    
    // 设置亮度级别
    myLumi.setBright(level);
    
    // 根据级别提供用户友好的反馈
    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("通过controlLEDBrightness函数,LED亮度已设置为");
    Serial.print(levelDescription);
  } else {
    Serial.println("无效的亮度级别,必须在1-5之间");
  }
}

/*
 * ========================================================================
 * 【附录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-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
 * }
 * 
 * 说明:
 * - 这是一个带有Tool Calls定义的OpenAI兼容API调用格式。
 * - 服务器收到后会使用指定模型分析用户指令,并决定调用哪个工具函数。
 * - 如果用户说"打开LED",模型会调用controlLEDOnOFF函数;
 *   如果用户说"把LED调到亮度最大",模型会调用controlLEDBrightness函数。
 * - ESP32通过HTTPS安全地传输此请求,并解析返回的tool_calls执行相应操作。
 * ========================================================================
 * 【附录2】本程序通过百炼平台收到的HTTP响应内容(供参考)
 * ======================================================================== 
    {
      "choices": [
        {
          "finish_reason": "tool_calls",
          "index": 0,
          "message": {
            "content": "",
            "role": "assistant",
            "tool_calls": [
              {
                "function": {
                  "arguments": "{\"level\": 5}",
                  "name": "controlLEDBrightness"
                },
                "id": "call_fac27c12mfdd4d32ac161c",
                "index": 0,
                "type": "function"
              }
            ]
          }
        }
      ],
      "created": 1777816971,
      "id": "chatcmpl-1c2f3b32-2cd1-9c61-abaa-dea4c256ac5",
      "model": "qwen-flash",
      "object": "chat.completion",
      "usage": {
        "completion_tokens": 21,
        "prompt_tokens": 276,
        "prompt_tokens_details": {
          "cached_tokens": 0
        },
        "total_tokens": 297
      }
    }
 */

本程序的新增功能

新增功能1. 新增了可调用的工具

本程序在 tools 数组中定义了两个工具函数。这两个工具并列存在于同一个 tools 数组中,AI 模型会根据用户的自然语言指令,自动判断应该调用哪一个工具。如果用户说的是开关相关指令(如”打开LED”),AI 会选择 controlLEDOnOFF;如果用户说的是亮度相关指令(如”把LED调亮一点”),AI 会选择 controlLEDBrightness

以下是完整的 tools 数组结构示意:

"tools": [
  {
    "type": "function",
    "function": {
      "name": "controlLEDOnOFF",
      // ... 开关控制工具的定义(包括description、parameters等字段)
    }
  },
  {
    "type": "function",
    "function": {
      "name": "controlLEDBrightness",
      // ... 亮度调节工具的定义(包括description、parameters等字段)
    }
  }
]

可以看到,两个工具对象作为数组元素并列排列。每个工具都包含 typefunction(内含 namedescriptionparameters)等字段。AI 模型通过比较用户指令与每个工具的 description,来决定调用最合适的工具。

工具一:LED 开关控制(controlLEDOnOFF)

{
  "type": "function",
  "function": {
    "name": "controlLEDOnOFF",
    "description": "控制LED的打开或关闭",
    "parameters": {
      "type": "object",
      "properties": {
        "action": {
          "type": "string",
          "description": "LED控制动作",
          "enum": ["on", "off"]
        }
      },
      "required": ["action"]
    }
  }
}

这个工具与上一节完全相同,用于控制 LED 的开关状态。

工具二:LED 亮度调节(controlLEDBrightness)

{
  "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"]
    }
  }
}

这是本程序新增的工具函数,用于调节 LED 的亮度级别。下面对这个工具的定义进行逐层解析:

第一层:type 和 function

  • "type": "function":声明这是一个函数类型的工具,与开关控制工具相同
  • "function":包含该函数的详细定义

第二层:name 和 description

  • "name": "controlLEDBrightness":函数的唯一标识名。当 AI 决定调节亮度时,会在响应中返回这个名称
  • "description": "控制LED的亮度级别":功能描述,帮助 AI 理解这个工具的用途。当用户说”调亮一点”、”把灯调暗”等指令时,AI 通过这段描述判断应该使用这个工具

第三层:parameters(参数规范)

  • "type": "object":声明参数整体是一个 JSON 对象
  • "properties":定义参数的具体属性

第四层:level 参数详解

字段作用
type"integer"限定参数必须是整数类型,AI 不会生成小数或字符串
description"亮度级别,1-5级,其中1为最暗,5为最亮"语义描述,帮助 AI 理解参数含义和取值规则
minimum1最小值约束,AI 生成的数值不会小于 1
maximum5最大值约束,AI 生成的数值不会大于 5
  • "required": ["level"]:声明 level 是必填参数,AI 必须在响应中提供这个值

与开关控制工具的对比

对比项controlLEDOnOFFcontrolLEDBrightness
功能控制开关控制亮度
参数名actionlevel
参数类型string(字符串)integer(整数)
约束方式enum: ["on", "off"]minimum: 1maximum: 5
适用场景“打开LED”、”关闭LED”“调亮一点”、”调到最亮”

重点讲解:使用 minimum 和 maximum 约束参数范围

在 controlLEDBrightness 工具的参数定义中,我们使用了两个新的 JSON Schema 关键字:

"level": {
  "type": "integer",
  "description": "亮度级别,1-5级,其中1为最暗,5为最亮",
  "minimum": 1,
  "maximum": 5
}

这两个关键字的作用

  • minimum: 1 —— 告诉 AI 模型,level 参数的最小值是 1,不能小于 1
  • maximum: 5 —— 告诉 AI 模型,level 参数的最大值是 5,不能大于 5

为什么需要这种约束?

在没有 minimum 和 maximum 约束的情况下,如果用户说”把 LED 调到最亮”,AI 模型可能会返回各种不同的值:

  • 可能返回 10(假设 10 是最大值)
  • 可能返回 100(假设百分比制)
  • 可能返回 255(假设 8 位亮度值)

这些值对于 ESP32 程序来说都是”合法”的整数,但可能超出了硬件实际支持的范围,导致控制失败或产生不可预期的行为。

通过设置 "minimum": 1 和 "maximum": 5,我们明确告诉 AI 模型:

“亮度级别只有 1 到 5 这五个档位,请一定在这个范围内选择。”

这样,无论用户用什么样的自然语言表达亮度需求(”最亮”、”亮一点”、”中等亮度”等),AI 模型都会将其映射到 1-5 之间的整数,确保 ESP32 程序能够正确处理。

实际效果示例

用户指令AI 推断的 level 值
“把 LED 调到最亮”5
“把 LED 调到最暗”1
“把 LED 亮度调高一点”4(假设当前是3)
“把 LED 调到中等亮度”3

代码解析:关键变化点

1. JSON 请求体的变化(第88-141行)

与上一节相比,最显著的变化是 tools 数组从包含 1 个工具扩展为包含 2 个工具。同时,用户指令也根据演示需求发生了变化。

下面通过两个具体示例,展示 ESP32、AI 平台和硬件之间的完整交互流程。


示例一:用户指令”把LED调到亮度最大”

第一步:ESP32 构造并发送请求

ESP32 将用户指令和工具定义组装成 JSON 请求包,通过 HTTPS POST 发送给 AI 平台:

{
  "model": "qwen-flash",
  "messages": [
    {
      "role": "system",
      "content": "你是一个智能家居助手,可以控制 LED 的开关和亮度。"
    },
    {
      "role": "user",
      "content": "把LED调到亮度最大"
    }
  ],
  "tools": [
    { /* controlLEDOnOFF 工具定义 */ },
    { /* controlLEDBrightness 工具定义 */ }
  ],
  "tool_choice": "auto",
  "temperature": 0.1
}

第二步:AI 平台分析请求

AI 模型收到请求后,执行以下分析:

  1. 理解用户意图:用户说”把LED调到亮度最大” → 语义解析为”需要调节LED亮度”
  2. 匹配可用工具:查看 tools 列表
    • controlLEDOnOFF 的描述是”控制LED的打开或关闭” → 不匹配
    • controlLEDBrightness 的描述是”控制LED的亮度级别” → 匹配
  3. 确定工具选择:选择 controlLEDBrightness 工具
  4. 生成合法参数:根据 "maximum": 5 的约束,”亮度最大”对应级别 5

第三步:AI 平台返回响应

AI 平台返回包含工具调用指令的 JSON 响应:

{
  "choices": [
    {
      "finish_reason": "tool_calls",
      "message": {
        "role": "assistant",
        "content": "",
        "tool_calls": [
          {
            "function": {
              "name": "controlLEDBrightness",
              "arguments": "{\"level\": 5}"
            }
          }
        ]
      }
    }
  ]
}

注意:content 为空,说明 AI 没有使用自然语言回复,而是通过 tool_calls 下达结构化指令。

第四步:ESP32 解析响应并执行

ESP32 的 parseToolCallsResponse 函数解析上述响应:

  1. 检查 finish_reason 为 "tool_calls" → 确认是工具调用响应
  2. 提取 tool_calls[0].function.name → 得到 "controlLEDBrightness"
  3. 提取 tool_calls[0].function.arguments → 得到 {"level": 5}
  4. 解析参数 JSON → level = 5
  5. 调用 controlLEDBrightness(5) → LED 亮度设置为最亮

示例二:用户指令”关闭LED”

第一步:ESP32 构造并发送请求

用户指令部分变为:

{
  "model": "qwen-flash",
  "messages": [
    {
      "role": "system",
      "content": "你是一个智能家居助手,可以控制 LED 的开关和亮度。"
    },
    {
      "role": "user",
      "content": "关闭LED"
    }
  ],
  "tools": [
    { /* controlLEDOnOFF 工具定义 */ },
    { /* controlLEDBrightness 工具定义 */ }
  ],
  "tool_choice": "auto",
  "temperature": 0.1
}

tools 数组内容保持不变(两个工具都包含在内)。

第二步:AI 平台分析请求

  1. 理解用户意图:用户说”关闭LED” → 语义解析为”需要关闭LED”
  2. 匹配可用工具
    • controlLEDOnOFF 的描述是”控制LED的打开或关闭” → 匹配
    • controlLEDBrightness 的描述是”控制LED的亮度级别” → 不匹配
  3. 确定工具选择:选择 controlLEDOnOFF 工具
  4. 生成合法参数:根据 "enum": ["on", "off"],”关闭”对应 "off"

第三步:AI 平台返回响应

{
  "choices": [
    {
      "finish_reason": "tool_calls",
      "message": {
        "role": "assistant",
        "content": "",
        "tool_calls": [
          {
            "function": {
              "name": "controlLEDOnOFF",
              "arguments": "{\"action\": \"off\"}"
            }
          }
        ]
      }
    }
  ]
}

第四步:ESP32 解析响应并执行

  1. 提取 function.name → "controlLEDOnOFF"
  2. 提取 function.arguments → {"action": "off"}
  3. 解析参数 → action = "off"
  4. 调用 controlLEDOnOFF("off") → LED 关闭

两个示例的对比总结

步骤亮度调节示例开关控制示例
用户指令“把LED调到亮度最大”“关闭LED”
AI 选择的工具controlLEDBrightnesscontrolLEDOnOFF
生成的参数{"level": 5}{"action": "off"}
ESP32 调用的函数controlLEDBrightness(5)controlLEDOnOFF("off")
硬件执行结果LED 亮度设为最亮LED 关闭

通过这两个示例可以看出,同一套 tools 定义可以应对不同类型的用户指令。AI 模型会根据指令内容自动选择最合适的工具,而 ESP32 只需要根据返回的函数名和参数执行相应操作即可。

新增功能 2. 解析函数的扩展(第236-359行)

parseToolCallsResponse 函数在原有基础上增加了对 controlLEDBrightness 的处理分支。下面是完整的解析函数中新增部分的代码:

 } else if (functionName == "controlLEDBrightness") {
        // 解析LED亮度函数参数
        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;
        }
        
        int level = argsDoc["level"].as<int>();
        Serial.print("执行亮度调节: ");
        Serial.println(level);
        
        // 调用LED亮度控制函数
        controlLEDBrightness(level);
        
      } else {
        Serial.print("未知函数: ");
        Serial.println(functionName);
      }

下面对这段新增的亮度控制解析逻辑进行逐行说明:

第一步:判断函数名称

} else if (functionName == "controlLEDBrightness") {

当 AI 返回的函数名不是 controlLEDOnOFF 时,程序会继续检查是否为 controlLEDBrightness。如果是,就进入亮度控制的处理分支。

第二步:提取参数字符串

String arguments = function["arguments"].as<String>();

从 AI 响应中提取 arguments 字段的值。这是一个 JSON 格式的字符串,例如 {"level": 5}

第三步:解析参数 JSON

DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, arguments);

if (argsError) {
  Serial.print("参数解析失败: ");
  Serial.println(argsError.c_str());
  return;
}

使用 ArduinoJson 库将参数字符串解析为可操作的数据结构。如果解析失败(如 JSON 格式错误),打印错误信息并退出函数。

第四步:提取具体参数值

int level = argsDoc["level"].as<int>();
Serial.print("执行亮度调节: ");
Serial.println(level);

从解析后的参数对象中提取 level 字段的整数值,并通过串口打印出来,方便调试观察。

第五步:调用亮度控制函数

controlLEDBrightness(level);

将解析得到的亮度级别传递给 controlLEDBrightness 函数,由该函数执行实际的硬件控制操作。

第六步:处理未知函数

} else {
  Serial.print("未知函数: ");
  Serial.println(functionName);
}

如果 AI 返回的函数名既不是 controlLEDOnOFF 也不是 controlLEDBrightness,则打印”未知函数”的提示信息。这是一种防御性编程,确保程序在遇到意外情况时能够给出明确的反馈。

新增功能3. 新增的亮度控制函数(第406-448行)

void controlLEDBrightness(int level) {
  if (level >= 1 && level <= 5) {
    // 确保LED是打开状态
    if (!myLumi.getSwitch()) {
      myLumi.setSwitch(true);
      myLumi.setRGB(255, 255, 255);
    }
    
    // 设置亮度级别
    myLumi.setBright(level);
    
    // 根据级别提供用户友好的反馈
    String levelDescription;
    switch (level) {
      case 1: levelDescription = "最暗"; break;
      case 2: levelDescription = "较暗"; break;
      case 3: levelDescription = "中等"; break;
      case 4: levelDescription = "较亮"; break;
      case 5: levelDescription = "最亮"; break;
    }
    
    Serial.print("LED亮度已设置为");
    Serial.println(levelDescription);
  }
}

这个函数做了以下几件事:

  1. 参数校验:检查 level 是否在 1-5 的合法范围内
  2. 自动开灯:如果 LED 当前是关闭状态,先自动打开 LED 并设置为白色
  3. 设置亮度:调用 myLumi.setBright(level) 设置亮度级别
  4. 友好反馈:通过 switch 语句将数字级别转换为用户易懂的描述文字

常见的 Tool Calls 参数约束方式

在定义工具的参数时,除了本节使用的 minimum 和 maximum 之外,JSON Schema 还提供了多种约束机制,可以帮助 AI 模型更准确地理解和生成参数。

以下是最常用的几种约束方式,每种都配有详细的解释和实际应用场景示例。


1. type(类型约束)

作用:明确指定参数的数据类型,防止 AI 生成错误类型的值。

类型值含义适用场景
"string"字符串设备名称、状态描述、颜色名称等
"integer"整数亮度级别、温度值、数量等离散数值
"number"数字(整数或浮点数)精确的温度值、湿度百分比等连续数值
"boolean"布尔值(true/false)开关状态、启用/禁用标志等
"array"数组多个颜色值、多个设备ID列表等
"object"对象复杂的配置参数、坐标信息等

示例:定义一个整数类型的亮度参数

"brightness": {
  "type": "integer",
  "description": "LED亮度,0-100之间的整数"
}

实际效果:当用户说”把灯调亮”时,AI 会返回类似 85 的整数值,而不会返回 "很亮"(字符串)或 85.5(浮点数)。

2. enum(枚举值)

作用:限制参数只能取预定义的几个值,从根本上杜绝非法输入。

示例:LED 开关控制

"action": {
  "type": "string",
  "description": "LED控制动作",
  "enum": ["on", "off"]
}

实际效果

用户指令AI 生成的参数
“打开LED”"on"
“把灯关掉”"off"
“启动LED”"on"(通过语义理解映射)
“熄灭灯光”"off"(通过语义理解映射)

关键点:即使用户使用同义词或不同表达方式,AI 也会将语义映射到 enum 中预定义的值。如果没有 enum,AI 可能会生成 "open""start""close" 等多种不同的值,导致 ESP32 程序无法处理。


3. minimum / maximum(数值范围)

作用:对数字类型参数设置上下限,确保 AI 生成的数值在硬件可接受的范围内。

示例:LED 亮度级别(本节重点)

"level": {
  "type": "integer",
  "description": "亮度级别,1-5级,其中1为最暗,5为最亮",
  "minimum": 1,
  "maximum": 5
}

实际效果

用户指令无约束时 AI 可能返回有约束时 AI 返回
“调到最亮”10 或 100 或 2555
“调到最暗”0 或 11
“中等亮度”50(百分比思维)3

为什么重要:硬件驱动库(如 AvantLumi)的 setBright() 函数可能只接受 1-5 的范围。如果 AI 返回 100,程序要么报错,要么产生不可预期的行为。通过 minimum 和 maximum,我们在源头上就约束了 AI 的输出。


4. minLength / maxLength(字符串长度限制)

作用:控制字符串的最小或最大长度,防止过长或过短的输入。

示例:设备名称

"device_name": {
  "type": "string",
  "description": "设备名称,用于在APP中显示",
  "minLength": 1,
  "maxLength": 20
}

实际效果

用户指令AI 生成的参数是否符合约束
“命名为客厅灯”"客厅灯"(3个字符)符合(1 ≤ 3 ≤ 20)
“命名为我的超级智能多功能客厅主照明灯”会被截断为 "我的超级智能多功能客"(15个字符)符合(不超过20)
“命名为空”""(0个字符)不符合(小于1),AI 会重新生成

应用场景:在需要将设备名称显示在 OLED 屏幕或手机 APP 上的场景中,限制长度可以确保 UI 显示正常,不会出现文字截断或溢出。


5. pattern(正则表达式)

作用:要求字符串必须匹配某个正则模式,用于验证格式化的数据。

示例:时间格式(HH:MM)

"time": {
  "type": "string",
  "description": "定时时间,格式为HH:MM,例如07:30",
  "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
}

实际效果

用户指令AI 生成的参数是否匹配正则
“早上七点半”"07:30"匹配
“晚上十一点”"23:00"匹配
“凌晨一点”"01:00"匹配
“二十五点”不会生成 "25:00"不匹配(小时不能超过23)

应用场景:定时任务、闹钟设置、预约开关等需要精确时间格式的场景。正则表达式确保 AI 生成的时间字符串可以被 sscanf() 等函数正确解析。


6. required(必填字段)

作用:在 parameters 的顶层指定哪些属性是必须提供的,防止 AI 遗漏关键参数。

示例:智能家居设备控制

"parameters": {
  "type": "object",
  "properties": {
    "device_id": {
      "type": "string",
      "description": "设备ID"
    },
    "action": {
      "type": "string",
      "enum": ["on", "off"]
    }
  },
  "required": ["device_id", "action"]
}

实际效果

AI 必须同时返回 device_id 和 action 两个参数,例如:

{"device_id": "living_room_light", "action": "on"}

应用场景:当需要同时指定”哪个设备”和”做什么操作”时,required 确保两个信息都不会遗漏。


7. description(语义描述)

作用:虽然不是硬性约束,但清晰的自然语言描述是最关键的引导手段,能极大提升模型理解意图的准确性。

示例:温度设置

"temperature": {
  "type": "integer",
  "description": "目标温度,单位摄氏度,范围16-30度。例如:用户说'有点冷'对应22度,'很热'对应26度",
  "minimum": 16,
  "maximum": 30
}

实际效果

用户指令无 description 时 AI 可能返回有详细 description 时 AI 返回
“有点冷”随机值(如 1822(根据描述中的示例推断)
“很热”随机值(如 3026(根据描述中的示例推断)

最佳实践:在 description 中提供:

  • 单位信息(如”摄氏度”、”百分比”)
  • 具体示例(如”1为最暗,5为最亮”)
  • 映射关系(如”用户说’有点冷’对应22度”)

8. items(数组元素约束)

作用:当参数是数组时,用 items 定义每个元素的结构,并可配合 minItems / maxItems 限制数组长度。

示例:RGB 颜色列表

"colors": {
  "type": "array",
  "description": "要显示的颜色列表,最多3种颜色",
  "items": {
    "type": "string",
    "enum": ["red", "green", "blue", "yellow", "white"]
  },
  "minItems": 1,
  "maxItems": 3
}

实际效果

用户指令AI 生成的参数
“显示红色和蓝色”["red", "blue"]
“显示彩虹色”["red", "green", "blue"](限制最多3种)
“不要显示颜色”不会生成 [](因为 minItems: 1

应用场景:批量控制多个设备、设置多色呼吸灯效果、配置场景模式等需要列表参数的场景。


小结

约束类型作用典型应用场景
type限定数据类型确保 AI 返回整数而非字符串
enum枚举可选值开关状态、模式选择等离散值
minimum / maximum数值范围亮度级别、温度值等连续数值范围
minLength / maxLength字符串长度设备名称、显示文本等
pattern正则匹配时间格式、设备ID格式等
required必填字段确保关键参数不遗漏
description语义提示提供单位、示例、映射关系
items数组元素结构颜色列表、设备列表等

最佳实践:组合使用类型 + 范围 + 描述 + 必填声明,可以让 AI 模型生成最准确、最可靠的参数。例如本节亮度控制的参数定义就同时使用了 type(integer)、description(语义说明)、minimum/maximum(范围约束)和 required(必填声明)四种约束方式。