ESP32 gọi API mô hình ngôn ngữ qua HTTPS: gửi câu hỏi kèm số đo cảm biến, đọc JSON, giữ an toàn
ESP32 hỏi mô hình ngôn ngữ lớn qua API kiểu Chat Completions: luồng HTTPS, JSON gửi và nhận, chương trình dùng ArduinoJson, kiểm tra trước khi điều khiển, bảo vệ khóa API và chi phí.
Mô hình ngôn ngữ lớn (loại trả lời câu hỏi, viết văn bản) cần máy chủ hàng chục GB bộ nhớ, không thể chạy trên ESP32. Nhưng ESP32 có Wi-Fi, nên có thể gửi câu hỏi tới API của một dịch vụ mô hình ngôn ngữ và đọc câu trả lời, kèm theo số đo cảm biến để câu trả lời sát với tình huống thật. Bài này giải thích luồng một lần hỏi, cấu trúc JSON gửi và nhận, chương trình ESP32 dùng HTTPS và ArduinoJson, cách giữ an toàn khi dùng câu trả lời để điều khiển thiết bị, bảo vệ khóa API và kiểm soát chi phí.
1. ESP32 hỏi mô hình ngôn ngữ như thế nào

Nhiều nhà cung cấp mô hình ngôn ngữ hỗ trợ cùng một kiểu API gọi là Chat Completions: gửi một danh sách tin nhắn dạng JSON qua HTTPS, nhận lại câu trả lời dạng JSON. Chương trình trong bài viết theo kiểu này nên dùng được với nhiều nhà cung cấp, chỉ cần đổi địa chỉ API, tên mô hình và khóa API.
2. Chuẩn bị
- Một ESP32 DevKit V1, dây USB, mạng Wi-Fi 2,4 GHz.
- Tài khoản ở một nhà cung cấp API mô hình ngôn ngữ có hỗ trợ kiểu Chat Completions: lấy địa chỉ API, tên mô hình và khóa API. Chi phí tính theo số token (đơn vị chữ) của cả câu hỏi lẫn câu trả lời; xem bảng giá, hạn mức của nhà cung cấp.
- Thư viện ArduinoJson bản 7 (cài trong Library Manager của Arduino IDE).
- Tùy chọn: một cảm biến analog hoặc biến trở ở GPIO34 để có số đo gửi kèm.
3. JSON gửi đi và nhận về

- system: hướng dẫn chung cho mô hình (trả lời ngắn, bằng tiếng Việt) kèm bối cảnh là số đo cảm biến.
- user: câu hỏi.
- max_tokens: giới hạn độ dài câu trả lời; vừa giữ chi phí, vừa để ESP32 không phải xử lý chuỗi quá dài.
- Khóa API gửi trong tiêu đề
Authorization: Bearer …, không nằm trong JSON.
4. Chương trình
Điền tên Wi-Fi, mật khẩu, địa chỉ API, tên mô hình và khóa API vào các chỗ giữ chỗ. Mở Serial Monitor ở 115200, chọn kiểu kết thúc dòng "New Line", gõ câu hỏi rồi Enter.
// PYWORLD.VN - ESP32 gọi API mô hình ngôn ngữ qua HTTPS (định dạng Chat Completions): hỏi từ Serial, kèm số đo cảm biến
// PYWORLD.VN - Địa chỉ API, tên mô hình, khóa API là chữ giữ chỗ: điền theo nhà cung cấp bạn chọn, không chia sẻ khóa
// PYWORLD.VN - Thư viện ArduinoJson 7; dán chứng chỉ gốc của máy chủ API vào CHUNG_CHI_GOC để kiểm tra kết nối an toàn
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const char *DIA_CHI_API = "https://DIA_CHI_API/v1/chat/completions";
const char *TEN_MO_HINH = "TEN_MO_HINH";
const char *KHOA_API = "KHOA_API";
const char *CHUNG_CHI_GOC = ""; // PEM của chứng chỉ gốc; để trống = không kiểm tra (chỉ dùng khi thử)
const int SO_TOKEN_TOI_DA = 200; // giới hạn độ dài câu trả lời (và chi phí)
const int CHAN_CAM_BIEN = 34; // ví dụ: một cảm biến analog hoặc biến trở
String hoiMoHinh(const String &cauHoi, const String &boiCanh) {
WiFiClientSecure ketNoi;
if (strlen(CHUNG_CHI_GOC) > 0) {
ketNoi.setCACert(CHUNG_CHI_GOC);
} else {
ketNoi.setInsecure();
}
HTTPClient http;
http.setTimeout(30000); // mô hình có thể mất vài giây mới trả lời
http.useHTTP10(true); // tránh kiểu truyền "chunked" để đọc JSON thẳng từ luồng
if (!http.begin(ketNoi, DIA_CHI_API)) return "Loi: khong mo duoc dia chi API";
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", String("Bearer ") + KHOA_API);
JsonDocument yeuCau;
yeuCau["model"] = TEN_MO_HINH;
yeuCau["max_tokens"] = SO_TOKEN_TOI_DA;
JsonArray tinNhan = yeuCau["messages"].to<JsonArray>();
JsonObject heThong = tinNhan.add<JsonObject>();
heThong["role"] = "system";
heThong["content"] = String("Bạn là trợ lý trong một thiết bị ESP32. Trả lời ngắn gọn bằng tiếng Việt, tối đa 3 câu. ") + boiCanh;
JsonObject nguoiDung = tinNhan.add<JsonObject>();
nguoiDung["role"] = "user";
nguoiDung["content"] = cauHoi;
String than;
serializeJson(yeuCau, than);
int ma = http.POST(than);
if (ma != 200) {
String loi = "Loi HTTP " + String(ma) + ": " + http.getString().substring(0, 200);
http.end();
return loi;
}
// Chỉ giữ lại choices[0].message.content để tiết kiệm RAM
JsonDocument boLoc;
boLoc["choices"][0]["message"]["content"] = true;
JsonDocument phanHoi;
DeserializationError loiJson = deserializeJson(phanHoi, http.getStream(), DeserializationOption::Filter(boLoc));
http.end();
if (loiJson) return String("Loi doc JSON: ") + loiJson.c_str();
const char *noiDung = phanHoi["choices"][0]["message"]["content"];
return noiDung ? String(noiDung) : String("Khong co noi dung tra loi");
}
void setup() {
Serial.begin(115200);
WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
Serial.print("Ket noi Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print('.');
}
Serial.println(" xong");
if (strlen(CHUNG_CHI_GOC) == 0) Serial.println("Canh bao: chua kiem tra chung chi may chu API");
Serial.println("Go cau hoi roi bam Enter");
}
void loop() {
if (!Serial.available()) return;
String cauHoi = Serial.readStringUntil('\n');
cauHoi.trim();
if (cauHoi.length() == 0) return;
uint32_t mv = analogReadMilliVolts(CHAN_CAM_BIEN);
String boiCanh = "Số đo hiện tại của cảm biến ở chân GPIO34: " + String(mv) + " mV.";
Serial.println("Hoi: " + cauHoi);
uint32_t batDau = millis();
String traLoi = hoiMoHinh(cauHoi, boiCanh);
Serial.printf("Tra loi (%lu ms): %s\n", (unsigned long)(millis() - batDau), traLoi.c_str());
}
- HTTPS và chứng chỉ: dán chứng chỉ gốc (PEM) của máy chủ API vào
CHUNG_CHI_GOCđể ESP32 kiểm tra đúng máy chủ thật; cách lấy chứng chỉ gốc có trong bài HTTPS và chứng chỉ của loạt ESP32 nâng cao. Để trống thì chương trình bỏ qua bước kiểm tra (setInsecure()) và in cảnh báo: chỉ nên dùng khi thử. useHTTP10(true)buộc máy chủ gửi phản hồi liền một khối để ArduinoJson đọc thẳng từ luồng mạng.- Bộ lọc của ArduinoJson chỉ giữ
choices[0].message.content, bỏ phần còn lại, tiết kiệm RAM. - Mỗi lần hỏi thường mất vài giây; thời gian chờ đặt 30 giây.

5. Dùng câu trả lời để điều khiển: luôn kiểm tra trước

Câu trả lời của mô hình ngôn ngữ có thể sai, có thể khác nhau giữa các lần hỏi, và có thể bị lừa bởi câu hỏi được viết khéo. Nếu muốn dùng câu trả lời để bật tắt thiết bị: yêu cầu mô hình trả về một từ khóa trong danh sách cố định, ESP32 tự kiểm tra bằng số đo thật và giới hạn cứng rồi mới thực hiện. Không để mô hình điều khiển trực tiếp thiết bị điện công suất lớn, máy móc, khóa cửa.
6. Bảo vệ khóa API và chi phí
- Không đưa mã nguồn có khóa API thật lên mạng, không chụp màn hình có khóa.
- Đặt hạn mức chi tiêu ở trang quản lý của nhà cung cấp; ESP32 lỗi lặp vô hạn có thể gửi hàng nghìn yêu cầu.
- Giới hạn số lần hỏi trong chương trình (ví dụ tối đa vài lần mỗi phút) và giữ
max_tokensnhỏ. - Với sản phẩm phát cho nhiều người: đặt khóa ở một máy chủ trung gian của bạn, ESP32 chỉ gọi máy chủ đó, không chứa khóa.
7. Lỗi hay gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Loi HTTP 401 | Khóa API sai, hết hạn | Tạo khóa mới, kiểm tra dấu cách thừa |
| Loi HTTP 404 hoặc 400 | Sai địa chỉ API, sai tên mô hình | Đối chiếu tài liệu của nhà cung cấp |
| Loi HTTP 429 | Gọi quá nhanh, hết hạn mức | Giãn khoảng cách giữa các lần hỏi; kiểm tra hạn mức |
| Loi HTTP -1 (không kết nối) | Chứng chỉ gốc không đúng máy chủ, mất Wi-Fi | Lấy lại chứng chỉ gốc; kiểm tra mạng |
| Loi doc JSON NoMemory | Phản hồi quá lớn | Giảm max_tokens; giữ bộ lọc JSON |
| Câu trả lời bị cắt giữa chừng | max_tokens quá nhỏ | Tăng vừa đủ hoặc yêu cầu trả lời ngắn hơn |
8. Xem thêm
- Kết nối Wi-Fi trên ESP32
- Các bài cùng loạt AI trên vi điều khiển: TinyML là gì (vì sao mô hình lớn không chạy trên chip), loa trợ lý AI trên ESP32-S3; bài HTTPS và chứng chỉ gốc trong loạt ESP32 nâng cao.
Xem thêm