导航栏:首页 / AI教程目录 / AI创客项目开发教程目录 / 第1篇 基础知识篇 / AID101-1-2-05 – 大模型API交互基础知识总结及练习
本节内容简介
本节课程里我们将回顾本章课程的核心知识,梳理它们之间的递进关系,并通过一个练习程序,将所学知识付诸实践。
课程回顾:从单次请求到多轮对话
本章四节课遵循循序渐进的教学原则,每一节课都在前一节课的基础上引入新的概念,逐步构建起完整的 AI 对话知识体系。
📌AID101-1-2-01 – 使用ESP32调用AI大模型API的基本操作
核心内容: 学习ESP32 如何向 AI 平台发送请求并获得回答
这是整个系列课程的起点。本节课讲解了 ESP32 调用 AI 平台 API 的完整流程,包含四个阶段:
| 阶段 | 关键操作 | 涉及的核心代码 |
|---|---|---|
| 初始化与准备 | 引入库、定义请求 JSON、创建客户端对象 | WiFi.h、WiFiClientSecure.h、HTTPClient.h |
| 系统启动 | 串口初始化、Wi-Fi 连接、HTTPS 配置 | WiFi.begin()、client.setInsecure() |
| 发送 AI 请求 | 构造认证头、发送 POST 请求、处理响应 | https.POST()、https.getString() |
| 主循环 | 空循环(仅发送一次请求) | loop() |
本节课的关键收获:
- 请求体的 JSON 结构:理解
model、messages、temperature、max_tokens四个参数的含义 - HTTPS 通信流程:
begin()→ 设置请求头 →POST()→getString()→end() - messages 数组的基本格式:这是与 AI 通信的核心数据结构
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "请解释一下什么是人工智能大语言模型"
}
]此时,messages 数组中只有 system 和 user 两种角色,程序仅发送一次请求,AI 的回答也不会被保存或再次使用。
📌 AID101-1-2-02 – System Role详解
核心内容: 学习如何让 AI 按照我们期望的方式回答问题
本节课深入讲解了 System Role 的作用——它就像给 AI 下达”人设指令”,让 AI 按照特定的身份、风格和规则来回答问题。
System Role 的三大核心应用场景:
| 场景 | 作用 | 示例 |
|---|---|---|
| 指定角色身份 | 让 AI 扮演特定角色 | “你是豪迈勇猛的三国猛将——张飞” |
| 强制输出格式 | 让 AI 输出结构化数据 | “以纯文本 JSON 格式输出结果” |
| 限制语言风格 | 控制 AI 的表达方式 | “不要使用现代术语” |
本节课相比 2-1 的关键变化:
// 2-1:最基础的系统提示词
"content": "You are a helpful assistant."
// 2-2:丰富的角色设定
"content": "你是豪迈勇猛、性如烈火的三国猛将——张飞。你忠义无双,嫉恶如仇,
说话直率粗犷。请以张飞的口吻回答问题,保持其性格特点,
只使用《三国演义》小说中的语言风格进行交流,不要使用现代术语。"编写 System Role 的几个技巧:
- 明确角色身份:”你是豪迈勇猛、性如烈火的三国猛将——张飞”
- 描述性格特点:”你忠义无双,嫉恶如仇,说话直率粗犷”
- 指定语言风格:”只使用《三国演义》小说中的语言风格进行交流”
- 给出具体指令:”不要使用现代术语”
此时,messages 数组中仍然只有 system 和 user 两种角色,但 system 的内容变得丰富而强大。
📌 AID101-1-2-03 – User Role 详解
核心内容: 学习如何有效地向 AI 表达我们的需求
本节课聚焦于 User Role,讲解了如何编写高质量的提示词(Prompt),让 AI 更准确地理解用户意图。
User Role 与 System Role 的分工:
| 角色 | 作用 | 类比 |
|---|---|---|
| System Role | 设定 AI 的行为规则和身份 | 给 AI 定”规矩” |
| User Role | 发送用户的具体问题或指令 | 向 AI 提”要求” |
提示词工程的四大核心原则:
| 原则 | 说明 | 示例 |
|---|---|---|
| 清晰具体 | 避免模糊提问 | ❌ “帮我写个函数” → ✅ “用 C++ 写一个判断闰年的函数” |
| 提供背景 | 给 AI 足够的上下文 | ❌ “推荐编程书” → ✅ “我是 C++ 初学者,推荐进阶书籍” |
| 明确格式 | 指定输出格式 | ❌ “怎么处理 bug” → ✅ “按原因、方案、预防三部分回答” |
| 分步处理 | 复杂任务拆解 | ❌ “解释量子计算并实现” → ✅ “先解释原理,再给代码示例” |
此时,messages 数组中仍然是 system 和 user 两种角色,但用户学会了如何更有效地编写 user 消息。
📌 AID101-1-2-04 – Assistant Role详解
核心内容: 学习如何让 AI 理解多轮对话的上下文
这是本系列课程的关键转折点。前三节课中,每次请求都是独立的,AI 无法”记住”之前说过的话。本节课引入了 Assistant Role,将 AI 的历史回答带入下一次请求,实现了上下文感知的连续对话。
三种角色的协作关系:
System Role → 设定 AI 的行为规则(固定不变)
User Role → 用户的输入(每轮变化)
Assistant Role → AI 的回答记录(每轮累积)
messages 数组中消息的顺序规则:
1. system (最先,设定 AI 身份)
2. user (第一轮用户问题)
3. assistant (第一轮 AI 回答)
4. user (第二轮用户问题)
5. assistant (第二轮 AI 回答)
6. user (第三轮用户问题)
...以此类推
为什么需要 Assistant Role?
没有 Assistant Role 时:
用户: "帮我计算2的2次方" → AI: "等于4"
用户: "那3呢" → AI: "3什么?"(无法理解上下文)
有了 Assistant Role 时:
用户: "帮我计算2的2次方" → AI: "等于4"
用户: "那3呢" → AI: "3的2次方等于9"(理解上下文!)
本节课的关键认识: 多轮对话的本质,就是在每次请求中携带完整的对话历史(包括 AI 之前的回答),让 AI 能够理解对话的完整脉络。
📊 四节课的知识递进关系
2-1 API请求基础
│ 学会:ESP32 如何与 AI 平台通信
│ 掌握:请求体 JSON 结构、HTTPS 通信流程
│
▼
2-2 System Role
│ 学会:如何通过 system 消息控制 AI 的行为
│ 掌握:角色定位、格式约束、风格控制
│
▼
2-3 User Role
│ 学会:如何编写高质量的提示词
│ 掌握:提示词工程的四大核心原则
│
▼
2-4 Assistant Role
│ 学会:如何通过 assistant 消息实现上下文感知
│ 掌握:多轮对话的消息构建方式
│
▼
2-5 综合练习(本节)
学会:将三种角色综合运用,实现交互式多轮对话
掌握:动态构建请求 JSON、管理对话上下文
思考题:实现交互式多轮对话
在前四节课中,我们学习了三种消息角色的作用和用法,但示例程序中的对话内容都是硬编码在程序中的——也就是说,对话内容在程序上传之前就已经固定了,无法在运行时改变。
现在,请思考以下问题:
如何编写一个程序,让用户通过串口监视器实时输入消息,与 AI 进行真正的交互式多轮对话?
具体要求:
- 用户可以在启动时自定义 System Role(AI 的身份设定)
- 用户通过串口监视器输入消息,AI 实时回复
- 程序自动管理对话上下文,保留最近 5 轮对话历史
- 每次请求和响应的 JSON 都打印在串口监视器上,便于学习观察
提示与建议:
- 建议使用 ArduinoJson 库来动态构建请求 JSON(比起硬编码字符串,ArduinoJson 库可以很好的简化代码)
思考题答案:交互式多轮对话示例程序
/*
* ESP32 AI平台调用 - 交互式多轮对话示例
*
* 功能描述:
* 本程序演示如何在 ESP32(特别是 ESP32-S3)开发板上,通过 HTTPS 协议与 AI 平台
* 进行带有上下文的交互式多轮对话。
*
* 与参考程序的区别:
* - 参考程序将对话内容硬编码在程序中,仅演示一次请求
* - 本程序允许用户通过串口监视器实时输入消息,实现真正的交互式对话
* - 支持自定义 System Role(AI身份设定)
* - 自动管理5轮对话上下文,超过5轮的旧对话会自动清除
* - 每次请求和响应的完整JSON都会打印在串口监视器上,便于学习理解
*
* 对话场景示例:
* - 用户输入 System Role: "你是一个数学助手"
* - 用户: "帮我计算2的2次方"
* - AI: "2的2次方等于4。"
* - 用户: "那3呢" <- AI通过上下文理解这是问"3的2次方"
* - AI: "3的2次方等于9。"
*
* 通过本程序,初学者可以深入理解:
* 1. 如何通过 assistant role 传递对话历史
* 2. 如何实现真正的交互式多轮对话
* 3. 如何管理对话上下文(保留最近5轮)
* 4. 如何查看和分析 HTTP 请求/响应的 JSON 结构
*
* 作者:Taichi-Maker
* 作者官网:http://ai.taichi-maker.com
* 创建日期:2026年04月09日
* 版本:1.0.0
*
* 硬件要求:
* - 支持 Wi-Fi 的 ESP32 开发板(推荐使用 ESP32-S3-DevKitC-1)
*
* 所需软件库:
* - ArduinoJson 库(版本 7.0.0 或更高)——用于动态构建和解析 JSON
* 官网:https://arduinojson.org/
*
* 配置说明:
* 1. 在 my_info.h 文件中填写您的 Wi-Fi 名称(ssid)和密码(password)
* 2. 在 my_info.h 文件中将 ai_api_key 替换为您从 AI 平台获取的有效 API 密钥
* 3. 确保开发板已正确连接到电脑,并选择正确的开发板型号与端口
*
* 使用方法:
* 1. 将本程序上传至 ESP32
* 2. 打开串口监视器(波特率设置为 115200,换行符设置为"换行(NL)"或"回车换行(CR+NL)")
* 3. 按提示输入 System Role(AI身份设定)
* 4. 开始对话:输入消息,按发送,等待AI回复
* 5. 观察串口监视器中打印的请求JSON和响应JSON,理解数据传输过程
*
* 注意事项:
* - 本例使用 client.setInsecure() 跳过了 SSL 证书验证,仅适用于测试环境
* - 请确保串口监视器的换行符设置正确,否则无法正确读取输入
* - 对话上下文存储在内存中,重启后会丢失
*
* 许可证:MIT License
*
* 程序源:
* 本程序源自太极创客团队精心开发的《AI创客项目开发教程》。
* 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"
// ==================== 常量定义 ====================
// 最大保留的对话轮数(一问一答算一轮)
const int MAX_CONTEXT_ROUNDS = 5;
// 单条消息的最大长度
const int MAX_MESSAGE_LENGTH = 500;
// JSON 文档的容量(根据消息长度和数量调整)
const size_t JSON_CAPACITY = 8192;
// ==================== 全局对象 ====================
// 创建安全的 Wi-Fi 客户端(用于 HTTPS 连接)
WiFiClientSecure client;
// 创建 HTTP 客户端对象
HTTPClient https;
// ==================== 对话上下文管理结构 ====================
/*
* 对话消息结构体
* 用于存储单条对话消息的角色和内容
* - role: 消息角色 ("system"/"user"/"assistant")
* - content: 消息内容
*/
struct ChatMessage {
String role;
String content;
};
// 对话历史数组,采用循环缓冲区设计
// 数组大小为 MAX_CONTEXT_ROUNDS * 2 + 1 的原因是:
// - system 消息占 1 个位置
// - 每轮对话包含 user 和 assistant 两条消息,共 MAX_CONTEXT_ROUNDS * 2 个位置
ChatMessage chatHistory[MAX_CONTEXT_ROUNDS * 2 + 1];
// 当前存储的消息数量
int messageCount = 0;
// System Role 内容
String systemRole = "";
// ==================== 函数声明 ====================
void connectWiFi();
void setupSystemRole();
String readSerialInput();
void addMessage(const String& role, const String& content);
void buildRequestJson(JsonDocument& doc);
void printChatHistory();
void sendAIRequest(const String& payload);
String extractAIResponse(const String& response);
// ==================== 初始化函数 ====================
void setup() {
// 初始化串口通信,波特率为 115200
Serial.begin(115200);
delay(1000); // 短暂等待串口稳定
Serial.println();
Serial.println("╔══════════════════════════════════════════════════════════╗");
Serial.println("║ ESP32 AI 交互式多轮对话示例 ║");
Serial.println("║ Interactive Multi-round Chat with Context ║");
Serial.println("╚══════════════════════════════════════════════════════════╝");
Serial.println();
// 步骤1:连接 Wi-Fi
connectWiFi();
// 步骤2:设置 SSL 客户端
Serial.println("\n---------- 初始化 HTTPS 连接 ----------");
// 【重要】跳过 SSL 证书验证(仅用于开发测试!)
client.setInsecure();
// 设置连接超时时间
client.setTimeout(15);
Serial.println("HTTPS 客户端初始化完成");
// 步骤3:设置 System Role
setupSystemRole();
// 步骤4:打印使用说明
Serial.println("\n╔══════════════════════════════════════════════════════════╗");
Serial.println("║ 使用说明 ║");
Serial.println("╠══════════════════════════════════════════════════════════╣");
Serial.println("║ 1. 在下方输入框输入消息,按发送即可与AI对话 ║");
Serial.println("║ 2. 程序会自动保留最近5轮对话上下文 ║");
Serial.println("║ 3. 每次请求和响应的JSON都会打印在串口监视器上 ║");
Serial.println("║ 4. 输入 'reset' 可清空对话历史重新开始 ║");
Serial.println("║ 5. 输入 'history' 可查看当前对话历史 ║");
Serial.println("╚══════════════════════════════════════════════════════════╝");
Serial.println("\n请开始对话:");
Serial.print("> ");
}
// ==================== 主循环 ====================
void loop() {
// 检查串口是否有输入
if (Serial.available() > 0) {
// 读取用户输入
String userInput = readSerialInput();
userInput.trim(); // 去除首尾空白字符
// 检查特殊命令
if (userInput.equalsIgnoreCase("reset")) {
// 清空对话历史,保留 system role
messageCount = 1; // 只保留 system 消息
Serial.println("\n[系统] 对话历史已清空,重新开始对话\n");
Serial.print("> ");
return;
}
if (userInput.equalsIgnoreCase("history")) {
// 显示当前对话历史
printChatHistory();
Serial.print("> ");
return;
}
// 忽略空输入
if (userInput.length() == 0) {
Serial.print("> ");
return;
}
Serial.println("\n[用户] " + userInput);
// 将用户消息添加到历史记录
addMessage("user", userInput);
// 构建请求 JSON
StaticJsonDocument<JSON_CAPACITY> requestDoc;
buildRequestJson(requestDoc);
// 将 JSON 文档序列化为字符串
String requestPayload;
serializeJson(requestDoc, requestPayload);
// 打印请求 JSON(用于学习观察)
Serial.println("\n========== 发送的 HTTP 请求体(JSON)==========");
serializeJsonPretty(requestDoc, Serial);
Serial.println("\n==============================================\n");
// 发送请求到 AI 平台
sendAIRequest(requestPayload);
// 提示用户继续输入
Serial.print("> ");
}
}
// ==================== Wi-Fi 连接函数 ====================
/*
* 连接 Wi-Fi 网络
* 循环等待直到连接成功,显示连接进度
*/
void connectWiFi() {
Serial.println("---------- 连接 Wi-Fi ----------");
Serial.print("正在连接: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
// 循环等待连接成功
int attempt = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
attempt++;
if (attempt > 40) { // 20秒超时
Serial.println("\n[错误] Wi-Fi 连接超时,请检查配置");
return;
}
}
Serial.println("\n[成功] Wi-Fi 已连接!");
Serial.print("IP 地址: ");
Serial.println(WiFi.localIP());
}
// ==================== System Role 设置函数 ====================
/*
* 在启动时允许用户输入自定义的 System Role
* System Role 用于设定 AI 的身份和行为方式
*/
void setupSystemRole() {
Serial.println("\n---------- 设置 AI 身份(System Role) ----------");
Serial.println("System Role 用于设定 AI 助手的身份和行为方式。");
Serial.println("例如:");
Serial.println(" - '你是一个智能助理'");
Serial.println(" - '你是一个幽默的聊天伙伴'");
Serial.println("\n请输入 System Role(直接按回车使用默认:'你是一个智能助理'):");
Serial.print("> ");
// 等待用户输入
while (Serial.available() == 0) {
delay(100);
}
systemRole = readSerialInput();
systemRole.trim();
// 如果用户未输入,使用默认值
if (systemRole.length() == 0) {
systemRole = "你是一个智能助理";
}
Serial.println("[已设置] System Role: " + systemRole);
// 将 system 消息添加到历史记录(始终位于第一条)
chatHistory[0].role = "system";
chatHistory[0].content = systemRole;
messageCount = 1;
}
// ==================== 串口输入读取函数 ====================
/*
* 从串口读取一行输入
* 支持以换行符(\n)或回车符(\r)结束输入
* 返回读取到的字符串(不包含换行符)
*/
String readSerialInput() {
String input = "";
char c;
while (Serial.available() > 0) {
c = Serial.read();
// 检测到换行或回车时结束读取
if (c == '\n' || c == '\r') {
break;
}
input += c;
delay(2); // 短暂延迟确保字符接收完整
}
// 清除缓冲区中可能残留的换行符
while (Serial.available() > 0 && (Serial.peek() == '\n' || Serial.peek() == '\r')) {
Serial.read();
}
return input;
}
// ==================== 添加消息到历史记录 ====================
/*
* 将新消息添加到对话历史
* 如果超过最大轮数限制,会自动移除最旧的用户-助手对话对
*
* 参数:
* - role: 消息角色 ("user" 或 "assistant")
* - content: 消息内容
*/
void addMessage(const String& role, const String& content) {
// 检查是否需要清理旧消息(超过5轮时)
// messageCount 包含 system 消息,所以最大为 1 + 5*2 = 11
if (messageCount >= MAX_CONTEXT_ROUNDS * 2 + 1) {
// 需要移除最旧的一对 user-assistant 消息
// 将数组中的消息向前移动两位(跳过最旧的 user 和 assistant)
for (int i = 2; i < messageCount; i++) {
chatHistory[i - 1] = chatHistory[i];
}
messageCount--; // 减少计数,为新消息腾出位置
Serial.println("[系统] 对话历史已超过5轮,自动清除最早的一轮");
}
// 添加新消息
chatHistory[messageCount].role = role;
chatHistory[messageCount].content = content;
messageCount++;
}
// ==================== 构建请求 JSON ====================
/*
* 根据当前对话历史构建 HTTP 请求的 JSON 文档
* 符合 OpenAI API 兼容格式
*
* 参数:
* - doc: 用于存储结果的 JsonDocument 引用
*/
void buildRequestJson(JsonDocument& doc) {
// 设置模型名称
doc["model"] = ai_model;
// 创建 messages 数组
JsonArray messages = doc.createNestedArray("messages");
// 将所有历史消息添加到数组
for (int i = 0; i < messageCount; i++) {
JsonObject msg = messages.createNestedObject();
msg["role"] = chatHistory[i].role;
msg["content"] = chatHistory[i].content;
}
}
// ==================== 打印对话历史 ====================
/*
* 在串口监视器上显示当前的对话历史
* 用于调试和学习观察
*/
void printChatHistory() {
Serial.println("\n========== 当前对话历史 ==========");
Serial.println("共 " + String(messageCount) + " 条消息(最多保留 " + String(MAX_CONTEXT_ROUNDS) + " 轮对话):");
Serial.println();
for (int i = 0; i < messageCount; i++) {
String roleDisplay = chatHistory[i].role;
// 美化角色显示
if (roleDisplay == "system") roleDisplay = "[系统设定]";
else if (roleDisplay == "user") roleDisplay = "[用户]";
else if (roleDisplay == "assistant") roleDisplay = "[AI助手]";
Serial.println(roleDisplay + ": " + chatHistory[i].content);
}
// 计算当前轮数
int currentRounds = (messageCount - 1) / 2;
Serial.println("\n当前对话轮数: " + String(currentRounds) + " / " + String(MAX_CONTEXT_ROUNDS));
Serial.println("==================================");
}
// ==================== 发送 HTTP 请求到 AI 平台 ====================
/*
* 向 AI 平台发送 HTTPS POST 请求
* 并处理响应,提取 AI 回复内容
*
* 参数:
* - payload: JSON 格式的请求体
*/
void sendAIRequest(const String& payload) {
// 构造 Authorization 请求头
String auth = "Bearer ";
auth += ai_api_key;
Serial.println("正在向 AI 平台发送请求...");
// 初始化 HTTPS 连接
if (https.begin(client, ai_host, ai_port, ai_endpoint)) {
// 设置超时时间
https.setTimeout(30000); // 30秒请求超时
https.setConnectTimeout(15000); // 15秒连接超时
// 添加 HTTP 请求头
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", auth);
// 发送 POST 请求
int httpCode = https.POST(payload);
if (httpCode > 0) {
// 请求成功,获取响应
String response = https.getString();
// 打印响应 JSON(用于学习观察)
Serial.println("\n========== 接收到的 HTTP 响应体(JSON)==========");
// 尝试美化打印 JSON
StaticJsonDocument<JSON_CAPACITY> responseDoc;
DeserializationError error = deserializeJson(responseDoc, response);
if (!error) {
serializeJsonPretty(responseDoc, Serial);
} else {
// 如果解析失败,直接打印原始响应
Serial.println(response);
}
Serial.println("\n================================================\n");
// 检查 HTTP 状态码
if (httpCode == HTTP_CODE_OK) {
// 提取 AI 的回复内容
String aiReply = extractAIResponse(response);
if (aiReply.length() > 0) {
Serial.println("[AI助手] " + aiReply);
// 将 AI 回复添加到对话历史
addMessage("assistant", aiReply);
} else {
Serial.println("[错误] 无法从响应中提取 AI 回复");
}
} else {
Serial.println("[错误] HTTP 状态码: " + String(httpCode));
}
} else {
// 请求失败
Serial.println("[错误] HTTP 请求失败: " + String(https.errorToString(httpCode).c_str()));
}
// 结束 HTTP 会话
https.end();
} else {
Serial.println("[错误] 无法连接到 AI 服务器");
}
}
// ==================== 提取 AI 回复内容 ====================
/*
* 从 AI 平台的 JSON 响应中提取 AI 的回复内容
*
* 参数:
* - response: 服务器返回的完整 JSON 字符串
* 返回值:AI 的回复内容字符串
*/
String extractAIResponse(const String& response) {
StaticJsonDocument<JSON_CAPACITY> doc;
DeserializationError error = deserializeJson(doc, response);
if (error) {
Serial.print("[错误] JSON 解析失败: ");
Serial.println(error.c_str());
return "";
}
// 根据 OpenAI API 格式提取内容
// 路径: choices[0].message.content
JsonArray choices = doc["choices"];
if (choices.isNull() || choices.size() == 0) {
Serial.println("[错误] 响应中没有 choices 数据");
return "";
}
JsonObject message = choices[0]["message"];
if (message.isNull()) {
Serial.println("[错误] 响应中没有 message 数据");
return "";
}
const char* content = message["content"];
if (content == nullptr) {
Serial.println("[错误] 响应中没有 content 数据");
return "";
}
return String(content);
}
/*
* ====================================================================================
* 【教学附录】本程序核心概念详解
* ====================================================================================
*
* 1. Assistant Role 的作用:
* 在 OpenAI 兼容的 API 中,assistant role 用于表示 AI 助手的回复。
* 通过将 assistant 的历史回复包含在请求中,AI 能够理解对话的上下文,
* 从而回答需要依赖前文理解的问题(如"那3呢"指代前文提到的"2次方")。
*
* 2. 对话上下文管理:
* 本程序使用数组存储对话历史,采用循环缓冲区的设计思想。
* 当超过5轮时,移除最旧的一对 user-assistant 消息,保留 system 消息
* 和最近的对话记录。
*
* 3. JSON 构建过程:
* 使用 ArduinoJson 库动态构建请求体:
* {
* "model": "模型名称",
* "messages": [
* {"role": "system", "content": "系统设定"},
* {"role": "user", "content": "用户消息1"},
* {"role": "assistant", "content": "AI回复1"},
* {"role": "user", "content": "用户消息2"},
* ...
* ]
* }
*
* 4. 请求-响应流程:
* [用户输入] -> [添加到历史] -> [构建JSON] -> [HTTP POST] -> [AI服务器]
* |
* [显示给用户] <- [提取内容] <- [解析JSON] <- [HTTP响应]
*
* ====================================================================================
*/
配合以上示例程序的my_info.h代码:
/*
* my_info.h - 个人信息配置文件
*
* 功能描述:
* 本文件用于存放用户的 Wi-Fi 网络配置信息以及阿里云百炼 API 密钥。
* 请在使用前务必填入真实的配置信息,否则程序将无法正常运行。
*
* 作者:Taichi-Maker
* 作者官网:http://ai.taichi-maker.com
* 创建日期:2025年11月30日
* 版本:1.0.0
*
* 重要提示:
* - 请勿将包含真实 API 密钥的 my_info.h 文件提交到公开仓库!
* - 建议将 my_info.h 添加到 .gitignore 文件中以防止泄露敏感信息。
* - 在分享代码前,请务必将 api_key 替换为占位符或删除真实密钥。
*
* 许可证:MIT License
*
* 程序源:
* 本程序源自太极创客团队精心开发的《AI创客项目开发教程》。该教程专为热爱科技创新、热衷于动手实践
* 的创客爱好者与初学者量身打造,是一套完全免费、开源且注重实战的AIoT(人工智能物联网)入门学习资源。
*
* 通过本教程,您可以系统地掌握从项目构思、方案设计、软硬件选型,到实际搭建、系统集成与调试优化的
* 完整开发流程。
*
* 您可以通过以下链接获得更多关于本教程的详细信息:
* http://ai.taichi-maker.com/index.php/homepage/ai-tutorial/
*/
#ifndef MY_INFO_H
#define MY_INFO_H
// ==================== Wi-Fi 配置 ====================
// 请将下方的 "YourWiFiName" 替换为您的 Wi-Fi 名称(SSID)
// 注意:Wi-Fi 名称区分大小写,请确保填写正确
const char* ssid = "YourWiFiName";
// 请将下方的 "YourWiFiPassword" 替换为您的 Wi-Fi 密码
// 注意:Wi-Fi 密码区分大小写,请确保填写正确
const char* password = "YourWiFiPassword";
// ==================== AI平台 API 配置 ====================
// API 主机地址(默认使用阿里云百炼服务)
// 如需使用其他 AI 服务提供商,请根据实际情况修改此地址
const char* ai_host = "dashscope.aliyuncs.com";
// API 端口(HTTPS 标准端口 443)
// 不要修改此项,除非您明确知道要使用的端口号
const uint16_t ai_port = 443;
// API 端点路径(OpenAI 兼容格式)
// 用于指定调用 AI 大模型的 API 接口路径
const char* ai_endpoint = "/compatible-mode/v1/chat/completions";
// 请将下方的 "sk-xxxxxxxxxxxxxxxxxxxxxxxx" 替换为您的阿里云百炼 API 密钥
// 获取方式:
// 1. 访问 https://dashscope.console.aliyun.com/
// 2. 注册/登录阿里云账号
// 3. 在"API-KEY 管理"中创建或查看您的 API 密钥
// 注意:API 密钥是您的个人凭证,请勿泄露给他人!
const char* ai_api_key = "sk-xxxxxxxxxxxxxxxxxxxxxxxx";
// 请填入使用的大模型代码(默认使用阿里云Qwen Flash模型)
const char* ai_model = "qwen-flash";
#endif
程序讲解:将所学知识融会贯通
这个综合练习程序将前四节课的知识点全部串联起来。我们将以程序中的关键代码为线索,逐一讲解每个知识点是如何在实际应用中发挥作用的。
一、动态构建请求 JSON —— 从硬编码到程序化
对应课程:2-1(API 请求基础)
在 2-1 课程中,请求 JSON 是用原始字符串字面量硬编码的:
const char* ai_payload = R"rawliteral({
"model": "qwen-flash",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Hi. Who are you?"
}
],
"temperature": 0.7,
"max_tokens": 150
})rawliteral";这种方式无法在运行时修改内容。而在本程序中,我们使用 ArduinoJson 库动态构建请求 JSON:
void buildRequestJson(JsonDocument& doc) {
doc["model"] = ai_model;
JsonArray messages = doc.createNestedArray("messages");
for (int i = 0; i < messageCount; i++) {
JsonObject msg = messages.createNestedObject();
msg["role"] = chatHistory[i].role;
msg["content"] = chatHistory[i].content;
}
}关键变化:
doc["model"] = ai_model:模型名称从 my_info.h 读取,灵活可配doc.createNestedArray("messages"):动态创建 messages 数组- 循环遍历
chatHistory数组,将所有历史消息逐条添加到 messages 中
这种动态构建方式是整个程序的基础——正是因为能够动态构建 JSON,我们才能在每次请求中携带不同数量和内容的消息。
二、System Role 的动态设定 —— 让用户自定义 AI 身份
对应课程:2-2(System Role)
在 2-2 课程中,System Role 的内容是固定写在程序里的。而在本程序中,用户可以在启动时自定义 System Role:
void setupSystemRole() {
Serial.println("请输入 System Role(直接按回车使用默认:'你是一个智能助理'):");
Serial.print("> ");
// 等待用户输入
while (Serial.available() == 0) {
delay(100);
}
// 读取用户输入
systemRole = readSerialInput();
systemRole.trim(); // 去除首尾空白字符
if (systemRole.length() == 0) {
systemRole = "你是一个智能助理";
}
// 将 system 消息添加到历史记录(始终位于第一条)
chatHistory[0].role = "system";
chatHistory[0].content = systemRole;
messageCount = 1;
}这段代码体现了 2-2 课程的核心知识:
- System Role 始终位于 messages 数组的第一条:
chatHistory[0]固定存储 system 消息 - System Role 定义 AI 的行为方式:用户可以输入”你是一个数学助手”或”你是一个幽默的聊天伙伴”
- 提供默认值:如果用户直接按回车,使用默认的 System Role
你可以尝试输入不同的 System Role,观察 AI 的回答风格如何变化——这正是 2-2 课程中”角色定位”和”风格控制”的实际应用。
三、User Role 的实时输入 —— 从串口读取用户消息
对应课程:2-3(User Role)
在 2-3 课程中,User Role 的内容也是硬编码的。而在本程序中,用户通过串口监视器实时输入消息:
void loop() {
// 检查串口是否有输入
if (Serial.available() > 0) {
// 读取用户输入
String userInput = readSerialInput();
userInput.trim(); // 去除首尾空白字符
// 检查特殊命令
if (userInput.equalsIgnoreCase("reset")) {
// 清空对话历史,保留 system role
messageCount = 1; // 只保留 system 消息
Serial.println("\n[系统] 对话历史已清空,重新开始对话\n");
Serial.print("> ");
return;
}
if (userInput.equalsIgnoreCase("history")) {
// 显示当前对话历史
printChatHistory();
Serial.print("> ");
return;
}
// 忽略空输入
if (userInput.length() == 0) {
Serial.print("> ");
return;
}
// 将用户消息添加到历史记录
addMessage("user", userInput);
// ... 构建请求并发送
}
}这段代码体现了 2-3 课程的核心知识:
- User Role 承载用户的具体问题:每一条用户输入都被封装为
role: "user"的消息 - User Role 是动态变化的:每次对话的内容都不同
- 提示词质量影响回答质量:你可以尝试用 2-3 课程中学到的提示词技巧,比如提供背景信息、明确输出格式等,观察 AI 回答的差异
四、Assistant Role 与对话上下文管理 —— 多轮对话的核心
对应课程:2-4(Assistant Role)
这是本程序最核心的部分,也是 2-4 课程知识的直接应用。
4.1 将 AI 回复添加到对话历史
当 AI 返回回答后,程序将其提取并添加到对话历史中:
String aiReply = extractAIResponse(response);
if (aiReply.length() > 0) {
Serial.println("[AI助手] " + aiReply);
addMessage("assistant", aiReply);
}这就是 Assistant Role 的本质——将 AI 的回答记录下来,以便在下一次请求中携带。
4.2 对话上下文的自动管理
当对话超过 5 轮时,程序自动移除最旧的一对 user-assistant 消息:
void addMessage(const String& role, const String& content) {
if (messageCount >= MAX_CONTEXT_ROUNDS * 2 + 1) {
// 移除最旧的一对 user-assistant 消息
for (int i = 2; i < messageCount; i++) {
chatHistory[i - 1] = chatHistory[i];
}
messageCount--;
Serial.println("[系统] 对话历史已超过5轮,自动清除最早的一轮");
}
chatHistory[messageCount].role = role;
chatHistory[messageCount].content = content;
messageCount++;
}注意这里的关键细节:
- 移除时从索引 2 开始(跳过索引 0 的 system 消息和索引 1 的最旧 user 消息)
- system 消息始终保留,不会被清除
- 每次移除一对消息(user + assistant),保持对话的完整性
4.3 完整的请求构建过程
当用户发送新消息时,buildRequestJson 函数将所有历史消息(包括 system、user、assistant)打包成请求 JSON:
void buildRequestJson(JsonDocument& doc) {
// 设置模型名称
doc["model"] = ai_model;
// 创建 messages 数组
JsonArray messages = doc.createNestedArray("messages");
// 将所有历史消息添加到数组
for (int i = 0; i < messageCount; i++) {
JsonObject msg = messages.createNestedObject();
msg["role"] = chatHistory[i].role;
msg["content"] = chatHistory[i].content;
}
}经过三轮对话后,生成的 messages 数组结构如下:
"messages": [
{ "role": "system", "content": "你是一个数学助手" },
{ "role": "user", "content": "帮我计算2的2次方" },
{ "role": "assistant", "content": "2的2次方等于4。" },
{ "role": "user", "content": "那3呢" },
{ "role": "assistant", "content": "3的2次方等于9。" },
{ "role": "user", "content": "那5呢" }
]这正是 2-4 课程中讲解的消息顺序规则的实际体现:
- system 消息始终在最前面
- user 和 assistant 消息严格按时间顺序排列
- 每一轮对话包含一对 user-assistant 消息
五、从 AI 响应中提取内容 —— 解析 JSON 响应
对应课程:2-1(API 请求基础)
在 2-1 课程中,我们只是简单地打印了整个响应内容。而在本程序中,我们需要从响应 JSON 中精确提取 AI 的回复内容:
String extractAIResponse(const String& response) {
StaticJsonDocument<JSON_CAPACITY> doc;
DeserializationError error = deserializeJson(doc, response);
if (error) {
Serial.print("[错误] JSON 解析失败: ");
Serial.println(error.c_str());
return "";
}
JsonArray choices = doc["choices"];
if (choices.isNull() || choices.size() == 0) {
Serial.println("[错误] 响应中没有 choices 数据");
return "";
}
JsonObject message = choices[0]["message"];
if (message.isNull()) {
Serial.println("[错误] 响应中没有 message 数据");
return "";
}
const char* content = message["content"];
if (content == nullptr) {
Serial.println("[错误] 响应中没有 content 数据");
return "";
}
return String(content);
}提取路径: choices[0].message.content
AI 平台的响应 JSON 结构如下:
{
"choices": [
{
"message": {
"role": "assistant",
"content": "3的2次方等于9。"
}
}
]
}程序按照这个路径逐层解析,并在每一层都进行错误检查。
六、完整的请求-响应流程
将以上所有知识点串联起来,本程序的完整工作流程如下:
程序启动
1. 连接 Wi-Fi
2. 配置 HTTPS 客户端 3. 用户输入 System Role → chatHistory[0]
------------------------------
主循环
用户输入消息
1. 读取串口输入
2. 添加 user 消息到 chatHistory
3. 动态构建请求 JSON(包含所有历史消息)
4. 打印请求 JSON 到串口监视器
5. 发送 HTTPS POST 请求到 AI 平台
6. 接收响应,打印响应 JSON 到串口监视器
7. 从响应中提取 AI 回复内容
8. 添加 assistant 消息到 chatHistory
9. 等待用户下一次输入
------------------------------
如果对话超过5轮,自动清除最早的一轮(保留 system 消息) 总结
四节课的知识体系
| 课程 | 核心概念 | 关键技能 | 在综合程序中的体现 |
|---|---|---|---|
| 2-1 API 请求基础 | ESP32 与 AI 平台的通信流程 | HTTPS 请求、JSON 格式 | 动态构建请求 JSON、解析响应 JSON |
| 2-2 System Role | 通过 system 消息控制 AI 行为 | 角色定位、格式约束、风格控制 | 用户自定义 System Role |
| 2-3 User Role | 编写高质量的提示词 | 清晰具体、提供背景、明确格式 | 用户实时输入消息 |
| 2-4 Assistant Role | 通过 assistant 消息实现上下文感知 | 多轮对话的消息构建 | 自动管理对话历史、携带 AI 回复 |
三种角色的协同工作
System Role → 定义"AI 是谁、怎么回答"(始终在第一条)
User Role → 表达"用户想要什么"(每轮新增)
Assistant Role → 记录"AI 说了什么"(每轮累积,实现上下文理解)
从静态到动态的跨越
这四节课的另一个重要线索是:从静态硬编码到动态程序化的跨越。
| 阶段 | 请求 JSON 的构建方式 | 对话方式 |
|---|---|---|
| 2-1 ~ 2-3 | 原始字符串硬编码 | 固定的、一次性的 |
| 2-4 | 硬编码但包含多轮历史 | 模拟的多轮对话 |
| 2-5 综合练习 | ArduinoJson 动态构建 | 真正的交互式多轮对话 |
这种从静态到动态的跨越,正是从”理解原理”到”实际应用”的关键一步。掌握了动态构建 JSON 和管理对话上下文的能力,你就可以开发出各种基于 AI 的交互式应用了。