ChatGPT和Gemini的接入和封装

1.chatgpt的接入和封装:

1.头文件的编写

实际上就是把deepseek接入的头文件拷贝过来 然后改名字和一些细节即可:

此时头文件就封装好了

cpp 复制代码
#include <string>
#include <map>
#include <vector>
#include "common.h"
#include <functional>
#include "LLMProvider.h"



namespace ai_chat_sdk{
    class ChatGPTProvider:public LLMProvider{
        public:
           virtual bool initModel(const std::map<std::string, std::string>& modelConfig);//初始化模型  通过map传递模型配置参数 通过key获取配置参数
           //获取模型名称
           virtual std::string getModelName()  const;
           //获取模型描述
           virtual std::string getModelDesc()  const;//获取模型描述
           //判断模型是否可用
           virtual bool isAvailable()  const;
           //发送消息-全量返回
           virtual std::string sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam);//第一个参数发送的信息  第二个参数是大模型的请求参数
           //发送消息-流式返回  //第一个参数发送的信息  第二个参数是大模型的请求参数 第三个参数是回调函数
           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);//function是回调函数  callback:对模型返回的增量结果进行处理  第一个参数是增量数据  
                                                                                          // 第二个参数是是否是最后一个增量
    };
   
}

2.源文件的编写:

整个格局跟deepseek的接入一样 只是API的调用方式与deepseek有区别

所以现在我们要去了解一下chatgpt的API

3.API的介绍:

其实差不多跟deep seek的API介绍差不多

4.API的测试:

到后面就要解析这一段。

5.发送消息的实现(全量返回)

这些画框的字段和deepseek的实现不一样

最大的不一样就是将响应体序列化完后存储在responesJson中 我们要如何把他从中解析出来拿到我们想要的文本内容:

转化解析的思路:

deepseek中的choice就是这里的output 所以这里有改变

这里解析时就按照上面所提供的思路来进行判断即可。

cpp 复制代码
//发送消息-全量返回
    std::string ChatGPTProvider::sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam)
    {
        //检测模型是否可用
        if(!isAvailable()){
            ERR("ChatGPTProvider sendMessage model not available");
            return "";
        }
        //2.构造请求参数
        double temperature = 0.7;
        int maxOutputTokens = 2048;
        //去找requestParam中是否有temperature和max_tokens参数 有就使用模型提供的参数值  没找到就使用默认值
        if(requestParam.find("temperature") != requestParam.end()){
            temperature = std::stod(requestParam.at("temperature"));//stod是将字符串转换为double类型
        }
        if(requestParam.find("max_output_tokens") != requestParam.end()){
            maxOutputTokens = std::stoi(requestParam.at("max_output_tokens"));//stoi是将字符串转换为int类型
        }

        //构建消息列表
        Json::Value messagesArray(Json::arrayValue);
        for(const auto& msg: messages){
           Json::Value messageJson(Json::objectValue);//创建一个空的对象
            messageJson["role"] = msg._role;//role是消息的角色  有user、assistant、system三种角色
            messageJson["content"] = msg._content;//content是消息的内容
            messagesArray.append(messageJson);//将消息对象添加到数组中
        }

        //3.构造请求体
        Json::Value requestBody;//创建一个空的对象
        requestBody["model"] = getModelName();//model是模型的名称
        requestBody["input"] = messagesArray;//input是输入的消息列表
        requestBody["temperature"] = temperature;//temperature是温度参数  控制模型的输出随机性
        requestBody["max_output_tokens"] = maxOutputTokens;//max_output_tokens是最大输出token数  控制模型的输出长度

        //序列化请求体  将Json::Value对象转换为JSON字符串
        Json::StreamWriterBuilder writerBuilder;
        writerBuilder["indentation"] = "  ";//缩进参数  用于格式化JSON字符串
        std::string requestBodyStr = Json::writeString(writerBuilder, requestBody);

        //创建客户端
        httplib::Client client(_endpoint.c_str());
        client.set_connection_timeout(30,0 );//设置超时时间  30秒
        client.set_read_timeout(60,0);//设置读取超时时间为60秒  0表示不设置超时时间
        client.set_proxy("127.0.0.1", 7890);//设置代理  用于访问互联网

       //设置请求头  包含API Key
        httplib::Headers headers={
            {"Authorization", "Bearer " + _apikey}, //Authorization是请求头 认证方式  包含API Key  用于验证请求
            {"Content-Type", "application/json"} //Content-Type是请求头 内容类型  表示请求体的类型  这里是application/json
        };

        //发送POST请求
        auto response=client.Post("v1/responses",headers,requestBodyStr, "application/json");//发送POST请求  v1/responses是模型的API路径  requestBodyStr是请求体  headers是请求头  "application/json"是请求体的类型
        if(!response){//如果响应失败 就返回空字符串
            ERR("ChatGPTProvider sendMessage POST request failed   ");
            return "";
        }

        //检测响应状态码是否为200 是否成功
        if(response->status != 200){
            ERR("ChatGPTProvider sendMessage POST request failed, status: {}", response->status);
            return "";
        }
        
        INFO("ChatGPTProvider sendMessage POST request success body: {}", response->body);//打印响应体

        //对模型返回的结构进行序列化  上面我们拿到的是字符流  这里我们使用Json::parseFromStream函数将字符流转换为Json::Value对象
        Json::CharReaderBuilder reader;
        std::string errorJson;//用于存储解析错误信息
        Json::Value responseJson;//用于存储解析后的JSON对象
        std::istringstream responseStream(response->body);//将响应体转换为字符流
        if(!Json::parseFromStream(reader,responseStream, &responseJson,&errorJson)){ 
            ERR("ChatGPTProvider sendMessage POST request failed, response body parse failed");
            return "";
        }

        //从响应体中提取模型返回的内容 序列化的结果保存在responseJson对象中
        if(responseJson.isMember("output") && responseJson["output"].isArray() && !responseJson["output"].empty()){
            auto outPut = responseJson["output"][0];//获取第一个元素  即模型返回的内容
            if(outPut.isMember("content") && outPut["content"].isArray() && !outPut["content"].empty() && outPut["content"][0].isMember("text")){ 
                //判断content字段是否为字符串数组  如果不为空且第一个元素包含了text字段  就返回text字段的值
                std::string replyString = outPut["content"][0]["text"].asString();
                INFO("ChatGPTProvider sendMessage POST request success, reply: {}", replyString);
                return replyString;
                
            }
        }

        ERR("ChatGPTProvider sendMessage POST request failed, response body parse failed,errorJson: {}", errorJson);//打印解析错误信息
        return "";
    }

6.测试全量返回:

1.测试遇到的问题:

一开始我不能同时包含这两个头文件是因为这两个头文件中都包含了LLMProvider.h 而且我没有加#pragma once 所以不能被同时调用两次 所以要加上这个。

这样就可以了。

测试一下代码成功运行 但是因为我的APIkey没有了 所以这里显示连接错误 服务器端禁止我们访问。

7.发送消息的实现(流式返回)

1.主要实现

这些的实现跟deepseek的实现是一样的

2.数据接收器的实现

这一小部分的判断是先找到\n\n这个地方 然后从上面截取到这个\n\n然后获得event 这就是我们要的内容 然后再去解析。

这个跟deepseek不一样这个不是代码块而是事件流。

我们要去捕获这个响应的数据

还需要去检测这两个事件

这里我们不直接去写代码 因为我们先看不解析时打印的结果是什么 再根据打印的结果 再根据机制再去解析

这里我们先去实现剩下的东西再去根据结果实现数据接收器的解析

然后直接测试看看返回来的是什么样的

chatgpt返回的格式就像这样的

去比较一下前六个字符是不是"event:"是的话后面的那一行内容就是事件类型

再去比较一下前五个字符是不是"data:"是的话直接从第六个位置截取到末尾就是事件数据

把我们想要的文本内容给取出来

这样整个的数据接收器就实现好了。

数据接收器的代码:

cpp 复制代码
 //处理所有的流式响应的数据块 注意数据块是以\n\n分隔的
           size_t pos=0;//初始化位置为0
           while((pos=buffer.find("\n\n"))!=std::string::npos){//如果找到\n\n  npos就是-1
                //截取当前找到的数据块
                std::string event = buffer.substr(0, pos);//从位置0开始  截取pos个字符
                buffer.erase(0, pos+2);//删除buffer中从位置0开始的pos+2个字符  因为\n\n是2个字符
               
                //解析事件类型和具体数据位置
                std::istringstream eventStream(event);//将event字符串转换为输入流
                std::string eventType;//用于存储事件类型
                std::string eventData;//用于存储事件数据
                std::string line;//用于存储每一行数据
                while(std::getline(eventStream, line)){//从输入流中读取每一行数据
                    if(line.empty()){//如果line为空 就跳过
                        continue;
                    }
                    if(line.compare(0, 6, "event:")==0){//如果line以"event:"开头
                        eventType = line.substr(6);//从line的第6个字符开始  截取到line的末尾  作为事件类型
                    }else if(line.compare(0, 5, "data:")==0){//如果line以"data:"开头
                        eventData += line.substr(6);//从line的第6个字符开始  截取到line的末尾  作为事件数据
                    }
                }//end while getline


                //对模型返回的结果序列化
                Json::Value chunk;//保存序列化后的JSON对象
                Json::CharReaderBuilder reader;
                std::string errs;//保存序列化错误信息
                std::istringstream eventDateStream(eventData);//把eventData转换为输入流  用于反序列化JSON对象
                if(!Json::parseFromStream(reader, eventDateStream, &chunk, &errs)){//如果序列化失败  就打印错误日志
                        ERR("ChatGPTProvider sendMessageStream parse body failed,error : {}", errs);//打印错误日志
                        continue;//跳过当前数据块
                }

                //按照事件类型进行数据分析
                if(eventType == "response.output_text.delta"){
                    //如果是response.output_text.delta事件 
                    if(chunk.isMember("delta") && chunk["delta"].isString()){//如果delta是chunk的成员   且是字符串类型
                        std::string delta = chunk["delta"].asString();//将delta添加到fullResponse中
                        callback(delta, false);//将delta传递给回调函数  表示不是最后一个增量
                    }
                }else if(eventType == "response.output_item.done"){
                    //如果是response.output_item.done事件 
                    // 表⽰该块输出结束
                    if(chunk.isMember("item") && chunk["item"].isObject()){
                        Json::Value item = chunk["item"];
                        if(item.isMember("content") &&
                           item["content"].isArray() &&
                           !item["content"].empty() &&
                           item["content"][0].isMember("text") &&
                           item["content"][0]["text"].isString())
                           {
                             fullResponse += item["content"][0]["text"].asString();//将text添加到fullResponse中
                           }
                    }
                }else if(eventType == "response.completed"){
                    //如果是response.completed事件 
                    streamFinish = true;//设置流式响应完成标志
                    callback("", true);//将空字符串传递给回调函数  表示流式响应结束
                    return true;
                }



            }//end while pos!=std::string::npos


            return true;//继续接受数据块
        };//end lambda

3.整个流式返回的代码实现

cpp 复制代码
//发送消息-流式返回
    std::string ChatGPTProvider::sendMessageStream(const std::vector<Message>& messages, 
                                                   const std::map<std::string, std::string>& requestParam, 
                                                   std::function<void(const std::string&, bool)> callback)
    {

        //检测模型是否可用
        if(!isAvailable()){
            ERR("ChatGPTProvider sendMessageStream model not available");
            return "";
        }
        
        //构造请求参数
        double temperature = 0.7;
        int maxOutputTokens = 2048;
        //去找requestParam中是否有temperature和max_output_tokens参数 有就使用用户提供的参数值  没找到就使用默认值
        if(requestParam.find("temperature") != requestParam.end()){
            temperature = std::stod(requestParam.at("temperature"));//stod是将字符串转换为double类型
        }
        if(requestParam.find("max_output_tokens") != requestParam.end()){
            maxOutputTokens = std::stoi(requestParam.at("max_output_tokens"));//stoi是将字符串转换为int类型
        }

        //构造历史消息
        Json::Value messagesArray(Json::arrayValue);
        for(const auto& msg: messages){
           Json::Value messageJson(Json::objectValue);//创建一个空的对象
            messageJson["role"] = msg._role;//role是消息的角色  有user、assistant、system三种角色
            messageJson["content"] = msg._content;//content是消息的内容
            messagesArray.append(messageJson);//将消息对象添加到数组中
        }

        //构造请求体
        Json::Value requestBody;//创建一个空的对象
        requestBody["model"] = getModelName();//model是模型的名称
        requestBody["input"] = messagesArray;//input是输入的消息列表
        requestBody["temperature"] = temperature;//temperature是温度参数  控制模型的输出随机性
        requestBody["max_output_tokens"] = maxOutputTokens;//max_output_tokens是最大输出token数  控制模型的输出长度
        requestBody["stream"] = true;//stream是是否开启流式返回  true开启  false关闭

         //序列化请求体  将Json::Value对象转换为JSON字符串
        Json::StreamWriterBuilder writerBuilder;
        writerBuilder["indentation"] = "  ";//缩进参数  用于格式化JSON字符串
        std::string requestBodyStr = Json::writeString(writerBuilder, requestBody);

        //创建客户端
        httplib::Client client(_endpoint.c_str());
        client.set_connection_timeout(60,0 );//设置超时时间  60秒
        client.set_read_timeout(300,0);//设置读取超时时间为300秒  0表示不设置超时时间
        client.set_proxy("127.0.0.1", 7890);//设置代理  用于访问互联网

       //设置请求头  包含API Key
        httplib::Headers headers={
            {"Authorization", "Bearer " + _apikey}, //Authorization是请求头 认证方式  包含API Key  用于验证请求
            {"Content-Type", "application/json"}, //Content-Type是请求头 内容类型  表示请求体的类型  这里是application/json
            {"Accept", "text/event-stream"} //Accept是请求头 接受的响应类型  这里是text/event-stream
        };
        //流式处理的相关变量
        std::string buffer;//用于存储模型返回的内容
        bool gotError = false;//用于判断是否获取到错误信息  
        std::string errorMsg;//用于存储解析错误信息
        int statusCode = 0;//用于存储响应状态码 
        bool streamFinish = false;//用于判断是否流式返回完成
        std::string fullResponse;//存储完整的流式响应内容

        //创建请求对象
        httplib::Request request;
        request.method = "POST";
        request.path = "/v1/responses";
        request.body = requestBodyStr;
        request.headers = headers;

        //设置响应处理器
        //利用lambda表达式设置响应处理器  用于处理DeepSeek返回的流式响应
        request.response_handler = [&](const httplib::Response& res){
            statusCode = res.status;
            if(statusCode != 200){ //如果响应状态码不是200  就认为是失败    
                gotError = true;
                errorMsg = "ChatGPTProvider sendMessageStream POST request failed, status  " +std::to_string(statusCode);
                return false;//终止请求
            }
           return true; //继续接受流式响应数据块
        };

        //设置数据接收处理器
        //利用lambda表达式设置数据接受处理器  用于处理ChatGPT返回的流式响应数据块  第一个参数是数据块的指针  第二个参数是数据块的长度  第三个参数是当前数据块的偏移量  第四个参数是总数据块的长度
        request.content_receiver = [&](const char* data, size_t dataLenth,size_t offset,size_t totalLength){
            //验证响应头是否出错 如果出错就不再接受数据
            if(gotError){
                return false;//终止请求
            }
            //将数据块添加到buffer中
            buffer.append(data, dataLenth);
            INFO("ChatGPTProvider sendMessageStream received data: {}", buffer);//打印数据块字符串


            //处理所有的流式响应的数据块 注意数据块是以\n\n分隔的
           size_t pos=0;//初始化位置为0
           while((pos=buffer.find("\n\n"))!=std::string::npos){//如果找到\n\n  npos就是-1
                //截取当前找到的数据块
                std::string event = buffer.substr(0, pos);//从位置0开始  截取pos个字符
                buffer.erase(0, pos+2);//删除buffer中从位置0开始的pos+2个字符  因为\n\n是2个字符
               
                //解析事件类型和具体数据位置
                std::istringstream eventStream(event);//将event字符串转换为输入流
                std::string eventType;//用于存储事件类型
                std::string eventData;//用于存储事件数据
                std::string line;//用于存储每一行数据
                while(std::getline(eventStream, line)){//从输入流中读取每一行数据
                    if(line.empty()){//如果line为空 就跳过
                        continue;
                    }
                    if(line.compare(0, 6, "event:")==0){//如果line以"event:"开头
                        eventType = line.substr(6);//从line的第6个字符开始  截取到line的末尾  作为事件类型
                    }else if(line.compare(0, 5, "data:")==0){//如果line以"data:"开头
                        eventData += line.substr(6);//从line的第6个字符开始  截取到line的末尾  作为事件数据
                    }
                }//end while getline


                //对模型返回的结果序列化
                Json::Value chunk;//保存序列化后的JSON对象
                Json::CharReaderBuilder reader;
                std::string errs;//保存序列化错误信息
                std::istringstream eventDateStream(eventData);//把eventData转换为输入流  用于反序列化JSON对象
                if(!Json::parseFromStream(reader, eventDateStream, &chunk, &errs)){//如果序列化失败  就打印错误日志
                        ERR("ChatGPTProvider sendMessageStream parse body failed,error : {}", errs);//打印错误日志
                        continue;//跳过当前数据块
                }

                //按照事件类型进行数据分析
                if(eventType == "response.output_text.delta"){
                    //如果是response.output_text.delta事件 
                    if(chunk.isMember("delta") && chunk["delta"].isString()){//如果delta是chunk的成员   且是字符串类型
                        std::string delta = chunk["delta"].asString();//将delta添加到fullResponse中
                        callback(delta, false);//将delta传递给回调函数  表示不是最后一个增量
                    }
                }else if(eventType == "response.output_item.done"){
                    //如果是response.output_item.done事件 
                    // 表⽰该块输出结束
                    if(chunk.isMember("item") && chunk["item"].isObject()){
                        Json::Value item = chunk["item"];
                        if(item.isMember("content") &&
                           item["content"].isArray() &&
                           !item["content"].empty() &&
                           item["content"][0].isMember("text") &&
                           item["content"][0]["text"].isString())
                           {
                             fullResponse += item["content"][0]["text"].asString();//将text添加到fullResponse中
                           }
                    }
                }else if(eventType == "response.completed"){
                    //如果是response.completed事件 
                    streamFinish = true;//设置流式响应完成标志
                    callback("", true);//将空字符串传递给回调函数  表示流式响应结束
                    return true;
                }



            }//end while pos!=std::string::npos


            return true;//继续接受数据块
        };//end lambda


        //给模型发送请求
        auto result = client.send(request);//我们需要对result进行检测
        if(!result){
            //如果result为空  就认为是失败
            //请求失败 出现网络问题,比如DNS解析失败
            //auto err = result.error();
            //ERR("Network error : {}", std::to_string(static_cast<int>(err)));
            ERR("Network error : {}", to_string(result.error()));
            return "";//返回空字符串
        }
        // 确保流式操作正常结束
        if(!streamFinish){
            WARN("Stream ended without response completed");//如果流式响应没有完成 就打印警告日志
            callback("", true);//将空字符串传递给回调函数  表示流式响应结束
        }
        return fullResponse;
    }//end sendMessageStream 流式返回函数的结束

4.测试流式返回:

这里告诉我们返回了空的数据 所以代码写的还是有问题

那么返回时就没有走我们的fullresponse返回返回的是这两个空字符串

这个是网络问题

还有个问题之前我们是从第6个字符来截取的

而这里还有个空格如果从第六个字符截取的话把空格也截取进去了 也会报错。

2.Gemini的接入和封装:

1.初步的实现:

头文件的实现

源文件的实现

2.API的介绍

这里我们接入的是和opean ai 兼容的API

3.发送消息的实现(全量返回)

这里跟deepseek的实现都差不多的就不过多赘述了

cpp 复制代码
 //发送消息-全量返回
    std::string GeminiProvider::sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam){
         //检测模型是否可用
        if(!isAvailable()){
            ERR("GeminiProvider sendMessage model not available");
            return "";
        }
        //2.构造请求参数
        double temperature = 0.7;
        int max_tokens = 2048;
        //去找requestParam中是否有temperature和max_tokens参数 有就使用模型提供的参数值  没找到就使用默认值
        if(requestParam.find("temperature") != requestParam.end()){
            temperature = std::stod(requestParam.at("temperature"));//stod是将字符串转换为double类型
        }
        if(requestParam.find("max_tokens") != requestParam.end()){
            max_tokens = std::stoi(requestParam.at("max_tokens"));//stoi是将字符串转换为int类型
        }


        //3.构造历史消息
        Json::Value messagesArray(Json::arrayValue);//创建一个空的数组  用于存储历史消息
        for(const auto& message : messages){//遍历messages向量  每个消息都添加到数组中
            Json::Value messageObject;//创建一个空的对象
            messageObject["role"] = message._role;//role是消息的角色  有user、assistant、system三种角色
            messageObject["content"] = message._content;//content是消息的内容
            messagesArray.append(messageObject);//将消息对象添加到数组中
        }

        //4.构造请求体
        Json::Value requestBody;//创建一个空的对象  用于存储请求体
        //方括号里要使用正确的字段 这样模型才会去解析
        requestBody["model"] = getModelName();//model是模型名称  这里是gemini-2.0-flash
        requestBody["messages"] = messagesArray;//messages是历史消息数组
        requestBody["temperature"] = temperature;//temperature是温度参数  控制生成文本的随机性
        requestBody["max_tokens"] = max_tokens;//max_tokens是最大生成token数  控制生成文本的长度

        //5.请求序列化
        Json::StreamWriterBuilder writeBuilder;//创建一个空的构建器  用于将Json::Value转换为字符串
        writeBuilder["indentation"] = "  ";//设置缩进为2个空格
        std::string requestBodyStr = Json::writeString(writeBuilder, requestBody);//将Json::Value转换为字符串builder
        INFO("GeminiProvider sendMessage requestBody: {}", requestBodyStr);//打印请求体

         //6.使用cpp-httplib库构造HTTP客户端
        httplib::Client client(_endpoint.c_str());//创建一个HTTP客户端  用于发送HTTP请求 把根端点告诉客户端  客户端会根据根端点去发送请求
        client.set_connection_timeout(30,0);//设置连接超时时间为30秒  0表示不设置超时时间
        client.set_read_timeout(60,0);//设置读取超时时间为60秒  0表示不设置超时时间
        client.set_proxy("127.0.0.1", 7890);//设置代理  用于访问互联网


        //7.设置请求头  包含API Key
        httplib::Headers headers={
            {"Authorization", "Bearer " + _apikey}, //Authorization是请求头 认证方式  包含API Key  用于验证请求
            //{"Content-Type", "application/json"} //Content-Type是请求头 内容类型  表示请求体的类型  这里是application/json
        };

        //8.发送POST请求
        auto response=client.Post("/v1beta/openai/chat/completions",headers,requestBodyStr, "application/json");//发送POST请求  /v1beta/openai/chat/completions是模型的API路径  requestBodyStr是请求体  headers是请求头  "application/json"是请求体的类型
        if(!response){//如果响应失败 就返回空字符串
            ERR("GeminiProvider sendMessage POST request failed, error: {}", to_string(response.error()));
            return "";
        }
        INFO("GeminiProvider sendMessage POST request success status: {}", response->status);//打印响应状态码
        INFO("GeminiProvider sendMessage POST request success body: {}", response->body);//打印响应体

        //检测响应是否成功
        if(response->status != 200){//如果响应状态码不是200 就返回
            return "";
        }
        //9.解析响应体
        //对模型返回的结构进行序列化  上面我们拿到的是字符流  这里我们使用Json::parseFromStream函数将字符流转换为Json::Value对象
        Json::CharReaderBuilder readerBuilder;
        std::string errorJson;//用于存储解析错误信息
        Json::Value responseBody;//用于存储解析后的JSON对象
        std::istringstream responseStream(response->body);//将响应体转换为字符流
        if(!Json::parseFromStream(readerBuilder,responseStream, &responseBody,&errorJson)){     
            ERR("GeminiProvider sendMessage POST request failed, response body parse failed");
            return "";
        }

            //获取choices数组  用于存储模型返回的消息
        if(responseBody.isMember("choices") && responseBody["choices"].isArray() && !responseBody["choices"].empty())
        {
            auto choice=responseBody["choices"][0];//获取choices数组的第一个元素  用于存储模型返回的消息
            if(choice.isMember("message")  && choice["message"].isMember("content"))//如果choices数组的第一个元素有message成员  且message成员有content成员
            {
                std::string replyContent=choice["message"]["content"].asString();//获取message成员的content成员  用于存储模型返回的消息内容
                INFO("GeminiProvider sendMessage replyContent: {}", replyContent);//打印模型返回的消息内容
                return replyContent;//返回模型返回的消息内容
            }
            return "";
        }

        //解析失败
        ERR("GeminiProvider response body parse failed");
        return "";
    }

4.全量返回的测试:

测试代码 测试不光要改这个还需要去更改CMakeLists.txt 让他多去生成一个Gemini.cpp

还是代理有错误 是因为apikey错误了 无法连接。

5.发送消息的实现(流式返回)

我们还是老样子去测试一下流式返回有哪些参数 可以看出这个参数跟deepseek很相似

就是这种数据格式也就是SSH协议的数据 所以接下来的流式返回实现跟deepseek是差不多的。

这就是整个的实现因为Gemini和deepseek的返回格式都是SSH协议这样返回的不像chatgpt一样还要单独去处理 所以上基本一致

cpp 复制代码
//发送消息-流式返回
    std::string GeminiProvider::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("GeminiProvider sendMessageStream model not available");//打印模型不可用错误信息
            return "";
        }

        //2.构造请求参数
        double temperature = 0.7;
        int max_tokens = 2048;
        //去找requestParam中是否有temperature和max_tokens参数 有就使用用户提供的参数值  没找到就使用默认值
        if(requestParam.find("temperature") != requestParam.end()){
            temperature = std::stod(requestParam.at("temperature"));//stod是将字符串转换为double类型
        }
        if(requestParam.find("max_tokens") != requestParam.end()){
            max_tokens = std::stoi(requestParam.at("max_tokens"));//stoi是将字符串转换为int类型
        }

        //3.构造历史消息
       Json::Value messagesArray(Json::arrayValue);//创建一个空的数组  用于存储历史消息
        for(const auto& message : messages){//遍历messages向量  每个消息都添加到数组中
            Json::Value messageObject;//创建一个空的对象
            messageObject["role"] = message._role;//role是消息的角色  有user、assistant、system三种角色
            messageObject["content"] = message._content;//content是消息的内容
            messagesArray.append(messageObject);//将消息对象添加到数组中
        }
        

        //4.构造请求体
        Json::Value requestBody;//创建一个空的对象  用于存储请求体
        requestBody["model"] = getModelName();//获取模型的名称
        requestBody["messages"] = messagesArray;//messages是历史消息数组  用于存储历史消息
        requestBody["temperature"] = temperature;//temperature是温度参数  用于控制模型的随机性
        requestBody["max_tokens"] = max_tokens;//max_tokens是最大token数  用于控制模型的输出长度
        requestBody["stream"] = true;//stream是流式参数  用于控制是否返回流式结果

        //5.序列化
        Json::StreamWriterBuilder writeBuilder;//创建一个空的构建器  用于将Json::Value转换为字符串
        writeBuilder["indentation"] = "  ";//设置缩进为2个空格
        std::string requestBodyStr = Json::writeString(writeBuilder, requestBody);//将Json::Value转换为字符串builder
        INFO("GeminiProvider sendMessageStream requestBody: {}", requestBodyStr);//打印请求体字符串

        //6。使用HTTPlib创建客户端
        httplib::Client client(_endpoint.c_str());//创建一个HTTP客户端  用于发送HTTP请求 把根端点告诉客户端  客户端会根据根端点去发送请求
        client.set_connection_timeout(60,0);//设置连接超时时间为60秒  0表示不设置超时时间
        client.set_read_timeout(300,0);//设置读取超时时间为300秒(流式返回时需要设置为较大值)  0表示不设置超时时间
        client.set_proxy("127.0.0.1", 7890);//设置代理  用于访问互联网
       
       //7.设置请求头
       httplib::Headers headers={
            {"Authorization", "Bearer " + _apikey}, //Authorization是请求头 认证方式  包含API Key  用于验证请求
        };

        //定义一些用来流式处理的变量
        std::string buffer;//接受流式响应的数据块
        bool gotError = false;//标记响应是否成功
        std::string errorMsg;//描述错误信息
        int statusCode = 0;//响应状态码  用于判断响应是否成功
        bool streamFinish = false;//标记流式响应是否完成
        std::string fullResponse;//接受完整的流式响应

         //创建请求对象
        httplib::Request request;//创建一个空的请求对象  用于存储请求信息
        request.path = "/v1beta/openai/chat/completions";//设置请求路径  这里是Gemini模型的API路径
        request.method = "POST";//设置请求方法  这里是POST方法
        request.headers = headers;//设置请求头  这里是设置的请求头
        request.body = requestBodyStr;//设置请求体  这里是序列化后的请求体字符串

        //设置响应处理器
        //利用lambda表达式设置响应处理器  用于处理DeepSeek返回的流式响应
        request.response_handler = [&](const httplib::Response& res){
            statusCode = res.status;//获取响应状态码
            if(statusCode != 200){ //如果响应状态码不是200  就认为是失败
                gotError = true;
                errorMsg = "HTTP status code: " + std::to_string(statusCode);
                return false;//终止请求
            }
           return true; //继续接受流式响应数据块
        };

        //设置数据接受处理器
        //利用lambda表达式设置数据接受处理器  用于处理DeepSeek返回的流式响应数据块
        request.content_receiver = [&](const char* data, size_t dataLenth,size_t offset,size_t totalLength){
            //验证响应头是否出错 如果出错就不再接受数据
            if(gotError){
                return false;//终止请求
            }

            //将数据块添加到buffer中
            buffer.append(data, dataLenth);
            INFO("GeminiProvider sendMessageStream received data: {}", buffer);//打印数据块字符串

            //处理所以的流式响应的数据块 注意数据块是以\n\n分隔的
            size_t pos=0;//初始化位置为0
            while((pos=buffer.find("\n\n"))!=std::string::npos){//如果找到\n\n  npos就是-1
                //截取当前找到的数据块
                std::string chunk = buffer.substr(0, pos);//从位置0开始  截取pos个字符
                buffer.erase(0, pos+2);//删除buffer中从位置0开始的pos+2个字符  因为\n\n是2个字符
               
                //解析该块响应数据中的模型返回的有效数据  现在chunk已经截取出来了  可以解析了

                //处理空行和注释行 以':'开头的行是注释行  不需要解析
                if(chunk.empty()||chunk[0]==':'){
                    continue;//如果chunk为空 或者 第一个字符是':' 就跳过
                }

                //获取模型返回的有效数据  
               if(chunk.compare(0,6,"data: ")==0){//如果chunk中没有data字段  就跳过
                    std::string modelData = chunk.substr(6);//从data: 后面开始截取  截取到chunk的结束
                    
                    //检测是否为结束标记
                    if(modelData=="[DONE]"){
                        streamFinish = true;//设置流式响应完成标记为true
                        return true;//返回true  流式响应完成
                    }

                    //反序列化
                    Json::Value modelDataJson;//保存反序列化后的JSON对象
                    Json::CharReaderBuilder reader;
                    std::string errors;//保存反序列化错误信息
                    std::istringstream modelDataStream(modelData);//把modelData转换为输入流  用于反序列化JSON对象
                    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();//获取delta字段中的content字段  并将其转换为字符串
                            fullResponse += content;//将content添加到fullResponse中

                            //将本次解析出的模型返回的有效数据转给调用sendMessageStream的用户使用 利用callback 回调函数
                            callback(content,false);//第一个参数将content传递给回调函数  第二个参数将false传递给回调函数  表示流式响应没有结束 继续去找下一个数据块
                        }
                    }else{ //如果反序列化解析失败
                        WARN("GeminiProvider sendMessageStream parse modelDataJson error: {}", errors);//打印反序列化错误信息
                    }

               }

            }//end while
            return true;
            
        };//end content_receiver lambda函数结束

6.整个代码的实现:

cpp 复制代码
#include"../include/GeminiProvider.h"
#include"../include/util/myLog.h"
#include<jsoncpp/json/json.h>
#include<httplib.h>
#include <jsoncpp/json/reader.h>
#include <jsoncpp/json/value.h>




namespace ai_chat_sdk{
    //初始化模型  通过map传递模型配置参数 通过key获取配置参数
    bool GeminiProvider::initModel(const std::map<std::string, std::string>& modelConfig){
        auto it =modelConfig.find("api_key");
        if(it == modelConfig.end()){//modelConfig是map类型  如果没有apikey参数 就返回false
            ERR("GeminiProvider initModel api_key not found");
            return false;
        }else{
            _apikey= it->second;
        }
        //初始化Base URL
        it =modelConfig.find("endpoint");//endpoint是根端点模型的API base url
        if(it == modelConfig.end()){//modelConfig是map类型  如果没有endpoint参数 就返回false
            ERR("ChatGPTProvider initModel endpoint not found");
            return false;
        }else{
            _endpoint= it->second;
        }
        _isAvailable= true;
        INFO("GeminiProvider initModel success, endpoint: {}", _endpoint);
        return true;
    }
    //检测模型是否可用
    bool GeminiProvider::isAvailable() const
    {
        return _isAvailable;
    }
     //获取模型名称
     std::string GeminiProvider::getModelName()  const{
        return "gemini-2.0-flash";
    }
    //获取模型描述
    std::string GeminiProvider::getModelDesc() const
    {
        return "Google推出的Gemini-2.0-flash模型,支持中文对话和任务完成 高性能 高效率";
    }
    //发送消息-全量返回
    std::string GeminiProvider::sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam){
         //检测模型是否可用
        if(!isAvailable()){
            ERR("GeminiProvider sendMessage model not available");
            return "";
        }
        //2.构造请求参数
        double temperature = 0.7;
        int max_tokens = 2048;
        //去找requestParam中是否有temperature和max_tokens参数 有就使用模型提供的参数值  没找到就使用默认值
        if(requestParam.find("temperature") != requestParam.end()){
            temperature = std::stod(requestParam.at("temperature"));//stod是将字符串转换为double类型
        }
        if(requestParam.find("max_tokens") != requestParam.end()){
            max_tokens = std::stoi(requestParam.at("max_tokens"));//stoi是将字符串转换为int类型
        }


        //3.构造历史消息
        Json::Value messagesArray(Json::arrayValue);//创建一个空的数组  用于存储历史消息
        for(const auto& message : messages){//遍历messages向量  每个消息都添加到数组中
            Json::Value messageObject;//创建一个空的对象
            messageObject["role"] = message._role;//role是消息的角色  有user、assistant、system三种角色
            messageObject["content"] = message._content;//content是消息的内容
            messagesArray.append(messageObject);//将消息对象添加到数组中
        }

        //4.构造请求体
        Json::Value requestBody;//创建一个空的对象  用于存储请求体
        //方括号里要使用正确的字段 这样模型才会去解析
        requestBody["model"] = getModelName();//model是模型名称  这里是gemini-2.0-flash
        requestBody["messages"] = messagesArray;//messages是历史消息数组
        requestBody["temperature"] = temperature;//temperature是温度参数  控制生成文本的随机性
        requestBody["max_tokens"] = max_tokens;//max_tokens是最大生成token数  控制生成文本的长度

        //5.请求序列化
        Json::StreamWriterBuilder writeBuilder;//创建一个空的构建器  用于将Json::Value转换为字符串
        writeBuilder["indentation"] = "  ";//设置缩进为2个空格
        std::string requestBodyStr = Json::writeString(writeBuilder, requestBody);//将Json::Value转换为字符串builder
        INFO("GeminiProvider sendMessage requestBody: {}", requestBodyStr);//打印请求体

         //6.使用cpp-httplib库构造HTTP客户端
        httplib::Client client(_endpoint.c_str());//创建一个HTTP客户端  用于发送HTTP请求 把根端点告诉客户端  客户端会根据根端点去发送请求
        client.set_connection_timeout(30,0);//设置连接超时时间为30秒  0表示不设置超时时间
        client.set_read_timeout(60,0);//设置读取超时时间为60秒  0表示不设置超时时间
        client.set_proxy("127.0.0.1", 7890);//设置代理  用于访问互联网


        //7.设置请求头  包含API Key
        httplib::Headers headers={
            {"Authorization", "Bearer " + _apikey}, //Authorization是请求头 认证方式  包含API Key  用于验证请求
            //{"Content-Type", "application/json"} //Content-Type是请求头 内容类型  表示请求体的类型  这里是application/json
        };

        //8.发送POST请求
        auto response=client.Post("/v1beta/openai/chat/completions",headers,requestBodyStr, "application/json");//发送POST请求  /v1beta/openai/chat/completions是模型的API路径  requestBodyStr是请求体  headers是请求头  "application/json"是请求体的类型
        if(!response){//如果响应失败 就返回空字符串
            ERR("GeminiProvider sendMessage POST request failed, error: {}", to_string(response.error()));
            return "";
        }
        INFO("GeminiProvider sendMessage POST request success status: {}", response->status);//打印响应状态码
        INFO("GeminiProvider sendMessage POST request success body: {}", response->body);//打印响应体

        //检测响应是否成功
        if(response->status != 200){//如果响应状态码不是200 就返回
            return "";
        }
        //9.解析响应体
        //对模型返回的结构进行序列化  上面我们拿到的是字符流  这里我们使用Json::parseFromStream函数将字符流转换为Json::Value对象
        Json::CharReaderBuilder readerBuilder;
        std::string errorJson;//用于存储解析错误信息
        Json::Value responseBody;//用于存储解析后的JSON对象
        std::istringstream responseStream(response->body);//将响应体转换为字符流
        if(!Json::parseFromStream(readerBuilder,responseStream, &responseBody,&errorJson)){     
            ERR("GeminiProvider sendMessage POST request failed, response body parse failed");
            return "";
        }

            //获取choices数组  用于存储模型返回的消息
        if(responseBody.isMember("choices") && responseBody["choices"].isArray() && !responseBody["choices"].empty())
        {
            auto choice=responseBody["choices"][0];//获取choices数组的第一个元素  用于存储模型返回的消息
            if(choice.isMember("message")  && choice["message"].isMember("content"))//如果choices数组的第一个元素有message成员  且message成员有content成员
            {
                std::string replyContent=choice["message"]["content"].asString();//获取message成员的content成员  用于存储模型返回的消息内容
                INFO("GeminiProvider sendMessage replyContent: {}", replyContent);//打印模型返回的消息内容
                return replyContent;//返回模型返回的消息内容
            }
            return "";
        }

        //解析失败
        ERR("GeminiProvider response body parse failed");
        return "";
    }
    //发送消息-流式返回
    std::string GeminiProvider::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("GeminiProvider sendMessageStream model not available");//打印模型不可用错误信息
            return "";
        }

        //2.构造请求参数
        double temperature = 0.7;
        int max_tokens = 2048;
        //去找requestParam中是否有temperature和max_tokens参数 有就使用用户提供的参数值  没找到就使用默认值
        if(requestParam.find("temperature") != requestParam.end()){
            temperature = std::stod(requestParam.at("temperature"));//stod是将字符串转换为double类型
        }
        if(requestParam.find("max_tokens") != requestParam.end()){
            max_tokens = std::stoi(requestParam.at("max_tokens"));//stoi是将字符串转换为int类型
        }

        //3.构造历史消息
       Json::Value messagesArray(Json::arrayValue);//创建一个空的数组  用于存储历史消息
        for(const auto& message : messages){//遍历messages向量  每个消息都添加到数组中
            Json::Value messageObject;//创建一个空的对象
            messageObject["role"] = message._role;//role是消息的角色  有user、assistant、system三种角色
            messageObject["content"] = message._content;//content是消息的内容
            messagesArray.append(messageObject);//将消息对象添加到数组中
        }
        

        //4.构造请求体
        Json::Value requestBody;//创建一个空的对象  用于存储请求体
        requestBody["model"] = getModelName();//获取模型的名称
        requestBody["messages"] = messagesArray;//messages是历史消息数组  用于存储历史消息
        requestBody["temperature"] = temperature;//temperature是温度参数  用于控制模型的随机性
        requestBody["max_tokens"] = max_tokens;//max_tokens是最大token数  用于控制模型的输出长度
        requestBody["stream"] = true;//stream是流式参数  用于控制是否返回流式结果

        //5.序列化
        Json::StreamWriterBuilder writeBuilder;//创建一个空的构建器  用于将Json::Value转换为字符串
        writeBuilder["indentation"] = "  ";//设置缩进为2个空格
        std::string requestBodyStr = Json::writeString(writeBuilder, requestBody);//将Json::Value转换为字符串builder
        INFO("GeminiProvider sendMessageStream requestBody: {}", requestBodyStr);//打印请求体字符串

        //6。使用HTTPlib创建客户端
        httplib::Client client(_endpoint.c_str());//创建一个HTTP客户端  用于发送HTTP请求 把根端点告诉客户端  客户端会根据根端点去发送请求
        client.set_connection_timeout(60,0);//设置连接超时时间为60秒  0表示不设置超时时间
        client.set_read_timeout(300,0);//设置读取超时时间为300秒(流式返回时需要设置为较大值)  0表示不设置超时时间
        client.set_proxy("127.0.0.1", 7890);//设置代理  用于访问互联网
       
       //7.设置请求头
       httplib::Headers headers={
            {"Authorization", "Bearer " + _apikey}, //Authorization是请求头 认证方式  包含API Key  用于验证请求
        };

        //定义一些用来流式处理的变量
        std::string buffer;//接受流式响应的数据块
        bool gotError = false;//标记响应是否成功
        std::string errorMsg;//描述错误信息
        int statusCode = 0;//响应状态码  用于判断响应是否成功
        bool streamFinish = false;//标记流式响应是否完成
        std::string fullResponse;//接受完整的流式响应

         //创建请求对象
        httplib::Request request;//创建一个空的请求对象  用于存储请求信息
        request.path = "/v1beta/openai/chat/completions";//设置请求路径  这里是Gemini模型的API路径
        request.method = "POST";//设置请求方法  这里是POST方法
        request.headers = headers;//设置请求头  这里是设置的请求头
        request.body = requestBodyStr;//设置请求体  这里是序列化后的请求体字符串

        //设置响应处理器
        //利用lambda表达式设置响应处理器  用于处理DeepSeek返回的流式响应
        request.response_handler = [&](const httplib::Response& res){
            statusCode = res.status;//获取响应状态码
            if(statusCode != 200){ //如果响应状态码不是200  就认为是失败
                gotError = true;
                errorMsg = "HTTP status code: " + std::to_string(statusCode);
                return false;//终止请求
            }
           return true; //继续接受流式响应数据块
        };

        //设置数据接受处理器
        //利用lambda表达式设置数据接受处理器  用于处理DeepSeek返回的流式响应数据块
        request.content_receiver = [&](const char* data, size_t dataLenth,size_t offset,size_t totalLength){
            //验证响应头是否出错 如果出错就不再接受数据
            if(gotError){
                return false;//终止请求
            }

            //将数据块添加到buffer中
            buffer.append(data, dataLenth);
            INFO("GeminiProvider sendMessageStream received data: {}", buffer);//打印数据块字符串

            //处理所以的流式响应的数据块 注意数据块是以\n\n分隔的
            size_t pos=0;//初始化位置为0
            while((pos=buffer.find("\n\n"))!=std::string::npos){//如果找到\n\n  npos就是-1
                //截取当前找到的数据块
                std::string chunk = buffer.substr(0, pos);//从位置0开始  截取pos个字符
                buffer.erase(0, pos+2);//删除buffer中从位置0开始的pos+2个字符  因为\n\n是2个字符
               
                //解析该块响应数据中的模型返回的有效数据  现在chunk已经截取出来了  可以解析了

                //处理空行和注释行 以':'开头的行是注释行  不需要解析
                if(chunk.empty()||chunk[0]==':'){
                    continue;//如果chunk为空 或者 第一个字符是':' 就跳过
                }

                //获取模型返回的有效数据  
               if(chunk.compare(0,6,"data: ")==0){//如果chunk中没有data字段  就跳过
                    std::string modelData = chunk.substr(6);//从data: 后面开始截取  截取到chunk的结束
                    
                    //检测是否为结束标记
                    if(modelData=="[DONE]"){
                        callback("", true);//将空字符串传递给回调函数  表示流式响应结束
                        streamFinish = true;//设置流式响应完成标记为true
                        return true;//返回true  流式响应完成
                    }

                    //反序列化
                    Json::Value modelDataJson;//保存反序列化后的JSON对象
                    Json::CharReaderBuilder reader;
                    std::string errors;//保存反序列化错误信息
                    std::istringstream modelDataStream(modelData);//把modelData转换为输入流  用于反序列化JSON对象
                    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();//获取delta字段中的content字段  并将其转换为字符串
                            fullResponse += content;//将content添加到fullResponse中

                            //将本次解析出的模型返回的有效数据转给调用sendMessageStream的用户使用 利用callback 回调函数
                            callback(content,false);//第一个参数将content传递给回调函数  第二个参数将false传递给回调函数  表示流式响应没有结束 继续去找下一个数据块
                        }
                    }else{ //如果反序列化解析失败
                        WARN("GeminiProvider sendMessageStream parse modelDataJson error: {}", errors);//打印反序列化错误信息
                    }

               }

            }//end while
            return true;
            
        };//end content_receiver lambda函数结束

        //给模型发送请求
        auto result = client.send(request);//我们需要对result进行检测
        if(!result){
            //如果result为空  就认为是失败
            //请求失败 出现网络问题,比如DNS解析失败
            //auto err = result.error();
            //ERR("Network error : {}", std::to_string(static_cast<int>(err)));
            ERR("Network error : {}", to_string(result.error()));
            return "";//返回空字符串
        }

        // 确保流式操作正常结束
        if(!streamFinish){
            WARN("Stream ended without [DONE] marker");//如果流式响应没有结束标记  就打印警告日志
            callback("", true);//将空字符串传递给回调函数  表示流式响应结束
        }


        return fullResponse;
    }//end sendMessageStream流式返回
}//end namespace ai_chat_sdk

7.测试:

这里也就测试成功了

相关推荐
Daydream.V2 小时前
基于Opencv和Dlib的人脸换脸实现
人工智能·opencv·计算机视觉·仿射变换·换脸·视频换脸·图片换脸
贺小涛2 小时前
DeepSeek vs ChatGPT:技术架构深度解析与核心优势对比
chatgpt·架构
没有退路那我就不要散步2 小时前
升级NPU驱动和固件,对上层的AI推理服务有多大影响?
人工智能
CSDN官方博客2 小时前
【奖励到账】CSDN AI 社区镜像创作激励活动第十二批奖励补发发放!
人工智能
电子科技圈2 小时前
赋能高端音频功能促进多样化设备创新——XMOS USB Audio平台实现四大功能升级
人工智能·mcu·音视频·智能家居·边缘计算·语音识别·智能硬件
nunca_te_rindas2 小时前
deepseek专家模式--20260408
人工智能
AI成长日志2 小时前
【AI原生开发实战】2.1 Prompt工程基础:编写高质量提示词
人工智能·prompt·ai-native
ar01232 小时前
AR远程协助平台:重塑工业与服务协作的新模式
人工智能·ar
ar01232 小时前
AR远程指导:赋能工业智能化的关键力量
人工智能·ar