前言
目前chatGPT热度很高,打算利用cmake与c++实现一个简易的ChatGP3.5客户端。总共分为4部分:1,http客户端封装;2,gpt请求与响应逻辑封装;3,界面封装;4,其他依赖及总结。工程的构建全部通过cmake实现。
一 http客户端
http客户端封装其实就是封装的libcurl( github.com/curl/curl )库,libcurl库是采用c语言实现的,使用不当很容易出内存泄露,且只需要用到库里面的部分功能,因此利用RAII机制封装一下,并将其生成一个动态库,方便调用。libcurl库最好参考www.cnblogs.com/openiris/p/… 来编译,否则很容易在调用curl_easy_perform函数时报错CURLE_UNSUPPORTED_PROTOCOL,这是因为默认编译的libcurl库文件是不支持https请求的。实际过程中还要考虑发送中文和接受中文的情况,需要进行转码,否则curl_easy_perform函数虽然调用成功,但是通过curl_easy_getinfo得到错误码是400。
二 gpt请求与响应逻辑封装
ChatGPT账号申请个人感觉还是比较麻烦的,而自己搭建ChatGPT客户端只需要一个有效的open ai key就可以了,建议直接在网上买一个key。ChatGPT用到的常用json参数写成对应的结构体简化后如下,为方便实现c++对象与json的互转,在每个结构中定义一个JSON_HELPER(...),具体原理可参考网上大佬实现:github.com/ACking-you/… github.com/yaronzz/AIG… 我这里将其拿过来,使用上还是挺方便的。
struct GptMessageBody {
std::string role;
std::string content;
JSON_HELPER(role, content);
};
struct GptRequest {
std::string model;
std::vector<GptMessageBody> messages;
JSON_HELPER(model, messages);
};
struct GptResponseUser {
std::string prompt_tokens;
std::string completion_tokens;
std::string total_tokens;
JSON_HELPER(prompt_tokens, completion_tokens, total_tokens);
};
struct GptResponseBody {
unsigned int index;
GptMessageBody message;
std::string finish_reason;
JSON_HELPER(index, message, finish_reason);
};
struct GptResponse {
std::string id;
std::string object;
unsigned int created;
std::string model;
std::vector<GptResponseBody> choices;
GptResponseUser usage;
JSON_HELPER(id, object, created, model, choices, usage);
};
struct GptErrorInfo {
std::string message;
std::string type;
std::string param;
unsigned int code;
JSON_HELPER(message, type, param, code);
};
struct GptError {
GptErrorInfo error;
JSON_HELPER(error);
};
三 界面封装
本来是没打算做界面的,但既然是客户端,做一个界面还是有必要的,打算利用qt实现。由于qt实在太大,不可能将所有库都链接到工程中。其实对于一个简单qt界面,依赖qt(图左)的东西就Qt6Core,Qt6gui,Qt6Widgets三个库,plugins目录里面是qt那三个库自身所依赖的基本库,moc.exe是qt用来生成实现信号与槽机制功能的相关文件的,在CMakeLists.txt里面加上如下一行,cmake过程中会自动生成对应的moc_xxx.cpp到指定目录(图右),否则没有qt环境是无法编译通过的。
execute_process(COMMAND moc ${AIGC_INCLUDE}/GPT_client/gpt_widget.h -o
${PROJECT_SOURCE_DIR}/AIGC/source/GPT_client/moc_gpt_widget.cpp WORKING_DIRECTORY ${QT_PATH})
上面的库可以从安装目录里面拷贝或者源码编译,我采用的是qt6.5.0,需要支持c++17,编译过程中可能遇到
C1189 #error: "Qt requires a C++17 compiler, and a suitable value for __cplusplus. On MSVC, you must pass the /Zc:__cplusplus option to the compiler."的问题,需要设置set(CMAKE_CXX_FLAGS "/permissive- /Zc:__cplusplus")属性。main函数所在CMakeLists.txt简化后如下:
set(TARGET GPT_client)
execute_process(COMMAND moc ${AIGC_INCLUDE}/GPT_client/gpt_widget.h -o
${PROJECT_SOURCE_DIR}/AIGC/source/GPT_client/moc_gpt_widget.cpp WORKING_DIRECTORY ${QT_PATH})
file(GLOB HEADER "${PROJECT_SOURCE_DIR}/AIGC/include/GPT_client/*.h")
source_group("include" FILE ${HEADER})
file(GLOB_RECURSE SOURCE "${PROJECT_SOURCE_DIR}/AIGC/source/GPT_client/*.cpp")
source_group("source" FILE ${SOURCE})
set(SRC_LIST ${SOURCE} ${HEADER})
add_executable(${TARGET} ${SRC_LIST})
add_dependencies(${TARGET} http_client)
target_link_libraries(${TARGET} PRIVATE libconfigService)
target_link_libraries(${TARGET} PRIVATE liblogService)
target_link_libraries(${TARGET} PRIVATE libthreadPoolService)
target_link_libraries(${TARGET} PRIVATE libhttpClientService)
target_link_libraries(${TARGET} PRIVATE ${QT_PATH}/lib/*.lib)
INSTALL(TARGETS ${TARGET} DESTINATION ${CMAKE_INSTALL_PREFIX})
set(QT_NEED_INSTALL ${QT_PATH}/bin/Qt6Core.dll ${QT_PATH}/bin/Qt6Gui.dll ${QT_PATH}/bin/Qt6Widgets.dll)
INSTALL(FILES ${QT_NEED_INSTALL} DESTINATION ${CMAKE_INSTALL_PREFIX})
INSTALL(DIRECTORY ${QT_PATH}/plugins DESTINATION ${CMAKE_INSTALL_PREFIX})
界面及gpt相关逻辑如下。点击发送按钮后,读取lineEdit的内容,新建gpt请求任务发送给线程池,处理完后再通过qt的postEvent将结果告诉界面主线程,gpt请求流程主要包括:请求参数编码格式转换->根据格式组请求参数->通过libcurl发送到服务器->提取返回结果->编码格式转换。
QEvent::Type gptWidgetTask::m_taskFinishEvent = (QEvent::Type)QEvent::registerEventType();
void GptWidget::clickSend()
{
QString text = m_lineEdit->text();
if (text.isEmpty()) {
LOG_WARN("no text need to send!");
return;
}
m_gptWidgetTaskPtr->setRequest(text);
CThreadPool::getInstance()->pushTask(m_gptWidgetTaskPtr);
m_lineEdit->clear();
m_button->setEnabled(false);
m_textEdit->setTextColor(Qt::blue);
QDateTime dateTime = QDateTime::currentDateTime();
QString currentTime = dateTime.toString("yyyy-MM-dd hh:mm:ss : ");
m_textEdit->append(currentTime + text);
}
void gptWidgetTask::threadhandle() {
do {
if (m_content.isEmpty()) {
LOG_ERROR("resquest content is empty!");
break;
}
std::string response;
if (!m_requestGPTPtr->getRequest(m_content.toLocal8Bit().constData(), response)) {
LOG_ERROR("get request failed. resquest content: %s", m_content.toLocal8Bit().constData());
break;
}
m_reply = QString::fromLocal8Bit(QByteArray::fromRawData(response.c_str(), response.size()));
} while (0);
if (m_parentWidgetPtr)
QCoreApplication::postEvent(m_parentWidgetPtr, new QEvent(m_taskFinishEvent));
return;
}
bool RequestGPT::getRequest(const char *body, std::string &responseData) {
if (CodeFormatConvertHelper::hasChinese(body)) {
m_gptRequest.messages[0].content = CodeFormatConvertHelper::ANSIToUTF8(body);
}
else
m_gptRequest.messages[0].content = body;
std::string jsonStr;
JsonHelper::ObjectToJson(m_gptRequest, jsonStr);
HttpClient::ResponseData response;
if (!m_HttpClientPtr->getResquestToStruct(response, jsonStr)) {
LOG_ERROR("getResquestToStruct failed.");
return false;
}
JsonHelper::JsonToObject(m_gptResponse, response.response);
if (m_gptResponse.choices.empty()) {
JsonHelper::JsonToObject(m_gptError, response.response);
if (!m_gptError.error.message.empty()) {
LOG_ERROR("JsonToObject failed. error: %s", m_gptError.error.message.c_str());
}
else
LOG_ERROR("JsonToObject failed. m_gptResponse.choices is empty");
return false;
}
if (CodeFormatConvertHelper::hasChinese(m_gptResponse.choices[0].message.content.c_str()))
responseData = std::move(CodeFormatConvertHelper::UTF8ToANSI(m_gptResponse.choices[0].message.content.c_str()));
else
responseData = std::move(m_gptResponse.choices[0].message.content);
return true;
}
其他依赖及总结
其它库主要是用于获取配置,打印日志及线程相关等,这里是将要访问的url和key放到配置yaml文件里了,最后通过make install之后得到目录拿到其他机器相同平台下是可以运行的。工程除了qt外的其他部分支持跨平台编译。可以看到ChatGPT3.5的回答有很多是错误的。我自己测试是必须要通过代理网站(api.openai-proxy.com/v1/chat/com… 才能正确返回结果,且整体运行还很耗时。