admin管理员组

文章数量:1531793

乐鑫 ESP32-ChatGLM 大模型自定义对话

使用乐鑫 ESP32 平台来享受单片机上的开放的大语言模型 ChatGLM


ESP32 烧入 ChatGLM-4 项目

使用官方的异步调用方式来请求其 APIChatGLM API 可从以下网站获取:

API 获取地址:https://open.bigmodel/

主页如下(官方可能会进行更新或更改):

😲 请按照以下步骤操作:

步骤 1

下载 Arduino IDE 并安装。 打开 Arduino IDE 并找到 File -> Perference。

请使用 ESP32 官方的开发板管理器地址。

步骤2

下载项目并下载库。 (内置库的话即可直接使用,否则需要找到相应的资源下载)
这些是该项目的库:

#include <Arduino.h> //内置
#include <CustomJWT.h> //从开发平台库中查找
#include <ESPAsyncWebServer.h> // 从 https://github/me-no-dev/ESPAsyncWebServer 获取
#include <ArduinoJson.h> //从开发平台库中查找
#include <WiFiClientSecure.h> //内置
#include <WiFiUdp.h> //内置
#include <time.h> //内置
#include <HTTPClient.h> //从开发平台库中查找

步骤 3 🤨

找到库文件夹并找到 CustomJWT 文件夹。 (我也上传了 CustomJWT.h 文件,直接替换成我 CustomJWT ☝️)

Windows 电脑默认应该是在:C:\Users\xxxx\Documents\Arduino\libraries\CustomJWT\src

CustomJWT.h 文件中的下面的代码不正确:
sprintf(headerJSON, "{\"alg\": \"%s\",\"typ\":\"%s\",\"sign_type\":\"%s\"}", alg、typ、sign_type);

错误: 这是 “alg”: “%s” 之间的问题,在 “alg”: 和 “%s” 之间有一个空格! 请删除它! 如果没有,它将显示不同的 Base64 编码。

修复 CustomJWT.h 文件中的代码,如下所示:
sprintf(headerJSON, "{\"alg\":\"%s\",\"typ\":\"%s\",\"sign_type\":\"%s\"}", alg、typ、sign_type);

步骤4

ChatGLM.inoindex.h 项目放入您自己的项目文件夹中,文件夹名称为:ChatGLM(⚠️这里文件夹名称需要和 *.ino 文件保持一致)。你可以更改您的个人 请求APIWiFI ,甚至可以更改 system_role 的角色内容以及 NTP 网络时间服务器,甚至您可以为index.h 文件

步骤 5

通过串口和电脑的 USB 连接 ESP32 模块。 请选择正确的 ESP32 板,我的是 ESP32-S3 的单片机

终于可以愉快烧录你的 ESP32 设备了! 😄🥇


📔 代码详情

Github ESP32-ChatGLM 详细代码

Gitee ESP32-ChatGLM 详细代码

主程序:
#include <Arduino.h>            //build in
#include <CustomJWT.h>          //find from library
#include <ESPAsyncWebServer.h>  // Get from https://github/me-no-dev/ESPAsyncWebServer
#include <ArduinoJson.h>        //find from library
#include <WiFiClientSecure.h>   //build in
#include <WiFiUdp.h>            //build in
#include <HTTPClient.h>         //find from library
#include <NTPClient.h>          //find from library

#include "chatconfig.h"
#include "index.h"
#include "async_invoke.h"
#include "sync_invoke.h"

char header[50];
char payload[500];
char signature[100];
char out[500];
char jsonString[500];

char idCharArray[150];
char secretCharArray[100];


String invokeChoice = "Async_invoke";

//String invokeChoice = "Sync_invoke";

String LLM_Model = "glm-4";

String JsonToken, responseMessage, userMessage;
HTTPClient http, http_id;

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp.aliyun", 0, 30000);

AsyncWebServer server(9898);  // Web Page IP:9898
DynamicJsonDocument doc(20480);

long long int timestamp_generation() {
  if (wifiConnect) {
    timeClient.update();
    long long int timestamp_generation = timeClient.getEpochTime() * 1000ULL;  //get Timestamp
    return timestamp_generation;
  }
}

void splitApiKey(const char *apikey) {
  const char *delimiter = strchr(apikey, '.');

  if (delimiter != NULL) {
    size_t idLength = delimiter - apikey;

    if (idLength < sizeof(idCharArray)) {
      strncpy(idCharArray, apikey, idLength);
      idCharArray[idLength] = '\0';
      strcpy(secretCharArray, delimiter + 1);
      snprintf(jsonString, sizeof(jsonString), "{\"api_key\":\"%s\",\"exp\":%lld,\"timestamp\":%lld}", idCharArray, static_cast<long long>(timestamp_generation()) * 3, static_cast<long long>(timestamp_generation()));
      CustomJWT jwt(secretCharArray, header, sizeof(header), payload, sizeof(payload), signature, sizeof(signature), out, sizeof(out));
      jwt.encodeJWT(jsonString);
      JsonToken = jwt.out;

      Serial.println(JsonToken);  //Debug

      jwt.clear();
    } else {
      Serial.println("ID part of API key is not valid.");
    }
  } else {
    Serial.println("Invalid API key format.");
  }
}

int tryWiFiConnection(const char *ssid, const char *identity, const char *password, int networkNumber) {
  Serial.printf("Connecting to WiFi_%d...\n", networkNumber);

  int attempts = 0;
  const int maxAttempts = 10;

  while (attempts < maxAttempts) {
    if (strcmp(identity, "none") == 0) {
      WiFi.begin(ssid, password);
    } else {
      WiFi.begin(ssid, WPA2_AUTH_PEAP, identity, identity, password);  //WPA2_ENTERPRISE | Eduroam calling
    }

    int connectionAttempt = 0;
    while (connectionAttempt < 4) {
      if (WiFi.status() == WL_CONNECTED) {
        Serial.printf("Connected to WiFi_%d.\n", networkNumber);
        return networkNumber;
      }
      delay(1000);
      connectionAttempt++;
    }

    Serial.printf("Retry WiFi%d connection...\n", networkNumber);
    WiFi.disconnect();
    attempts++;
  }

  Serial.printf("WiFi%d connection failed.\n", networkNumber);
  return -1;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  for (int networkNumber = 0; networkNumber < 3 && !wifiConnect; networkNumber++) {
    Serial.printf("Connecting to WiFi_%d...\n", networkNumber + 1);

    int successfulConnection = tryWiFiConnection(ssidList[networkNumber], identityList[networkNumber], passwordList[networkNumber], networkNumber + 1);

    if (successfulConnection != -1 && !wifiConnect) {
      Serial.printf("Connected to WiFi_%d\n", successfulConnection);
      Serial.print("The Internet IP: ");
      Serial.println(WiFi.localIP());

      if (timestamp_generation > 0) {
        splitApiKey(setApiKey);

        if (invokeChoice == "Async_invoke") {
          asyncMessage(server, http_id, doc, JsonToken, responseMessage, userMessage, checkEmpty);  //Async
        } else if (invokeChoice == "Sync_invoke") {
          syncMessage(server, responseMessage, userMessage, checkEmpty);  //Sync
        }
        wifiConnect = true;
      } else {
        Serial.println(F("Failed to obtain Beijing time"));
      }
    }
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Failed to connect to any WiFi network.");
  }
}

void loop() {
  if (wifiConnect && WiFi.status() == WL_CONNECTED) {
    if (invokeChoice == "Async_invoke") {
      loopingSetting(http, LLM_Model, doc, JsonToken, userMessage, invokeChoice, checkEmpty);  //Async
    } else if (invokeChoice == "Sync_invoke") {
      loopingSetting(http, LLM_Model, JsonToken, responseMessage, userMessage, invokeChoice, checkEmpty);  //Sync
    }
  }
  delay(100);
}
异步请求调用:
void asyncMessage(AsyncWebServer &server, HTTPClient &http_id, DynamicJsonDocument &doc, String &JsonToken, String &responseMessage, String &userMessage, bool &checkEmpty) {

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(200, "text/html", html);
  });

  server.begin();

  server.on("/send", HTTP_GET, [&responseMessage, &userMessage, &checkEmpty](AsyncWebServerRequest *request) {
    responseMessage.clear();
    userMessage = request->getParam("message")->value();

    if (userMessage.length() > 0) {
      checkEmpty = true;
      Serial.print(F("My Question is: "));
      Serial.print(userMessage);
      Serial.println();
    }
    request->send(200, "text/plain", "Message sent to Serial");
  });


  server.on("/receiveTextMessage", HTTP_GET, [&http_id, &doc, &JsonToken, &responseMessage](AsyncWebServerRequest *request) {
    const char *getMessage = doc["id"];
    String web_search_id = "https://open.bigmodel/api/paas/v4/async-result/" + String(getMessage);  //GET Method(+below)

    http_id.begin(web_search_id);
    http_id.addHeader("Accept", "application/json");
    http_id.addHeader("Content-Type", "application/json; charset=UTF-8");
    http_id.addHeader("Authorization", JsonToken);
    int httpResponseIDCode = http_id.GET();
    if (httpResponseIDCode > 0) {
      responseMessage = http_id.getString();
    } else {
      responseMessage = "Error: External API request failed!";
    }
    request->send(200, "text/html", responseMessage);
  });
}

void loopingSetting(HTTPClient &http, String &LLM, DynamicJsonDocument &doc, String &JsonToken, String &userMessage, String &invokeChoice, bool &checkEmpty) {
  const char *async_web_hook = "https://open.bigmodel/api/paas/v4/async/chat/completions";  //New ChatGLM4 async

  if (invokeChoice == "Async_invoke") {
    if (checkEmpty) {
      int maxRetries = 5;  // 最大重试次数
      int retryCount = 0;

      while (retryCount < maxRetries) {
        http.begin(async_web_hook);
        http.addHeader("Accept", "application/json");
        http.addHeader("Content-Type", "application/json; charset=UTF-8");
        http.addHeader("Authorization", JsonToken);

        String payloadMessage = "{\"model\":\"" + LLM + "\", \"messages\":[{\"role\":\"system\",\"content\":\"" + String(system_role) + "\"},{\"role\":\"user\",\"content\":\"" + userMessage + "\"}]}";

        int httpResponseCode = http.POST(payloadMessage);

        //Serial.println(httpResponseCode);  //Debug

        if (httpResponseCode > 0) {
          String messages = http.getString();

          //Serial.println(messages);  //debug

          DeserializationError error = deserializeJson(doc, messages);
          if (error) {
            Serial.print(F("JSON parsing failed: "));
            Serial.println(F(error.c_str()));
          }
          break;
        } else if (httpResponseCode == -2) {
          retryCount++;
          delay(500);  // 可以根据需求调整重试间隔
        }
      }
    }
    http.end();
    checkEmpty = false;
  }
}
同步请求调用:
void syncMessage(AsyncWebServer &server, String &responseMessage, String &userMessage, bool &checkEmpty) {

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(200, "text/html", html);
  });

  server.begin();

  server.on("/send", HTTP_GET, [&responseMessage, &userMessage, &checkEmpty](AsyncWebServerRequest *request) {
    responseMessage.clear();

    userMessage = request->getParam("message")->value();

    if (userMessage.length() > 0) {
      checkEmpty = true;
      Serial.print(F("My Question is: "));
      Serial.print(userMessage);
      Serial.println();
      request->send(200, "text/plain", "Message sent to Serial");
    }
  });

  server.on("/receiveTextMessage", HTTP_GET, [&responseMessage](AsyncWebServerRequest *request) {
    if (responseMessage.length() > 0) {
      request->send(200, "text/html", responseMessage);
    } else {
      request->send(400, "text/html", "Message error! Please Check Token or Others!");
    }
  });
}

void loopingSetting(HTTPClient &http, String &LLM, String &JsonToken, String &responseMessage, String &userMessage, String &invokeChoice, bool &checkEmpty) {
  const char *sync_web_hook = "https://open.bigmodel/api/paas/v4/chat/completions";

  if (invokeChoice == "Sync_invoke") {
    if (checkEmpty) {
      int maxRetries = 3;
      int retryCount = 0;

      while (retryCount < maxRetries) {
        http.begin(sync_web_hook);
        http.setTimeout(30000);
        http.addHeader("Accept", "application/json");
        http.addHeader("Content-Type", "application/json; charset=UTF-8");
        http.addHeader("Authorization", JsonToken);

        String payloadMessage = "{\"model\":\"" + LLM + "\", \"messages\":[{\"role\":\"system\",\"content\":\"" + String(system_role) + "\"},{\"role\":\"user\",\"content\":\"" + userMessage + "\"}],\"stream\":false}";

        int httpResponseCode = http.POST(payloadMessage);
        responseMessage.clear();

        if (httpResponseCode > 0) {
          responseMessage = http.getString();
          //Serial.println(responseMessage);  //debug
          break;
        } else {
          retryCount++;
          Serial.print(F("HTTP POST request failed, error: "));
          Serial.println(httpResponseCode);
          delay(500);
        }
      }
    }
    http.end();
    checkEmpty = false;
  }
}

✏️ 未来新增功能

  1. 可能会添加流式请求(SSE)的调用方法
  2. 添加上下文的理解功能
  3. 修改已知或未知 BUG
  4. 优化代码,减少性能损耗

其他问题:

目前最新支持 ChatGLM-4 的,默认集成了 AI 角色扮演的内容,这一个版本去掉了 0.0.2 版本的SSE请求。由于官方给的文档是大范围修改,将流式传输和同步请求的基本上合二为一,这里我就留下了同步请求,如果您还有其他问题,可以在 Github / Gitee 发起 讨论(Discussion) 或者发起 Issue, 同时也可以 fork 本项目,感谢对本项目的支持,感恩 ☺️!


本文标签: 自定义模型乐鑫ChatGLM