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

🔥小叶-duck:个人主页

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

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

《Qt 方寸极境》 《MySQL》

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


目录

前言

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

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

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

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

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

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

[二、OllamaLLMProvider 类设计与初始化实现](#二、OllamaLLMProvider 类设计与初始化实现)

[2.1 类设计思路](#2.1 类设计思路)

[2.1.1 继承关系](#2.1.1 继承关系)

[2.1.2 核心成员变量](#2.1.2 核心成员变量)

[2.1.3 设计原则](#2.1.3 设计原则)

[2.2 头文件定义(OllamaLLMProvider.h)](#2.2 头文件定义(OllamaLLMProvider.h))

[2.3 初始化函数实现(OllamaLLMProvider.cpp)](#2.3 初始化函数实现(OllamaLLMProvider.cpp))

[2.3.1 初始化执行步骤](#2.3.1 初始化执行步骤)

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

三、全量响应实现流程

[3.1 校验 Provider 可用状态](#3.1 校验 Provider 可用状态)

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

[3.2.1 设置默认值,读取本次请求动态参数](#3.2.1 设置默认值,读取本次请求动态参数)

[3.2.2 组装 options 配置对象](#3.2.2 组装 options 配置对象)

[3.2.3 组装历史消息 messages 数组](#3.2.3 组装历史消息 messages 数组)

[3.3 构造请求体](#3.3 构造请求体)

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

[3.5 创建 httplib 客户端 + 设置超时](#3.5 创建 httplib 客户端 + 设置超时)

[3.6 发送 POST 请求](#3.6 发送 POST 请求)

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

[3.8 提取响应内容](#3.8 提取响应内容)

[四、全量接口 sendMessage 完整代码](#四、全量接口 sendMessage 完整代码)

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

[5.1 测试用例编写](#5.1 测试用例编写)

[5.2 CMake 构建配置](#5.2 CMake 构建配置)

结束语


前言

Ollama 为本地部署大模型提供了轻量化的服务方案,我们可以在本地主机或者内网服务器上跑起模型,通过 HTTP 接口直接调用,不需要依赖外部公网服务。本章我们就围绕 Ollama 完成 Provider 的完整落地。

首先我们会在 Apifox 中完成 Ollama 接口的调试,直观熟悉它的请求报文、返回格式,区分它和 OpenAI 系列 API 的差异;随后完成OllamaLLMProvider的类结构设计、初始化逻辑编写;接着实现非流式全量响应接口,一步步拆解每一处参数组装、网络请求、JSON 解析的逻辑;最后基于 GTest 编写测试用例,搭配 CMake 完成编译验证。

一、Apifox 接口测试实操流程

在实现 OllamaProvider 模块之前,我们先使用 Apifox 第三方工具对 Ollama 完成接口调试,通过可视化工具我们就能先直观验证 Ollama 的请求、返回报文结构,把非流式、流式两种返回格式摸清楚,这会为后续 C++ 的 JSON 解析、SSE 事件处理提供直接的参考依据。

1.1 环境管理

在 Apifox 中先配置好环境信息,方便后续复用服务地址,适配本地、局域网多套环境切换。

  • 全局前置 Base URL: http://127.0.0.1:11434
  • 环境说明: Ollama 默认本地服务地址为 127.0.0.1:11434;如果需要局域网其他设备访问,需要提前把 Ollama 的 OLLAMA_HOST 设置为 0.0.0.0,再把地址替换为本机局域网 IP。
  • **环境隔离:**可以区分本地开发、内网服务器两套环境,互不干扰。

注意: Ollama 原生 /api/chat 接口不需要 API Key 鉴权 ,和前面 Gemini、DeepSeek、ChatGPT 云端 API 不一样,请求头不需要携带 Authorization 密钥。

1.2 接口基础信息填写

Ollama 原生对话接口为 /api/chat,它的报文结构和我们之前对接的 Chat Completions 系列接口有明显差异,字段命名、响应结构都属于 Ollama 自有规范,这也是我们实现 OllamaProvider 需要重点适配的地方。

  • 请求方式:POST
  • 完整地址:http://127.0.0.1:11434/api/chat
  • 拆分理解:
    • base_url:http://127.0.0.1:11434
    • 接口路径:/api/chat

1.3 请求 Body(JSON)配置

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

bash 复制代码
{
  "model": "deepseek-r1:7b",
  "options": {
    "temperature": 0.7,
    "num_ctx": 2048
  },
  "messages": [
    {
      "role": "user",
      "content": "你好,请简单介绍自己"
    }
  ],
  "stream": false
}
  • model:指定本地已经拉取好的模型名称,必须和 ollama list 列出的模型名保持一致
  • options:模型推理参数对象,用来控制本次生成的推理配置
    • temperature:生成随机性,值越高创造性越强
    • num_ctx:上下文窗口长度,控制单次对话能容纳的 token 上限
  • messages:对话消息数组,role/content 结构和 OpenAI 体系保持一致,支持 user /assistant/system
  • stream:流式开关
    • stream: false:关闭流式,一次性返回完整全量响应
    • stream: true:开启 SSE 流式增量返回,分片持续推送内容

1.4 请求头配置

在 Headers 标签只需要添加一项:

  1. Content-Type: application/json Ollama 原生接口无 token 鉴权,不需要额外配置 Authorization 请求头获取 apikey 的情况了。

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

① 全量响应(stream=false)

响应为单条完整 JSON,顶层没有 choices 数组,直接使用 message 字段存放回答内容,同时附带 done、done_reason、推理耗时、token 统计等 Ollama 专属字段,这是我们后续全量响应解析代码的参考依据。

bash 复制代码
{
  "model": "deepseek-r1:7b",
  "created_at": "2026-11-03T14:25:23.74674712Z",
  "message": {
    "role": "assistant",
    "content": "\n\n您好!我是由中国的深度求索(DeepSeek)公司开发的智能助手DeepSeek-R1。如您有任何问题,我会尽我所能为您提供帮助。"
  },
  "done": true,
  "done_reason": "stop",
  "total_duration": 1714442600,
  "load_duration": 3092700,
  "prompt_eval_count": 8,
  "prompt_eval_cached_count": 2,
  "prompt_eval_duration": 228479000,
  "eval_count": 75,
  "eval_duration": 1480458000
}

关键字段说明:

  • done: true:代表本次对话全部结束
  • message.content:模型返回的完整回答文本
  • eval_count / prompt_eval_count:统计本次推理消耗的 token 数量

② 流式响应(stream=true)

开启流式之后,Ollama 接口同样使用 stream=true 开启增量推送,但它和我们前面对接的 DeepSeek、OpenAI 远端服务的 SSE 报文格式有明显区别:

远端大模型标准 SSE 会使用 data: 包裹 JSON 块,块与块之间以 \n\n 分隔 ;

而Ollama 内部已经做了一层封装处理 ,直接返回一行一条独立 JSON,分片之间只用单个换行符 \n 分隔 ,**不再使用 \n\n 作为分割标记。**这是对接 Ollama 流式接口最核心的差异点,也意味着我们之前写的 SSE 解析逻辑不能直接复用,解析器需要单独适配。

每一条独立 JSON 都携带增量内容,没有 delta 字段 ,新增文本直接放在 message.content 字段中; 通过 done 字段判断会话是否结束:

  • done: false:代表当前只是中间分片,模型还有后续增量内容持续返回;
  • done: true:代表本轮流式输出全部完成,这条分片里还会附带耗时、token 计数等统计附属字段,作为结束标记。
bash 复制代码
{
  "model": "deepseek-r1:7b",
  "created_at": "2026-11-03T14:16:55.7797762Z",
  "message": {
    "role": "assistant",
    "content": "我会"
  },
  "done": false
}

末尾结束分片示例:

bash 复制代码
{
  "model": "deepseek-r1:7b",
  "created_at": "2026-11-03T14:16:55.91648522Z",
  "message": {
    "role": "assistant",
    "content": ""
  },
  "done": true,
  "done_reason": "stop",
  "total_duration": 187439500,
  "load_duration": 2571800,
  "prompt_eval_count": 7,
  "prompt_eval_cached_count": 0,
  "prompt_eval_duration": 376366000,
  "eval_count": 75,
  "eval_duration": 148828000
}

重点对比: DeepSeek、Gemini 的 Chat Completions 流式是 delta 增量字段;Ollama 原生接口直接复用 message.content,并且依靠 done 布尔值判断结束,这是 OllamaProvider 代码里最核心的差异化适配点。

二、OllamaLLMProvider 类设计与初始化实现

2.1 类设计思路

2.1.1 继承关系

OllamaLLMProvider 继承自抽象基类 LLMProvider,遵守我们 SDK 统一定义的大模型接入抽象接口契约。依托多态,它实现 Ollama 本地模型服务专属的接入逻辑,上层业务不需要关心底层是 DeepSeek 远端、OpenAI 还是 Ollama 本地模型,统一使用基类提供的接口调用。

2.1.2 核心成员变量

  • _modelName :Ollama 内的模型标识名称,和ollama pull拉取的模型名称保持一致,例如deepseek-r1:1.5b
  • _modelDesc:模型描述文本,用于上层界面、日志展示模型信息
  • _baseUrl :Ollama 服务的 HTTP 根地址,默认本地一般为http://127.0.0.1:11434
  • _isAvailable:布尔标记,代表当前 Provider 是否初始化成功、可用

2.1.3 设计原则

配置与代码解耦:模型名称、服务地址、描述信息全部通过外部配置传入。切换不同 Ollama 模型、切换 Ollama 部署的机器地址时,只修改配置内容,不需要改动业务代码。

2.2 头文件定义(OllamaLLMProvider.h)

cpp 复制代码
#ifndef OLLAMA_LLM_PROVIDER_H
#define OLLAMA_LLM_PROVIDER_H

#include "LLMProvider.h"

namespace ai_chat_sdk
{
//  Ollamad第三方工具提供者:实现Ollama本地接入模型的调用接口
class OllamaLLMProvider : public LLMProvider
{
  public:
    // 初始化模型:根据配置初始化模型
    bool initModel(const std::unordered_map<std::string, std::string> &modelConfig) override;
    // 判断模型是否有效
    bool IsAvailable() override;
    // 获取模型描述
    std::string getModelDesc() override;

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

    // 发送消息:增量调用,实时返回响应
    // requestParams:针对Ollama模型本次请求专属参数,包含model、options(包括temperature、num_ctx等)
    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;

  private:
    // 由于ollama只是作为第三方工具,这个工具的作用是可以本地接入不同的大模型,所以我们需要额外添加模型名称字段来指定接入具体的模型
    std::string _modelName; // 模型名称,如deepseek-r1:7b
    std::string _modelDesc; // 模型描述,如deepseek-r1:7b是一个7b参数的模型
};

} // namespace ai_chat_sdk
#endif

代码说明:

  1. 所有对外接口都使用override关键字,强制对齐基类LLMProvider的纯虚函数签名,保证多态正常工作;
  2. 区分两类参数:modelConfig是初始化阶段固定配置 (baseUrl、模型名、描述);requestParams是每次发请求时动态传入的参数,比如 temperature、上下文窗口大小,每次对话可以独立修改;
  3. 私有成员仅存放 Provider 实例固定不变的属性,和每次请求的动态参数做隔离,和我们前面整套 SDK 的设计规范保持统一。

2.3 初始化函数实现(OllamaLLMProvider.cpp)

2.3.1 初始化执行步骤

  1. 在传入的modelConfig中查找base_url,如果不存在,打印错误日志,直接返回初始化失败;
  2. 读取base_url的值,赋值给成员变量_baseUrl;
  3. 在modelConfig中查找modelName,缺失则打印错误日志,返回失败;
  4. 读取modelName的值,赋值给成员变量_modelName;
  5. 在modelConfig中查找modelDesc,缺失则打印错误日志,返回失败;
  6. 读取modelDesc的值,赋值给成员变量_modelDesc;
  7. 全部参数校验读取成功后,将_isAvailable置为 true,返回 true,标记初始化完成。

2.3.2 实现代码

cpp 复制代码
#include "../include/OllamaLLMProvider.h"
#include "../include/util/myLog.h"

namespace ai_chat_sdk
{
bool OllamaLLMProvider::initModel(const std::unordered_map<std::string, std::string> &modelConfig)
{
    // 从配置提取Ollama服务地址 base_url
    auto it = modelConfig.find("base_url");
    if (it == modelConfig.end())
    {
        ERR("OllamaLLMProvider: _baseUrl not found in modelConfig");
        return false;
    }
    else
    {
        _baseUrl = it->second;
    }

    // 提取模型名称
    it = modelConfig.find("modelName");
    if (it == modelConfig.end())
    {
        ERR("OllamaLLMProvider: _modelName not found in modelConfig");
        return false;
    }
    else
    {
        _modelName = it->second;
    }

    // 提取模型描述
    it = modelConfig.find("modelDesc");
    if (it == modelConfig.end())
    {
        ERR("OllamaLLMProvider: _modelDesc not found in modelConfig");
        return false;
    }
    else
    {
        _modelDesc = it->second;
    }

    _isAvailable = true;
    return true;
}

// 判断模型是否可用
bool OllamaLLMProvider::IsAvailable()
{
    return _isAvailable;
}

// 获取模型描述信息
std::string OllamaLLMProvider::getModelDesc()
{
    return _modelDesc;
}
}

代码说明:

  1. 使用unordered_map读取配置,每一个必填字段都做存在性校验,避免空配置导致后续网络请求异常;
  2. 借助我们前面封装好的日志组件ERR输出错误信息,方便定位配置缺失问题;
  3. IsAvailable、getModelDesc为简单的成员读取接口,作为发送请求的条件判断以及给上层框架做状态查询、展示使用;
  4. 初始化只负责固化实例的基础信息,不会填充对话请求参数 ,温度、最大 token 等参数依旧放在每次调用sendMessage/sendMessageStream时传入,和整套 SDK 设计保持一致。

三、全量响应实现流程

说明: 本接口为非流式完整应答接口,一次性拿到模型全部返回结果,和前面 DeepSeek、ChatGPT 的接口架构对齐,但请求报文、返回 JSON 字段遵循 Ollama 接口规范。

还有一个需要注意的点:Ollama 不同于前面的云端模型发送请求,请求参数 stream 默认是流式开启的 ,所以我们设计非流式响应接口时,不能像之前的模型设计一样直接忽略,必须手动设置为 false。

3.1 校验 Provider 可用状态

cpp 复制代码
if (!IsAvailable())
{
    ERR("OllamaLLMProvider sendMessage not available");
    return std::nullopt;
}

代码分析: 在执行网络请求之前,先判断当前实例是否初始化成功。如果initModel没有正常执行、配置缺失,直接返回空,避免发起无效 HTTP 请求。这是所有 Provider 统一的前置校验逻辑。

3.2 构造并获取请求参数

3.2.1 设置默认值,读取本次请求动态参数

cpp 复制代码
double temperature = 0.7;
int max_tokens = 2048;
auto it = requestParams.find("temperature");
if (it != requestParams.end())
{
    temperature = std::stod(it->second);
}
it = requestParams.find("max_tokens");
if (requestParams.find("max_tokens") != requestParams.end())
{
    max_tokens = std::stoi(it->second);
}

代码分析: temperature、max_tokens属于单次请求可变参数 ,放在requestParams,而不是初始化配置里。

  • 外部传入则使用传入值;
  • 不传就使用内置默认值;
  • 注意: Ollama 接口里,上下文窗口大小参数名称为num_ctx,我们把 max_tokens 映射给num_ctx。

3.2.2 组装 options 配置对象

cpp 复制代码
Json::Value options;
options["temperature"] = temperature;
options["num_ctx"] = max_tokens;

代码分析: Ollama 的参数放在options子对象中,这是和 DeepSeek、OpenAI 接口最大的区别之一。温度、上下文长度等参数都打包进 options,而不是直接平铺在请求根节点。

3.2.3 组装历史消息 messages 数组

cpp 复制代码
Json::Value messageArray(Json::arrayValue);
for (const auto &message : messages)
{
    Json::Value messageObject(Json::objectValue);
    messageObject["role"] = message._role;
    messageObject["content"] = message._content;
    messageArray.append(messageObject);
}

代码分析: 遍历上层传入的messages消息数组,把我们 SDK 内部定义的Message结构,转换成 Ollama 接口要求的role + contentJSON 数组。 这里消息角色体系和 OpenAI 兼容,所以转换逻辑和 DeepSeek 大体一致。

3.3 构造请求体

cpp 复制代码
Json::Value requestBody;
requestBody["model"] = _modelName;
requestBody["options"] = options;
requestBody["messages"] = messageArray;
requestBody["stream"] = false;

代码分析:

  • model:使用初始化时保存的_modelName,也就是 Ollama 本地模型名;
  • options:上面组装好的温度、上下文参数;
  • messages:对话历史;
  • stream=false:关键标记,关闭流式,代表本次是全量一次性返回。Ollama 默认 stream 就是开启状态,不设置默认就是走分片回调逻辑,全量响应必须手动设置为 false。

对比:流式接口这里会设置stream=true,同时走分片回调逻辑。

3.4 JSON 序列化

cpp 复制代码
Json::StreamWriterBuilder writerbuilder;
std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
INFO("OllamaLLMProvider sendMessage requestBodyStr: {}", requestBodyStr);

**代码分析:**借助 JsonCpp 将 JSON 对象序列成字符串,同时打印请求报文日志,方便调试,查看发给 Ollama 服务的内容是否符合预期。

3.5 创建 httplib 客户端 + 设置超时

cpp 复制代码
httplib::Client client(_baseUrl);
client.set_read_timeout(60, 0);
client.set_connection_timeout(30, 0);
httplib::Headers headers = {
    {"Content-Type", "application/json"}
};

代码分析:

  • 客户端地址来自初始化保存的_baseUrl,一般本地默认是http://127.0.0.1:11434;
  • 设置连接超时、读取超时,防止请求卡死;
  • 请求头固定为application/json,Ollama 接口接收 JSON 报文。

3.6 发送 POST 请求

cpp 复制代码
auto res = client.Post("/api/chat", headers, requestBodyStr, "application/json");
if (!res)
{
    ERR("OllamaLLMProvider sendMessage failed: {}", httplib::to_string(res.error()));
    return std::nullopt;
}
if (res->status != 200)
{
    ERR("OllamaLLMProvider sendMessage failed, status code:{}, body:{}", res->status, res->body);
    return std::nullopt;
}

代码分析:

  • 请求路径: /api/chat,是 Ollama 对话接口地址;
  • **第一层判断:**网络层面失败(连接失败、超时等),打印 httplib 内部错误;
  • **第二层判断:**HTTP 状态码非 200,打印返回体,方便排查 Ollama 服务报错,比如模型不存在、参数非法。

3.7 解析响应体

cpp 复制代码
Json::Value responseJson;
std::stringstream responseStream(res->body);
std::string errorMsg;
if (!Json::parseFromStream(readerbuilder, responseStream, &responseJson, &errorMsg))
{
    ERR("OllamaLLMProvider sendMessage: parse responseJson failed, errorMsg:{}", errorMsg);
    return std::nullopt;
}

**代码分析:**将服务返回的完整字符串反序列化为 Json 对象;如果 JSON 格式损坏,直接返回失败并打印解析错误。

3.8 提取响应内容

cpp 复制代码
if (responseJson.isMember("message") && responseJson["message"].isObject() &&
    responseJson["message"]["content"].isString())
{
    std::string content = responseJson["message"]["content"].asString();
    INFO("OllamaLLMProvider sendMessage success, content: {}", content);
    return content;
}
else
{
    ERR("OllamaLLMProvider sendMessage failed: responseJson not contain message.content");
    return std::nullopt;
}

四、全量接口 sendMessage 完整代码

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

    // 2、构造并获取请求参数
    // 2.1 从requestParams中提取temperature和max_tokens参数,若未指定则使用默认值
    double temperature = 0.7;
    int max_tokens = 2048;
    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);
    }
    // 2.2 构造Ollama模型请求参数
    Json::Value options;
    options["temperature"] = temperature;
    options["num_ctx"] = max_tokens;

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

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

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

    // 5、使用cpp-httplib构建HTTP客户端
    httplib::Client client(_baseUrl);
    // 5.1设置超时时间
    client.set_read_timeout(60, 0);       // 读取超时时间60秒
    client.set_connection_timeout(30, 0); // 连接超时时间30秒
    // 5.2设置请求头
    httplib::Headers headers = {
        {"Content-Type", "application/json"}};

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

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

    // 8、从responceBody中提取响应内容
    if (responseJson.isMember("message") && responseJson["message"].isObject() &&
        responseJson["message"].isMember("content") && responseJson["message"]["content"].isString())
    {
        std::string content = responseJson["message"]["content"].asString();
        INFO("OllamaLLMProvider sendMessage success, content: {}", content);
        return content;
    }
    else
    {
        ERR("OllamaLLMProvider sendMessage failed: responseJson not contain message.content");
        return std::nullopt;
    }
}

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

5.1 测试用例编写

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

using namespace ai_chat_sdk;

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

    // 2、构建模型配置
    std::unordered_map<std::string, std::string> modelConfig;
    modelConfig["modelName"] = "deepseek-r1:7b";
    // modelConfig["base_url"] = "http://localhost:11434";
    modelConfig["base_url"] = "http://192.168.159.1:11434";
    modelConfig["modelDesc"] = "本地部署deepseek-r1:7b模型, 是一个参数规模为7000万的预训练语言模型, 能够提供强大的文本生成能力和多任务处理能力";

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

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

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

    // 6、调用sendMessage发送消息
    auto response = ollamaProvider->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();
}

5.2 CMake 构建配置

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
               ../SDK/src/OllamaLLMProvider.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)

运行结果展示:

结束语

本章我们完成了 Ollama 本地模型 Provider 从接口调试、类设计、初始化,到全量应答接口编码与 GTest 测试的全部流程。 我们可以看到,依托之前设计好的LLMProvider抽象契约,只需要针对 Ollama 独有的报文结构、参数组织规则进行适配,就能快速新增一套本地模型接入能力,这也印证了策略模式和多态抽象给 SDK 带来的良好扩展性。

Ollama 的全量接口采用一次性返回结果的方式,实现逻辑相对简单;而 Ollama 的流式响应并不遵循标准 SSE 协议,采用换行分隔 JSON 块的形式推送数据,还需要我们自己处理分片半包缓冲。下一章,我们就基于当前的代码基础,继续实现sendMessageStream流式接口,完成 Ollama 流式对话能力的开发。

相关推荐
zaizai10071 小时前
【Effective Modern C++】学习第一章
c++
Fruiticecake1 小时前
C 程序员速通现代 C++(一):lambda、std::function、移动语义与智能指针
c语言·c++
总要学点什么1 小时前
第98篇:三维特效-材质线效果——给一条线换十种材质,虚线的图案只填了一半
开发语言·前端·javascript·ecmascript·gis·webgl·材质
田甲1 小时前
做一款 Type-C 母座读卡器:GL3224 双卡方案实测
c语言·开发语言
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:中缀表达式求值解析
c++·python·算法·bfs·信息学竞赛
严肃小白兔2 小时前
MATLAB 绘图与科研论文插图制作:图形设计、统计表达与矢量导出
开发语言·matlab·科研绘图
无足鸟ICT2 小时前
【RHCA+】iptraf-ng(网络监控分析)
linux·网络
Funlang3 小时前
给机器写的 JavaScript 造个笼子
开发语言·javascript·ecmascript
普通人64 小时前
PHP文件读写完全指南
开发语言·php·1024程序员节