文章目录


一、对日志库spdlog进行封装

1. 使用单例模式 对日志库spdlog进行封装

  • …/AI_mode-acess-SDK/sdk/include/util/mylog.h
#include <spdlog/async.h>
#include <mutex>
#include <string>
#include <fmt/core.h>
#include <fmt/format.h>

// 对spdlog库采用单例模式进行简单封装
namespace MyLog {
class Logger {
    std::shared_ptr<spdlog::logger> _tp;
    std::mutex _mutex;                      // 线程安全锁

public:
    static Logger &instance();                                     // 获取单例实例,线程安全的
    void init_logger(const std::string &name, spdlog::level::level_enum level, 
                            const std::string &file_path);        // 初始化日志记录器
    std::shared_ptr<spdlog::logger> get_logger();                 // 获取日志记录器

    Logger() = default;                          // 禁用外部构造
    Logger(const Logger &) = delete;             // 禁用拷贝
    Logger &operator=(const Logger &) = delete;  // 禁用赋值
};
} // namespace MyLog

#define DBG(format, ...) MyLog::Logger::instance().get_logger()->debug(std::string(" [{:>10s}:{:<4d}] ")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define INFO(format, ...) MyLog::Logger::instance().get_logger()->info(std::string(" [{:>10s}:{:<4d}] ")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define WARN(format, ...) MyLog::Logger::instance().get_logger()->warn(std::string(" [{:>10s}:{:<4d}] ")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define ERR(format, ...) MyLog::Logger::instance().get_logger()->error(std::string(" [{:>10s}:{:<4d}] ")+format, __FILE__, __LINE__, ##__VA_ARGS__)
#define CRIT(format, ...) MyLog::Logger::instance().get_logger()->critical(std::string(" [{:>10s}:{:<4d}] ")+format, __FILE__, __LINE__, ##__VA_ARGS__)
  • …/AI_mode-acess-SDK/sdk/src/util/mylog.h
#include "../../include/util/mylog.cpp"

namespace MyLog {
    Logger &Logger::instance()
    {
        static Logger instance;     // 只能创建一个静态实例
        return instance;
    }

    void Logger::init_logger(const std::string &name, spdlog::level::level_enum level, 
                             const std::string &file_path)
    {
        if (!_tp)
        {
            std::lock_guard<std::mutex> lock(_mutex);
            if (!_tp)
            {
                spdlog::set_level(level);    // 设置日志输出级别
                spdlog::set_pattern("%Y-%m-%d %H:%M:%S [%t] [%-8l] %v");   // 设置日志输出格式
                if(file_path == "stdout")
                {
                    // 同步日志记录器,用于控制台输出
                    _tp = spdlog::stdout_color_mt(name);
                }
                else
                {
                    // 初始化全局线程池 的任务队列数 和 线程数量
                    spdlog::init_thread_pool(4096, 2);
                    // 异步日志记录器,用于文件输出
                    _tp = spdlog::basic_logger_mt<spdlog::async_factory>(name, file_path);
                }
            }
        }
    }

    std::shared_ptr<spdlog::logger> Logger::get_logger()
    {
        return _tp;
    }
} // namespace MyLog

2. 问题:__VA_ARGS__前添加##的作用

在 C/C++ 的预处理器中,## 是连接运算符(token-pasting operator)。当它被放置在 __VA_ARGS__之前时,其核心作用是:在可变参数宏中,处理可变参数为空的情况,以避免产生语法错误。

具体到提供的日志宏定义中:

#define DBG(format, ...) MyLog::Logger::instance().get_logger()->debug(std::string(" [{:>10s}:{:<4d}] ")+format, __FILE__, __LINE__, ##__VA_ARGS__)

其作用机制如下:

  • 正常情况(提供了可变参数): 当宏被调用如 DBG(“value = {}”, 42) 时,… 对应参数 42,__VA_ARGS__展开为 42。## 在此场景下与直接使用 __VA_ARGS__效果相同,宏展开后为:
get_logger()->debug(std::string(" [{:>10s}:{:<4d}] ")+"value = {}", __FILE__, __LINE__, 42)

语法完全正确。

  • 关键情况(可变参数为空): 当宏被调用如 DBG(“Service started”) 时,… 没有对应任何参数,__VA_ARGS__展开为空。如果没有 ## 前缀,宏展开后末尾会留下一个多余的逗号:
get_logger()->debug(std::string(" [{:>10s}:{:<4d}] ")+"Service started", __FILE__, __LINE__, )

这个多余的逗号在 C++ 中构成语法错误。而使用了 ##__VA_ARGS__后,预处理器在 __VA_ARGS__为空时,会“吞噬”掉它前面的那个逗号。因此,展开后的代码变为:

get_logger()->debug(std::string(" [{:>10s}:{:<4d}] ")+"Service started", __FILE__, __LINE__)

语法正确。

总结:在日志宏中使用 ##__VA_ARGS __,是为了确保在日志消息不需要额外格式化参数(即只有格式字符串 format)时,宏依然能够正确展开,从而增强宏的健壮性和使用的便利性。这是一种处理可变参数宏边缘情况的常见技巧。

3. 使用对日志库spdlog封装后的单例类

#include <gtest/gtest.h>
#include "../sdk/include/util/mylog.h"

TEST(MyTest, TestMylog)
{
    // 初始化后,便可直接使用
    DBG("Debug message: {}", "Hello, World!");
    INFO("Info message: {}", "Hello, World!");
    WARN("Warn message: {}", "Hello, World!");
    ERR("Error message: {}", "Hello, World!");
    CRIT("Critical message: {}", "Hello, World!");
}

int main(int argc, char *argv[])
{
    testing::InitGoogleTest(&argc, argv);
    // 对日志类单例对象 进行初始化
    MyLog::Logger::instance().init_logger("sync_logger", spdlog::level::level_enum::debug, "stdout");
    return RUN_ALL_TESTS();
}

在这里插入图片描述

二、Apifox 使用API访问大模型

1. API key的介绍

API Key是一种用于身份验证和授权的密钥,本质就是一个通过特定算法生成的字符串,主要用于:

  • 身份验证: 验证你的应用程序是否被授权,只有拥有API Key的用户才能调用大模型的API
  • 授权: 限制应用程序对特定功能或资源的访问权限。比如:某些高级功能需要特定API Key才能访问
  • 监控和限制: 监控API的使用情况,比如统计请求次数、限制请求频率,以防滥用
  • 计费: 根据API的使用情况,向用户收取费用

考上大学后,家长为方便给你提供生活费以及学费,帮你办了一张某银行的银行卡,拿着银行卡就可以享受该银行提供的查询、取钱、转账等服务。

API Key: 就像银行卡,每次办理业务时都需要带上银行卡

⾝份认证: 你去银行柜台办理业务,柜员会要求你出示银行卡。刷卡或读卡的过程就是验证这张卡是否真实、有效,是否属于本行

授权: 你的普通储蓄卡只能存取款和转账,而你父母的VIP黑金卡还能享受机场贵宾厅、专属理财经理等服务。卡的等级决定了你能做什么

监控和限制: 银行会监控每张卡的交易。你的银行卡可能有单日转账限额1万元的规定,防止卡片被盗后损失过大

计费: 银行会根据你的账户流水向你收取手续费、短信通知费等。

2. 获取API key(以DeepSeek为例)

【DeepSeek api-key获取】

官网: https://www.deepseek.com

  • 进入官网,点击 API 开放平台

在这里插入图片描述

  • 进入 API 开放平台,点击 API keys

在这里插入图片描述

  • 创建 API keys 即可

在这里插入图片描述

3. DeepSeek提供的API接口

DeepSeek的 Chat Completion的参数说明: https://api-docs.deepseek.com/zh-cn/api/create-chat-completion

Base URL: https://api.deepseek.com

请求URL: POST /chat/completions

为了兼容open AI格式,也支持 POST /v1/chat/completions

  • 请求头参数:
字段名称 字段类型 字段说明
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 字段说明
messages array 历史对话,内部为每个对话的object,包含role(system:指定模型的身份,比如 你是一名资深数学家;user:用户的问题;assistant:模型的回答) 和 content两个字段
model string 模型名称(deepseek-v4-flash / deepseek-v4-pro)
temperature number 采样温度,介于 0 和 2 之间。更高的值,如 0.8,会使输出更随机,而更低的值,如 0.2,会使其更加集中和确定。
max_tokens integer 限制一次请求中模型生成 completion 的最大 token 数
stream boolean - 未设置时,默认为 false
- 如果设置为 true,将会以 SSE(server-sent events)的形式以流式发送消息增量。消息流以 data: [DONE] 结尾。

4. Apifox 使用API访问大模型

4.1 在设计模式 设置好各种参数(先设置 全量返回 的参数)

  • 将测试环境的 前置 URL(Base URL)设置为 https://api.deepseek.com

在这里插入图片描述

在这里插入图片描述

  • 将请求URL设置为 POST /chat/completions

在这里插入图片描述

  • 设置请求头参数

api_key比较私密,可以设置成环境变量

在这里插入图片描述
在这里插入图片描述

  • 设置请求体参数

在这里插入图片描述

4.2 在调试模式测试 全量返回

在这里插入图片描述

4.3 在设计模式设置 流式返回的参数

  • 设置请求头参数(和全量返回的参数一模一样)

在这里插入图片描述

  • 设置请求体参数(将stream的值设置为true)

在这里插入图片描述

4.4 在调试模式测试 流式返回

在这里插入图片描述

在这里插入图片描述

三、通过C++代码 调用API访问大模型

1. 头文件common.h (Message类的定义,其它类的定义后续都会用到)

#pragma once
#include <string>
#include <vector>
#include <ctime>
#include <cstdlib>

namespace ai_chat_sdk {
    // 消息结构
    struct Message {
        std::string _id;         // 消息ID(唯一标识, 可选)(流式返回时使用)
        std::string _role;       // "user" or "assistant"
        std::string _content;    // 消息内容
        std::time_t _create_at;  // 消息生成时间

        Message(const std::string& role = "", const std::string& content = "")
            : _role(role), _content(content), _create_at(std::time(nullptr)) 
            {}
        // std::time(nullptr) 的作用是获取当前系统的时间戳
    };

    // 会话结构
    struct Session {
        std::string _id;                 // 会话ID(唯一标识)
        std::string _model_name;         // 模型名称
        std::vector<Message> _messages;  // 会话中的消息列表
        std::time_t _created_at;         // 会话创建时间
        std::time_t _updated_at;         // 会话更新时间

        Session(const std::string& model_name = "")
            : _model_name(model_name), _created_at(std::time(nullptr)), _updated_at(std::time(nullptr)) 
            {}
        // std::time(nullptr) 的作用是获取当前系统的时间戳
    };

    // 模型信息
    struct ModelInfo {
        std::string _name;         // 模型名称
        std::string _desc;         // 模型描述信息
        bool _available = false;   // 模型是否可用

        ModelInfo(const std::string& name = "", const std::string& desc = "")
            : _name(name), _desc(desc) 
            {}
        // 构造函数,初始化模型名称、描述信息
    };

    // 调用模型时的配置信息
    struct Config {
        std::string _model_name;         // 模型名称
        double _temperature;             // 温度参数,默认值为0.7;
        int _max_tokens;                 // 最大生成token数,默认值为2048;

        Config(const std::string& model_name, double temperature = 0.7, int max_tokens = 2048)
            : _model_name(model_name), _temperature(temperature), _max_tokens(max_tokens) 
            {}
        // 构造函数,初始化模型名称和配置参数

        virtual ~Config() = default; // 虚析构函数,确保子类可以调用析构函数用于释放资源
                                     // dynamic_pointer_cast使用要求:基类有虚函数
    };

    // API配置结构(API接入云端模型时的配置信息)
    struct ApiConfig : public Config {
        std::string _api_key;            // API密钥(接入云端模型时的认证信息)
        std::string _api_url;            // API基础URL(例如:"https://api.deepseek.cn/v1")

        // 构造函数,初始化模型名称、温度参数、最大生成token数参数 以及 API密钥参数、API基础URL参数
        ApiConfig(const std::string& model_name, double temperature = 0.7, int max_tokens = 2048,
                  const std::string& key = getenv("deepseek_apikey"), const std::string& url = "")
            : Config(model_name, temperature, max_tokens), 
              _api_key(key), 
              _api_url(url) 
              {}
    };

    // Ollama本地接入大模型时的配置信息
    struct OllamaConfig : public Config {
        std::string _ollama_url;         // Ollama本地模型地址(默认值为"http://localhost:11434")
        std::string _model_desc;         // 模型描述信息(默认值为空字符串)

        // 构造函数,初始化模型名称、温度参数、最大生成token数参数 以及 Ollama本地模型地址、模型描述信息
        OllamaConfig(const std::string& model_name, double temperature = 0.7, int max_tokens = 2048,
                     const std::string& url = "http://localhost:11434", const std::string& desc = "")
            : Config(model_name, temperature, max_tokens), 
              _ollama_url(url), 
              _model_desc(desc) 
              {}
    };
} // namespace ai_chat_sdk

2. 模型Provider

2.1 策略模式(多态)

假设你现在要从宿舍去学校图书馆,但宿舍到图书馆之间有一段距离,你可以采用以下三种方式去:

  • 走路(最节省钱,但慢)
  • 骑自行车(中等速度,中等花销)
  • 坐校内公交车(最快,但贵)

策略方式实现:

  • 定义一个接口 TransportStrategy (出行策略)。
  • 分别实现 WalkStrategy 、 BikeStrategy 、 TaxiStrategy 。
  • 在运行时,你可以随时切换策略。
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;
}

程序非常美观且灵活,在使用时只需和TransportStrategy 打交道,不需要知道背后到底是 WalkStrategy、BikeStrategy 或 BusStrategy。如果想更换模式,只需要更换⼀个具体的策略对象即可,程序基本不需要改动。

策略模式是设计模式的一种,它的核心思想是它定义了一些列算法,将每一个算法(或行为)封装起来,使它们可以相互替换。即把 “做事的方式” 抽象出来,运行时根据需要选择哪种方式去执行。

2.2 LLMProvider(抽象类)

后续会借助API的方式接入 DeepSeek、ChatGPT、Gemini等大模型,每个大模型将来都需要:

(1)初始化
(2)检测模型是否有效
(3)获取模型名称
(4)获取模型描述
(5)发送消息给模型
(6)保存模型的有效状态、API Key、模型描述。

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

在这里插入图片描述

#pragma once
#include <map>
#include <string>
#include <vector>
#include <functional>
#include "common.h"

namespace ai_chat_sdk{
    class LLMProvider {
    public:
        // 初始化模型
        virtual bool init_Model(const std::map<std::string, std::string>& model_config) = 0;
        // 检测模型是否有效
        virtual bool isAvailable() = 0;
        // 获取模型名称
        virtual std::string getModelName() = 0;
        // 获取模型描述信息
        virtual std::string getModelDesc() = 0;
        // 发送消息给模型
        virtual std::string sendMessage(const std::vector<Message>& messages, 
                                        const std::map<std::string,std::string>& request_param) = 0;
        // 发送消息给模型-流式返回
        virtual std::string sendMessageStream(const std::vector<Message>& messages, 
                                            const std::map<std::string,std::string>& request_param,
                                            std::function<void(const std::string&, bool)> callback) = 0;
    
    protected:
        bool _isAvailable = false;  // 标记模型是否有效
        std::string _api_key;       // 保存模型的API Key
        std::string _base_url;      // 保存模型的base url
    };
} // namespace ai_chat_sdk

3. deepseek大模型接入

3.1 DeepSeek提供的API接口

DeepSeek的 Chat Completion的参数说明: https://api-docs.deepseek.com/zh-cn/api/create-chat-completion

Base URL: https://api.deepseek.com

请求URL: POST /chat/completions

为了兼容open AI格式,也支持 POST /v1/chat/completions

  • 请求头参数:
字段名称 字段类型 字段说明
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 字段说明
messages array 历史对话,内部为每个对话的object,包含role(system:指定模型的身份,比如 你是一名资深数学家;user:用户的问题;assistant:模型的回答) 和 content两个字段
model string 模型名称(deepseek-v4-flash / deepseek-v4-pro)
temperature number 采样温度,介于 0 和 2 之间。更高的值,如 0.8,会使输出更随机,而更低的值,如 0.2,会使其更加集中和确定。
max_tokens integer 限制一次请求中模型生成 completion 的最大 token 数
stream boolean - 未设置时,默认为 false
- 如果设置为 true,将会以 SSE(server-sent events)的形式以流式发送消息增量。消息流以 data: [DONE] 结尾。

3.2 大模型初始化

在使用 deepseek前,需要先配置好deepseek需要的一些参数信息,比如 api-key、base url、model、temperature、max_tokens等信息,否则无法正常使用 deepseek的api。

api-key 和 base url 这两个参数的内容基本是固定的,在模型初始化时设置好,后续进行对话时直接使用设置好的参数即可

  • DeepSeekProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class DeepSeekProvider : public LLMProvider {
    public:
        // 初始化模型(初始化模型的API Key 和 base url)
        bool init_Model(const std::map<std::string, std::string>& model_config) override;
        // 检测模型是否有效
        bool isAvailable() override;
        // 获取模型名称
        std::string getModelName() override;
        // 获取模型描述信息
        std::string getModelDesc() override;
        
				......
				
    };
} // namespace ai_chat_sdk
  • DeepSeekProvider.cpp(部分)
#include "../include/DeepSeekProvider.h"
#include "../include/util/mylog.h"
#include <jsoncpp/json/json.h>
#include <httplib.h>
#include <sstream>
#include <iostream>
#include <memory>
#include <map>

namespace ai_chat_sdk{
    // 初始化模型(初始化模型的API Key和 base url)
    bool DeepSeekProvider::init_Model(const std::map<std::string, std::string>& model_config) 
    {
        auto it = model_config.find("api_key");
        if(it == model_config.end()) {
            ERR("api_key is not found in model_config");
            return false;
        }
        _api_key = it->second;
        it = model_config.find("base_url");
        if(it == model_config.end()) {
            ERR("base_url is not found in model_config");
            return false;
        }
        _base_url = it->second;

        // 设置初始化成功标记
        _isAvailable = true;
        INFO("init success!!!!");
        INFO("DeepSeek provider init successs with base_url : {}", _base_url);
        return true;
    }

    bool DeepSeekProvider::isAvailable() {
        return _isAvailable;
    }

    std::string DeepSeekProvider::getModelName() {
        return "deepseek-v4-flash";
    }

    std::string DeepSeekProvider::getModelDesc() {
        return "一款实用性强、中文优化的通用对话助手,适合日常问答与创作";
    }
} // namespace ai_chat_sdk

3.3 发送消息-全量返回

Base URL: https://api.deepseek.com

请求URL: POST /chat/completions

  • 请求头参数:
字段名称 字段类型 设置值
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 设置值
messages array 通过参数 const std::vector< Message >& messages 获取
model string deepseek-v4-flash
temperature number 通过参数 const std::mapstd::string,std::string& request_param 获取
max_tokens integer 通过参数 const std::mapstd::string,std::string& request_param 获取
stream boolean false(全量返回,设置为false)

请求体示例(json格式):

{
    "messages": [
        {
            "content": "你是谁",
            "role": "user"
        }
    ],
    "model": "deepseek-v4-flash",
    "temperature ": 0.8,
    "max_tokens ": 2048,
    "stream": false
}

模型回复 的响应体示例(json格式):

最终是要提取 “content” 对应的内容(里面是模型的回复信息)

{
    "id": "1e2afdd3-dde1-4569-8aba-8c85723d6cbc",
    "object": "chat.completion",
    "created": 1780398683,
    "model": "deepseek-v4-flash",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "你好!我是 DeepSeek,由深度求索公司创造的 AI 助手。😊\n\n我是一个纯文本模型,擅长回
答问题、协助创作、分析问题、编程等等。我支持:\n- 📱 阅读链接内容\n- 📄 上传并处理图片、txt、pdf、ppt、word、excel
文件(从中提取文字)\n- 🌐 联网搜索(需要你在网页/App 手动开启)\n- 💬 超长上下文(1M tokens,相当于能一次性处理
《三体》三部曲那么多内容!)\n\n我的知识截止于 20255月,完全免费使用,无论是网页端还是 App 端都不收费。App 端还支
持语音输入哦!\n\n有什么我可以帮你的吗?尽管问!🌟",
                "reasoning_content": "嗯,用户问了一个很基础的自我介绍问题:“你是谁”。这是一个简单直接的问题,不
需要复杂拆解。用户可能是第一次接触我,想了解我的身份和基本能力。\n\n我需要给出清晰、友好、全面的自我介绍。想到了要说明我
是DeepSeek,由深度求索公司创造。可以简要列出我的核心特点:免费使用、文本模型、文件处理能力、联网搜索(需手动开启)、长上
下文,以及知识截止日期。最后以热情的语气收尾,表明我愿意提供帮助。\n\n用简洁的段落呈现这些信息,避免使用列表格式,保持自
然流畅的对话感。"
            },
            "logprobs": null,
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 5,
        "completion_tokens": 288,
        "total_tokens": 293,
        "prompt_tokens_details": {
            "cached_tokens": 0
        },
        "completion_tokens_details": {
            "reasoning_tokens": 124
        },
        "prompt_cache_hit_tokens": 0,
        "prompt_cache_miss_tokens": 5
    },
    "system_fingerprint": "fp_8b330d02d0_prod0820_fp8_kvcache_20260402"
}

  • DeepSeekProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class DeepSeekProvider : public LLMProvider {
    public:
    	                      		   ......
    	                      		   
        // 发送消息给模型
        std::string sendMessage(const std::vector<Message>& messages, 
                                const std::map<std::string,std::string>& request_param) override;
                                        
                                        ......
    };
} // namespace ai_chat_sdk
  • DeepSeekProvider.cpp(部分)
namespace ai_chat_sdk{
                                      .......
    
    // 发送消息(非流式响应)
    std::string DeepSeekProvider::sendMessage(const std::vector<Message>& messages, 
                                              const std::map<std::string,std::string>& request_param) 
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();    // getModelName()返回 "deepseek-v4-flash"
        request_body["messages"] = messages_array;
        request_body["stream"] = false;
        request_body["temperature"] = temperature;
        request_body["max_tokens"] = max_tokens;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);
        
        /////////////////////////////// http相关内容 ///////////////////////////////
        // 创建HTTP客户端
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(60, 0); // 60秒读取超时

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

        // 发送POST请求
        httplib::Result res = client.Post("/chat/completions", headers, 
                                            request_body_str, "application/json");
        // 网络层失败(连接失败、超时等)
        if(!res) {
            ERR("HTTP Post error: {}", httplib::to_string(res.error()));
            return "";
        }
        // 请求成功但应用层失败(HTTP 状态码非 200)
        if(res->status != 200) {
            ERR("POST请求失败, status code : {}", res->status);
            return "";
        }
        DBG("POST请求成功, response body : {}", res->body);
        /////////////////////////////////////////////////////////////////////////

        // 解析响应体(字符串格式的JSON字符串)为 Json::Value对象
        Json::CharReaderBuilder srder;
        std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
        Json::Value resp;
        std::string error;
        bool parse_ret = srd->parse(res->body.c_str(),res->body.c_str() + res->body.size(), &resp, &error);
        if(!parse_ret) {
            ERR("Json解析失败, error : {}", error);
            return "";
        }

        // 从解析后的Json::Value对象中提取响应内容
        if(resp.isMember("choices") && resp["choices"].isArray() && !resp["choices"].empty()) 
        {
            Json::Value choice = resp["choices"][0];
            if(choice.isMember("message") && choice["message"].isObject()) {
                Json::Value message = choice["message"];
                if(message.isMember("content") && message["content"].isString()) 
                {
                    DBG("message content : {}", message["content"].asString());
                    return message["content"].asString();
                }
            }
        }
        
        // 解析失败,返回错误信息
        ERR("Invalid response format from DeepSeek API");
        return "";
    }

                                      .......
} // namespace ai_chat_sdk

3.4 测试代码:测试全量返回

  • test.cpp
#include <gtest/gtest.h>
#include "../sdk/include/util/mylog.h"
#include "../sdk/include/DeepSeekProvider.h"

TEST(DeepSeekProviderTest, sendMessageDeepSeek)
{
    // 创建DeepSeekProvider实例
    auto deepseekProvider = std::make_shared<ai_chat_sdk::DeepSeekProvider>();
    ASSERT_TRUE(deepseekProvider != nullptr);

    // 初始化模型配置
    std::map<std::string, std::string> model_config;
    // api_key比较私密, 我将api_key设置为环境变量中的deepseek_apikey的值
    model_config["api_key"] = std::getenv("deepseek_apikey");
    model_config["base_url"] = "https://api.deepseek.com";
    deepseekProvider->init_Model(model_config);
    ASSERT_TRUE(deepseekProvider->isAvailable());
    
    // 测试发送消息(非流式响应)
    std::vector<ai_chat_sdk::Message> messages;
    messages.push_back({"user", "你好"});
    std::map<std::string, std::string> param_map = {
        {"temperature", "0.7"},
        {"max_tokens", "2048"}
    };
    std::string response = deepseekProvider->sendMessage(messages, param_map);
    ASSERT_FALSE(response.empty());
    INFO("fulldata : {}", response);
}

int main(int argc, char *argv[])
{
    // 初始化gtest库
    testing::InitGoogleTest(&argc, argv);
    // 初始化⽇志库
    MyLog::Logger::instance().init_logger("sync_logger", spdlog::level::level_enum::debug, "stdout");
    // 运⾏所有测试
    return RUN_ALL_TESTS();
}
  • CMakeLists.txt

注意:httplib库默认使用http协议,而deepseek的官网链接使用https协议,因此在编译时需要链接 OpenSSL开发库以支持SSL/TLS。否则在使用httplib创建http客户端进行通信时会报错。

ubuntu下安装OpenSSL开发库命令: sudo apt-get install libssl-dev

# CMake 最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(test_DeepSeekProvider)

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)

# 设置构建类型为Debug
set(CMAKE_BUILD_TYPE Debug)

# 添加可执行文件
add_executable(test_provider
        test.cpp 
        ../sdk/src/util/mylog.cpp 
        ../sdk/src/DeepSeekProvider.cpp
)

# 查找OpenSSL包
find_package(OpenSSL REQUIRED)

# 包含OpenSSL头文件路径
include_directories(${OPENSSL_INCLUDE_DIR})

# 添加编译定义
target_compile_definitions(test_provider PUBLIC
        CPPHTTPLIB_OPENSSL_SUPPORT
)

# 链接库
target_link_libraries(test_provider PRIVATE fmt gtest spdlog jsoncpp OpenSSL::SSL OpenSSL::Crypto)
  • 代码运行结果:

在这里插入图片描述

3.5 流式响应需要的协议

3.5.1 标准的HTTP协议(全量返回使用)

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

在这里插入图片描述

3.5.2 SSE协议(流式返回使用)

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

在这里插入图片描述

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

SSE协议特点

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

数据格式(每个事件可以包含以下字段):

  • data:消息内容(必须)
  • event:事件类型(可选)
  • id:消息ID(可选)
  • retry:重连时间(可选,单位:毫秒)
data: Hello, world!
event: message
id: 123
retry: 10000

data: Hello, hello
event: message
id: 1245
retry: 10000

......

data: [DONE]

每条消息以两个换行符 (\n\n) 结束,消息流传输完毕后会有专门的结束标记,不同实现结束标记不同,比如 data: [DONE]。

当我们向 DeepSeek、ChatGPT、Gemini等大模型提问时,这些大模型并不是⼀次性将完整回答丢给用户,而是服务器边思考,边主动将思考结果吐(推送)给用户的,就和打字一样一点点输出,用户不需要长时间的等待,能及时看到服务器响应的结果,体验比较好,这种方式称为流式响应。SSE推出后实际不温不火,大模型爆火后,正式大模型场景的需要,SSE协议就爆火了。

3.5.3 WebSocket协议(后续补充)

SSE协议有⼀个缺陷就是单向传输,即数据只能由服务器给客户端推送,在新闻推送、股票行情、体育比分等场景是比较合适的,因为这些场景客户端无需给服务器发数据。

但有些场景 SSE就束手无策了。比如:你在宿舍的微信群里发了一个消息 “谁去食堂帮我捎个饭”,服务器收到后需要 “谁去食堂帮我捎个个饭” 这条消息主动推送给群中其他人,其他人收到消息后,就需要发消息回应你 而不是不闻不问。此处由舍友回复"滚犊子",那服务器收到后又要推送给其他人…

该场景中,不仅需要服务器主动给客户端推送消息,也需要客户端给服务器发送消息。这种场景下 WebSocket协议就派上用场了。

在这里插入图片描述

3.5.4 对比SSE协议 和 WebSocket协议(流式返回选择SSE的理由)
特性 SSE WebSocket
通信方向 单向通道:服务器 → 客户端 双向通道:服务器 <=> 客户端
设计目的 服务器主动推送数据(如新闻、状态更新) 双向实时对话(如聊天、游戏操作同步)
协议 HTTP 独立的TCP协议,在HTTP握手后升级协议(ws/wss)
数据格式 纯文本 二进制 或 文本
自动重连 内置 需要手动实现
使用场景 实时通知、日志流、LLM响应等单向场景 实时聊天、多人在线游戏、实时交易等双向交互场景
  • 为什么DeepSeek的助手消息使用 SSE,不使用 websocket?

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

3.6 发送消息-流式返回

Base URL: https://api.deepseek.com

请求URL: POST /chat/completions

  • 请求头参数:
字段名称 字段类型 设置值
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 设置值
messages array 通过参数 const std::vector< Message >& messages 获取
model string deepseek-v4-flash
temperature number 通过参数 const std::mapstd::string,std::string& request_param 获取
max_tokens integer 通过参数 const std::mapstd::string,std::string& request_param 获取
stream boolean true(流式返回,设置为true)

请求体示例(json格式):

{
    "messages": [
        {
            "content": "你是谁",
            "role": "user"
        }
    ],
    "model": "deepseek-v4-flash",
    "temperature ": 0.8,
    "max_tokens ": 2048,
    "stream": true
}

模型回复 的响应体示例(json格式):

最终是要提取 “content” 对应的内容(里面是模型的回复信息)

data: {
    "id": "2765a174-d885-4d84-a226-b91c9b334e7e",
    "object": "chat.completion.chunk",
    "created": 1780486795,
    "model": "deepseek-v4-flash",
    "system_fingerprint": "fp_8b330d02d0_prod0820_fp8_kvcache_20260402",
    "choices": [
        {
            "index": 0,
            "delta": {
                "role": "assistant",
                "content": null,
                "reasoning_content": ""
            },
            "logprobs": null,
            "finish_reason": null
        }
    ]
}

data: {
    "id": "2765a174-d885-4d84-a226-b91c9b334e7e",
    "object": "chat.completion.chunk",
    "created": 1780486795,
    "model": "deepseek-v4-flash",
    "system_fingerprint": "fp_8b330d02d0_prod0820_fp8_kvcache_20260402",
    "choices": [
        {
            "index": 0,
            "delta": {
                "content": null,
                "reasoning_content": "嗯"
            },
            "logprobs": null,
            "finish_reason": null
        }
    ]
}

data: {
    "id": "2765a174-d885-4d84-a226-b91c9b334e7e",
    "object": "chat.completion.chunk",
    "created": 1780486795,
    "model": "deepseek-v4-flash",
    "system_fingerprint": "fp_8b330d02d0_prod0820_fp8_kvcache_20260402",
    "choices": [
        {
            "index": 0,
            "delta": {
                "content": null,
                "reasoning_content": ","
            },
            "logprobs": null,
            "finish_reason": null
        }
    ]
}

                        ......

data: {
    "id": "2765a174-d885-4d84-a226-b91c9b334e7e",
    "object": "chat.completion.chunk",
    "created": 1780486795,
    "model": "deepseek-v4-flash",
    "system_fingerprint": "fp_8b330d02d0_prod0820_fp8_kvcache_20260402",
    "choices": [
        {
            "index": 0,
            "delta": {
                "content": "✨",
                "reasoning_content": null
            },
            "logprobs": null,
            "finish_reason": null
        }
    ]
}

data: {
    "id": "2765a174-d885-4d84-a226-b91c9b334e7e",
    "object": "chat.completion.chunk",
    "created": 1780486795,
    "model": "deepseek-v4-flash",
    "system_fingerprint": "fp_8b330d02d0_prod0820_fp8_kvcache_20260402",
    "choices": [
        {
            "index": 0,
            "delta": {
                "content": "",
                "reasoning_content": null
            },
            "logprobs": null,
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 5,
        "completion_tokens": 245,
        "total_tokens": 250,
        "prompt_tokens_details": {
            "cached_tokens": 0
        },
        "completion_tokens_details": {
            "reasoning_tokens": 104
        },
        "prompt_cache_hit_tokens": 0,
        "prompt_cache_miss_tokens": 5
    }
}

data: [DONE]

  • DeepSeekProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class DeepSeekProvider : public LLMProvider {
    public:
    	                      		   ......
    	                      		   
        // 发送消息给模型-流式返回
        std::string sendMessageStream(const std::vector<Message>& messages, 
                                      const std::map<std::string,std::string>& request_param,
                                      std::function<void(const std::string&, bool)> callback) override;
                                        
                                        ......
    };
} // namespace ai_chat_sdk
  • DeepSeekProvider.cpp(部分)
namespace ai_chat_sdk{
                                      .......
    
    // 实现流式响应
    std::string DeepSeekProvider::sendMessageStream(const std::vector<Message>& messages, 
                                 const std::map<std::string,std::string>& request_param,
                                 std::function<void(const std::string&, bool)> callback)
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = true;             // 开启流式响应
        request_body["temperature"] = temperature;
        request_body["max_tokens"] = max_tokens;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);

        //////////////////////////////////////// 与非流式响应有区别的内容 ////////////////////////////////////////
        // 创建HTTP Client
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(300, 0);      // 流式响应需要更⻓的时间

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

        // 流式处理变量
        std::string buffer;         // 接收流式响应的数据块
        bool gotError = false;      // 响应是否成功(成功false,失败true)
        std::string errorMsg;       // 错误描述符
        int statusCode = 0;         // 状态码
        bool streamFinish = false;  // 标记流式返回数据是否结束
        std::string fullResponse;   // 累积完整的响应

        /////////////////////////////// 构建HTTP请求(设置处理函数) ///////////////////////////////
        // 创建请求对象
        httplib::Request req;
        req.method = "POST";
        req.path = "/chat/completions";
        req.headers = headers;
        req.body = request_body_str;
        // HTTP协议响应的格式: 
        // ⼀个状态⾏:HTTP/1.1 200 OK
        // ⼀组响应头:Content-Type,Content-Length
        // 空⾏:\r\n\r\n
        // 响应体
        
        // 普通的HTTP响应是 ⼀头⼀体
        // 流式响应:只有⼀个响应头,但响应体被拆分成多个块(chunks)陆续发送,即⼀头多块
        
        // 响应头处理(检查状态码): 状态码不是200时,记录错误信息
        req.response_handler = [&](const httplib::Response& res) {
            int statusCode = res.status;
            if(200 != statusCode){
                gotError = true;
                errorMsg = "HTTP Error:" + std::to_string(statusCode);
                return false; // 终⽌请求(不再调用 content_receiver)
            }
            return true; // 继续接收数据(继续调用 content_receiver 接收响应体chunk)
        };

        // 响应体处理(流式回调)
        // data:指向当前接收到的数据块的指针
        // len:当前数据块的长度
        // offset:当前数据块在响应体中的偏移量
        // totalLength:响应体的总长度
        // 流式响应 主要关注前两个参数 data 和 len 就足够了。因为在流式响应的场景下,总长度通常未知,需要持续接收直到 [DONE] 标记出现
        req.content_receiver = [&](const char* data, size_t len, uint64_t offset, uint64_t totalLength)
        {
            // 如果http请求头出错,就不需要再继续接收了
            if(gotError)
            {
                return false;
            }
            // 追加新数据到缓冲区
            buffer.append(data, len);
            DBG("buffer: {}", buffer);
            // 处理所有完整的事件,事件和事件之间以\n\n分隔
            size_t pos = 0;
            while((pos = buffer.find("\n\n")) != std::string::npos)
            {
                std::string event = buffer.substr(0, pos);
                buffer.erase(0, pos+2); // 移除已经处理的事件
                // 处理空⾏和注释, 以:开头的是注释⾏
                if(event.empty() || event[0] == ':')
                {
                    continue;
                }
                // 检查事件类型: data: JSON字符串
                if(event.compare(0, 6, "data: ") == 0)
                {
                    std::string jsonStr = event.substr(6);
                    ////// 关键代码:处理结束标记(判断流式响应是否结束) ///////
                    if(jsonStr == "[DONE]")
                    {
                        callback("", true);    // 返回true, 代表流式响应结束
                        streamFinish = true;   // 标记流式响应结束
                        return true;
                    }

                    // 反序列化:将JSON字符串 转换为 Json::Value对象
                    Json::Value chunk;  
                    Json::CharReaderBuilder srder;
                    std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
                    std::string errs;
                    bool read_ret = srd->parse(jsonStr.c_str(), jsonStr.c_str() + jsonStr.size(), &chunk, &errs);
                    if(!read_ret)
                    {
                        ERR("Json反序列化失败: {}", errs);
                        return false;
                    }

                    // 提取Json::Value对象中的 content字段
                    if(chunk.isMember("choices") && chunk["choices"].isArray() &&
                        !chunk["choices"].empty() && chunk["choices"][0].isMember("delta") &&
                        chunk["choices"][0]["delta"].isMember("content"))
                    {
                        std::string content = chunk["choices"][0]["delta"]["content"].asString();
                        // 累积到完整响应
                        fullResponse += content;
                        callback(content, false); // 调用回调函数处理增量内容, false代表流式响应未结束
                    }
                }           
            }
            return true; // 继续接收数据
        };
        ///////////////////////////// HTTP请求构建结束 //////////////////////////////

        // 发送HTTP请求并处理结果
        auto res = client.send(req);
        // send的返回值类型是Result类型,Result类型内部实现了operator bool(), 允许将Result类型的实例隐式转换为bool类型
        if(!res)
        {
            // 请求连接失败,⽹络问题、DNS解析失败等
            ERR("HTTP Post error: {}", httplib::to_string(res.error()));
            return "";
        }

        // 确保流正常结束
        if(!streamFinish){
            WARN("Stream ended without [DONE] marker");
            callback("", true);
        }
        return fullResponse;
    }

                                      .......
} // namespace ai_chat_sdk

3.7 测试代码:测试流式返回

  • test.cpp
#include <gtest/gtest.h>
#include "../sdk/include/util/mylog.h"
#include "../sdk/include/DeepSeekProvider.h"

TEST(DeepSeekProviderTest, sendMessageDeepSeek)
{
    // 创建DeepSeekProvider实例
    auto deepseekProvider = std::make_shared<ai_chat_sdk::DeepSeekProvider>();
    ASSERT_TRUE(deepseekProvider != nullptr);

    // 初始化模型配置
    std::map<std::string, std::string> model_config;
    // api_key比较私密, 我将api_key设置为环境变量中的deepseek_apikey的值
    model_config["api_key"] = std::getenv("deepseek_apikey");
    model_config["base_url"] = "https://api.deepseek.com";
    deepseekProvider->init_Model(model_config);
    ASSERT_TRUE(deepseekProvider->isAvailable());
    
    // 测试发送消息(流式响应)
    std::vector<ai_chat_sdk::Message> messages;
    messages.push_back({"user", "你好"});
    std::map<std::string, std::string> param_map = {
        {"temperature", "0.7"},
        {"max_tokens", "2048"}
    };
    auto write_chunk = [&](const std::string& chunk, bool last){
        INFO("chunk : {}", chunk);
        if(last){
            INFO("[DONE]");
        }
    };
    std::string response = deepseekProvider->sendMessageStream(messages, param_map, write_chunk);
    ASSERT_FALSE(response.empty());
    INFO("fulldata : {}", response);
}

int main(int argc, char *argv[])
{
    // 初始化gtest库
    testing::InitGoogleTest(&argc, argv);
    // 初始化⽇志库
    MyLog::Logger::instance().init_logger("sync_logger", spdlog::level::level_enum::debug, "stdout");
    // 运⾏所有测试
    return RUN_ALL_TESTS();
}
  • 代码运行结果:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4. ChatGPT大模型接入

4.1 ChatGPT提供的API接口

Chat Completion的官方参数文档:https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create

Base url: https://api.openai.com

请求URL: POST /v1/chat/completions

  • 请求头参数:
字段名称 字段类型 字段说明
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 字段说明
messages array 历史对话,内部为每个对话的object,包含role(developer:指定模型的身份,比如 你是一名资深数学家;user:用户的问题;assistant:模型的回答)和 content两个字段
model string 模型名称(“gpt-5.4” / “gpt-5.4-mini” / “gpt-5.4-nano”)
temperature number 采样温度,介于 0 和 2 之间。更高的值,如 0.8,会使输出更随机,而更低的值,如 0.2,会使其更加集中和确定。
max_tokens integer 限制一次请求中模型生成 completion 的最大 token 数
stream boolean - 未设置时,默认为 false
- 如果设置为 true,将会以 SSE(server-sent events)的形式以流式发送消息增量。

4.2 大模型初始化

在使用 Chatgpt前,需要先配置好Chatgpt需要的一些参数信息,比如 api-key、base url、model、temperature、max_tokens等信息,否则无法正常使用 Chatgpt的api。

api-key 和 base url 这两个参数的内容基本是固定的,在模型初始化时设置好,后续进行对话时直接使用设置好的参数即可

  • ChatgptProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class ChatgptProvider : public LLMProvider {
    public:
        // 初始化模型(初始化模型的API Key和base url)
        bool init_Model(const std::map<std::string, std::string>& model_config) override;
        // 检测模型是否有效
        bool isAvailable() override;
        // 获取模型名称
        std::string getModelName() override;
        // 获取模型描述信息
        std::string getModelDesc() override;
        
				......
				
    };
} // namespace ai_chat_sdk
  • ChatgptProvider.cpp(部分)
#include "../include/ChatgptProvider.h"
#include "../include/util/mylog.h"
#include <jsoncpp/json/json.h>
#include <httplib.h>
#include <sstream>
#include <iostream>
#include <memory>
#include <map>

namespace ai_chat_sdk{
    // 初始化模型(初始化模型的API Key和base url)
    bool ChatgptProvider::init_Model(const std::map<std::string, std::string>& model_config) 
    {
        auto it = model_config.find("api_key");
        if(it == model_config.end()) {
            ERR("api_key is not found in model_config");
            return false;
        }
        _api_key = it->second;
        it = model_config.find("base_url");
        if(it == model_config.end()) {
            ERR("base_url is not found in model_config");
            return false;
        }
        _base_url = it->second;

        // 设置初始化成功标记
        _isAvailable = true;
        INFO("init success!!!!");
        INFO("DeepSeek provider init successs with base_url : {}", _base_url);
        return true;
    }

    bool ChatgptProvider::isAvailable() {
        return _isAvailable;
    }

    std::string ChatgptProvider::getModelName() {
        return "gpt-5.4-mini";   // 模型名称
    }

    std::string ChatgptProvider::getModelDesc() {
        return "一款推理速度快、资源消耗低的轻量级语言模型, 适合快速响应的场景";
    }
} // namespace ai_chat_sdk

4.3 发送消息-全量返回

Base url:https://api.openai.com

请求URL: POST /v1/chat/completions

  • 请求头参数:
字段名称 字段类型 设置值
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 设置值
messages array 通过参数 const std::vector< Message >& messages 获取
model string gpt-5.4-mini
temperature number 通过参数 const std::mapstd::string,std::string& request_param 获取
max_tokens integer 通过参数 const std::mapstd::string,std::string& request_param 获取
stream boolean false(全量返回,设置为false)

请求体示例(json格式):

{
   "messages": [
     {
       "role": "developer",
       "content": "You are a helpful assistant."
     },
     {
       "role": "user",
       "content": "Hello!"
     }
   ]
   "model": "gpt-5.4-mini",
   "temperature ": 0.8,
   "max_tokens ": 2048,
   "stream": false
}

模型回复 的响应体示例(json格式):

最终是要提取 “content” 对应的内容(里面是模型的回复信息)

{
  "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT",
  "object": "chat.completion",
  "created": 1741569952,
  "model": "gpt-5.4",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hello! How can I assist you today?",
        "refusal": null,
        "annotations": []
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 19,
    "completion_tokens": 10,
    "total_tokens": 29,
    "prompt_tokens_details": {
      "cached_tokens": 0,
      "audio_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0,
      "audio_tokens": 0,
      "accepted_prediction_tokens": 0,
      "rejected_prediction_tokens": 0
    }
  },
  "service_tier": "default"
}

  • ChatgptProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class ChatgptProvider : public LLMProvider {
    public:
    	                      		   ......
    	                      		   
        // 发送消息给模型
        std::string sendMessage(const std::vector<Message>& messages, 
                                const std::map<std::string,std::string>& request_param) override;
                                        
                                        ......
    };
} // namespace ai_chat_sdk
  • ChatgptProvider.cpp(部分)
namespace ai_chat_sdk{
                                      .......
    
    // 发送消息(非流式响应)
    std::string ChatgptProvider::sendMessage(const std::vector<Message>& messages, 
                                             const std::map<std::string,std::string>& request_param)    
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = false;
        request_body["temperature"] = temperature;
        request_body["max_tokens"] = max_tokens;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);
        
        /////////////////////////////// http相关内容(我还不太了解) ///////////////////////////////
        // 创建HTTP客户端
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(60, 0); // 60秒读取超时

        // 设置请求头 (Content-Type是请求体的类型,和post最后一个参数重复)
        httplib::Headers headers = {
            {"Authorization", "Bearer " + _api_key},
            /* {"Content-Type", "application/json"} */
        };

        // 发送POST请求 (最后一个参数是请求体的类型, 这里是"application/json")
        httplib::Result res = client.Post("/v1/chat/completions", headers, 
                                            request_body_str, "application/json");
        // Result类型的 operator->重载 返回的是 Response对象的指针
        if(!res || res->status != 200) {
            ERR("POST请求失败, status code : {}", res->status);
            return "";
        }
        DBG("POST请求成功, response body : {}", res->body);
        ////////////////////////////////////////////////////////////////////////////////////////

        // 解析响应体(字符串格式的JSON字符串)为 Json::Value对象
        Json::CharReaderBuilder srder;
        std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
        Json::Value resp;
        std::string error;
        bool parse_ret = srd->parse(res->body.c_str(),res->body.c_str() + res->body.size(), &resp, &error);
        if(!parse_ret) {
            ERR("Json解析失败, error : {}", error);
            return "";
        }

        // 从解析后的Json::Value对象中提取响应内容
        if(resp.isMember("choices") && resp["choices"].isArray() && !resp["choices"].empty()) 
        {
            Json::Value choice = resp["choices"][0];
            if(choice.isMember("message") && choice["message"].isObject()) {
                Json::Value message = choice["message"];
                if(message.isMember("content") && message["content"].isString()) 
                {
                    DBG("message content : {}", message["content"].asString());
                    return message["content"].asString();
                }
            }
        }
        
        // 解析失败,返回错误信息
        ERR("Invalid response format from ChatGPT API");
        return "";
    }

                                      .......
} // namespace ai_chat_sdk

4.4 发送消息-流式返回

Base url:https://api.openai.com

请求URL: POST /v1/chat/completions

  • 请求头参数:
字段名称 字段类型 设置值
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 设置值
messages array 通过参数 const std::vector< Message >& messages 获取
model string gpt-5.4-mini
temperature number 通过参数 const std::mapstd::string,std::string& request_param 获取
max_tokens integer 通过参数 const std::mapstd::string,std::string& request_param 获取
stream boolean true(设置为true,开启流式返回)

请求体示例(json格式):

{
   "messages": [
     {
       "role": "developer",
       "content": "You are a helpful assistant."
     },
     {
       "role": "user",
       "content": "Hello!"
     }
   ]
   "model": "gpt-5.4-mini",
   "temperature ": 0.8,
   "max_tokens ": 2048,
   "stream": true
}

模型回复 的响应体示例(json格式):

最终是要提取 “content” 对应的内容(里面是模型的回复信息)

DeepSeek流式响应结束时,会发 data: [DONE];但 ChatGPT流式响应结束时通常是不会发 data: [DONE]的!
ChatGPT流式响应结束靠 finish_reason字段判断,finish_reason字段为null,代表流式响应继续,finish_reason字段不为null(被设置为"stop",代表正常结束;被设置为"length",代表达到 max_tokens 限制),代表流式响应结束

data: {
    "id": "chatcmpl-123",
    "object": "chat.completion.chunk",
    "created": 1694268190,
    "model": "gpt-4o-mini",
    "system_fingerprint": "fp_44709d6fcb",
    "choices": [
        {
            "index": 0,
            "delta": {
                "role": "assistant",
                "content": ""
            },
            "logprobs": null,
            "finish_reason": null
        }
    ]
}

data: {
    "id": "chatcmpl-123",
    "object": "chat.completion.chunk",
    "created": 1694268190,
    "model": "gpt-4o-mini",
    "system_fingerprint": "fp_44709d6fcb",
    "choices": [
        {
            "index": 0,
            "delta": {
                "content": "Hello"
            },
            "logprobs": null,
            "finish_reason": null
        }
    ]
}

                        ......

data: {
    "id": "chatcmpl-123",
    "object": "chat.completion.chunk",
    "created": 1694268190,
    "model": "gpt-4o-mini",
    "system_fingerprint": "fp_44709d6fcb",
    "choices": [
        {
            "index": 0,
            "delta": {},
            "logprobs": null,
            "finish_reason": "stop"
        }
    ]
}

  • ChatgptProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class ChatgptProvider : public LLMProvider {
    public:
    	                      		   ......
    	                      		   
        // 发送消息给模型-流式返回
        std::string sendMessageStream(const std::vector<Message>& messages, 
                                      const std::map<std::string,std::string>& request_param,
                                      std::function<void(const std::string&, bool)> callback) override;
                                        
                                        ......
    };
} // namespace ai_chat_sdk
  • ChatgptProvider.cpp(部分)
namespace ai_chat_sdk{
                                      .......
    
    // 实现流式响应
    std::string ChatgptProvider::sendMessageStream(const std::vector<Message>& messages, 
                                 const std::map<std::string,std::string>& request_param,
                                 std::function<void(const std::string&, bool)> callback)
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = true;            // 开启流式响应
        request_body["temperature"] = temperature;
        request_body["max_tokens"] = max_tokens;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);

        ///////////////////////////// 与非流式响应有区别的内容 //////////////////////////////
        // 创建HTTP Client
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(300, 0);      // 流式响应需要更⻓的时间

        // 设置请求头
        httplib::Headers headers = {
            {"Authorization", "Bearer " + _api_key},
            {"Content-Type", "application/json"},
            {"Accept", "text/event-stream"}       // 流式响应需要设置Accept头为text/event-stream
        };

        // 流式处理变量
        std::string buffer; // 接收流式响应的数据块
        bool gotError = false; // 响应是否成功(成功false,失败true)
        std::string errorMsg; // 错误描述符
        int statusCode = 0; // 状态码
        bool streamFinish = false; // 标记流式返回数据是否结束
        std::string fullResponse; // 累积完整的响应

        /////////////////////////// 构建HTTP请求(设置处理函数) ///////////////////////////
        // 创建请求对象
        httplib::Request req;
        req.method = "POST";
        req.path = "/v1/chat/completions";
        req.headers = headers;
        req.body = request_body_str;
        // HTTP协议响应的格式: 
        // ⼀个状态⾏:HTTP/1.1 200 OK
        // ⼀组响应头:Content-Type,Content-Length
        // 空⾏:\r\n\r\n
        // 响应体
        // 普通的HTTP响应是 ⼀头⼀体
        // 流式响应:只有⼀个响应头,但响应体被拆分成多个块(chunks)陆续发送,即⼀头多块
        
        // 响应头处理(检查状态码): 状态码不是200时,记录错误信息
        req.response_handler = [&](const httplib::Response& res) {
            int statusCode = res.status;
            if(200 != statusCode){
                gotError = true;
                errorMsg = "HTTP Error:" + std::to_string(statusCode);
                return false; // 终⽌请求
            }
            return true; // 继续接收数据
        };

        // 响应体处理(流式回调)
        // data:指向当前接收到的数据块的指针
        // len:当前数据块的长度
        // offset:当前数据块在响应体中的偏移量
        // totalLength:响应体的总长度
        // 流式响应 主要关注前两个参数 data 和 len 就足够了。因为在流式响应的场景下,总长度通常未知,需要持续接收直到 [DONE] 标记出现
        req.content_receiver = [&](const char* data, size_t len, uint64_t offset, uint64_t totalLength)
        {
            // 如果http请求头出错,就不需要再继续接收了
            if(gotError)
            {
                return false;
            }
            // 追加新数据到缓冲区
            buffer.append(data, len);
            DBG("buffer: {}", buffer);
            // 处理所有完整的事件,事件和事件之间以\n\n分隔
            size_t pos = 0;
            while((pos = buffer.find("\n\n")) != std::string::npos)
            {
                std::string event = buffer.substr(0, pos);
                buffer.erase(0, pos+2); // 移除已经处理的事件
                // 处理空⾏和注释, 以:开头的是注释行
                if(event.empty() || event[0] == ':')
                {
                    continue;
                }
                // 检查事件类型: data: JSON字符串
                if(event.compare(0, 6, "data: ") == 0)
                {
                    std::string jsonStr = event.substr(6);

                    // 反序列化:将JSON字符串 转换为 Json::Value对象
                    Json::Value chunk;  
                    Json::CharReaderBuilder srder;
                    std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
                    std::string errs;
                    bool read_ret = srd->parse(jsonStr.c_str(), jsonStr.c_str() + jsonStr.size(), &chunk, &errs);
                    if(!read_ret)
                    {
                        ERR("Json反序列化失败: {}", errs);
                        return false;
                    }

                    //// 关键代码:处理结束标记(判断流式响应是否结束) /////
                    // ChatGPT流式响应结束靠 finish_reason字段判断,finish_reason字段为null,代表流式响应继续,
                    // finish_reason字段不为null(被设置为"stop",代表正常结束;被设置为"length",代表达到 max_tokens 限制),
                    // 代表流式响应结束
                    if(chunk.isMember("choices") && chunk["choices"].isArray() &&
                        !chunk["choices"].empty() && chunk["choices"][0].isMember("finish_reason") &&
                        chunk["choices"][0]["finish_reason"].isString() &&
                        !chunk["choices"][0]["finish_reason"].isNull())
                    {
                        callback("", true);    // 返回true, 代表流式响应结束
                        streamFinish = true;   // 标记流式响应结束
                        return true;
                    }

                    // 提取Json::Value对象中的 content字段
                    if(chunk.isMember("choices") && chunk["choices"].isArray() &&
                        !chunk["choices"].empty() && chunk["choices"][0].isMember("delta") &&
                        chunk["choices"][0]["delta"].isMember("content"))
                    {
                        std::string content = chunk["choices"][0]["delta"]["content"].asString();
                        // 累积到完整响应
                        fullResponse += content;
                        callback(content, false); // 调用回调函数处理增量内容, false代表流式响应未结束
                    }
                }           
            }
            return true; // 继续接收数据
        };
        ////////////////////////////// HTTP请求构建结束 /////////////////////////////

        // 发送HTTP请求并处理结果
        auto res = client.send(req);
        // send的返回值类型是Result类型,Result类型内部实现了operator bool(), 允许将Result类型的实例隐式转换为bool类型
        if(!res)
        {
            // 请求连接失败,⽹络问题、DNS解析失败等
            ERR("HTTP Post error: {}", httplib::to_string(res.error()));
            return "";
        }

        // 确保流正常结束
        if(!streamFinish){
            WARN("Stream ended without stop marker");
            callback("", true);
        }
        return fullResponse;
    }

                                      .......
} // namespace ai_chat_sdk

5. Gemini大模型接入

5.1 Gemini提供的API接口

官方文档: https://ai.google.dev/gemini-api/docs?hl=zh-cn

Base URL: https://generativelanguage.googleapis.com

请求URL: POST /v1beta/openai/chat/completions

  • 请求头参数:
字段名称 字段类型 字段说明
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 字段说明
messages array 历史对话,内部为每个对话的object,包含role(user:用户的问题;assistant:模型的回答)和 content两个字段
model string 模型名称(“gemini-2.5-flash” / “gemini-2.5-pro”)
temperature number 采样温度,介于 0 和 2 之间。更高的值,如 0.8,会使输出更随机,而更低的值,如 0.2,会使其更加集中和确定。
max_tokens integer 限制一次请求中模型生成 completion 的最大 token 数
stream boolean - 未设置时,默认为 false
- 如果设置为 true,将会以 SSE(server-sent events)的形式以流式发送消息增量。消息流以 data: [DONE] 结尾。

5.2 大模型初始化

在使用 Gemini前,需要先配置好 Gemini需要的一些参数信息,比如 api-key、base url、model、temperature、max_tokens等信息,否则无法正常使用 Gemini的api。

api-key 和 base url 这两个参数的内容基本是固定的,在模型初始化时设置好,后续进行对话时直接使用设置好的参数即可

  • GeminiProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class GeminiProvider : public LLMProvider {
    public:
        // 初始化模型(初始化模型的API Key和base url)
        bool init_Model(const std::map<std::string, std::string>& model_config) override;
        // 检测模型是否有效
        bool isAvailable() override;
        // 获取模型名称
        std::string getModelName() override;
        // 获取模型描述信息
        std::string getModelDesc() override;
        
				......
				
    };
} // namespace ai_chat_sdk
  • GeminiProvider.cpp(部分)
#include "../include/GeminiProvider.h"
#include "../include/util/mylog.h"
#include <jsoncpp/json/json.h>
#include <httplib.h>
#include <sstream>
#include <iostream>
#include <memory>
#include <map>

namespace ai_chat_sdk{
    // 初始化模型(初始化模型的API Key和base url)
    bool GeminiProvider::init_Model(const std::map<std::string, std::string>& model_config) 
    {
        auto it = model_config.find("api_key");
        if(it == model_config.end()) {
            ERR("api_key is not found in model_config");
            return false;
        }
        _api_key = it->second;
        it = model_config.find("base_url");
        if(it == model_config.end()) {
            ERR("base_url is not found in model_config");
            return false;
        }
        _base_url = it->second;

        // 设置初始化成功标记
        _isAvailable = true;
        INFO("init success!!!!");
        INFO("DeepSeek provider init successs with base_url : {}", _base_url);
        return true;
    }

    bool GeminiProvider::isAvailable() {
        return _isAvailable;
    }

    std::string GeminiProvider::getModelName() {
        return "gemini-2.5-flash";
    }

    std::string GeminiProvider::getModelDesc() {
        return "Gemini-2.5-Flash 是谷歌面向高并发、实时性场景的轻量化多模态模型, 平衡性能与成本, 适用于开发者构建高效AI应用";
    }
} // namespace ai_chat_sdk

5.3 发送消息-全量返回

Base URL: https://generativelanguage.googleapis.com

请求URL: POST /v1beta/openai/chat/completions

  • 请求头参数:
字段名称 字段类型 设置值
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 设置值
messages array 通过参数 const std::vector< Message >& messages 获取
model string gemini-2.5-flash
temperature number 通过参数 const std::mapstd::string,std::string& request_param 获取
max_tokens integer 通过参数 const std::mapstd::string,std::string& request_param 获取
stream boolean false(全量返回,设置为false)

请求体示例(json格式):

{
    "messages": [
        {
            "content": "你是谁",
            "role": "user"
        }
    ],
    "model": "gemini-2.5-flash",
    "temperature ": 0.8,
    "max_tokens ": 2048,
    "stream": false
}

模型回复 的响应体示例(json格式):

最终是要提取 “content” 对应的内容(里面是模型的回复信息)

{
	"choices": [
		{
			"finish_reason": "stop",
			"index": 0,
			"message": {
				"content": "我是⼀个⼤型语言模型,由 Google 训练。\n",
				"role": "assistant"
			}
		}
	],
	"created": 1754897666,
	"id": "Ap2ZaPGBEMS1nvgPu6DPwAg",
	"model": "gemini-2.5-flash",
	"object": "chat.completion",
	"usage": {
		"completion_tokens": 12,
		"prompt_tokens": 3,
		"total_tokens": 15
	}
}

  • GeminiProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class GeminiProvider : public LLMProvider {
    public:
    	                      		   ......
    	                      		   
        // 发送消息给模型
        std::string sendMessage(const std::vector<Message>& messages, 
                                        const std::map<std::string,std::string>& request_param) override;
                                        
                                        ......
    };
} // namespace ai_chat_sdk
  • GeminiProvider.cpp(部分)
namespace ai_chat_sdk{
                                      .......
    
    // 发送消息(非流式响应)
    std::string GeminiProvider::sendMessage(const std::vector<Message>& messages, 
                                              const std::map<std::string,std::string>& request_param) 
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = false;
        request_body["temperature"] = temperature;
        request_body["max_tokens"] = max_tokens;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);
        
        /////////////////////////////// http相关内容(我还不太了解) ///////////////////////////////
        // 创建HTTP客户端
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(60, 0); // 60秒读取超时

        // 设置请求头 (Content-Type是请求体的类型,和post最后一个参数重复)
        httplib::Headers headers = {
            {"Authorization", "Bearer " + _api_key},
            /*{"Content-Type", "application/json"}*/
        };

        // 发送POST请求 (最后一个参数是请求体的类型, 这里是"application/json")
        httplib::Result res = client.Post("/v1beta/openai/chat/completions", headers, 
                                            request_body_str, "application/json");
        // Result类型的 operator->重载 返回的是 Response对象的指针
        if(!res || res->status != 200) {
            ERR("POST请求失败, status code : {}", res->status);
            return "";
        }
        DBG("POST请求成功, response body : {}", res->body);
        ////////////////////////////////////////////////////////////////////////////////////////

        // 解析响应体(字符串格式的JSON字符串)为 Json::Value对象
        Json::CharReaderBuilder srder;
        std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
        Json::Value resp;
        std::string error;
        bool parse_ret = srd->parse(res->body.c_str(),res->body.c_str() + res->body.size(), &resp, &error);
        if(!parse_ret) {
            ERR("Json解析失败, error : {}", error);
            return "";
        }

        // 从解析后的Json::Value对象中提取响应内容
        if(resp.isMember("choices") && resp["choices"].isArray() && !resp["choices"].empty()) 
        {
            Json::Value choice = resp["choices"][0];
            if(choice.isMember("message") && choice["message"].isObject()) {
                Json::Value message = choice["message"];
                if(message.isMember("content") && message["content"].isString()) 
                {
                    DBG("message content : {}", message["content"].asString());
                    return message["content"].asString();
                }
            }
        }
        
        // 解析失败,返回错误信息
        ERR("Invalid response format from Gemini API");
        return "";
    }

                                      .......
} // namespace ai_chat_sdk

5.4 发送消息-流式返回

Base URL: https://generativelanguage.googleapis.com

请求URL: POST /v1beta/openai/chat/completions

  • 请求头参数:
字段名称 字段类型 设置值
Content-Type string application/json
Authorization string "Bearer " + _api_key
  • 请求体参数:
字段名称 字段类型 设置值
messages array 通过参数 const std::vector< Message >& messages 获取
model string gemini-2.5-flash
temperature number 通过参数 const std::mapstd::string,std::string& request_param 获取
max_tokens integer 通过参数 const std::mapstd::string,std::string& request_param 获取
stream boolean true(设置为true,开启流式响应)

请求体示例(json格式):

{
    "messages": [
        {
            "content": "你是谁",
            "role": "user"
        }
    ],
    "model": "gemini-2.5-flash",
    "temperature ": 0.8,
    "max_tokens ": 2048,
    "stream": true
}

模型回复 的响应体示例(json格式):

最终是要提取 “content” 对应的内容(里面是模型的回复信息)

data: {
    "choices": [
        {
            "delta": {
                "content": " 通常",
                "role": "assistant"
            },
            "finish_reason": null,
            "index": 0
        }
    ],
    "created": 1756720923,
    "id": "Gm-1aKjSM-ag7dcP77rtoAs",
    "model": "gemini-2.5-flash",
    "object": "chat.completion.chunk",
}

                        ......

data: {
    "choices": [
        {
            "delta": {
                "content": "你在研究股票市场吗?\n",
                "role": "assistant"
            },
            "finish_reason": "stop",
            "index": 0
        }
    ],
    "id": "Gm-1aKjSM-ag7dcP77rtoAs",
    "model": "gemini-2.5-flash",
    "object": "chat.completion.chunk",
}

data: [DONE]

  • GeminiProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class GeminiProvider : public LLMProvider {
    public:
    	                      		   ......
    	                      		   
        // 发送消息给模型-流式返回
        std::string sendMessageStream(const std::vector<Message>& messages, 
                                      const std::map<std::string,std::string>& request_param,
                                      std::function<void(const std::string&, bool)> callback) override;
                                        
                                        ......
    };
} // namespace ai_chat_sdk
  • GeminiProvider.cpp(部分)
namespace ai_chat_sdk{
                                      .......
    
    // 实现流式响应
    std::string GeminiProvider::sendMessageStream(const std::vector<Message>& messages, 
                                 const std::map<std::string,std::string>& request_param,
                                 std::function<void(const std::string&, bool)> callback)
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = true;            // 开启流式响应
        request_body["temperature"] = temperature;
        request_body["max_tokens"] = max_tokens;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);

        ///////////////////////////// 与非流式响应有区别的内容 ///////////////////////////////
        // 创建HTTP Client
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(300, 0);      // 流式响应需要更⻓的时间

        // 设置请求头
        httplib::Headers headers = {
            {"Authorization", "Bearer " + _api_key},
            {"Content-Type", "application/json"},
            {"Accept", "text/event-stream"}       // 流式响应需要设置Accept头为text/event-stream
        };

        // 流式处理变量
        std::string buffer; // 接收流式响应的数据块
        bool gotError = false; // 响应是否成功(成功false,失败true)
        std::string errorMsg; // 错误描述符
        int statusCode = 0; // 状态码
        bool streamFinish = false; // 标记流式返回数据是否结束
        std::string fullResponse; // 累积完整的响应

        ///////////////////////////// 构建HTTP请求(设置处理函数) //////////////////////////////
        // 创建请求对象
        httplib::Request req;
        req.method = "POST";
        req.path = "/v1beta/openai/chat/completions";
        req.headers = headers;
        req.body = request_body_str;
        // HTTP协议响应的格式: 
        // ⼀个状态⾏:HTTP/1.1 200 OK
        // ⼀组响应头:Content-Type,Content-Length
        // 空⾏:\r\n\r\n
        // 响应体
        // 普通的HTTP响应是 ⼀头⼀体
        // 流式响应:只有⼀个响应头,但响应体被拆分成多个块(chunks)陆续发送,即⼀头多块
        
        // 响应头处理(检查状态码): 状态码不是200时,记录错误信息
        req.response_handler = [&](const httplib::Response& res) {
            int statusCode = res.status;
            if(200 != statusCode){
                gotError = true;
                errorMsg = "HTTP Error:" + std::to_string(statusCode);
                return false; // 终⽌请求
            }
            return true; // 继续接收数据
        };

        // 响应体处理(流式回调)
        // data:指向当前接收到的数据块的指针
        // len:当前数据块的长度
        // offset:当前数据块在响应体中的偏移量
        // totalLength:响应体的总长度
        // 流式响应 主要关注前两个参数 data 和 len 就足够了。因为在流式响应的场景下,总长度通常未知,需要持续接收直到 [DONE] 标记出现
        req.content_receiver = [&](const char* data, size_t len, uint64_t offset, uint64_t totalLength)
        {
            // 如果http请求头出错,就不需要再继续接收了
            if(gotError)
            {
                return false;
            }
            // 追加新数据到缓冲区
            buffer.append(data, len);
            DBG("buffer: {}", buffer);
            // 处理所有完整的事件,事件和事件之间以\n\n分隔
            size_t pos = 0;
            while((pos = buffer.find("\n\n")) != std::string::npos)
            {
                std::string event = buffer.substr(0, pos);
                buffer.erase(0, pos+2); // 移除已经处理的事件
                // 处理空⾏和注释, 以:开头的是注释⾏
                if(event.empty() || event[0] == ':')
                {
                    continue;
                }
                // 检查事件类型: data: JSON字符串
                if(event.compare(0, 6, "data: ") == 0)
                {
                    std::string jsonStr = event.substr(6);
                    ////// 关键代码:处理结束标记(判断流式响应是否结束) ///////
                    if(jsonStr == "[DONE]")
                    {
                        callback("", true);    // 返回true, 代表流式响应结束
                        streamFinish = true;   // 标记流式响应结束
                        return true;
                    }

                    // 反序列化:将JSON字符串 转换为 Json::Value对象
                    Json::Value chunk;  
                    Json::CharReaderBuilder srder;
                    std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
                    std::string errs;
                    bool read_ret = srd->parse(jsonStr.c_str(), jsonStr.c_str() + jsonStr.size(), &chunk, &errs);
                    if(!read_ret)
                    {
                        ERR("Json反序列化失败: {}", errs);
                        return false;
                    }

                    // 提取Json::Value对象中的 content字段
                    if(chunk.isMember("choices") && chunk["choices"].isArray() &&
                        !chunk["choices"].empty() && chunk["choices"][0].isMember("delta") &&
                        chunk["choices"][0]["delta"].isMember("content"))
                    {
                        std::string content = chunk["choices"][0]["delta"]["content"].asString();
                        // 累积到完整响应
                        fullResponse += content;
                        callback(content, false); // 调用回调函数处理增量内容, false代表流式响应未结束
                    }
                }           
            }
            return true; // 继续接收数据
        };
        ///////////////////////////// HTTP请求构建结束 /////////////////////////////

        // 发送HTTP请求并处理结果
        auto res = client.send(req);
        // send的返回值类型是Result类型,Result类型内部实现了operator bool(), 允许将Result类型的实例隐式转换为bool类型
        if(!res)
        {
            // 请求连接失败,网络问题、DNS解析失败等
            ERR("HTTP Post error: {}", httplib::to_string(res.error()));
            return "";
        }

        // 确保流正常结束
        if(!streamFinish){
            WARN("Stream ended without [DONE] marker");
            callback("", true);
        }
        return fullResponse;
    }
} // namespace ai_chat_sdk

6. Ollama本地接入Deepseek

6.1 为什么需要本地接入大模型

各大模型厂商已经提供了网页版的大模型使用服务,比如 DeepSeek、ChatGPT等,用户直接在网页上提问,就能得到需要的答案,为什么还要本地接入大模型呢?

(1) 使用云端大模型的优点:

  • 效果强: 云端算力足、模型大,输出质量通过高于本地模型
  • 即开即用: 无需下载和配置,注册后即可使用
  • 自动升级: 官方会不断更新和优化模型
  • 插件生态: ChatGPT plus、Gemini Advanced等往往自带额外功能

(2) 使用云端大模型的缺陷:

  • 隐私风险: 输入的数据会传送到云端,虽然大厂承诺,但仍有顾虑。许多行业(如医疗、金融、法律、政府)的数据高度敏感,法律禁止将数据上传到第三方。而且企业内部的战略文档、代码库、设计图等核心资产,如果通过API发送给第三方,存在泄露的风险
  • 费用问题: ⼤规模调用API需要付费,费用可能很高。虽然官网按token收费看起来单价不高,但对于高频使用的企业或个人开发者来说,长期积累的成本非常巨大
  • 网络依赖: 需要网络,有时访问受限,延迟高。在无网络请求下无法使用,比如保密单位、偏远地区等,而且网络高峰期可能还会遇到无法响应情况
  • 可控性差: 无法选择模型版本的内部细节,比如调整参数、控制模型输出格式、集成自定义函数等

(3) 本地部署大模型优点

  • 隐私保护: 数据完全在本地处理,不会上传云端
  • 零调用费用: 模型下载后随便用,不会产生API调用费用
  • 离线可用: 没有网络也能用,非常适合边缘场景
  • 灵活可控: 可以随时切换模型,甚至加载自己的训练模型

(4) 本地部署大模型的缺陷

  • 硬件要求高: 对显卡、内容要求比较高
  • 效果有限: 在低成本下效果有限
  • 初始成本高: 模型下载很大,运行时占用资源多

因此对于普通用户和非敏感任务,直接使用官网的云端服务是最简单、最经济的选择。 但对于企业、有隐私或特殊需求的用户,就需要本地部署大模型。


(5)本地接入大模型步骤:

在这里插入图片描述

6.2 Ollama

6.2.1 Ollama介绍

Linux下安装Ollama的命令: curl -fsSL https://ollama.com/install.sh | sh

正常情况下,下载速度极慢,因为是国外软件。可通过网络代理 加快下载速度

Ollama官方主页: https://ollama.com/

Ollama官方仓库: https://github.com/ollama/ollama

Get up and running with large language models.
“快速启动并运行大语言模型”,官方的宣传语简洁地概括了 Ollama的核心功能 和 价值主张。

Ollama 是一个开源的大型语言模型服务工具,旨在帮助用户快速在本地运行大模型。通过简单的安装指令,用户可以通过一条命令轻松启动 和 运行开源的大型语言模型。 它提供了一个简洁易用的命令行界面 和 服务器,专为构建大型语言模型应用而设计。用户可以轻松下载、运行 和 管理各种开源 LLM。与传统 LLM 需要复杂配置 和 强大硬件不同,Ollama 能够让用户在消费级的 PC 上体验 LLM 的强大功能。

Ollama 会自动监测本地计算资源,如有 GPU 的条件,会优先使用 GPU 的资源,同时模型的推理速度也更快。如果没有 GPU 条件,直接使用 CPU 资源。


Ollama特点:

开源免费: Ollama 及其支持的模型完全开源且免费,用户可以随时访问 和 使用这些资源,而无需支付任何费用。

简单易用: Ollama 无需复杂的配置 和 安装过程,只需几条简单的命令即可启动 和 运行,为用户节省了大量时间 和 精力。

支持多平台: Ollama 提供了多种安装方式,支持 Mac、Linux 和 Windows 平台,并提供 Docker镜像,满足不同用户的需求。

模型丰富: Ollama 支持包括 DeepSeek-R1、 Llama3.3、Gemma2、Qwen2 在内的众多热门开源 LLM,用户可以轻松一键下载 和 切换模型,享受丰富的选择。

功能齐全: Ollama 将模型权重、配置和数据捆绑成一个包,定义为 Modelfile,使得模型管理更加简便 和 高效。

支持工具调用: Ollama 支持使用 Llama 3.1 等模型进行工具调用。这使模型能够使用它所知道的工具来响应给定的提示,从而使模型能够执行更复杂的任务。

资源占用低: Ollama 优化了设置和配置细节,包括 GPU 使用情况,从而提高了模型运行的效率,确保在资源有限的环境下也能顺畅运行。

隐私保护: Ollama 所有数据处理都在本地机器上完成,可以保护用户的隐私。

社区活跃: Ollama 拥有一个庞大且活跃的社区,用户可以轻松获取帮助、分享经验,并积极参与到模型的开发和改进中,共同推动项目的发展。

6.2.2 Ollama常用指令
命令 描述
ollama serve 启动Ollama
ollama show + 模型名 显示模型信息
ollama run + 模型名 运行模型
ollama stop + 模型名 停止正在运行的模型
ollama pull 从ollama官方维护的模型库中拉去模型
ollama官方维护的模型库
ollama list 列出所有可用模型
ollama ps 列出正在运行模型
ollama rm + 模型名 删除模型
ollama help 显示任意命令的帮助信息
标志 描述
-h 、–help 显示Ollama的帮助信息
-v、–version 显示版本信息

注意: ollama serve 启动的是一个前台进程,终端关闭进程也就关闭了。在生产环境中,推荐使用 systemctl 来管理 Ollama 服务,该种方式下 Ollama 服务在后台运行,即使终端关闭服务仍会继续运行。

sudo systemctl start ollama # 启动服务
sudo systemctl stop ollama # 停⽌服务
sudo systemctl restart ollama # 重启服务
sudo systemctl status ollama # 查看服务状态
6.2.3 在后台启动Ollama服务器,使用Ollama客户端运行本地模型
  • 在后台启动Ollama服务器
    在这里插入图片描述

  • 使用Ollama客户端运行本地模型

ollama run deepseek-r1:1.5b  # ollama 会⾃动下载 deepseek-r1:1.5b模型,⼤概1.1GB

如未提前下载deepseek-r1:1.5b本地模型,第一次运行该模型时,会自动下载该模型

在这里插入图片描述
在这里插入图片描述

deepseek-r1:1.5b本地模型下载好后,后续可以直接启动该模型

在这里插入图片描述

/bye:结束与模型对话

在这里插入图片描述

6.3 ollama提供的API接口

由于现在是通过Ollama本地接入某个大模型,Ollama实际是在本地搭建了一个服务器,用户可以通过 Ollama下载需要接入的模型,Ollma会替用户管理模型,并真正和大模型对接,用户通过 Ollama提供的HTTP接口访问。用户向大模型发的消息实际是,先发给Ollama服务器,Ollama服务器将消息发给大模型,大模型响应之后,Ollama再将消息返回给用户,用户不直接和大模型交互,因此初始化时不需要设置 api key进行身份认证。

Ollama服务器监听本地的11434端口

注意:本地部署可以在自己的本地机器上,也可以在企业自己的局域网 或 云服务器上。

在这里插入图片描述

用户使用 客户端 与ollama服务器交互。
用户向本地大模型发送消息(必须指定大模型名称),先发给Ollama服务器,Ollama服务器会解析出模型名称,如果发现该本地模型没有在运行,还会查找模型文件将该模型加载到内存中,然后将用户消息经过处理后,发送给指定本地大模型,大模型响应之后,Ollama再将模型回复内容经过处理后,返回给用户,用户不直接和大模型交互

Ollama可以接入许多大模型,具体接入那个大模型看用户选择,因此需加入_model_name 和 _model_desc来保存接入的大模型的名称 和 描述信息。


Base URL: http://127.0.0.1:11434

请求URL: POST /api/chat

  • 请求头参数:
字段名称 字段类型 字段说明
Content-Type string application/json
  • 请求体参数:
字段名称 字段类型 字段说明
messages array 历史对话,内部为每个对话的object,包含role(user:用户的问题;assistant:模型的回答)和 content两个字段
model string 模型名称(“deepseek-r1:1.5b” 或 其它本地模型)
stream boolean - 未设置时,默认为 false
- 如果设置为 true,将会以 SSE(server-sent events)的形式以流式发送消息增量。消息流以 data: [DONE] 结尾。
options object 设置⼀些高级的可选参数,比如:temperature、最大tokens数。
注意:Ollama的最大 tokens字段为 num_ctx
// 使⽤curl给ollama发送请求
curl -s -X POST "http://127.0.0.1:11434/api/chat" -H "Content-Type: application/json" 
-d '{"model" : "deepseek-r1:1.5b", "stream" : false,
"messages" : [{"role" : "user", "content" : "你是谁?"}], 
"options" : {"temperature" : 0.7, "num_ctx" : 2048}}'

6.4 本地大模型初始化

在使用 本地deepseek-r1:1.5b模型前,需要先配置好deepseek-r1:1.5b需要的一些参数信息,比如 base url、model名称、temperature、max_tokens等信息,否则无法正常使用 deepseek的api。

base url 这个参数的内容基本是固定的,model名称最好在初始化时指定(ollama管理很多本地大模型,指定model名称确定向哪一个本地大模型提问),模型描述 与 本地大模型对应(不同本地大模型 对应 不同模型描述),所以也可以在初始化时一并传入

  • OllamaProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class OllamaProvider : public LLMProvider {
    public:
        // 初始化模型的 模型名称、模型描述 和 base url
        // 与本地大模型通信,不需要API Key
        bool init_Model(const std::map<std::string, std::string>& model_config) override;
        // 检测模型是否有效
        bool isAvailable() override;
        // 获取模型名称
        std::string getModelName() override;
        // 获取模型描述信息
        std::string getModelDesc() override;
        
				......
				
    };
} // namespace ai_chat_sdk
  • OllamaProvider.cpp(部分)
#include "../include/OllamaProvider.h"
#include "../include/util/mylog.h"
#include <jsoncpp/json/json.h>
#include <httplib.h>
#include <sstream>
#include <iostream>
#include <memory>
#include <map>

namespace ai_chat_sdk{
    // 初始化模型的 模型名称、模型描述 和 base url
    bool OllamaProvider::init_Model(const std::map<std::string, std::string>& model_config) 
    {
        // 初始化模型名称
        auto it = model_config.find("model_name");
        if(it == model_config.end())
        {
            ERR("Ollama model name is empty!");
            return false;
        }
        _model_name = it->second;

        // 初始化模型描述
        it = model_config.find("model_desc");
        if(it == model_config.end())
        {
            ERR("Ollama model desc is empty!");
            return false;
        }
        _model_desc = it->second;

        // 初始化base url
        it = model_config.find("base_url");
        if(it == model_config.end()) {
            ERR("base_url is not found in model_config");
            return false;
        }
        _base_url = it->second;

        // 设置初始化成功标记
        _isAvailable = true;
        INFO("init success!!!!");
        INFO("DeepSeek provider init successs with base_url : {}", _base_url);
        return true;
    }

    bool OllamaProvider::isAvailable() {
        return _isAvailable;
    }

    std::string OllamaProvider::getModelName() {
        return _model_name;
    }

    std::string OllamaProvider::getModelDesc() {
        return _model_desc;
    }
} // namespace ai_chat_sdk

6.5 发送消息-全量返回

Base URL: http://127.0.0.1:11434

请求URL: POST /api/chat

  • 请求头参数:
字段名称 字段类型 设置值
Content-Type string application/json
  • 请求体参数:
字段名称 字段类型 设置值
messages array 通过参数 const std::vector< Message >& messages 获取
model string deepseek-r1:1.5b
stream boolean false(全量返回,设置为false)
options object 通过参数 const std::mapstd::string,std::string& request_param 获取

请求体示例(json格式):

{
    "messages": [
        {
            "role": "user",
            "content": "你是谁?"
        }
    ],
    "model": "deepseek-r1:1.5b",
    "stream": false,
    "options":{
	    "temperature": 0.7
	    "num_ctx": 2048
    }
}

模型回复 的响应体示例(json格式):

最终是要提取 “content” 对应的内容(里面是模型的回复信息)

{
	"model":"deepseek-r1:1.5b",
	"created_at":"2025-09-02T09:24:03.117965426Z",
	"message":{
		"role":"assistant",
		"content":"\n\n\u003c/think\u003e\n\n你好!很⾼兴见到你,有什么我可以帮忙的吗?"
	},
	"done_reason":"stop",
	"done":true,
	"total_duration":24879553617,
	"load_duration":97011891,
	"prompt_eval_count":2,
	"prompt_eval_duration":133646497,
	"eval_count":181,
	"eval_duration":24647987800
}

  • OllamaProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class OllamaProvider : public LLMProvider {
    public:
    	                      		   ......
    	                      		   
        // 发送消息给模型
        std::string sendMessage(const std::vector<Message>& messages, 
                                const std::map<std::string,std::string>& request_param) override;
                                        
                                        ......
    };
} // namespace ai_chat_sdk
  • OllamaProvider.cpp(部分)
namespace ai_chat_sdk{
                                      .......
    
    // 发送消息(非流式响应)
    std::string OllamaProvider::sendMessage(const std::vector<Message>& messages, 
                                              const std::map<std::string,std::string>& request_param) 
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建 options_obj 对象
        Json::Value options_obj(Json::objectValue);
        options_obj["temperature"] = temperature;
        options_obj["num_ctx"] = max_tokens;

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = false;
        request_body["options"] = options_obj;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);
        
        /////////////////////////////// http相关内容(我还不太了解) ///////////////////////////////
        // 创建HTTP客户端
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(60, 0); // 60秒读取超时

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

        // 发送POST请求 
        httplib::Result res = client.Post("/api/chat", headers, 
                                            request_body_str, "application/json");
        // Result类型的 operator->重载 返回的是 Response对象的指针
        if(!res || res->status != 200) {
            ERR("POST请求失败, status code : {}", res->status);
            return "";
        }
        DBG("POST请求成功, response body : {}", res->body);
        ////////////////////////////////////////////////////////////////////////////////////////

        // 解析响应体(字符串格式的JSON字符串)为 Json::Value对象
        Json::CharReaderBuilder srder;
        std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
        Json::Value resp;
        std::string error;
        bool parse_ret = srd->parse(res->body.c_str(),res->body.c_str() + res->body.size(), &resp, &error);
        if(!parse_ret) {
            ERR("Json解析失败, error : {}", error);
            return "";
        }

        // 从解析后的Json::Value对象中提取响应内容
        if(resp.isMember("message") && resp["message"].isObject()) 
        {
            Json::Value message = resp["message"];
            if(message.isMember("content") && message["content"].isString()) 
            {
                DBG("message content : {}", message["content"].asString());
                return message["content"].asString();
            }
        }
        
        // 解析失败,返回错误信息
        ERR("Invalid response format from Ollama API");
        return "";
    }
} // namespace ai_chat_sdk

6.6 测试代码:测试全量返回

  • test.cpp
#include <gtest/gtest.h>
#include "../sdk/include/util/mylog.h"
#include "../sdk/include/OllamaProvider.h"

TEST(OllamaProviderTest, sendMessageOllama)
{
    // 创建OllamaProvider实例
    auto OllamaProvider = std::make_shared<ai_chat_sdk::OllamaProvider>();
    ASSERT_TRUE(OllamaProvider != nullptr);

    // 初始化模型配置
    std::map<std::string, std::string> model_config;
    model_config["model_name"] = "deepseek-r1:1.5b";
    model_config["model_desc"] = "一款实用性强、中文优化的通用对话助手,适合日常问答与创作";
    model_config["base_url"] = "http://127.0.0.1:11434";
    OllamaProvider->init_Model(model_config);
    ASSERT_TRUE(OllamaProvider->isAvailable());
    
    // 测试发送消息(全量返回)
    std::vector<ai_chat_sdk::Message> messages;
    messages.push_back({"user", "你好"});
    std::map<std::string, std::string> param_map = {
        {"temperature", "0.7"},
        {"max_tokens", "2048"}
    };
    std::string response = OllamaProvider->sendMessage(messages, param_map);
    ASSERT_FALSE(response.empty());
    INFO("fulldata : {}", response);
}

int main(int argc, char *argv[])
{
    // 初始化gtest库
    testing::InitGoogleTest(&argc, argv);
    // 初始化⽇志库
    MyLog::Logger::instance().init_logger("sync_logger", spdlog::level::level_enum::debug, "stdout");
    // 运⾏所有测试
    return RUN_ALL_TESTS();
}
  • CMakeLists.txt

客户端直接与Ollama服务器通信,Ollama服务器的base_url为:http://127.0.0.1:11434,支持http协议,因此在编译时不需要链接 OpenSSL开发库 去启用SSL

# CMake 最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(test_DeepSeekProvider)

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)

# 设置构建类型为Debug
set(CMAKE_BUILD_TYPE Debug)

# 添加可执行文件
add_executable(test_provider
        test.cpp 
        ../sdk/src/util/mylog.cpp 
        ../sdk/src/OllamaProvider.cpp
)

# 链接库
target_link_libraries(test_provider PRIVATE fmt gtest spdlog jsoncpp)
  • 代码运行结果:

在这里插入图片描述

6.7 发送消息-流式返回

Base URL: http://127.0.0.1:11434

请求URL: POST /api/chat

  • 请求头参数:
字段名称 字段类型 设置值
Content-Type string application/json
  • 请求体参数:
字段名称 字段类型 设置值
messages array 通过参数 const std::vector< Message >& messages 获取
model string deepseek-r1:1.5b
stream boolean true(设置未true,开启流式返回)
options object 通过参数 const std::mapstd::string,std::string& request_param 获取

请求体示例(json格式):

{
    "messages": [
        {
            "role": "user",
            "content": "你是谁?"
        }
    ],
    "model": "deepseek-r1:1.5b",
    "stream": true,
    "options":{
	    "temperature": 0.7
	    "num_ctx": 2048
    }
}

模型回复 的响应体示例(json格式):

最终是要提取 “content” 对应的内容(里面是模型的回复信息)

DeepSeek流式响应回复的每一个chunk块之间由 “/n/n” 隔开,且 DeepSeek流式响应结束时,会发 data: [DONE];但 Ollama服务器会对 deepseek-r1:1.5b流式响应返回的结果进行简化处理
简化处理后,Ollama流式响应返回的每一个chunk之间由一个"\n"分隔,流式响应结束靠 done字段(Bool类型)判断,done字段为false,代表流式响应继续,done字段为true,代表流式响应结束

{
	"model":"deepseek-r1:1.5b",
	"created_at":"2025-09-02T10:38:24.054739072Z",
	"message":{ 
		"role" : "assistant", 
		"content" : "您好" 
	},
	"done":false
}
...
{
	"model":"deepseek-r1:1.5b",
	"created_at":"2025-09-02T10:38:28.49268141Z",
	"message":{
		"role":"assistant",
		"content":"帮助"
	},
	"done":false
}
{
	"model":"deepseek-r1:1.5b",
	"created_at":"2025-09-02T10:38:28.626182393Z",
	"message":{
		"role":"assistant",
		"content":"。"
	},
	"done":false
}
{
	"model":"deepseek-r1:1.5b",
	"created_at":"2025-09-02T10:38:28.760770641Z",
	"message":{
		"role":"assistant",
		"content":""
	},
	"done_reason":"stop",
	"done":true,
	"total_duration":5720171892,
	"load_duration":90386941,
	"prompt_eval_count":6,
	"prompt_eval_duration":383758775,
	"eval_count":40,
	"eval_duration":5245057118
}

  • OllamaProvider.h(部分)
#pragma once
#include "LLMProvider.h"

namespace ai_chat_sdk{
    class OllamaProvider : public LLMProvider {
    public:
    	                      		   ......
    	                      		   
        // 发送消息给模型-流式返回
        std::string sendMessageStream(const std::vector<Message>& messages, 
                                      const std::map<std::string,std::string>& request_param,
                                      std::function<void(const std::string&, bool)> callback) override;
                                        
                                        ......
    };
} // namespace ai_chat_sdk
  • OllamaProvider.cpp(部分)
namespace ai_chat_sdk{
                                      .......
    
    // 实现流式响应
    std::string OllamaProvider::sendMessageStream(const std::vector<Message>& messages, 
                                 const std::map<std::string,std::string>& request_param,
                                 std::function<void(const std::string&, bool)> callback)
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建 options_obj对象
        Json::Value options_obj(Json::objectValue);
        options_obj["temperature"] = temperature;
        options_obj["max_tokens"] = max_tokens;

        // 构建请求体对象
        Json::Value request_body(Json::objectValue);
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = true;            // 开启流式响应
        request_body["options"] = options_obj;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);

        ////////////////////////////// 与非流式响应有区别的内容 ///////////////////////////////
        // 创建HTTP Client
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(300, 0);      // 流式响应需要更⻓的时间

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

        // 流式处理变量
        std::string buffer; // 接收流式响应的数据块
        bool gotError = false; // 响应是否成功(成功false,失败true)
        std::string errorMsg; // 错误描述符
        int statusCode = 0; // 状态码
        bool streamFinish = false; // 标记流式返回数据是否结束
        std::string fullResponse; // 累积完整的响应

        /////////////////////////// 构建HTTP请求(设置处理函数) ////////////////////////////
        // 创建请求对象
        httplib::Request req;
        req.method = "POST";
        req.path = "/api/chat";
        req.headers = headers;
        req.body = request_body_str;
        // HTTP协议响应的格式: 
        // ⼀个状态⾏:HTTP/1.1 200 OK
        // ⼀组响应头:Content-Type,Content-Length
        // 空⾏:\r\n\r\n
        // 响应体
        // 普通的HTTP响应是 ⼀头⼀体
        // 流式响应:只有⼀个响应头,但响应体被拆分成多个块(chunks)陆续发送,即⼀头多块
        
        // 响应头处理(检查状态码): 状态码不是200时,记录错误信息
        req.response_handler = [&](const httplib::Response& res) {
            int statusCode = res.status;
            if(200 != statusCode){
                gotError = true;
                errorMsg = "HTTP Error:" + std::to_string(statusCode);
                return false; // 终⽌请求
            }
            return true; // 继续接收数据
        };

        // 响应体处理(流式回调)
        // data:指向当前接收到的数据块的指针
        // len:当前数据块的长度
        // offset:当前数据块在响应体中的偏移量
        // totalLength:响应体的总长度
        // 流式响应 主要关注前两个参数 data 和 len 就足够了。因为在流式响应的场景下,总长度通常未知,需要持续接收直到 [DONE] 标记出现
        req.content_receiver = [&](const char* data, size_t len, uint64_t offset, uint64_t totalLength)
        {
            // 如果http请求头出错,就不需要再继续接收了
            if(gotError)
            {
                return false;
            }
            // 追加新数据到缓冲区
            buffer.append(data, len);
            DBG("buffer: {}", buffer);
            // 处理所有完整的事件,事件和事件之间以\n分隔
            size_t pos = 0;
            while((pos = buffer.find("\n")) != std::string::npos)
            {
                std::string event = buffer.substr(0, pos);
                buffer.erase(0, pos+1); // 移除已经处理的事件
                // 处理空⾏和注释, 以:开头的是注释⾏
                if(event.empty())
                {
                    continue;
                }

                // 反序列化:将JSON字符串 转换为 Json::Value对象
                Json::Value chunk;  
                Json::CharReaderBuilder srder;
                std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
                std::string errs;
                bool read_ret = srd->parse(event.c_str(), event.c_str() + event.size(), &chunk, &errs);
                if(!read_ret)
                {
                    ERR("Json反序列化失败: {}", errs);
                    return false;
                }

                ////// 关键代码:处理结束标记(判断流式响应是否结束) ///////
                if(chunk.isMember("done") && chunk["done"].isBool() && chunk["done"].asBool())
                {
                    callback("", true);    // 返回true, 代表流式响应结束
                    streamFinish = true;   // 标记流式响应结束
                    return true;
                }

                // 提取Json::Value对象中的 content字段
                if(chunk.isMember("message") && chunk["message"].isObject() &&
                    chunk["message"].isMember("content"))
                {
                    std::string content = chunk["message"]["content"].asString();
                    // 累积到完整响应
                    fullResponse += content;
                    callback(content, false); // 调用回调函数处理增量内容, false代表流式响应未结束
                }
        
            }
            return true; // 继续接收数据
        };
        ////////////////////////////// HTTP请求构建结束 /////////////////////////////

        // 发送HTTP请求并处理结果
        auto res = client.send(req);
        // send的返回值类型是Result类型,Result类型内部实现了operator bool(), 允许将Result类型的实例隐式转换为bool类型
        if(!res)
        {
            // 请求连接失败,网络问题、DNS解析失败等
            ERR("HTTP Post error: {}", httplib::to_string(res.error()));
            return "";
        }

        // 确保流正常结束
        if(!streamFinish){
            WARN("Stream ended without done:true marker");
            callback("", true);
        }
        return fullResponse;
    }
} // namespace ai_chat_sdk

6.8 测试代码:测试流式返回

  • test.cpp
#include <gtest/gtest.h>
#include "../sdk/include/util/mylog.h"
#include "../sdk/include/OllamaProvider.h"

TEST(OllamaProviderTest, sendMessageOllama)
{
    // 创建OllamaProvider实例
    auto OllamaProvider = std::make_shared<ai_chat_sdk::OllamaProvider>();
    ASSERT_TRUE(OllamaProvider != nullptr);

    // 初始化模型配置
    std::map<std::string, std::string> model_config;
    model_config["model_name"] = "deepseek-r1:1.5b";
    model_config["model_desc"] = "一款实用性强、中文优化的通用对话助手,适合日常问答与创作";
    model_config["base_url"] = "http://127.0.0.1:11434";
    OllamaProvider->init_Model(model_config);
    ASSERT_TRUE(OllamaProvider->isAvailable());
    
    // 测试发送消息(流式响应)
    std::vector<ai_chat_sdk::Message> messages;
    messages.push_back({"user", "你好"});
    std::map<std::string, std::string> param_map = {
        {"temperature", "0.7"},
        {"max_tokens", "2048"}
    };
    auto write_chunk = [&](const std::string& chunk, bool last){
        INFO("chunk : {}", chunk);
        if(last){
            INFO("[DONE]");
        }
    };
    std::string response = OllamaProvider->sendMessageStream(messages, param_map, write_chunk);
    ASSERT_FALSE(response.empty());
    INFO("fulldata : {}", response);
}

int main(int argc, char *argv[])
{
    // 初始化gtest库
    testing::InitGoogleTest(&argc, argv);
    // 初始化⽇志库
    MyLog::Logger::instance().init_logger("sync_logger", spdlog::level::level_enum::debug, "stdout");
    // 运⾏所有测试
    return RUN_ALL_TESTS();
}
  • 代码运行结果:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐