據(jù)結(jié)構(gòu)設(shè)計 日志庫封裝 Provider分析與實現(xiàn))
歡迎來到實戰(zhàn)項目專欄 ~~ 從零實現(xiàn)AI大模型接入SDK博客主頁張小姐的貓~江湖背景所屬專欄C項目 ~ AI大模型接入SDK作者水平很有限如果發(fā)現(xiàn)錯誤可在評論區(qū)指正感謝AI大模型接入SDK歡迎來到實戰(zhàn)項目專欄 ~~ 從零實現(xiàn)AI大模型接入SDK數(shù)據(jù)結(jié)構(gòu)設(shè)計消息結(jié)構(gòu)模型的公共配置信息 接入方式模型信息 會話信息日志庫概念與封裝Provider分析與實現(xiàn)策略模式LLMProvider寫在最后數(shù)據(jù)結(jié)構(gòu)設(shè)計雖然各個模型不同但有?些公共的配置和描述信息比如通過api調(diào)用模型時需要模型名稱、溫度值、最大tokens數(shù)、apikey等在和模型聊天時聊天信息需要管理每次開啟和模型的新一輪對話都是一次新的會話將來可能需要實現(xiàn)會話管理。這些數(shù)據(jù)在多個文件中都會用到因此提前先將這些數(shù)據(jù)結(jié)構(gòu)定義好以方便后續(xù)使用消息結(jié)構(gòu)消息結(jié)構(gòu)的構(gòu)造方法只需要給定兩個參數(shù)即可消息ID和時間戳是我們在進行創(chuàng)建的時候才會進行填充的而消息ID通常由系統(tǒng)/會話管理器自動生成不應(yīng)由調(diào)用方手填_timestamp一般是「創(chuàng)建時取當(dāng)前時間」也不該讓調(diào)用方傳入。創(chuàng)建一條「已知內(nèi)容」的用戶消息//消息結(jié)構(gòu)structMessage{std::string _messageId;//消息IDstd::string _role;//角色std::string _content;//消息內(nèi)容std::time_t _timestamp;//時間戳//構(gòu)造函數(shù)Message()default;Message(conststd::stringrole,conststd::stringcontent):_role(role),_content(content){}};模型的公共配置信息 接入方式接入方式有兩種通過API方式接入云端模型——繼承Config通過ollma方式接入本地模型 - 不需要api配置//模型的公共配置信息structConfig{std::string _modelName;//模型名稱double_temperature0.7;//溫度參數(shù),用來控制模型的輸出隨機性int_maxTokens2048;//最大輸出token數(shù)};//通過API方式接入云端模型structApiConfig:publicConfig{std::string _apiKey;//API密鑰};//通過ollma方式接入本地模型 - 不需要api配置模型信息 會話信息//LLM模型信息structLLMInfo{std::string _modelName;//模型名稱std::string _modelDesc;//模型描述std::string _provider;//模型提供方std::string _endpoint;//模型endpoint base urlbool_isAvailablefalse;//模型是否有效//構(gòu)造函數(shù)LLMInfo()default;LLMInfo(conststd::stringmodelName,conststd::stringmodelDesc,conststd::stringprovider,conststd::stringendpoint):_modelName(modelName),_modelDesc(modelDesc),_provider(provider),_endpoint(endpoint){}};//會話信息structSession{std::string _sessionId;//會話IDstd::string _modelName;//模型名稱std::vectorMessage_messages;//會話中的消息列表std::time_t _updatedAt;//會話最后更新時間戳std::time_t _createdAt;//會話創(chuàng)建時間戳//構(gòu)造函數(shù)Session()default;Session(conststd::stringmodelName)//創(chuàng)建時間是要我們創(chuàng)建會話時填入:_modelName(modelName){}};日志庫概念與封裝C中可以通過cout將信息打印到控制臺為什么還要封裝日志庫呢日志級別管理日志格式化日志存儲管理線程安全性能優(yōu)勢因此本項目采用google的spdlog日志庫進行日志管理為了使用方便對spdlog庫采用單例模式進行簡單封裝myLog.h日志類的頭文件fmt庫的使用方式hello,{}, world10s表示左對齊寬度為104d表示右對齊寬度為4VA_ARGS后續(xù)要拼接上的參數(shù)FILE會自動獲取當(dāng)前文件名LINE會自動獲取當(dāng)前行號二者都是宏#pragmaonce#includemutex#includespdlog/spdlog.hnamespacebite{classLogger{public://spdlog::level::level_enum 枚舉類型用于設(shè)置日志級別默認(rèn)是debugstaticvoidinitLogger(conststd::stringloggerName,conststd::stringloggerFile,spdlog::level::level_enum levelspdlog::level::level_enum::info);staticstd::shared_ptrspdlog::loggergetLogger();private:Logger();Logger(constLogger)delete;//拷貝構(gòu)造函數(shù)被禁用Loggeroperator(constLogger)delete;//賦值運算符被禁用private:staticstd::shared_ptrspdlog::logger_logger;staticstd::mutex _mutex;};//fmt庫 使用方式hello,{}, world// {:10} 表示右對齊寬度10文件名{:4} 表示左對齊寬度4行號// __FILE__ 會自動獲取當(dāng)前文件名, __LINE__ 會自動獲取當(dāng)前行號// ##__VA_ARGS__ 是GNU擴展可變參數(shù)為空時自動吞掉前面的逗號#defineTRACE(formar,...)Logger::getLogger()-trace(std::string([{:10s}:{:4d}]formar),__FILE__,__LINE__,##__VA_ARGS__)#defineDBG(formar,...)Logger::getLogger()-debug(std::string([{:10s}:{:4d}]formar),__FILE__,__LINE__,##__VA_ARGS__)//##__VA_ARGS__:其他的可變參數(shù)#defineINFO(formar,...)Logger::getLogger()-info(std::string([{:10s}:{:4d}]formar),__FILE__,__LINE__,##__VA_ARGS__)#defineERR(formar,...)Logger::getLogger()-error(std::string([{:10s}:{:4d}]formar),__FILE__,__LINE__,##__VA_ARGS__)#defineWARN(formar,...)Logger::getLogger()-warn(std::string([{:10s}:{:4d}]formar),__FILE__,__LINE__,##__VA_ARGS__)#defineCRITICAL(formar,...)Logger::getLogger()-trace(std::string([{:10s}:{:4d}]formar),__FILE__,__LINE__,##__VA_ARGS__)}//end bite那么后續(xù)我們?nèi)绾问褂眠@個DBG來打印出一串日志呢DBG(Database open successfully: {},localhost)預(yù)處理器做參數(shù)匹配formar ←Database open successfully: {}...可變參數(shù)←localhost即 __VA_ARGS__localhost替換進myLog.h的宏體得到Logger::getLogger()-debug(std::string([{:10s}:{:4d}]Database open successfully: {}),__FILE__,// 預(yù)處理器替換為當(dāng)前源文件名如 /home/lbw/.../main.cpp__LINE__,// 預(yù)處理器替換為當(dāng)前行號如 42localhost);交給spdlog輸出套上 myLog.cpp 的pattern時間戳、級別后最終得到[2026:09:0115:30:12][debug][main.cpp:42]Database open successfully:localhostmyLog.cpp日志類的實現(xiàn)#include../include/util/myLog.h#includespdlog/spdlog.h#includespdlog/sinks/stdout_color_sinks.h#includespdlog/sinks/basic_file_sink.h#includespdlog/async.hnamespacebite{std::shared_ptrspdlog::loggerLogger::_loggernullptr;std::mutex Logger::_mutex;Logger::Logger(){}voidLogger::initLogger(conststd::stringloggerName,conststd::stringloggerFile,spdlog::level::level_enum logLevel){std::lock_guardstd::mutexlock(Logger::_mutex);//出了作用域就解鎖if(_loggernullptr){//設(shè)置全局自動刷新級別當(dāng)日志等級達到指定級別時自動刷新日志文件spdlog::flush_on(logLevel);// 啟動異步日志將日志存放在隊列里由后臺線程負(fù)責(zé)寫入文件//參數(shù)1隊列大小參數(shù)2線程數(shù)spdlog::init_thread_pool(32768,1);if(stdoutloggerFile){//創(chuàng)建一個帶顏色的輸出到控制臺的日志記錄器_loggerspdlog::stdout_color(loggerName);}else{//創(chuàng)建一個文件日志記錄器將日志寫入指定文件_loggerspdlog::basic_logger_mtspdlog::async_factory(loggerName,loggerFile);}}//格式設(shè)置//[%H:%M:%S] 時分秒//[%n] 日志記錄器名稱//[%-7l] 日志等級左對齊寬度為7//%v 日志消息_logger-set_pattern([%H:%M:%S] [%n][%-7l] %v);_logger-set_level(logLevel);}std::shared_ptrspdlog::loggerLogger::getLogger(){return_logger;}}Provider分析與實現(xiàn)策略模式假設(shè)你現(xiàn)在要從宿舍去學(xué)校圖書館但宿舍到圖書館之間有?段距離你可以采用下面三種方法去實現(xiàn)策略方式實現(xiàn)定義?個接口TransportStrategy出行策略分別實現(xiàn)WalkStrategy、BikeStrategy、TaxiStrategy在運行時你可以隨時切換策略classTransportStrategy{public:virtualvoidgo()0;};classWalkStrategy:publicTransportStrategy{public:virtualvoidgo()override{cout?路去機房;}};classBikeStrategy:publicTransportStrategy{public:virtualvoidgo()override{cout騎?去機房;}};classBusStrategy:publicTransportStrategy{public:virtualvoidgo()override{cout打?去機房;}};classStudent{private:TransportStrategy*strategy;public:voidsetStrategy(TransportStrategy*s){strategys;}voidgoToLab(){strategy-go();}};intmain(){Student me;me.setStrategy(newWalkStrategy());me.goToLab();// 輸出: ?路去機房me.setStrategy(newBusStrategy());me.goToLab();// 輸出: 打?去機房return0;}程序非常美觀且靈活在使用時只需和TransportStrategy打交道不需要知道背后到底是WalkStrategy、BikeStrategy或BusStrategy。如果想更換模式只需要更換?個具體的策略對象即可程序基本不需要改動。策略模式是設(shè)計模式的?種它的核心思想是它定義了一些列算法將每一個算法(或行為)封裝起來使它們可以相互替換而不用再代碼中寫一堆if-else/switch來決定用哪個算法。即把“做事的方式”抽象出來運行時根據(jù)需要選擇哪種方式去執(zhí)行。LLMProvider#pragmaonce#includemap#includestring#includevector#includefunctionnamespaceai_chat_sdk{classLLMProvider{public://初始化模型virtualboolinitModel(conststd::mapstd::string,std::stringconfig)0;//檢查模型是否可用virtualboolisAvailable()const0;//獲取模型名稱virtualstd::stringgetModelName()const0;//獲取模型描述virtualstd::stringgetModelDesc()const0;//發(fā)送消息 —— 全量返回virtualvoidsendMessage(conststd::vectorMessagemessages,conststd::mapstd::string,std::stringrequestParam)0;//發(fā)送消息 —— 增量返回, 流式返回//messages: 消息列表//requestParam: 請求參數(shù)//callback: 對模型返回的增量內(nèi)容如何進行處理第一個為增量數(shù)據(jù)第二個為是否是最后一個增量virtualvoidsendMessageStream(conststd::vectorMessagemessages,conststd::mapstd::string,std::stringrequestParam,std::functionvoid(conststd::string,bool)callback)0;};protected:boolis_available_false;//標(biāo)記模型是否可用std::string _apikey;//模型的API密鑰std::string _endpoint;//模型的 base url}先實現(xiàn)這個LLMProvider的抽象類后續(xù)再用具體的大模型比如DeepSeek來繼承它即可寫在最后接下來登場的是deepseek接入封裝