《C++ 项目:手写大模型接入 SDK》Gemini接口设计(上):初始化、全量响应实现和测试

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已
择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

[一、Gemini API 体系概述](#一、Gemini API 体系概述)

[1.1 两套 API 设计机制与选型](#1.1 两套 API 设计机制与选型)

[1.1.1 设计背景](#1.1.1 设计背景)

[1.1.2 接入选型策略](#1.1.2 接入选型策略)

[1.2 Gemini OpenAI 兼容 API 协议规范](#1.2 Gemini OpenAI 兼容 API 协议规范)

[1.2.1 接口端点](#1.2.1 接口端点)

[1.2.2 请求头规范](#1.2.2 请求头规范)

[1.2.3 请求体核心参数](#1.2.3 请求体核心参数)

[二、Apifox 接口测试实操流程](#二、Apifox 接口测试实操流程)

[2.1 环境管理](#2.1 环境管理)

[2.2 接口基础信息填写](#2.2 接口基础信息填写)

[2.3 请求 Body(JSON)配置](#2.3 请求 Body(JSON)配置)

[2.4 请求头配置](#2.4 请求头配置)

[2.5 网络代理配置](#2.5 网络代理配置)

[2.6 发送请求,查看响应结果](#2.6 发送请求,查看响应结果)

[三、GeminiProvider 头文件设计](#三、GeminiProvider 头文件设计)

[3.1 文件与依赖规范](#3.1 文件与依赖规范)

[3.2 类接口标准声明](#3.2 类接口标准声明)

[四、GeminiProvider 源文件核心实现](#四、GeminiProvider 源文件核心实现)

[4.1 模型初始化接口](#4.1 模型初始化接口)

[4.1.1 处理逻辑](#4.1.1 处理逻辑)

[4.1.2 实现代码](#4.1.2 实现代码)

[4.2 状态与元信息接口](#4.2 状态与元信息接口)

[4.2.1 可用性检测](#4.2.1 可用性检测)

[4.2.2 模型描述获取](#4.2.2 模型描述获取)

[五、全量响应 sendMessage 实现](#五、全量响应 sendMessage 实现)

[5.1 检查模型是否有效](#5.1 检查模型是否有效)

[5.2 构造并获取请求参数](#5.2 构造并获取请求参数)

[5.3 构造历史消息,组装 message 数组](#5.3 构造历史消息,组装 message 数组)

[5.4 组装请求体 JSON](#5.4 组装请求体 JSON)

[5.5 JSON 序列化](#5.5 JSON 序列化)

[5.6 httplib 客户端初始化 + 代理环境变量处理](#5.6 httplib 客户端初始化 + 代理环境变量处理)

[5.7 设置请求头](#5.7 设置请求头)

[5.8 发送 POST 请求,校验返回状态](#5.8 发送 POST 请求,校验返回状态)

[5.9 解析响应体](#5.9 解析响应体)

[5.10 提取返回的响应内容](#5.10 提取返回的响应内容)

[六、GTest 全量响应代码测试(TestLLM.cpp)](#六、GTest 全量响应代码测试(TestLLM.cpp))

[6.1 CMakeLists.txt](#6.1 CMakeLists.txt)

全量响应接口完整代码

结束语


前言

在前序章节中,我们已经完成了 SDK 基础框架、日志组件、通用 LLMProvider 抽象基类的搭建,并且先后实现了 DeepSeek、ChatGPT 两个模型的接入逻辑,对 Chat Completions API、Responses API 两套接口的差异、流式 SSE 协议解析都有了比较完整的认知。

本章我们将继续扩展这套可复用的 SDK 框架,完成 Gemini 模型的接入开发。Gemini 存在原生 API 与 OpenAI 兼容 API 两套不同的调用体系,二者鉴权方式、报文结构差异明显,因此在正式编码之前,我们会先梳理两套 API 的设计区别,明确本次项目选用兼容 OpenAI 接口的选型理由,依托网关抹平协议差异,最大限度复用已有抽象层与上层调用逻辑。

内容会从接口协议规范、Apifox 预调试实操开始,再逐步完成 GeminiProvider 头文件声明、初始化、状态检测接口的实现,重点落地全量响应sendMessage接口,同时处理 Gemini 访问独有的代理配置、鉴权区别等工程问题;最后借助 GTest 编写测试用例、CMake 管理编译,验证整个链路的可用性。

一、Gemini API 体系概述

1.1 两套 API 设计机制与选型

1.1.1 设计背景

Gemini 对外提供两套完全不同的 API 体系,二者定位和发展路线差异很大:

  • OpenAI 兼容 API:属于厂商面向存量开发者的过渡方案,核心目的是降低迁移成本,让已经基于 OpenAI 技术栈开发的项目可以快速跑通 Gemini。但这套方案只是兼容层,长期依赖它会被限制在 OpenAI 的协议能力范围内,无法发挥 Gemini 独有的能力,厂商也很难建立属于自己的技术生态护城河。
  • 原生 API(models.generateContent):Google 主推的标准接口,完整释放 Gemini 多模态、工具调用、代码执行、实时检索等原生能力,适合垂直场景深度定制,是 Google 构建自身生态护城河的核心方案。

从行业演进规律来看,很多大模型厂商都会遵循相似路径 :早期 以兼容 API 快速吸引开发者,站稳脚跟之后,再推出自有原生接口标准。

这里有一个很关键的对比,帮我们理解 DeepSeek 和 Gemini 策略上的区别:

  1. DeepSeek 快速跟进 Responses API: DeepSeek 的产品定位就是对 OpenAI 做无痛平替 ,极低迁移成本是它生态上的核心优势。只要 OpenAI 推出新接口标准(例如从 Chat Completions 演进到面向 Agent 场景的 Responses API),DeepSeek 会快速跟进兼容。开发者甚至不用修改 SDK 调用代码,只替换api_key和base_url,就可以在 ChatGPT 与 DeepSeek 之间切换。
  2. Gemini 的 OpenAI 兼容接口定位是 "敲门砖": Google 和 OpenAI 属于直接竞争关系,并不希望 Gemini 仅仅作为 OpenAI 的替代品。它提供的/v1beta/openai/chat/completions只是一个最低限度的兼容入口,主要用来给存量旧系统做快速测试,吸引开发者体验 Gemini。 Google 重心维护的是自家 GenAI SDK 以及generateContent原生接口。Gemini 很多特色能力:实时搜索 Grounding、上下文缓存、原生音视频多模态、思维签名等,在 OpenAI 兼容协议里无法完整表达。Google 也没有跟进兼容 Responses API,这是我们后续编码实现时必须留意的重点。

1.1.2 接入选型策略

  • 快速迁移、复用已有 OpenAI 代码:选用 OpenAI 兼容 API,只需要修改密钥、BaseURL、模型名称三处配置。
  • 需要使用 Gemini 多模态、长上下文、工具调用等特色能力:选用 Google 原生 API。
  • **本工程接入方案:**优先使用 OpenAI 兼容 API 完成基础对话能力接入,降低开发成本;同时在代码架构上预留原生 API 的扩展入口,后续有需要可以平滑切换。

1.2 Gemini OpenAI 兼容 API 协议规范

Gemini 兼容层支持 chat/completions ,而前面我们的 deepseek 正好也是通过 Chat Completions API接入的,所以在 Gemini 在接口设计上和前面的 deepseek 整体是基本一致的,也就没有太多的注意事项了。

1.2.1 接口端点

  • 请求方法: POST
  • 基准 URL: https://generativelanguage.googleapis.com
  • 接口路径: /v1beta/openai/chat/completions
  • 完整请求地址: https://generativelanguage.googleapis.com/v1beta/openai/chat/completions

1.2.2 请求头规范

参数名 类型 取值规范
Content-Type string 固定值 application/json
Authorization string 格式为 Bearer {GEMINI_API_KEY}

1.2.3 请求体核心参数

参数名 类型 说明
model string 模型标识,例如 gemini-3.6-flash
messages array 对话消息列表,每条包含 role 和 content 字段
temperature float 采样温度,控制生成随机性,常用 0.7
max_tokens integer 单次生成最大 Token 上限
stream boolean 是否开启流式响应,true 增量流式返回,false 全量一次性返回

请求体示例:

cpp 复制代码
{
  "model": "gemini-3.6-flash",
  "messages": [
    {
      "role": "user",
      "content": "你是谁?"
    }
  ],
  "temperature": 0.7,
  "max_tokens": 2048,
  "stream": false
}

二、Apifox 接口测试实操流程

2.1 环境管理

在 Apifox 中先完成环境变量配置,方便后续复用密钥与地址,也便于多环境切换。

  • 全局前置 URL: https://generativelanguage.googleapis.com
  • 环境变量:
    • GEMINI_API_KEY:存放 Gemini 的访问密钥,在 Apifox 内加密存储,避免明文写在请求里。
  • **环境隔离:**可以区分开发、测试、正式、本地 Mock 多套环境,互不干扰。

2.2 接口基础信息填写

Gemini 兼容 OpenAI 的Chat Completions接口,不支持 Responses API,这一点是和 ChatGPT 最大的区别;报文结构和我们之前实现的 DeepSeek 保持一致。

  • 请求方式:POST
  • 完整地址:https://generativelanguage.googleapis.com/v1beta/openai/chat/completions
  • 拆分理解:
    • base_url:https://generativelanguage.googleapis.com/v1beta/openai
    • 接口路径:/chat/completions

2.3 请求 Body(JSON)配置

Body 选择 JSON 类型,核心参数如下:

bash 复制代码
{
  "model": "gemini-3.6-flash",
  "messages": [
    {
      "role": "user",
      "content": "你好,请简单介绍自己"
    }
  ],
  "temperature": 0.7,
  "max_tokens": 2048,
  "stream": false
}
  • stream: false:全量一次性返回;
  • stream: true:开启 SSE 流式增量返回。

2.4 请求头配置

在 Headers 标签页添加两项:

  1. Content-Type: application/json
  2. Authorization: Bearer {``{GEMINI_API_KEY}}
    {``{GEMINI_API_KEY}}会自动读取上面配置好的环境变量。

2.5 网络代理配置

和 OpenAI 的官方接口 一样,Gemini 官方接口也属于外网服务,需要代理网络才能访问,在 Apifox 的代理设置里单独配置:

  • **接口请求代理:**仅 API 流量走代理,Apifox 本身网页访问不受影响
  • 自定义代理地址: 填写代理 IP 和端口,示例:127.0.0.1:7890(本地代理工具常用默认端口,如果代理工具有指定端口则使用指定端口)
  • **代理绕过列表:**配置不需要走代理的域名,英文逗号分隔

2.6 发送请求,查看响应结果

① 全量响应(stream=false)

返回结构是标准 Chat Completions 格式,顶层choices数组,message中存放完整回答内容,包含finish_reason、usage消耗统计等字段。这个结构也是后续我们实现全量响应代码解析响应体的参考。

bash 复制代码
{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "你好!我是由Google训练的大型语言模型......",
        "role": "assistant"
      }
    }
  ],
  "created": 1793514015,
  "id": "GdrmapNKJylqtsPoL7MQ",
  "model": "gemini-3.6-flash",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 164,
    "prompt_tokens": 7,
    "total_tokens": 862
  }
}

② 流式响应(stream=true)

开启流式之后,服务端以 SSE 分片持续推送数据,每一条data:对应的 JSON 里面使用delta字段携带增量文本,没有额外事件类型区分 ,和 DeepSeek 的 Chat Completions 流式格式一致;流的末尾会单独推送一条 data: [DONE] 标记本次对话结束。

这个结构也是后续我们实现流式响应代码解析响应体的参考。

bash 复制代码
{
  "choices": [
    {
      "delta": {
        "content": "你好!我是由 Google 训练的大型语言模型。你可以把我",
        "role": "assistant"
      },
      "index": 0
    }
  ],
  "created": 1793541724,
  "id": "WEbnaq2_GabVz7IPrsLN8Qw",
  "model": "gemini-3.6-flash",
  "object": "chat.completion.chunk"
}

三、GeminiProvider 头文件设计

3.1 文件与依赖规范

  • 头文件路径 :SDK/include/GeminiProvider.h
  • 源文件路径 :SDK/src/GeminiProvider.cpp
  • 第三方依赖:jsoncpp(JSON 序列化与解析)、httplib(HTTP 网络请求)
  • 内部依赖:日志工具 myLog.h、抽象基类 LLMProvider.h、通用消息结构体定义

设计思路和前面 DeepSeekProvider、ChatGPTProvider 保持统一,遵循同一个抽象契约,保证 SDK 框架可以无差别切换不同厂商大模型,这也是策略模式带来的扩展性。

3.2 类接口标准声明

cpp 复制代码
#ifndef GEMINI_PROVIDER_H
#define GEMINI_PROVIDER_H

#include "LLMProvider.h"
namespace ai_chat_sdk
{
// GeminiProvider模型提供者类:用于与Gemini模型交互
class GeminiProvider : public LLMProvider
{
  public:
    // 初始化模型:根据配置初始化模型,只接收全局连接配置(api_key、base_url)
    bool initModel(const std::unordered_map<std::string, std::string> &modelConfig) override;
    // 判断模型是否有效
    bool IsAvailable() override;
    // 获取模型描述
    std::string getModelDesc() override;

    // 发送消息:全量调用,返回完整响应
    // requestParams:针对Gemini模型本次请求专属参数,包含model、temperature、max_output_tokens等
    std::optional<std::string> sendMessage(const std::vector<Message> &messages,
                                           const std::unordered_map<std::string, std::string> &requestParams) override;

    // 发送消息:增量调用,实时返回响应
    // requestParams:针对Gemini模型本次请求专属参数,包含model、temperature、max_output_tokens等
    std::optional<std::string> sendMessageStream(const std::vector<Message> &messages,
                                                 const std::unordered_map<std::string, std::string> &requestParams,
                                                 std::function<void(const std::string &response, bool isEnd)> callback) override;

    // 析构函数:释放模型资源
    ~GeminiProvider() = default;
};
} // namespace ai_chat_sdk

#endif

四、GeminiProvider 源文件核心实现

4.1 模型初始化接口

4.1.1 处理逻辑

  1. 从配置 map 读取api_key字段,不存在或者为空字符串,打印错误日志,返回 false
  2. 从配置 map 读取base_url字段,不存在则打印错误日志,返回 false
  3. 两个参数校验通过,赋值给成员变量,标记_isAvailable = true,打印初始化成功日志,返回 true

4.1.2 实现代码

cpp 复制代码
#include "../include/GeminiProvider.h"
namespace ai_chat_sdk
{
// 初始化模型:根据配置初始化模型,只接收全局连接配置(api_key、base_url)
bool GeminiProvider::initModel(const std::unordered_map<std::string, std::string> &modelConfig)
{
    auto it = modelConfig.find("api_key");
    if (it == modelConfig.end() || it->second == "")
    {
        ERR("GeminiProvider initModel api_key not found or not config_error");
        return false;
    }
    else
    {
        _apikey = it->second;
    }

    it = modelConfig.find("base_url");
    if (it == modelConfig.end())
    {
        ERR("GeminiProvider initModel base_url not found");
        return false;
    }
    else
    {
        _baseUrl = it->second;
    }
    _isAvailable = true;
    INFO("GeminiProvider initModel success, apikey: {}, base_url: {}", _apikey, _baseUrl);
    return true;
}

代码分析:

  • 查找api_key的时候同时做两层判断:key 是否存在 + value 是否为空,防止拿到空密钥;
  • base_url只判断 key 是否存在,这里可以根据后续需求补充空值校验;
  • 校验全部通过后,打开可用标记_isAvailable,同时打印 INFO 日志,方便调试的时候确认配置加载情况;
  • 初始化只保存连接信息,不绑定模型名称、温度等参数 ,遵循我们之前统一的设计规范:连接配置属于 Provider 实例,推理参数属于单次请求。

4.2 状态与元信息接口

4.2.1 可用性检测

cpp 复制代码
// 判断模型是否有效
bool GeminiProvider::IsAvailable()
{
    return _isAvailable;
}

分析: 直接返回内部布尔标记_isAvailable,上层调用方可以在发消息前先调用这个接口做前置校验,避免在未初始化成功的时候发起网络请求。

4.2.2 模型描述获取

cpp 复制代码
// 获取模型描述
std::string GeminiProvider::getModelDesc()
{
    return "gemini-3.6-flash 是 Google 专为 Agent 与复杂工作流优化的轻量主力模型,在保持极致低延迟和低成本的同时,提供前沿的编码、推理与全模态处理能力";
}

**分析:**返回 Gemini 模型的介绍文本,和 DeepSeek、ChatGPT 的 getModelDesc 保持一样的输出格式,上层代码不需要修改,就可以打印任意一个 Provider 的信息,充分体现多态的价值。

五、全量响应 sendMessage 实现

5.1 检查模型是否有效

cpp 复制代码
// 1. 检查模型是否有效
if (!IsAvailable())
{
    ERR("GeminiProvider sendMessage model not available");
    return std::nullopt;
}

代码分析: 在发起网络请求之前,先做前置校验。IsAvailable() 读取内部标记 _isAvailable,判断当前 Provider 是否已经完成initModel初始化、密钥和地址是否就绪。

如果没有初始化成功,直接打印错误日志,返回std::nullopt,避免无效的网络调用。

5.2 构造并获取请求参数

cpp 复制代码
// 2. 构造并获取请求参数
// 2.1 构造默认请求参数:如果requestParams中没有指定temperature、max_tokens,则使用默认值
double temperature = 0.7;
int max_tokens = 2048;
std::string model;

// 2.2 从requestParams中获取temperature、max_tokens参数;如果requestParams中指定了,则使用指定值
auto it = requestParams.find("temperature");
if (it != requestParams.end())
{
    temperature = std::stod(it->second);
}
it = requestParams.find("max_tokens");
if (it != requestParams.end())
{
    max_tokens = std::stoi(it->second);
}
it = requestParams.find("model");
if (it != requestParams.end())
{
    model = it->second;
}
else
{
    ERR("model name not found in requestParams, must config it");
    return std::nullopt;
}

代码分析

  • 先设置默认参数:temperature 默认 0.7、max_tokens 默认 2048,用户不传的时候自动兜底;
  • 从入参 requestParams(单次请求参数 map)读取参数,用户传入的值会覆盖默认值;
  • model 模型名称不设置默认值,强制要求调用方在每次请求时传入,缺失直接报错返回;
  • 符合我们整体框架设计:api_key/base_url 是实例初始化阶段的全局配置,model/temperature/max_tokens 属于单次推理参数,每次调用可以独立修改。

5.3 构造历史消息,组装 message 数组

cpp 复制代码
// 2.3 构造并获取历史消息,获取请求体中的 messages 这个json数组
Json::Value messageArray = Json::arrayValue;
for (auto &message : messages)
{
    Json::Value messageObj;
    messageObj["role"] = message._role;
    messageObj["content"] = message._content;
    messageArray.append(messageObj);
}

代码分析: 遍历上层传入的 std::vector<Message> 消息列表,循环把每一条消息转换成 JsonCpp 的 JSON 对象,放到数组里。

注意: 这里的 role/content 字段是适配 OpenAI 兼容格式 的 Gemini 接入方式;原生 Gemini 接口字段名称和结构不一样,本项目选用兼容 OpenAI 的网关方式,降低多 Provider 之间的代码差异。

5.4 组装请求体 JSON

cpp 复制代码
// 3. 构造请求体
// 和历史消息构造一样,请求体的字段名称必须与gemini模型的接口文档中要求的一致
Json::Value requestBody;
requestBody["messages"] = messageArray;
requestBody["temperature"] = temperature;
requestBody["max_tokens"] = max_tokens;
requestBody["model"] = model;

代码分析: 把上面组装好的消息数组、温度、最大输出 token、模型名称,全部填入请求根 JSON 对象。

这一步是报文组装的核心,字段必须和接口文档对齐,字段名写错会直接返回 400 参数错误。

5.5 JSON 序列化

cpp 复制代码
// 4. 对requestBody进行序列化
Json::StreamWriterBuilder writerbuilder;
std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
INFO("GeminiProvider sendMessage requestBody: {}", requestBodyStr);

代码分析: 使用 JsonCpp 的StreamWriterBuilder将 Json::Value 序列化为字符串,同时打印完整请求报文日志,方便调试时排查参数错误。

5.6 httplib 客户端初始化 + 代理环境变量处理

cpp 复制代码
// 5. 使用cpp-httplib构造HTTP客户端
httplib::Client client(scheme_host_port, _baseUrl);

// 5.1 设置超时时间
client.set_read_timeout(sec(60), usec(0));    // 读取超时时间60秒
client.set_connection_timeout(sec(20), usec(0)); // 连接超时时间20秒

// 5.2 关键点:从环境变量中获取代理配置
char *proxy_env = std::getenv("MY_APP_PROXY");
if (proxy_env != nullptr)
{
    std::string proxy_str(proxy_env);
    auto pos = proxy_str.find(":");
    if (pos == std::string::npos)
    {
        ERR("GeminiProvider sendMessage proxy format error, must be host:port");
        return std::nullopt;
    }
    else
    {
        std::string proxy_host = proxy_str.substr(0, pos);
        int proxy_port = std::stoi(proxy_str.substr(pos + 1));
        client.set_proxy(proxy_host, proxy_port);
    }
}
else
{
    // 针对Gemini模型的特殊情况:如果用户没有配置代理环境变量,那请求无法访问到Gemini模型的服务器
    ERR("GeminiProvider sendMessage proxy_env not found, must config it: MY_APP_PROXY=host:port");
    return std::nullopt;
}

代码分析:

  1. 创建 httplib 客户端,绑定提前保存的_baseUrl;设置连接超时、读取超时,防止网络卡死;
  2. Gemini 官方接口在国内无法直连,所以这里增加了强制代理校验 :读取环境变量MY_APP_PROXY;
  3. 解析代理字符串,格式要求host:port,找不到冒号判定格式错误;解析成功后给 httplib 客户端设置代理;
  4. 重点区别于 DeepSeek 可以直连,不需要强制代理;Gemini 这里没有代理直接报错,是该 Provider 独有的业务约束。

5.7 设置请求头

cpp 复制代码
// 5.3 构建请求头
httplib::Headers headers = {
    {"Authorization", "Bearer " + _apikey}
};

代码分析: 构造鉴权请求头,使用 Bearer 鉴权模式,和 OpenAI 体系保持一致,适配兼容 OpenAI 格式的 Gemini 网关。

如果使用 Gemini 原生官方接口,鉴权头为 x-goog-api-key: {apikey},报文结构、字段命名和 OpenAI 体系差异很大,这就是两种接入方式的核心差异点。

配套原生接口 curl 示例:

bash 复制代码
curl -X POST https://generativelanguage.googleapis.com/v1beta/interactions \
  -H "x-goog-api-key: $GEMINI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-flash-latest",
    "input": "Hello, how are you?"
  }'

5.8 发送 POST 请求,校验返回状态

cpp 复制代码
// 6. 向指定服务器发送POST请求
auto resp = client.Post(path: "/v1beta/openai/chat/completions", headers, body: requestBodyStr, content_type: "application/json");
if (!resp)
{
    ERR("GeminiProvider sendMessage failed, error: {}", httplib::to_string(resp.error()));
    return std::nullopt;
}
if (resp->status != 200)
{
    ERR("GeminiProvider sendMessage Post request failed, status: {}, body: {}", resp->status, resp->body);
    return std::nullopt;
}
INFO("GeminiProvider sendMessage Post request success, status: {}, body: {}", resp->status, resp->body);

代码分析:

  1. 调用 Post 发送 JSON 报文,目标路径是兼容 OpenAI 的 /v1beta/openai/chat/completions;
  2. 第一层判断:!resp代表网络层面失败(连接失败、超时、ssl 错误),打印 httplib 内部错误;
  3. 第二层判断:status 不等于 200,代表服务端返回业务错误(密钥错误、参数非法、额度耗尽),打印响应体方便定位;
  4. 200 成功后打印完整响应报文日志,用于调试。

5.9 解析响应体

cpp 复制代码
// 7. 解析响应体 -- 反序列化
Json::CharReaderBuilder readerbuilder;
Json::Value responseBody;
std::stringstream responseStream(str::resp->body);
std::string errorMsg;
if (!Json::parseFromStream(readerbuilder, &responseStream, &responseBody, &errorMsg))
{
    ERR("GeminiProvider sendMessage parse responseBody failed, err: {}", errorMsg);
    return std::nullopt;
}

代码分析: 使用 JsonCpp 的CharReaderBuilder流式解析返回的 JSON 字符串;解析失败时拿到错误信息返回空,防止非法 JSON 导致程序异常。

5.10 提取返回的响应内容

cpp 复制代码
// 8. 从responseBody中提取响应内容
// 8.1 先检查 choices 是否存在且是数组且不为空 -- 保证我们能取到有数据的choices字段
if (responseBody.isMember("choices") && responseBody["choices"].isArray() && !responseBody["choices"].empty())
{
    Json::Value choice = responseBody["choices"][0];
    // 8.2 检查choices数组中是否存在message字段且message字段中是否存在content
    if (choice.isMember("message") && choice["message"].isObject() &&
        choice["message"].isMember("content") && choice["message"]["content"].isString())
    {
        std::string generatedText = choice["message"]["content"].asString();
        INFO("GeminiProvider sendMessage generatedText: {}", generatedText);
        return generatedText;
    }
    else
    {
        ERR("GeminiProvider: message or content field missing in response.");
        return std::nullopt;
    }
}
else
{
    ERR("GeminiProvider: choices array is empty or not found in response.");
    return std::nullopt;
}

**代码分析:**按照 OpenAI 兼容格式的返回结构逐层校验:

  1. 判断顶层是否存在choices、是数组、数组不为空;
  2. 取第一个 choice 元素,再校验message对象、content字符串字段;
  3. 全部校验通过,取出 content 文本,返回给上层调用;任意一层字段缺失,打印对应错误日志,返回std::nullopt。

多层防御性判断,避免响应结构和预期不一致时,JsonCpp 访问字段抛出异常、造成程序崩溃。

六、GTest 全量响应代码测试(TestLLM.cpp)

cpp 复制代码
#include "../SDK/include/ChatGPTProvider.h"
#include "../SDK/include/DeepSeekProvider.h"
#include "../SDK/include/GeminiProvider.h"
#include "../SDK/include/OllamaLLMProvider.h"
#include <gtest/gtest.h>
#include <spdlog/common.h>

using namespace ai_chat_sdk;

TEST(GeminiProviderTest, sendMessage)
{
    // 1、创建GeminiProvider对象
    std::shared_ptr<LLMProvider> geminiProvider = std::make_shared<GeminiProvider>();
    // 1.1 检查GeminiProvider是否初始化成功
    ASSERT_TRUE(geminiProvider != nullptr);

    // 2、构建模型配置
    std::unordered_map<std::string, std::string> modelConfig;
    modelConfig["api_key"] = getenv("gemini_apikey") ? getenv("gemini_apikey") : "";
    modelConfig["base_url"] = "https://generativelanguage.googleapis.com";

    // 3、初始化GeminiProvider
    geminiProvider->initModel(modelConfig);
    // 3.1 检查模型是否有效
    ASSERT_TRUE(geminiProvider->IsAvailable());

    // 4、构建请求参数配置(不写则使用默认值)
    std::unordered_map<std::string, std::string> requestConfig;
    requestConfig["model"] = "gemini-3.6-flash";
    requestConfig["temperature"] = "0.7";
    requestConfig["max_tokens"] = "2048";

    // 5、构建消息列表
    std::vector<Message> messages;
    messages.push_back({"user", "你好, 请简单介绍一下你自己"});

    // 6、调用sendMessage发送消息
    auto response = geminiProvider->sendMessage(messages, requestConfig);
    // 6.1 检查响应是否为空
    ASSERT_TRUE(response.has_value());
    INFO("response: {}", response.value());
}

int main(int argc, char **argv)
{
    // 初始化Google Test框架
    testing::InitGoogleTest(&argc, argv);

    // 初始化日志系统
    Logger::logger_init("testLLM", "stdout", spdlog::level::info);

    // 运行所有测试用例
    return RUN_ALL_TESTS();
}

6.1 CMakeLists.txt

bash 复制代码
# 设置Cmake最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(testLLM)

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 要求必须使用C++17标准

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

# 添加可执行文件
add_executable(testLLM TestLLM.cpp 
               ../SDK/src/DeepSeekProvider.cpp 
               ../SDK/src/util/MyLog.cpp 
               ../SDK/src/ChatGPTProvider.cpp 
               ../SDK/src/GeminiProvider.cpp)   

#设置生成可执行文件的输出目录
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
# ${CMAKE_BINARY_DIR} 表示生成可执行文件的所在目录,即cmake指令执行所在目录

# 添加头文件搜索路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../SDK/include)
# ${CMAKE_CURRENT_SOURCE_DIR} 表示当前CMakeLists.txt所在目录
# ${CMAKE_SOURCE_DIR} 表示项目根目录,即所有CMakeLists.txt所在路径中最外层目录

# 查找第三方库:确保在使用前已经通过find_package命令找到并配置了这些库
find_package(GTest REQUIRED)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(jsoncpp REQUIRED)
find_package(OpenSSL REQUIRED)

# 链接第三方库
target_link_libraries(testLLM gtest fmt spdlog jsoncpp pthread OpenSSL::SSL OpenSSL::Crypto)

在 CMakeLists.txt 文件当中,只需要将 GeminiProvider 源文件 的所在位置 添加到add_executable当中即可。

测试结果展示:

全量响应接口完整代码

cpp 复制代码
// 发送消息:全量调用,返回完整响应
// requestParams:针对Gemini模型本次请求专属参数,包含model、temperature、max_output_tokens等
std::optional<std::string> GeminiProvider::sendMessage(const std::vector<Message> &messages,
                                                       const std::unordered_map<std::string, std::string> &requestParams)
{
    // 1. 检查模型是否有效
    if (!IsAvailable())
    {
        ERR("GeminiProvider sendMessage model not available");
        return std::nullopt;
    }

    // 2、构造并获取请求参数
    // 2.1 构造默认请求参数:如果requestParams中没有指定temperature、max_tokens,则使用默认值
    double temperature = 0.7;
    int max_tokens = 2048;
    std::string model;
    // 2.2 从requestParams中获取temperature、max_tokens参数:如果requestParams中指定了,则使用指定值
    auto it = requestParams.find("temperature");
    if (it != requestParams.end())
    {
        temperature = std::stod(it->second);
    }
    it = requestParams.find("max_tokens");
    if (it != requestParams.end())
    {
        max_tokens = std::stoi(it->second);
    }
    it = requestParams.find("model");
    if (it != requestParams.end())
    {
        model = it->second;
    }
    else
    {
        ERR("model name not found in requestParams, must config it");
        return std::nullopt;
    }

    // 2.3 构造并获取历史消息:获取请求体中的 messages 这个Json数组
    Json::Value messageArray(Json::arrayValue);
    for (auto &message : messages)
    {
        Json::Value messageObj;
        messageObj["role"] = message._role;
        messageObj["content"] = message._content;
        messageArray.append(messageObj);
    }

    // 3、构造请求体
    // 和历史消息构造一样,请求体的字段名称必须与gemini模型的接口文档中要求的一致
    Json::Value requestBody;
    requestBody["messages"] = messageArray;
    requestBody["temperature"] = temperature;
    requestBody["max_tokens"] = max_tokens;
    requestBody["model"] = model;

    // 4、对requestBody进行序列化
    Json::StreamWriterBuilder writerbuilder;
    std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
    INFO("GeminiProvider sendMessage requestBody: {}", requestBodyStr);

    // 5、使用cpp-httplib构建HTTP客户端
    httplib::Client client(_baseUrl);

    // 5.1设置超时时间
    client.set_read_timeout(60, 0);       // 读取超时时间60秒
    client.set_connection_timeout(20, 0); // 连接超时时间20秒
    // 5.2 关键点:从环境变量中获取代理配置
    char *proxy_env = std::getenv("MY_APP_PROXY");
    if (proxy_env != nullptr)
    {
        std::string proxy_str(proxy_env);
        auto pos = proxy_str.find(":");
        if (pos == std::string::npos)
        {
            ERR("GeminiProvider sendMessage proxy_env format error, must be host:port");
            return std::nullopt;
        }
        else
        {
            std::string proxy_host = proxy_str.substr(0, pos);
            int proxy_port = std::stoi(proxy_str.substr(pos + 1));
            client.set_proxy(proxy_host, proxy_port);
        }
    }
    else
    {
        // 针对Gemini模型的特殊情况:如果用户没有配置代理环境变量,那么请求无法访问到Gemini模型的服务器
        ERR("GeminiProvider sendMessage proxy_env not found, must config it: MY_APP_PROXY=host:port");
        return std::nullopt;
    }

    // 5.3 构建请求头
    httplib::Headers headers = {
        {"Authorization", "Bearer " + _apikey}};

    // 6、向指定服务器发送POST请求
    auto resp = client.Post("/v1beta/openai/chat/completions", headers, requestBodyStr, "application/json");
    if (!resp)
    {
        ERR("GeminiProvider sendMessage failed, error: {}", httplib::to_string(resp.error()));
        return std::nullopt;
    }
    if (resp->status != 200)
    {
        ERR("GeminiProvider sendMessage Post request failed, status: {}, body: {}", resp->status, resp->body);
        return std::nullopt;
    }
    INFO("GeminiProvider sendMessage Post request success, status: {}, body: {}", resp->status, resp->body);

    // 7、解析响应体 -- 反序列化
    Json::CharReaderBuilder readerbuilder;
    Json::Value responseBody;
    std::stringstream responceStream(resp->body);
    std::string errorMsg;
    if (!Json::parseFromStream(readerbuilder, responceStream, &responseBody, &errorMsg))
    {
        ERR("GeminiProvider sendMessage parse responceBody failed, err: {}", errorMsg);
        return std::nullopt;
    }

    // 8、从responceBody中提取响应内容
    // 8.1 先检查 choices 是否存在且是数组且不为空 -- 保证我们能获取到有数据的choices字段
    if (responseBody.isMember("choices") && responseBody["choices"].isArray() && !responseBody["choices"].empty())
    {
        Json::Value choice = responseBody["choices"][0];
        // 8.2 检查choices数组中是否存在message字段且message字段中是否存在content
        if (choice.isMember("message") && choice["message"].isObject() &&
            choice["message"].isMember("content") && choice["message"]["content"].isString())
        {
            std::string generatedText = choice["message"]["content"].asString();
            INFO("GeminiProvider sendMessage generatedText: {}", generatedText);
            return generatedText;
        }
        else
        {
            ERR("GeminiProvider: message or content field missing in response.");
            return std::nullopt;
        }
    }
    else
    {
        ERR("GeminiProvider: choices array is empty or not found in response.");
        return std::nullopt;
    }
}

结束语

到这里,Gemini 的全量响应接入开发与验证工作就全部完成了。我们先是区分了 Gemini 原生 API 和 OpenAI 兼容 API 两套体系,敲定了兼容网关的接入方案,依靠 Apifox 提前调通接口、确认报文规范;接着一步步完成 GeminiProvider 的类声明、基础接口实现,落地了全量 sendMessage 接口,处理了代理、鉴权这类和前面模型不一样的细节,最后通过 GTest 测试用例验证了整条调用链路可以正常工作。

对比之前 DeepSeek、ChatGPT 的接入过程,也能直观感受到我们抽象基类 LLMProvider 的价值:新增一个模型厂商,只需要实现对应的子类,上层调用逻辑几乎不用改动,框架的扩展性得到了很好的体现。

相关推荐
曦樂~40 分钟前
Cpp面筋
c语言·c++
道尔柯南1 小时前
C++ string 类详解:从常用接口到深拷贝,彻底掌握字符串处理
android·java·c++
来让爷抱一个2 小时前
Python 异步里混用同步库,事件循环被卡住的全过程
开发语言·python
Linux-lucky2 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
十二4 小时前
【洛谷题解】P1083 [NOIP 2012 提高组] 借教室(二分答案 + 差分)
数据结构·c++·算法·蓝桥杯
心祭丫丫5 小时前
【每天一个 Python 小案例17】Python 实现通讯录
开发语言·人工智能·笔记·python·学习
露天赏雪5 小时前
第3篇:Sentinel熔断降级实战:大模型接口抖动时的服务兜底方案
java·开发语言·人工智能·spring boot·spring·servlet·sentinel
lisw055 小时前
理解推理大语言模型的可监控性!
人工智能·语言模型·自然语言处理
博客zhu虎康5 小时前
实现局域网P2P通信:内网发送消息
网络·网络协议·p2p
D e re k ZH6 小时前
C++ 从零入门(一):基础语法与程序结构
数据结构·c++·算法