Chatsdk模型接口的设计

设计思路

采用多态继承 的思想,采用策略模式,定义一个基类LLMProvider,后续会借助API的方式接入DeepSeek、ChatGPT、Gemini等大模型,每个大模型将来都需要:

a. 初始化

b. 检测模型是否有效

c. 发送消息给模型(1.全量方式 2.增量方式)

d. 获取模型名称

e. 获取模型描述(1.标记模型是否初始化成功字段 2.模型的apikey 3.模型的描述信息 4.模型的名称)

f. 保存模型的有效状态、API Key、模型描述。

操作基本都是相同的,只是实现细节上稍微不同,因此借助策略模式将接⼊模块架构设计如下:

策略模式

策略模式是一种行为设计模式,它将一系列算法封装成独立的可互换对象,让算法的使用与其实现解耦,使得算法可以独立于使用它的客户端而变化。其核心是通过定义共同的策略接口,将每个具体算法实现为单独的类,客户端通过接口调用算法,从而在不修改代码的情况下动态切换算法。

通俗比喻:就像出行时选择不同的交通工具(策略)------开车、骑车、步行,每种方式独立封装,你可以根据情况随时切换,而无需改变"出行"这个行为本身。

cpp 复制代码
class TransportStrategy {
	public:
		virtual void go() = 0;
};
class WalkStrategy : public TransportStrategy {
	public:
		virtual void go() override { cout << "⾛路去机房🚶"; }
};
class BikeStrategy : public TransportStrategy {
	public:
		virtual void go() override { cout << "骑⻋去机房🚴"; }
};
class BusStrategy : public TransportStrategy {
	public:
		virtual void go() override { cout << "打⻋去机房🚕"; }
};
class Student {
private:
		TransportStrategy* strategy;
public:
		void setStrategy(TransportStrategy* s) { strategy = s; }
		void goToLab() { strategy->go(); }
};
int main(){
	Student me;
	me.setStrategy(new WalkStrategy());
	me.goToLab();
	// 输出: ⾛路去机房🚶
	me.setStrategy(new BusStrategy());
	me.goToLab();
	// 输出: 打⻋去机房🚕
	return 0;
}

基类LLMProvider的设计

基类指针可以指向子类的对象

本项目中利用C++多态的机制可以实现此功能,例如

cpp 复制代码
void SendMessage(LLMProvider& pro){
	pro.send();
	}

利用基类指针接收传递的参数,如果是deepseek对象就调用deepseek的方法,如果是chatgpt对象就调用chatgpt的send方法,需要注意的是在基类对象声明中要把需要实现多态的方法申明成虚函数,如下:

cpp 复制代码
//LLMProvider.h
#include "common.h"


namespace ai_chat_sdk{
    // LLMProvider 类
class LLMProvider{
public:
   // 初始化模型
   virtual bool initModel(const std::map<std::string, std::string>& modelConfig) = 0;
        // 检测模型是否有效
    virtual bool isAvailable() const = 0;
        // 获取模型名称
    virtual std::string getModelName() const = 0;
        // 获取模型描述
    virtual std::string getModelDesc() const = 0;
        // 发送消息 - 全量返回
    virtual std::string sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam) = 0;
        // 发送消息 - 增量返回 - 流式响应
    virtual std::string sendMessageStream(const std::vector<Message>& messages, 
                                              const std::map<std::string, std::string>& requestParam,
                                              std::function<void(const std::string&, bool)> callback) = 0;   // callback: 对模型返回的增量数据如何处理,第一个参数为增量数据,第二个参数为是否为最后一个增量数据

protected:
    bool _isAvailable = false;      // 标记模型是否有效
    std::string _apiKey;           // API密钥
    std::string _endpoint;         // 模型API endpoint  base url
    };
}

LLM作为抽象类,因此不应该实例化,利用c++语法把所有方法设置为接口(抽象方法)

具体Provider的设计以DeepseekProvider为例

无状态服务API

无状态服务原则:DeepSeek的API基于无状态设计,每次请求视为独立会话。若需维护对话连续性,必须由客户端主动管理并传递完整上下文 。这与HTTP协议的无状态特性⼀致。

系统提示:若需保持角色设定,如始终以专家身份回答,每次请求必须包含系统级指令。

对话历史:模型仅处理当前请求中的上下文,无法关联前序对话。

第一次告诉自己的名字

第二次问就不记得了

由此可见deepseek提供的api是基于http无状态服务的 ,因此在给模型发消息时需要带上历史的聊天记录

发送消息方法的实现(全量方式)

cpp 复制代码
//由于要带上历史消息,因此需采用vector存储之前的消息
std::string DeepSeekProvider::sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam)
    {
        // 1. 检测模型是否可用
        if(!isAvailable()){
            ERR("DeepSeekProvider sendMessage model not available");
            return "";
        }

        // 2. 构造请求参数
        double temperature = 0.7;
        int maxTokens = 2048;
        if(requestParam.find("temperature") != requestParam.end()){
            temperature = std::stod(requestParam.at("temperature"));
        }
        if(requestParam.find("max_tokens") != requestParam.end()){
            maxTokens = std::stoi(requestParam.at("max_tokens"));
        }

        // 构造历史消息
        Json::Value messageArray(Json::arrayValue);
        for(const auto& message : messages){
            Json::Value messageObject;
            messageObject["role"] = message._role;
            messageObject["content"] = message._content;
            messageArray.append(messageObject);
        }

        // 3. 构造请求体
        Json::Value requestBody;
        requestBody["model"] = getModelName();
        requestBody["messages"] = messageArray;
        requestBody["temperature"] = temperature;
        requestBody["max_tokens"] = maxTokens;

        // 4. 序列化
        Json::StreamWriterBuilder writerBuilder;
        writerBuilder["indentation"] = "";
        std::string requestBodyStr = Json::writeString(writerBuilder, requestBody);
        INFO("DeepSeekProvider sendMessage requestBody: {}", requestBodyStr);

        // 5. 使用cpp-httplib库构造HTTP客户端
        httplib::Client client(_endpoint.c_str());
        client.set_connection_timeout(30, 0);     // 连接超时时间为30秒
        client.set_read_timeout(60, 0);           // 设置超时时间为60秒

        // 设置请求头
        httplib::Headers headers = {
            {"Authorization", "Bearer " + _apiKey},
            {"Content-Type", "application/json"}
        };

        // 6. 发送POST请求
        auto response = client.Post("/v1/chat/completions", headers, requestBodyStr, "application/json");
        if(!response){
            ERR("DeepSeekProvider sendMessage POST request failed");
            return "";
        }
        INFO("DeepSeekProvider sendMessage POST request success, status : {}", response->status);
        INFO("DeepSeekProvider sendMessage POST request success, body : {}", response->body);

        // 检测响应是否成功
        if(response->status != 200){
            return "";
        }

        // 7. 解析响应体
        Json::Value responseBody;
        Json::CharReaderBuilder readerBuilder;
        std::string parseError;
        std::istringstream responseStream(response->body);
        if(Json::parseFromStream(readerBuilder, responseStream, &responseBody, &parseError)){
            // 获取message数组
            if(responseBody.isMember("choices") && responseBody["choices"].isArray() && !responseBody["choices"].empty()){
                auto choice = responseBody["choices"][0];
                if(choice.isMember("message") && choice["message"].isMember("content")){
                    std::string replyContent = choice["message"]["content"].asString();
                    INFO("DeepSeekProvider response text: {}", replyContent);
                    return replyContent;
                }
            }
        }

        // 8. json解析失败
        ERR("DeepSeekProvider sendMessage POST response body parse failed, error");
        return "deepseek response json parse failed";
    }

流式响应

HTTP协议是严格的"请求-响应"模型,永远是客户端发起请求,服务器才能响应,服务器就像个"哑巴",它知道更多内容,但是它无法主动告诉你。这种⼀问⼀答的模式对于⼤部分网页浏览器、数据提交等场景已经足够了。

但是有些场景下,服务器需要主动向客户端推送⼀些实时数据,⽐如,在看体育直播时,服务器要及时将⽐赛分数、⾦球球员等信息推送给客户端;在多⼈在线游戏中,服务器需要实时同步玩家的操作和游戏状态;在使⽤导航类应⽤时,服务器需要实时推动导航信息等。

⼤佬们也发现这个问题了,在2004年的时候Ian Hickson就提出了SSE概念,Opera浏览器是第⼀个⽀持SSE的,2011年开始,⼀些主流浏览器(Chrome、Firefox、Safari)开始逐步⽀持SSE,2015年时SSE规范才正式成为W3C的标准。

SSE协议

SSE是Server Send Event的缩写,即服务器发送事件,是建⽴在HTTP协议之上的开发标准,允许服务器主动向客户端(如浏览器)推送实时数据。

SSE通过单⼀的持久连接实现数据的实时传输,客户端⽆需频繁发起请求。

SSE协议特点

  • 单向通信:服务器可以主动推送数据到客⼾端,但客户端⽆法直接通过SSE向服务器发送数据
  • 基于HTTP协议:SSE使用标准的HTTP协议,无需额外的协议或端口配置,兼容性好易于实现
  • 轻量级:SSE的实现更简单,代码量少,适合简单的实时数据推送场景
  • ⾃动重连:如果连接断开,浏览器会自动尝试重新连接,无需开发者手动处理重连逻辑
  • ⽀持事件类型:服务器可以发送不同类型事件,客户端可以根据事件类型执行不同的操作
  • ⽀持消息ID:每条消息可以包含⼀个唯⼀的ID,用于断线重连后恢复消息流

每条消息以两个换行符 (\n\n) 结束,消息流传输完毕后会有专门的结束标记,不同实现结束标记不同,比如data: [DONE]。前⾯我们演⽰向DeepSeek、ChatGPT、Gemini等⼤模型提问时,这些⼤模型并不是⼀次性将完整回答丢给用户,而是服务器边思考,边主动将思考结果吐(推送)给用户的,就和打字⼀样⼀点点输出,用户不需要⻓时间的等待,能及时看到服务器响应的结果,体验⽐较好,这种⽅式称为流式响应。SSE推出后实际不温不⽕,⼤模型爆⽕后,正式⼤模型场景的需要,SSE协议就爆⽕了。

WebSocket协议

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

与sse协议的区别

为什么DeepSeek的助手消息使⽤SSE,不使用websocket?

答:⼤模型的回复是服务器向客户端推送数据的单项数据流,在此期间客户端不需要给⼤模型服务器发送消息,而SSE刚好是服务器主动单项给客户端推送数据,并且实现简单⾼效,因此⼤模型回复通常都使用SSE协议。

普通HTTP响应体中,⼀个响应包含⼀个响应头和⼀个响应体,

在HTTP流式返回响应体中,⼀个响应包含⼀个响应头和多个响应块。在流式返回时,会先返回响应头,然后在逐个返回各个响应体,因此在发送流式响应时,需要在请求参数中告知HTTP服务器,响应头和chunk该如何处理。

如何处理实际上就是针对每个chunk设置一个回调函数(可调用体),C++可以用lambda表达式或仿函数来实现

具体函数实现

cpp 复制代码
    // 发送消息 - 增量返回 - 流式响应
    std::string DeepSeekProvider::sendMessageStream(const std::vector<Message>& messages, 
                                             const std::map<std::string, std::string>& requestParam,
                                             std::function<void(const std::string&, bool)> callback)
    {
        // 1. 检测模型是否可用
        if(!isAvailable()){
            ERR("DeepSeekProvider sendMessageStream model not available");
            return "";
        }

        // 2. 构造请求参数
        double temperature = 0.7;
        int maxTokens = 2048;
        if(requestParam.find("temperature") != requestParam.end()){
            temperature = std::stod(requestParam.at("temperature"));
        }
        if(requestParam.find("max_tokens") != requestParam.end()){
            maxTokens = std::stoi(requestParam.at("max_tokens"));
        }

        // 构造历史消息
        Json::Value messageArray(Json::arrayValue);
        for(const auto& message : messages){
            Json::Value messageObject;
            messageObject["role"] = message._role;
            messageObject["content"] = message._content;
            messageArray.append(messageObject);
        }

        // 3. 构造请求体
        Json::Value requestBody;
        requestBody["model"] = getModelName();
        requestBody["messages"] = messageArray;
        requestBody["temperature"] = temperature;
        requestBody["max_tokens"] = maxTokens;
        requestBody["stream"] = true;

        // 4. 序列化
        Json::StreamWriterBuilder writerBuilder;
        writerBuilder["indentation"] = "";
        std::string requestBodyStr = Json::writeString(writerBuilder, requestBody);
        INFO("DeepSeekProvider sendMessageStream requestBody: {}", requestBodyStr);

        // 5. 使用cpp-httplib库构造HTTP客户端
        httplib::Client client(_endpoint.c_str());
        client.set_connection_timeout(30, 0);     // 连接超时时间为30秒
        client.set_read_timeout(300, 0);          // 流式响应需要更长的时间,设置超时时间为300秒

        // 设置请求头
        // 客户端发送带有 Accept: text/event-stream 的请求以实现增量返
        httplib::Headers headers = {
            {"Authorization", "Bearer " + _apiKey},
            {"Content-Type", "application/json"},
            {"Accept", "text/event-stream"}
        };

        // 流式处理变量
        std::string buffer;          // 接受流式响应的数据块
        bool gotError = false;       // 标记响应是否成功
        std::string errorMsg;        // 错误描述符
        int statusCode = 0;          // 响应状态码
        bool streamFinish = false;   // 标记流式响应是否完成
        std::string fullResponse;    // 累积完整的响应

        // 创建请求对象
        httplib::Request req;
        req.method = "POST";
        req.path = "/v1/chat/completions";
        req.headers = headers;
        req.body = requestBodyStr;
        // 设置响应处理器
        req.response_handler = [&](const httplib::Response& res) {
            if(res.status != 200){
                gotError = true;
                errorMsg = "HTTP status code: " + std::to_string(res.status);
                return false;    // 终止请求
            }
            return true;   // 继续接收后续数据
        };

        // 设置数据接收处理器--解析流式响应的每个块的数据
        req.content_receiver = [&](const char* data, size_t len, size_t offset, size_t totalLength){
            // 验证响应头是否错误,如果出错就不需要再继续接收数据
            if(gotError){
                return false;
            }

            // 追加数据到buffer
            buffer.append(data, len);
            INFO("DeepSeekProvider sendMessageStream buffer: {}", buffer);

            // 处理所有的流式响应的数据块,注意:数据块之间是一个\n\n分隔
            size_t pos= 0;
            while((pos = buffer.find("\n\n")) != std::string::npos){
                // 截取当前找到的数据块
                std::string chunk = buffer.substr(0, pos);
                buffer.erase(0, pos + 2);

                // 解析该块响应数据的中模型返回的有效数据
                // 处理空行和注释,注意:以:开头的行是注释行,需要忽略
                if(chunk.empty() || chunk[0] == ':'){
                    continue;
                }

                // 获取模型返回的有效数据
                if(chunk.compare(0, 6, "data: ") == 0){
                    std::string modelData = chunk.substr(6);

                    // 检测是否为结束标记
                    if(modelData == "[DONE]"){
                        callback("", true);
                        streamFinish = true;
                        return true;
                    }

                    // 反序列化
                    Json::Value modelDataJson;
                    Json::CharReaderBuilder reader;
                    std::string errors;
                    std::istringstream modelDataStream(modelData);
                    if(Json::parseFromStream(reader, modelDataStream, &modelDataJson, &errors)){
                        // 模型返回的json格式的数据现在就保存在modelDataJson
                        if(modelDataJson.isMember("choices") &&
                          modelDataJson["choices"].isArray() && 
                          !modelDataJson["choices"].empty() &&
                          modelDataJson["choices"][0].isMember("delta") &&
                          modelDataJson["choices"][0]["delta"].isMember("content")){
                            std::string content = modelDataJson["choices"][0]["delta"]["content"].asString();
                            // 处理deltaContent,例如追加到fullResponse
                            fullResponse += content;

                            // 将本次解析出的模型返回的有效数据转给调用sendMessageStraem函数的用户使用---callback
                            callback(content, false);
                        }
                    }else{
                        WARN("DeepSeekProvider sendMessageStream parse modelDataJson error: {}", errors);
                    }
                }
            }
            return true;
        };

        // 给模型发送请求
        auto result = client.send(req);
        if(!result){
            // 请求发送失败,出现网络问题,比如DNS解析失败、连接超时
            ERR("Network error {}", to_string(result.error()));
            return "";
        }

        // 确保流式操作正确结束
        if(!streamFinish){
            WARN("stream ended without [DONE] marker");
            callback("", true);
        }

        return fullResponse;
    }




} // end ai_chat_sdk
相关推荐
王老师青少年编程2 小时前
2024年12月GESP真题及题解(C++七级): 燃烧
c++·题解·真题·gesp·csp·七级·燃烧
汉克老师2 小时前
GESP2025年9月认证C++三级真题与解析(单选题9-15)
c++·算法·数组·string·字符数组·gesp三级·gesp3级
上海云盾-小余2 小时前
能用到高防ip的业务类型都有哪些
网络·网络协议·tcp/ip
编程大师哥2 小时前
如何在C++中使用Redis的事务功能?
开发语言·c++·redis
朔北之忘 Clancy2 小时前
第二章 分支结构程序设计(1)
c++·算法·青少年编程·竞赛·教材·考级·讲义
小李独爱秋2 小时前
计算机网络经典问题透视:狭义与广义IP电话的深度解析及连接方式全览
网络·tcp/ip·计算机网络·信息与通信·ip·电话
汉克老师2 小时前
GESP2025年9月认证C++二级真题与解析(编程题2(菱形))
c++·找规律·二维数组·枚举算法·曼哈顿距离·模拟画图
dddddppppp1232 小时前
linux 块设备驱动程序之helloworld
linux·服务器·网络
君义_noip2 小时前
信息学奥赛一本通 1528:【例 2】单词游戏
c++·算法·信息学奥赛·一本通·csp-s