导航栏:首页 / AI教程目录 / AI创客项目开发教程目录 / 第1篇 基础知识篇 / AID101-1-3-04 – 后续工具调用实例
本节内容简介
在前几节的课程中,我们已经学会了如何通过 Tool Calls 让 AI 模型控制 ESP32 的 LED 开关和亮度。但细心的朋友可能已经发现了一个问题:AI 模型只会返回结构化的工具调用指令,而不会用自然语言与我们交流。
比如当用户说”把LED调到亮度最大”时,AI 智能返回包含有用于控制LED亮度的工具函数信息,如下所示:
{
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}虽然 ESP32 能够解析并执行这个json信息并且用于控制LED亮度,但对于我们人类用户来说,AI并没有提供任何像”好的,已经把LED调到最亮了”这样友好的自然语言回复。这种”只做事、不说话”的交互方式,在智能家居等应用场景中显得不够自然和人性化。
本节课程将讲解 Follow-up Tool Calls(后续工具调用) 技术,让 ESP32 在执行完工具调用后,把执行结果再次发送给 AI 模型,由 AI 生成一段自然语言的反馈。通过本节课程,您将学会如何让 AI “既会做事,也会说话”。
什么是 Follow-up Tool Calls?
核心概念
Follow-up Tool Calls 是 Tool Calls 机制的扩展流程。它在标准 Tool Calls 的三阶段(请求 → 分析响应 → 执行)基础上,增加了第四阶段——获取自然语言反馈:

为什么需要 Follow-up?
| 场景 | 仅有 Tool Calls | 加上 Follow-up |
|---|---|---|
| 用户说”打开LED” | 无回复,LED直接亮 | “好的,已经为您打开LED” |
| 用户说”把灯调亮” | 无回复,亮度变化 | “已帮您把LED调到最亮” |
| 执行失败 | 用户不知道发生了什么 | “抱歉,LED控制失败,请重试” |
Follow-up 让 AI 助手更像一个真正的”智能管家”——不仅听得懂指令、做得到操作,还能给用户明确的反馈。
🔁 Follow-up Tool Calls 完整流程
整个流程分为四个阶段:
- 第一次请求:ESP32 发送用户指令 + 可用工具列表,获取 Tool Calls 响应
- 执行 Tool Calls:ESP32 解析 AI 的工具调用指令,执行实际的硬件操作
- 第二次请求(Follow-up):ESP32 将完整的对话历史(包括 Tool Calls 和执行结果)再次发送给 AI
- 获取自然语言反馈:AI 根据执行结果生成友好的人话回复在下面的内容中,我们将详细讲解这四个阶段的详细内容。
在下面的内容中,我们将详细讲解这四个阶段的详细内容。
第一阶段:第一次请求(与上一节相同)
第一次请求与上一节课程AID101-1-3-03 – 多工具定义实例 中的示例程序中所发送的请求Json完全一致。ESP32 向 AI 平台发送用户指令和工具定义,AI 分析后返回需要调用的函数和参数。
请求 JSON
{
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。"
},
{
"role": "user",
"content": "LED亮度调节为最大"
}
],
"tools": [
{ /* controlLEDOnOFF 工具定义 */ },
{ /* controlLEDBrightness 工具定义 */ }
],
"tool_choice": "auto",
"temperature": 0.1
}这部分内容在上一节已经详细讲解过,此处不再赘述。如果您还不熟悉 Tool Calls 的基本流程,建议先回顾本教程 第三章 AI大模型工具调用(tools call) 前三节课程内容。
第二阶段:执行 Tool Calls(与上一节相同)
ESP32 收到 AI 的第一次响应后,解析出函数名和参数,调用相应的函数控制 LED。这一步也与上一节完全相同。
例如,当 AI 返回调用函数 controlLEDBrightness、并且参数为 {"level": 5} 时,ESP32 会执行 controlLEDBrightness(5),将 LED 调到最亮。
但这里有一个关键变化:为了进行 Follow-up tool calls,ESP32 需要保存 AI 的第一次 Tool Calls 响应中的以下信息,包括:
tool_calls_id:工具调用的唯一标识function_name:调用的函数名称arguments:调用参数
这些信息将在第二次请求也就是follow-up tool calls中使用。为了便于您学习,我们将第一次 Tool Calls 响应的Json内容列在下面:
{
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": "",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"level\": 5}",
"name": "controlLEDBrightness"
},
"id": "call_a4b2372b98b1492681255a",
"index": 0,
"type": "function"
}
]
}
}
],
"created": 1782587266,
"id": "chatcmpl-e5b396d-99d7-4702-ac18-ed87f61aefb0f",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 21,
"prompt_tokens": 275,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 296
}
}第三阶段:构造第二次请求(Follow-up 请求)⭐
这是本节课程的核心内容。第二次请求的 JSON 结构与第一次有很大不同,它不再包含 tools 字段,而是包含完整的对话历史,包括一个特殊的 role: "tool" 消息。
第二次请求 JSON 示例
{
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "LED亮度调节为最大"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_a4b2372b98b1492681255a",
"type": "function",
"function": {
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_a4b2372b98b1492681255a",
"content": "LED亮度已成功调节到最亮级别"
}
],
"temperature": 0.1
}messages 数组的四个角色
第二次请求的 messages 数组包含四条消息,它们共同构成了完整的对话上下文:
| 顺序 | role | 作用 |
|---|---|---|
| 1 | system | 设定 AI 的角色,并明确要求它”根据执行结果提供自然语言反馈” |
| 2 | user | 用户的原始指令(与第一次请求相同) |
| 3 | assistant | AI 的第一次响应,包含它要求调用的 Tool Calls。注意这里不是 content,而是 tool_calls |
| 4 | tool | ESP32 的执行结果反馈,告诉 AI “我已经执行了操作,这是结果” |
重点解析:role: "tool" 消息
{
"role": "tool",
"tool_call_id": "call_a4b2372b98b1492681255a",
"content": "LED亮度已成功调节到最亮级别"
}这是 Follow-up 机制中最关键的部分:
role: “tool”:声明这是一条”工具执行结果”消息。AI 平台看到这个角色,就知道这不是用户说的话,而是某个 Tool Call 的执行结果。tool_call_id:必须与第一次响应中 AI 返回的tool_calls[0].id完全一致。这是 AI 平台匹配”哪个工具调用对应哪个结果”的依据。content:执行结果的描述文本。这段文字会被 AI 模型读取,并作为生成自然语言反馈的参考。
💡 为什么需要
tool_call_id? 因为一次对话中可能有多个 Tool Calls(比如同时开灯和调节亮度),AI 需要通过 ID 来确认每个tool消息对应的是哪个工具调用。
重点解析:role: "assistant" 中的 tool_calls
{
"role": "assistant",
"tool_calls": [
{
"id": "call_a4b2372b98b1492681255a",
"type": "function",
"function": {
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}
}
]
}这条消息完全复制了 AI 第一次返回的 Tool Calls 内容。它的作用是告诉 AI:”这是你刚才要求我执行的操作”。这样 AI 就能理解上下文,知道”我要求调亮度到5,现在 ESP32 告诉我已经执行了”。
第四阶段:解析 AI 的自然语言反馈
AI 收到第二次请求后,会分析对话历史:
- 用户要求”LED亮度调节为最大”
- AI 之前要求调用
controlLEDBrightness(level=5) - ESP32 报告”LED亮度已成功调节到最亮级别”
基于这些信息,AI 会生成一段自然语言回复,例如:
“已将LED亮度调节到最大值,现在灯光最亮哦!”
第二次响应 JSON 结构
为了便于您的理解,我们将简化后的第二次响应 JSON展示在下面。
{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "已将LED亮度调节到最大值,现在灯光最亮哦!✨",
"role": "assistant"
}
}
],
"created": 1782587267,
"id": "chatcmpl-4dc8fbf-1fb-9af9d-904f4dd-60286b17cd",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 16,
"prompt_tokens": 90,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 106
}
}注意这里的 finish_reason 是 "stop" 而不是 "tool_calls",说明 AI 这次没有要求调用工具,而是直接给出了文本回复。ESP32 只需要提取 message.content 并显示出来即可。
本节示例程序
为了帮助您更好的理解,我们将通过以下示例程序向您讲解 Follow-up Tool Calls(后续工具调用)是如何实现的。
注意
本程序需要配合my_info.h代码使用,否则程序将无法正常编译。
请点击以下链接前往该代码页面,复制下载该代码:
http://ai.taichi-maker.com/index.php/homepage/ai-tutorial-index/ai-maker-project-tutorial-index/my_info_h-code-description/
/*
* ESP32 AI平台调用 Follow-up Tool Calls 示例
*
* 功能描述:
* 本程序专门为ESP32-S3-DevKitC-1开发板设计,演示如何实现LLM模型的Follow-up Tool Calls功能。
* 程序首先通过Tool Calls控制LED,然后将执行结果再次发送给LLM,获取更自然的用户反馈。
*
* Follow-up Tool Calls流程:
* 1. 第一次请求:发送用户指令,获取Tool Calls响应
* 2. 执行Tool Calls:控制LED的开关或亮度
* 3. 第二次请求:将Tool Calls响应和执行结果发送给LLM
* 4. 获取自然语言反馈:LLM根据执行结果生成自然语言回复
*
* 本程序支持的指令类型:
* - 打开/关闭LED(如"打开LED"、"关闭LED")
* - 调节LED亮度(如"把LED调到亮度最大"、"将LED亮度调到中等")
*
* 作者:Taichi-Maker
* 作者官网:http://ai.taichi-maker.com
* 创建日期:2026年06月27日
* 版本:1.1.7
*
* 硬件要求:
* - ESP32-S3-DevKitC-1开发板(内置一颗WS2812 LED,数据引脚连接到GPIO 48)
*
* 所需软件库:
* - FastLED库
* - ArduinoJson库(版本7.0.0或更高)--用于处理JSON数据
*
* 配置说明:
* 1. 请先通过Arduino IDE的库管理器安装FastLED、ArduinoJson。
* 2. 在my_info.h文件中填写您的Wi-Fi名称(ssid)和密码(password)。
* 3. 在my_info.h文件中将ai_api_key替换为您从AI平台获取的有效API密钥(切勿泄露!)。
* 4. 确保开发板已正确连接到电脑,并选择正确的开发板型号与端口。
*
* 使用方法:
* 1. 将本程序上传至ESP32-S3-DevKitC-1开发板。
* 2. 打开串口监视器(波特率设置为115200)。
* 3. 程序会自动连接Wi-Fi并发送用户指令给AI平台。
* 4. 观察第一次Tool Calls响应和LED控制执行过程。
* 5. 观察第二次请求和LLM生成的自然语言反馈。
*
* 注意事项:
* - 本例使用client.setInsecure()跳过了SSL证书验证,仅适用于测试环境;
* 在生产环境中应使用有效证书以确保通信安全。
* - 亮度级别为1-5级,其中1为最暗,5为最亮。
*
* 兼容性说明:
* 本程序专为ESP32-S3-DevKitC-1开发板设计,已在ESP32-S3-DevKitC-1开发板上测试通过。
*
* 许可证:MIT License
*
* 程序源:
* 本程序源自太极创客团队精心开发的《AI创客项目开发教程》。该教程专为热爱科技创新、热衷于动手实践
* 的创客爱好者与初学者量身打造,是一套完全免费、开源且注重实战的AIoT(人工智能物联网)入门学习资源。
*
* 通过本教程,您可以系统地掌握从项目构思、方案设计、软硬件选型,到实际搭建、系统集成与调试优化的
* 整个开发流程。
* 您可以通过以下链接获得更多关于本教程的详细信息:
* http://ai.taichi-maker.com/index.php/homepage/ai-tutorial/
*/
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "my_info.h"
// 引入FastLED库
#include <FastLED.h>
// LED配置
#define DATA_PIN 48
#define NUM_LEDS 1
CRGB leds[NUM_LEDS];
String userCommand = "LED亮度调节为最大";
const char* ai_payload_first_template = R"rawliteral({
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。"
},
{
"role": "user",
"content": "{{USER_COMMAND}}"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "controlLEDOnOFF",
"description": "控制LED的打开或关闭",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "LED控制动作",
"enum": ["on", "off"]
}
},
"required": ["action"]
}
}
},
{
"type": "function",
"function": {
"name": "controlLEDBrightness",
"description": "控制LED的亮度级别",
"parameters": {
"type": "object",
"properties": {
"level": {
"type": "integer",
"description": "亮度级别,1-5级,其中1为最暗,5为最亮",
"minimum": 1,
"maximum": 5
}
},
"required": ["level"]
}
}
}
],
"tool_choice": "auto",
"temperature": 0.1
})rawliteral";
const char* ai_payload_second_template = R"rawliteral({
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "{{USER_COMMAND}}"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "{{TOOL_CALL_ID}}",
"type": "function",
"function": {
"name": "{{TOOL_CALL_NAME}}",
"arguments": "{{TOOL_CALL_ARGUMENTS}}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "{{TOOL_CALL_ID}}",
"content": "{{EXECUTION_RESULT}}"
}
],
"temperature": 0.1
})rawliteral";
WiFiClientSecure client;
HTTPClient https;
String firstToolCallResponse = "";
String firstToolCallId = "";
String firstToolCallName = "";
String firstToolCallArguments = "";
String toolExecutionResult = "";
void setup() {
Serial.begin(115200);
delay(1000);
// 初始化FastLED
FastLED.addLeds<WS2812, DATA_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(128);
leds[0] = CRGB::White;
FastLED.show();
Serial.println("FastLED initialized successfully!");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
client.setInsecure();
client.setTimeout(15);
Serial.println();
Serial.println("---------- AI Platform Config ----------");
Serial.print("AI Platform API Endpoint: ");
Serial.print(ai_host);
Serial.println(ai_endpoint);
Serial.println();
Serial.print("一切就绪,等待几秒钟,便于您观察板上LED状态.");
delay(5000);
Serial.println("========== Follow-up Tool Calls 示例开始 ==========");
callAIPlatformFirst();
callAIPlatformSecond();
Serial.println("\n========== Follow-up Tool Calls 示例结束 ==========");
}
void loop() {
delay(10);
}
// ==================== LED控制函数(FastLED版本) ====================
String controlLEDOnOFF(String action) {
Serial.print("控制LED开关: ");
Serial.println(action);
String resultDescription = "";
if (action == "on") {
leds[0] = CRGB::White;
FastLED.show();
resultDescription = "LED已成功打开";
} else if (action == "off") {
leds[0] = CRGB::Black;
FastLED.show();
resultDescription = "LED已成功关闭";
} else {
resultDescription = "LED控制失败:无效的控制指令";
}
return resultDescription;
}
String controlLEDBrightness(int level) {
Serial.print("控制LED亮度: 级别 ");
Serial.println(level);
String resultDescription = "";
if (level >= 1 && level <= 5) {
uint8_t brightness = map(level, 1, 5, 50, 255);
FastLED.setBrightness(brightness);
leds[0] = CRGB::White;
FastLED.show();
String levelDescription;
switch (level) {
case 1: levelDescription = "最暗"; break;
case 2: levelDescription = "较暗"; break;
case 3: levelDescription = "中等"; break;
case 4: levelDescription = "较亮"; break;
case 5: levelDescription = "最亮"; break;
}
resultDescription = "LED亮度已成功调节到" + levelDescription + "级别";
} else {
resultDescription = "亮度调节失败:无效的亮度级别";
}
return resultDescription;
}
/*
* -------------- 第一次向AI平台发送HTTPS请求的函数 --------------
*
* 该函数完成以下步骤:
* 1. 构造完整的HTTPS请求URL(基于my_info.h中定义的主机、端口和路径)
* 2. 设置请求头:Content-Type为application/json,Authorization为Bearer + API密钥
* 3. 发送POST请求,携带包含Tool Call定义的JSON请求体
* 4. 接收并打印HTTP响应状态码和响应内容
* 5. 解析响应中的Tool Call参数并执行相应操作
* 6. 存储Tool Calls响应信息,用于第二次请求
*/
void callAIPlatformFirst() {
Serial.println("\n>>> 第一次请求:发送用户指令,获取Tool Calls响应...");
// 构造Authorization请求头:格式为"Bearer <your-api-key>"
String auth = "Bearer ";
auth += ai_api_key;
// 构造第一次请求的JSON体
String firstPayload = buildFirstPayload();
Serial.println("----- 第一次请求JSON -----");
Serial.println(firstPayload);
Serial.println("----- 请求JSON结束 -----");
// 初始化HTTPS连接(使用client、主机名、端口、API路径)
if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
// 设置超时时间(毫秒)- 增加超时时间以处理更复杂的请求
https.setTimeout(20000); // 20秒超时
https.setConnectTimeout(10000); // 10秒连接超时
Serial.println("HTTPS连接已初始化,发送第一次请求...");
// 添加必要的HTTP请求头
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", auth);
// 发送POST请求,并获取HTTP状态码
int httpCode = https.POST(firstPayload);
if (httpCode > 0) {
// 打印HTTP响应状态码(如200表示成功)
Serial.printf("HTTP响应状态码: %d\n", httpCode);
// 如果响应成功(HTTP 200 OK)
if (httpCode == HTTP_CODE_OK) {
// 获取完整的响应字符串
String resp = https.getString();
Serial.println("----- 第一次响应 -----");
Serial.println(resp); // 打印AI返回的完整JSON
Serial.println("----- 响应结束 -----");
// 存储第一次响应,用于第二次请求
firstToolCallResponse = resp;
// 解析Tool Call响应并执行操作
parseToolCallResponse(resp);
}
} else {
// 打印HTTP错误信息(如连接失败、超时等)
Serial.printf("HTTP错误: %s\n", https.errorToString(httpCode).c_str());
Serial.printf("错误代码: %d\n", httpCode);
}
// 结束本次HTTP会话,释放资源
https.end();
} else {
// 如果无法建立HTTPS连接,打印错误提示
Serial.println("无法连接到服务器");
}
}
/*
* -------------- 第二次向AI平台发送HTTPS请求的函数 --------------
*
* 该函数完成以下步骤:
* 1. 构造包含Tool Calls响应和执行结果的JSON请求体
* 2. 设置请求头:Content-Type为application/json,Authorization为Bearer + API密钥
* 3. 发送POST请求,携带包含对话历史和执行结果的JSON请求体
* 4. 接收并打印HTTP响应状态码和响应内容
* 5. 解析并显示LLM生成的自然语言反馈
*/
void callAIPlatformSecond() {
Serial.println("\n>>> 第二次请求:发送Tool Calls响应和执行结果,获取自然语言反馈...");
// 构造Authorization请求头:格式为"Bearer <your-api-key>"
String auth = "Bearer ";
auth += ai_api_key;
// 构造第二次请求的JSON体
String secondPayload = buildSecondPayload();
Serial.println("----- 第二次请求JSON -----");
Serial.println(secondPayload);
Serial.println("----- 请求JSON结束 -----");
// 初始化HTTPS连接(使用client、主机名、端口、API路径)
if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
// 设置超时时间(毫秒)
https.setTimeout(20000); // 20秒超时
https.setConnectTimeout(10000); // 10秒连接超时
Serial.println("HTTPS连接已初始化,发送第二次请求...");
// 添加必要的HTTP请求头
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", auth);
// 发送POST请求,并获取HTTP状态码
int httpCode = https.POST(secondPayload);
if (httpCode > 0) {
// 打印HTTP响应状态码(如200表示成功)
Serial.printf("HTTP响应状态码: %d\n", httpCode);
// 如果响应成功(HTTP 200 OK)
if (httpCode == HTTP_CODE_OK) {
// 获取完整的响应字符串
String resp = https.getString();
Serial.println("----- 第二次响应 -----");
Serial.println(resp); // 打印AI返回的完整JSON
Serial.println("----- 响应结束 -----");
// 解析并显示自然语言反馈
parseSecondResponse(resp);
}
} else {
// 打印HTTP错误信息(如连接失败、超时等)
Serial.printf("HTTP错误: %s\n", https.errorToString(httpCode).c_str());
Serial.printf("错误代码: %d\n", httpCode);
}
// 结束本次HTTP会话,释放资源
https.end();
} else {
// 如果无法建立HTTPS连接,打印错误提示
Serial.println("无法连接到服务器");
}
}
/*
* -------------- 构造第一次请求的JSON体 --------------
*
* 该函数完成以下步骤:
* 1. 使用预定义的JSON模板(ai_payload_first_template)
* 2. 替换模板中的占位符为实际值
* 3. 返回完整的第一次请求JSON体
*
* 这种方法使第一次请求的结构更加直观,便于学习者理解Tool Calls的实现
*
* 返回值:
* - String: 构造好的第一次请求JSON体
*/
String buildFirstPayload() {
// 从模板开始构建请求体
String payload = ai_payload_first_template;
// 替换模板中的占位符
payload.replace("{{USER_COMMAND}}", userCommand);
return payload;
}
/*
* -------------- 构造第二次请求的JSON体 --------------
*
* 该函数完成以下步骤:
* 1. 使用预定义的JSON模板(ai_payload_second_template)
* 2. 解析第一次Tool Calls响应,提取关键信息
* 3. 替换模板中的占位符为实际值
* 4. 返回完整的第二次请求JSON体
*
* 这种方法使第二次请求的结构更加直观,便于学习者理解Follow-up Tool Calls的实现
*
* 返回值:
* - String: 构造好的第二次请求JSON体
*/
String buildSecondPayload() {
// 解析第一次Tool Calls响应
DynamicJsonDocument firstDoc(4096);
DeserializationError error = deserializeJson(firstDoc, firstToolCallResponse);
if (error) {
Serial.print("第一次响应JSON解析失败: ");
Serial.println(error.c_str());
return "";
}
// 从模板开始构建请求体
String payload = ai_payload_second_template;
// 准备替换的值
String escapedArguments = firstToolCallArguments;
// 转义参数字符串中的双引号,确保JSON格式正确
escapedArguments.replace("\"", "\\\"");
// 替换模板中的占位符
payload.replace("{{USER_COMMAND}}", userCommand);
payload.replace("{{TOOL_CALL_ID}}", firstToolCallId);
payload.replace("{{TOOL_CALL_NAME}}", firstToolCallName);
payload.replace("{{TOOL_CALL_ARGUMENTS}}", escapedArguments);
payload.replace("{{EXECUTION_RESULT}}", toolExecutionResult);
return payload;
}
/*
* -------------- 解析第一次Tool Call响应的函数--------------
*
* 该函数完成以下步骤:
* 1. 使用ArduinoJson库解析JSON响应
* 2. 检查响应中是否包含tool_calls字段
* 3. 提取函数名称和参数
* 4. 调用统一的LED控制函数执行操作
* 5. 存储Tool Calls信息用于第二次请求
*/
void parseToolCallResponse(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>();
// 存储Tool Calls信息
firstToolCallId = toolCall["id"].as<String>();
firstToolCallName = functionName;
firstToolCallArguments = function["arguments"].as<String>();
Serial.print("检测到工具调用: ");
Serial.println(functionName);
Serial.print("Tool Call ID: ");
Serial.println(firstToolCallId);
Serial.print("Tool Call参数: ");
Serial.println(firstToolCallArguments);
if (functionName == "controlLEDOnOFF") {
// 解析LED开关函数参数
DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, firstToolCallArguments);
if (argsError) {
Serial.print("参数解析失败: ");
Serial.println(argsError.c_str());
return;
}
String action = argsDoc["action"].as<String>();
Serial.print(">>> 第二步:执行Tool Calls,控制LED开关...");
Serial.print("执行动作: ");
Serial.println(action);
// 调用LED开关控制函数并保存执行结果
toolExecutionResult = controlLEDOnOFF(action);
} else if (functionName == "controlLEDBrightness") {
// 解析LED亮度函数参数
DynamicJsonDocument argsDoc(512);
DeserializationError argsError = deserializeJson(argsDoc, firstToolCallArguments);
if (argsError) {
Serial.print("参数解析失败: ");
Serial.println(argsError.c_str());
return;
}
int level = argsDoc["level"].as<int>();
Serial.print(">>> 第二步:执行Tool Calls,控制LED亮度...");
Serial.print("执行亮度调节: ");
Serial.println(level);
// 调用LED亮度控制函数并保存执行结果
toolExecutionResult = 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);
}
}
}
}
/*
* -------------- 解析第二次响应的函数--------------
*
* 该函数完成以下步骤:
* 1. 使用ArduinoJson库解析JSON响应
* 2. 提取LLM生成的自然语言反馈
* 3. 显示反馈内容
*/
void parseSecondResponse(String jsonResponse) {
// 解析JSON响应
DynamicJsonDocument doc(4096);
DeserializationError error = deserializeJson(doc, jsonResponse);
if (error) {
Serial.print("第二次响应JSON解析失败: ");
Serial.println(error.c_str());
return;
}
// 提取并显示自然语言反馈
if (doc.containsKey("choices") && doc["choices"].size() > 0) {
JsonObject choice = doc["choices"][0];
JsonObject message = choice["message"];
if (message.containsKey("content") && !message["content"].as<String>().isEmpty()) {
String content = message["content"].as<String>();
Serial.println(">>> LLM自然语言反馈:");
Serial.println(content);
} else {
Serial.println("LLM没有提供自然语言反馈");
}
} else {
Serial.println("无法解析第二次响应");
}
}
/*
========================================================================
【附录】本程序实际发送的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
}
第一次响应
{
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": "",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"level\": 5}",
"name": "controlLEDBrightness"
},
"id": "call_a4b2372b98b1492681255a",
"index": 0,
"type": "function"
}
]
}
}
],
"created": 1782587266,
"id": "chatcmpl-e5b396d-99d7-4702-ac18-ed87f61aefb0f",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 21,
"prompt_tokens": 275,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 296
}
}
第二次请求(Follow-up Tool Calls):
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的开关和亮度。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "LED亮度调节为最大"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_a4b2372b98b1492681255a",
"type": "function",
"function": {
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_a4b2372b98b1492681255a",
"content": "LED亮度已成功调节到最亮级别"
}
],
"temperature": 0.1
}
第二次响应:
{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "已将LED亮度调节到最大值,现在灯光最亮哦!✨",
"role": "assistant"
}
}
],
"created": 1782587267,
"id": "chatcmpl-4dc8fbf-1fb-9af9d-904f4dd-60286b17cd",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 16,
"prompt_tokens": 90,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 106
}
}
*
* 说明:
* - 第一次请求用于获取Tool Calls响应
* - 第二次请求包含完整的对话历史,包括Tool Calls和执行结果
* - 第二次请求中的tool角色消息提供了Tool Calls的执行结果
* - LLM根据执行结果生成自然语言反馈,提供更友好的用户体验
* ========================================================================
*/本程序是在上一节课程示例程序的基础上修改而来,新增了 Follow-up 相关的代码。下面逐段讲解新增和修改的部分。
1. 全局变量:存储第一次 Tool Calls 信息(第169-173行)
// 全局变量,用于存储第一次Tool Calls响应
String firstToolCallResponse = "";
String firstToolCallId = "";
String firstToolCallName = "";
String firstToolCallArguments = "";
// 全局变量,用于存储Tool Calls实际执行结果
String toolExecutionResult = "";这五个全局变量用于在两次请求之间传递信息:
| 变量名 | 用途 |
|---|---|
firstToolCallResponse | 保存 AI 第一次返回的完整 JSON 响应 |
firstToolCallId | 保存 Tool Call 的唯一 ID |
firstToolCallName | 保存调用的函数名称 |
firstToolCallArguments | 保存调用的参数 JSON 字符串 |
toolExecutionResult | 保存 LED 控制函数的执行结果描述 |
为什么用全局变量?因为
setup()函数中会先后调用callAIPlatformFirst()和callAIPlatformSecond(),需要一种方式让第二次请求访问第一次的结果。
2. 两个 JSON 请求模板(第78-164行)
与上一节使用一个 ai_payload 不同,本程序定义了两个模板:
const char* ai_payload_first_template = R"rawliteral({...})rawliteral";
const char* ai_payload_second_template = R"rawliteral({...})rawliteral";第一次请求模板与上一节的 ai_payload 基本相同,只是将用户指令改为 {{USER_COMMAND}} 占位符,方便运行时替换。
第二次请求模板是全新的,它定义了 Follow-up 请求的 JSON 结构:
const char* ai_payload_second_template = R"rawliteral({
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手...请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "{{USER_COMMAND}}"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "{{TOOL_CALL_ID}}",
"type": "function",
"function": {
"name": "{{TOOL_CALL_NAME}}",
"arguments": "{{TOOL_CALL_ARGUMENTS}}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "{{TOOL_CALL_ID}}",
"content": "{{EXECUTION_RESULT}}"
}
],
"temperature": 0.1
})rawliteral";模板中有五个占位符,将在运行时被替换为实际值:
| 占位符 | 替换内容 |
|---|---|
{{USER_COMMAND}} | 用户原始指令 |
{{TOOL_CALL_ID}} | AI 返回的 Tool Call ID |
{{TOOL_CALL_NAME}} | 调用的函数名 |
{{TOOL_CALL_ARGUMENTS}} | 调用的参数(JSON 字符串) |
{{EXECUTION_RESULT}} | 执行结果描述 |
需要特殊说明的是,我们在实际操作中,当需要建立类似以上提到的Json请求内容,会使用ArduinoJson库来实现,以保证格式准确。在本程序中,为了让学者可以更好的理解Json请求内容,因此我们将完整的Json内容罗列出来而没有使用ArduinoJson库来构建。
3. setup() 中的两次调用(第205-208行)
Serial.println("========== Follow-up Tool Calls 示例开始 ==========");
Serial.println("\n>>> 第一步:发送第一次请求,获取Tool Calls响应...");
callAIPlatformFirst();
Serial.println("\n>>> 第三步:发送第二次请求,获取自然语言反馈...");
callAIPlatformSecond();
Serial.println("\n========== Follow-up Tool Calls 示例结束 ==========");setup() 函数中依次调用两个函数,完成完整的 Follow-up 流程。注意串口打印中的”第一步”和”第三步”,中间的”第二步”(执行 Tool Calls)是在 callAIPlatformFirst() 内部完成的。
4. 第一次请求函数:callAIPlatformFirst()(第274-334行)
这个函数与上一节的 callAIPlatform() 基本相同,但有以下关键变化:
变化一:使用 buildFirstPayload() 构造请求体
String firstPayload = buildFirstPayload();而不是直接使用固定的 ai_payload。这样可以将用户指令动态插入模板。
变化二:保存响应并调用解析函数
firstToolCallResponse = resp;
parseToolCallResponse(resp);将 AI 的完整响应保存到全局变量,然后调用解析函数。解析函数会提取 Tool Call 信息并执行 LED 控制。
5. 第二次请求函数:callAIPlatformSecond()(第346-403行)
这是全新增加的函数,专门用于发送 Follow-up 请求:
void callAIPlatformSecond() {
// 构造第二次请求的JSON体
String secondPayload = buildSecondPayload();
// ...(HTTPS连接、发送POST请求、接收响应)
// 解析并显示自然语言反馈
parseSecondResponse(resp);
}流程与第一次请求类似,但:
- 使用
buildSecondPayload()构造请求体 - 响应解析使用
parseSecondResponse(),提取自然语言内容而不是 Tool Calls
6. 构造第一次请求体:buildFirstPayload()(第418-426行)
String buildFirstPayload() {
// 从模板开始构建请求体
String payload = ai_payload_first_template;
// 替换模板中的占位符
payload.replace("{{USER_COMMAND}}", userCommand);
return payload;
}非常简单:复制模板,替换用户指令占位符。
7. 构造第二次请求体:buildSecondPayload()(第442-469行)⭐
这是 Follow-up 流程中最核心的代码:
String buildSecondPayload() {
// 解析第一次Tool Calls响应
DynamicJsonDocument firstDoc(4096);
DeserializationError error = deserializeJson(firstDoc, firstToolCallResponse);
if (error) {
Serial.print("第一次响应JSON解析失败: ");
Serial.println(error.c_str());
return "";
}
// 从模板开始构建请求体
String payload = ai_payload_second_template;
// 准备替换的值
String escapedArguments = firstToolCallArguments;
// 转义参数字符串中的双引号,确保JSON格式正确
escapedArguments.replace("\"", "\\\"");
// 替换模板中的占位符
payload.replace("{{USER_COMMAND}}", userCommand);
payload.replace("{{TOOL_CALL_ID}}", firstToolCallId);
payload.replace("{{TOOL_CALL_NAME}}", firstToolCallName);
payload.replace("{{TOOL_CALL_ARGUMENTS}}", escapedArguments);
payload.replace("{{EXECUTION_RESULT}}", toolExecutionResult);
return payload;
}关键步骤解析:
步骤一:转义双引号
String escapedArguments = firstToolCallArguments;
escapedArguments.replace("\"", "\\\"");firstToolCallArguments 的值是 {"level": 5},其中包含双引号。如果直接把这个字符串插入到 JSON 模板中,会破坏 JSON 格式:
"arguments": "{"level": 5}" // ❌ 错误的JSON,内部双引号没有转义通过将 " 替换为 \",得到 \{"level": 5\},这样插入模板后就是合法的 JSON:
"arguments": "{\"level\": 5}" // ✅ 正确的JSON步骤二:使用已保存的执行结果
payload.replace("{{EXECUTION_RESULT}}", toolExecutionResult);toolExecutionResult 变量在 parseToolCallResponse() 中直接获取 LED 控制函数的返回值得到,包含了人类可读的执行结果描述,例如”LED亮度已成功调节到最亮级别”。
8. 解析第一次响应:parseToolCallResponse()(第483-570行)
这个函数与上一节的 parseToolCallsResponse() 类似,但增加了保存 Tool Call 信息的逻辑,并且直接获取 LED 控制函数的返回值保存到 toolExecutionResult:
// 存储Tool Calls信息
firstToolCallId = toolCall["id"].as<String>();
firstToolCallName = functionName;
firstToolCallArguments = function["arguments"].as<String>();在解析出函数名和参数后,立即保存到全局变量,供第二次请求使用。
关键变化:捕获 LED 控制函数的返回值
// 调用LED开关控制函数并保存执行结果
toolExecutionResult = controlLEDOnOFF(action);
// 调用LED亮度控制函数并保存执行结果
toolExecutionResult = controlLEDBrightness(level);LED 控制函数(如 controlLEDOnOFF() 和 controlLEDBrightness())会返回执行结果描述(如”LED已成功打开”或”LED亮度已成功调节到最亮级别”),这些返回值直接被保存到 toolExecutionResult 变量中,供第二次请求使用。
9. 解析第二次响应:parseSecondResponse()(第580-606行)⭐
这是全新增加的函数,用于提取 AI 的自然语言反馈:
// 提取并显示自然语言反馈
if (doc.containsKey("choices") && doc["choices"].size() > 0) {
JsonObject choice = doc["choices"][0];
JsonObject message = choice["message"];
if (message.containsKey("content") && !message["content"].as<String>().isEmpty()) {
String content = message["content"].as<String>();
Serial.println(">>> LLM自然语言反馈:");
Serial.println(content);
} else {
Serial.println("LLM没有提供自然语言反馈");
}
} else {
Serial.println("无法解析第二次响应");
}
}与解析 Tool Calls 不同,这里只需要提取 message.content 字段,这就是 AI 生成的自然语言回复。
10. LED 控制函数的返回值
为了让 Follow-up 能够获取执行结果,两个 LED 控制函数都增加了 String 返回值:
String controlLEDOnOFF(String action) {
String resultDescription = "";
// ...执行操作...
resultDescription = "LED已成功打开";
return resultDescription;
}
String controlLEDBrightness(int level) {
String resultDescription = "";
// ...执行操作...
resultDescription = "LED亮度已成功调节到最亮级别";
return resultDescription;
}在 parseToolCallResponse() 中,执行完函数后会将返回值保存到 toolExecutionResult 变量,供第二次请求使用。
AI在第二次接收到请求后,会根据toolExecutionResult的具体内容,得知ESP32具体执行指令的结果,并且会根据toolExecutionResult的具体内容配合请求中的其它信息生成自然语言信息,告知用户ESP32的执行结果。
这里需要说明的是,如果没有这第二次互动,那么ESP32自身只能按照我们的程序逻辑固定的生成程序运行结果信息,如“LED亮度已成功调节到最亮级别”或“LED亮度已成功调节到最低级别”。这是纯粹的机器式答复,不够智能化。
但是有了这第二次互动,那么ESP32就会在AI平台的帮助下,获得更加人性化的指令执行结果信息,向用户汇报结果。
完整交互流程示例
下面以用户指令 “LED亮度调节为最大” 为例,展示 ESP32、AI 平台和硬件之间的完整交互流程。(请注意,以下响应内容可能与实际响应内容存在差异,这里提供的信息仅供参考。)
第一步:ESP32 发送第一次请求
{
"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
}第二步:AI 返回的第一次 Tool Calls 响应
{
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": {
"content": "",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"level\": 5}",
"name": "controlLEDBrightness"
},
"id": "call_a4b2372b98b1492681255a",
"index": 0,
"type": "function"
}
]
}
}
],
"created": 1782587266,
"id": "chatcmpl-e5b396d-99d7-4702-ac18-ed87f61aefb0f",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 21,
"prompt_tokens": 275,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 296
}
}第三步:ESP32 执行操作
- 解析出
controlLEDBrightness(level=5) - 调用
myLumi.setBright(5),LED 变为最亮 - 保存 Tool Call ID、函数名、参数和执行结果
第四步:ESP32 发送第二次请求(Follow-up
{
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能家居助手,可以控制LED的开关和亮度。请根据Tool Calls的执行结果,提供友好的自然语言反馈。"
},
{
"role": "user",
"content": "LED亮度调节为最大"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_a4b2372b98b1492681255a",
"type": "function",
"function": {
"name": "controlLEDBrightness",
"arguments": "{\"level\": 5}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_a4b2372b98b1492681255a",
"content": "LED亮度已成功调节到最亮级别"
}
],
"temperature": 0.1
}第五步:AI 返回第二次响应(含有自然语言反馈)
{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "已将LED亮度调节到最大值,现在灯光最亮哦!✨",
"role": "assistant"
}
}
],
"created": 1782587267,
"id": "chatcmpl-4dc8fbf-1fb-9af9d-904f4dd-60286b17cd",
"model": "qwen-flash",
"object": "chat.completion",
"usage": {
"completion_tokens": 16,
"prompt_tokens": 90,
"prompt_tokens_details": {
"cached_tokens": 0
},
"total_tokens": 106
}
}第六步:ESP32 显示反馈
串口监视器输出:
>>> LLM自然语言反馈:
已将LED亮度调节到最大值,现在灯光最亮哦!✨
为什么第二次请求中 assistant 的 tool_calls 很重要?
有些初学者可能会有疑问:为什么第二次请求中,需要把 AI 第一次返回的 tool_calls 原样发回去?不能只发 role: "tool" 消息吗?
答案是:必须发送完整的对话历史,包括 assistant 的 tool_calls。
原因如下:
- 对话连续性:AI 模型是无状态的,每次请求都是独立的。如果不告诉它”你之前要求我做了什么”,它就不知道上下文。
- ID 匹配:
role: "tool"消息通过tool_call_id与assistant消息中的tool_calls进行匹配。AI 平台需要验证这个对应关系。 - 逻辑一致性:AI 需要知道”我要求调亮度到5″,才能理解”执行结果是调到最亮”的含义,从而生成合理的回复。
如果把第二次请求比作一场对话复盘,那么:
system是会议规则user是用户提出的需求assistant是 AI 之前下达的指令tool是执行者(ESP32)汇报的执行结果
只有四者齐全,AI 才能做出恰当的总结和回应。
实践建议
1. 执行结果描述要清晰
role: "tool" 的 content 内容直接影响 AI 生成的反馈质量。建议:
- 说明操作是否成功
- 包含具体的参数值
- 使用简洁明了的语言
| 较差的描述 | 较好的描述 |
|---|---|
| “OK” | “LED已成功打开” |
| “done” | “LED亮度已成功调节到最亮级别(级别5)” |
| “失败” | “LED控制失败:无效的亮度级别,必须在1-5之间” |
2. 处理执行失败的情况
如果 Tool Call 执行失败(比如参数无效、硬件故障),tool 消息应该如实报告:
{
"role": "tool",
"tool_call_id": "xxx",
"content": "LED控制失败:无效的亮度级别8,必须在1-5之间"
}AI 收到后可能会生成:
“抱歉,无法将LED调到级别8,亮度级别只能在1到5之间。”
3. 注意 tool_call_id 的准确性
tool 消息中的 tool_call_id 必须与 assistant 消息中的 tool_calls[].id 完全一致。如果 ID 不匹配,AI 平台可能会返回错误,或者 AI 模型无法正确理解上下文。
总结
本节课程讲解了 Follow-up Tool Calls 的完整实现流程:
- 第一次请求:发送用户指令和工具定义,获取 AI 的 Tool Calls 响应
- 执行并保存:解析 Tool Calls,执行硬件操作,保存 Tool Call ID、函数名、参数和结果
- 第二次请求:构造包含完整对话历史的 Follow-up 请求,关键是添加
role: "tool"消息 - 获取反馈:解析 AI 返回的自然语言回复,呈现给用户
通过 Follow-up Tool Calls,我们让 AI 助手实现了**”既能做事,也能说话”**的完整交互体验。这在智能音箱、智能家居控制、机器人交互等场景中非常实用。
在下一节课程中,我们将进一步扩展 Tool Calls 的应用场景,探索更多有趣的 AIoT 交互方式。
基础知识篇目录
第〇章 (AID101-1-0) 序言
第一章 (AID101-1-1) 准备工作
第二章 (AID101-1-2) AI大模型API交互基础
- AID101-1-2-01 – 使用ESP32调用AI大模型API的基本操作
- AID101-1-2-02 – System Role详解
- AID101-1-2-03 – User Role 详解
- AID101-1-2-04 – Assistant Role详解
- AID101-1-2-05 -大模型API交互基础知识总结及练习
第三章 (AID101-1-3) AI大模型工具调用(tools call)
- AID101-1-3-01 – AI大模型工具调用(tools call)基础
- AID101-1-3-02 – 单工具定义实例
- AID101-1-3-03 – 多工具定义实例
- AID101-1-3-04 – 后续工具调用(follow-up tools call)实例
- AID101-1-3-05 – 并行多工具调用(parallel tool calls)实例
- AID101-1-3-06 – 应用开发实例: 可用自然语言控制的人工智能LED
附录