🌈欢迎来到实战项目专栏 ~~ 从零实现AI大模型接入SDK
- 🌍博客主页 :张小姐的猫~江湖背景
- 🔥所属专栏 :C++项目 ~ AI大模型接入SDK
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏

AI大模型接入SDK
- [🌈欢迎来到实战项目专栏 ~~ 从零实现AI大模型接入SDK](#🌈欢迎来到实战项目专栏 ~~ 从零实现AI大模型接入SDK)
- 🌏为什么需要本地接入大模型
- 🌏Ollama介绍
- 🌏Ollama模型初始化
- [🌏发送消息 --- 全量消息](#🌏发送消息 — 全量消息)
- 🌏发送消息-全量消息测试
- [🌏发送消息 --- 流式响应](#🌏发送消息 — 流式响应)
- [🌏发送消息 --- 流式响应测试](#🌏发送消息 — 流式响应测试)
- 📢写在最后

🌏为什么需要本地接入大模型
各大模型厂商已经提供了网页版的大模型使用服务,比如DeepSeek、ChatGPT等,用户直接在网页上提问,就能得到需要的答案,为什么还要本地接入大模型呢?
使用云端大模型的优点
- 效果强:云端算力足、模型大,输出质量通过高于本地模型
- 即开即用:无需下载和配置,注册后即可使用
- 自动升级:官方会不断更新和优化模型
- 插件生态:ChatGPTplus、GeminiAdvanced等往往自带额外功能


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

下面介绍使用Ollama本地接入:deepseek
🌏Ollama介绍
Getup and running with large language models


🔥Ubuntu下Ollama的使用
ubuntu下Ollama下载安装
cpp
curl -fsSL https://ollama.com/install.sh | sh
Ollama常用指令
| 命令 | 描述 |
|---|---|
ollama serve |
启动 Ollama |
ollama show |
显示模型信息 |
ollama run |
运行模型 |
ollama stop |
停止正在运行的模型 |
ollama pull |
从 Ollama 官方维护的模型库中拉取模型 |
ollama list |
列出所有可用模型 |
ollama ps |
列出正在运行的模型 |
ollama rm |
删除模型 |
ollama help |
显示任意命令的帮助信息 |
| 标志 | 描述 |
|---|---|
-h、--help |
显示 Ollama 的帮助信息 |
-v、--version |
显示版本信息 |
注意: ollama serve 启动的是一个前台进程 ,终端关闭进程也就关闭了。在生产环境中,推荐使用 systemctl 来管理 Ollama 服务,该种方式下 Ollama 服务在后台运行,即使终端关闭服务仍会继续运行
cpp
sudo systemctl start ollama # 启动服务
sudo systemctl stop ollama # 停⽌服务
sudo systemctl restart ollama # 重启服务
sudo systemctl status ollama # 查看服务状态
Ollama 服务启动之后,查看ollama服务运行情况:
可以看到,Ollama监听localhost的11434端口,因此在模型接入时endpoint可设置为:
bash
http://127.0.0.1:11434
c
ollama run deepseek-r1:1.5b
ollama 会自动下载 deepseek-r1:1.5b模型,大概1.1GB
下载好,我们查看一下:

cpp
/bye //退出与模型聊天
blobs/目录下存储模型的实际权重数据文件,文件名是 sha256-<哈希值> ,用来保证唯一性和去重
manifests/ 目录下存储模型的清单信息,包括模型的metadata(名称、版本、描述等信息)、依赖关系和模型运行的参数配置

当在终端运行 ollama run deepseek-r1:1.5b 命令时,Ollama首先查看 manifests/ 目录,找到 deepseek-r1:1.5b 的清单文件,根据清单文件中的哈希值,找到 blobs/ 目录中对应的模型文件,加载并运行模型
🔥Windows下Ollama的使用
在终端下打开

或者在浏览器中输⼊: http://localhost:11434 ,看到 Ollama is running 时说明ollama服务已经启动了
win10中,ollama安装好之后会带一个图形化界面:

设置界面:

环境变量:
| 参数 | 标识与配置 |
|---|---|
| OLLAMA_MODELS | 表示模型文件的存放目录 ,默认目录为当前用户目录 ,即 C:\Users\%username%\.ollama\models。建议放在其他盘,例如 D:\ApplyTool\ProgramTool\ollama\models。 |
| OLLAMA_HOST | 表示 Ollama 服务监听的网络地址 ,默认为 127.0.0.1。如果想允许其他电脑访问 Ollama(如局域网中的其他电脑),建议设置为 0.0.0.0。 |
| OLLAMA_PORT | 表示 Ollama 服务监听的默认端口 ,默认为 11434。如果端口有冲突,可以修改为其他端口,例如 8080。 |
| OLLAMA_ORIGINS | 表示 HTTP 客户端的请求来源 ,使用半角逗号分隔列表。如果本地使用不受限制,可以设置成星号 *。 |
| OLLAMA_KEEP_ALIVE | 表示大模型加载到内存后的存活时间 ,默认为 5m,即 5 分钟。例如纯数字 300 代表 300 秒;0 代表处理请求响应后立即卸载模型;任何负数表示一直存活。建议设置成 24h,即模型在内存中保持 24 小时,提高访问速度。 |
| OLLAMA_NUM_PARALLEL | 表示请求处理的并发数量 ,默认为 1(即单并发串行处理请求)。建议按照实际需求进行调整。 |
| OLLAMA_MAX_QUEUE | 表示请求队列长度 ,默认值为 512。建议按照实际需求进行调整,超过队列长度的请求会被抛弃。 |
| OLLAMA_DEBUG | 表示输出 Debug 日志 ,应用开发阶段可以设置为 1(即输出详细日志信息,便于排查问题)。 |
| OLLAMA_MAX_LOADED_MODELS | 表示最多同时加载到内存中模型的数量 ,默认为 1(即只能有 1 个模型在内存中)。 |
🌏Ollama模型初始化
由于现在是通过Ollama本地接入某个大模型,Ollama实际是在本地搭建了一个服务器,用户可以通过Ollama下载需要接入的模型,Ollma会替用户管理模型,并真正和大模型对接,用户通过Ollama提供的HTTP接口访问 。用户向大模型发的消息实际是,先发给Ollama服务器,Ollama服务器将消息发给大模型,大模型响应之后,Ollama再将消息返回给用户,用户不直接和大模型交互,因此初始化时不需要设置apikey进行身份认证

注意:本地部署可以在自己的本地机器上,也可以在企业自己的局域网或云服务器上
Ollama接入本地模型:下载安装好Ollama,下载对应的模型 --- deepseek-r1 ,通过代码方式接入本地模型时,实现一个接入程序:LLMManager

Ollama可以接入许多大模型,具体接入那个大模型看用户选择,因此需加⼊_model_name和_model_desc来保存接入的大模型的名称和描述信息
OllamaProvider.h的实现
cpp
#pragma once
#include "LLMProvider.h"
namespace ai_chat_sdk{
class OllamaProvider : public LLMProvider{
public:
//初始化模型
bool initModel(const std::map<std::string, std::string>& config) override;
//检查模型是否可用
bool isAvailable() const override;
//获取模型名称
std::string getModelName() const override;
//获取模型描述
std::string getModelDesc() const override;
//发送消息 ------ 全量返回
std::string sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam) override;
//发送消息 ------ 增量返回, 流式返回
std::string sendMessageStream(const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam,
std::function<void(const std::string&, bool)> callback) override;
protected:
std::string _model_name; // 模型名称 因为Ollama中会存在多个模型,所以需要指定模型名称
std::string _model_desc; // 模型描述
};
}
OllamaProvider.cpp的实现
cpp
#pragma once
#include "../include/util/OllamaProvider.h"
#include "../include/util/myLog.h"
#include "jsoncpp/json/json.h"
#include "../include/util/httplib.h"
#include <jsoncpp/json/writer.h>
#include <sstream>
namespace ai_chat_sdk{
//初始化模型
bool OllamaProvider::initModel(const std::map<std::string, std::string>& config){
//初始化模型名称
auto it = config.find("_model_name");
if(it == config.end()){
ERR("OllamaProvider initModel: _model_name not found in config");
return false;
}
model_name = it->second;
//初始化模型描述信息
it = config.find("model_desc");
if(it == config.end()){
ERR("OllamaProvider initModel: model_desc not found in config");
return false;
}
model_desc = it->second;
//初始化endpoint
it = config.find("_endpoint");
if(it == config.end()){
ERR("OllamaProvider initModel: _endpoint not found in config");
return false;
}
_endpoint = it->second;
_is_available = true;
return true;
}
//检测模型是否可用
bool OllamaProvider::isAvailable() const{
return _is_available;
}
//获取模型名称
std::string OllamaProvider::getModelName() const{
return model_name;
}
//获取模型描述
std::string OllamaProvider::getModelDesc() const{
return model_desc;
}
//发送消息 ------ 全量返回
std::string OllamaProvider::sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam)
{
return "";
};
//发送消息 ------ 增量返回, 流式返回
std::string OllamaProvider::sendMessageStream(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam, std::function<void(const std::string&, bool)> callback)
{
return "";
}
}
接下来重点去实现全量返回和流式返回 以及二者的测试
🌏发送消息 --- 全量消息
发送全量消息接口:
URL : /api/chat

启动ollama之后,在终端中使用bash给ollama发送请求
cpp
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}}'
看看是否能收到模型的响应信息?此处有可能会失败:
- 尝试将代理关闭 ,不运行
clash工具 +~bashrc文件中添加的环境变量屏蔽掉 - 如果还是不行,检查curl工具是否还在走代理,将代理给屏蔽掉
成功后可以拿到模型的响应信息:

后续进行反序列化的时候,也是要在响应信息里找到有效的内容信息进行返回,和以往的实现方式差不多
只是在请求体上有变化:注意温度值和最大token数要放在一个options对象中
cpp
//发送消息 ------ 全量返回
std::string OllamaProvider::sendMessage(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam)
{
//1.模型是否有效
if(!_is_available){
ERR("OllamaProvider sendMessage: model is not available");
return "";
}
//2.构建请求参数:温度值和最大token数
float temperature = 0.7;
int max_tokens = 1024;
if(requestParam.find("temperature") != requestParam.end()){
temperature = std::stof(requestParam.at("temperature"));
}
if(requestParam.find("max_tokens") != requestParam.end()){
max_tokens = std::stoi(requestParam.at("max_tokens"));
}
//3.构造历史消息数组
Json::Value messageArray(Json::arrayValue);
for(const auto& msg : messages){
Json::Value message;
message["role"] = msg._role;
message["content"] = msg._content;
messageArray.append(message);
}
//4.构造请求体,这里注意温度值和最大token数要放在一个options对象中
Json::Value options;
options["temperature"] = temperature;
options["num_ctx"] = max_tokens;
Json::Value requestBody;
requestBody["model"] = model_name;
requestBody["messages"] = messageArray;
requestBody["options"] = options;
requestBody["stream"] = false;
//5.对requestBody进行序列化
Json::StreamWriterBuilder writerbuilder;
writerbuilder["indentation"] = "";
std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
//6.创建客户端
httplib::Client client(_endpoint);
client.set_connection_timeout(30, 0); // 30秒连接超时
client.set_read_timeout(60, 0); // 60秒读取超时
//7.设置请求头
httplib::Headers headers = {
{"Content-Type", "application/json"}
};
//8.发送请求
auto result = client.Post("/v1/chat/completions", headers, requestBodyStr, "application/json");
if(!result) // 请求失败
{
ERR("GeminiProvider sendMessage: request failed, status code: {}", to_string(result.error()));
return "";
}
//模型响应不成功
if(result->status != 200)
{
ERR("GeminiProvider sendMessage: request failed, status code: {}", result->status);
return "";
}
INFO("GeminiProvider sendMessage: HTTP响应成功, status: {}", result->status);
INFO("GeminiProvider sendMessage: HTTP响应体: {}", result->body);
//9.反序列化 模型返回的响应结果
Json::Value responseBody;
Json::CharReaderBuilder readerBuilder;
std::istringstream responsestream(result->body);
std::string errors;
if (!Json::parseFromStream(readerBuilder, responsestream, &responseBody, &errors)) {
ERR("GeminiProvider sendMessage: JSON解析失败: {}", errors);
return "";
}
//解析成功 ------ 提取信息内容
if(responseBody.isMember("message") && responseBody["message"].isObject() && responseBody["message"].isMember("content"))
{
std::string content = responseBody["message"]["content"].asString();
INFO("OllamaProvider sendMessage: 响应内容: {}", content);
return content;
};
//解析失败说明responseBody结构有问题
ERR("OllamaProvider sendMessage: response body structure is wrong");
return "";
}
🌏发送消息-全量消息测试
cpp
//Ollama -- 全量返回测试
TEST(OllamaProvider, sendMessage)
{
//实例化OllamaProvider的对象
auto provider = std::make_shared<ai_chat_sdk::OllamaProvider>();
ASSERT_TRUE(provider != nullptr); //不报错就是成功
std::map<std::string, std::string> modelParams;
modelParams["model_name"] = "deepseek-r1:1.5b";
modelParams["model_desc"] = "本地部署的模型:deepseek-r1:1.5b,专注于深度理解与推理";
modelParams["_endpoint"] = "http://127.0.0.1:11434";
//初始化对象
provider->initModel(modelParams);
ASSERT_TRUE(provider->isAvailable()); //
//因为调用sendMessage方法需要传入参数:requestParam、message
const std::map<std::string, std::string> requestParam = {
{"temperature", "0.7"},
{"max_tokens", "2048"}
};
//调用sendMessage方法,发送消息
std::vector<ai_chat_sdk::Message> messages;
messages.push_back(ai_chat_sdk::Message("user", "你是谁?"));
std::string fulldata = provider->sendMessage(messages, requestParam);
ASSERT_FALSE(fulldata.empty()); //期待响应是false,如果fulldata是有数据的,没问题;反之数据为空会导致测试失败,报错
INFO("response: {}", fulldata);
}
接着在CMakelist中添加对应OllamaProvider.cpp
接下来我们看看测试的效果

🌏发送消息 --- 流式响应
URL : /api/chat

首先还是启动ollama之后,在终端中使用bash给ollama发送请求试一下

同样的要找出message对象里的content字段的信息,如果"done"字段是false的话,说明流式的增量数据还没有发送完毕!为true说明发送完毕了
要注意的是:Ollama已经对模型返回来的信息是做过处理了的,数据块之间只有单个\n 进行分隔,并且已经把SSE返回的数据进行了二次加工变成了下图的样子:
- 所以我们对响应数据进行解析就直接对下图的返回数据来进行解析即可

注意:
- 收到所有完整的事件,事件和事件之间以
\n分隔 - 在解析数据时候,先处理
done = "false"的结尾事件,
cpp
chunkJson.get("done", false).asBool()
有 "done" 字段就取它的值,没有就返回 false。最终转化成bool值进行返回
cpp
//发送消息 ------ 增量返回, 流式返回
std::string OllamaProvider::sendMessageStream(const std::vector<Message>& messages, const std::map<std::string, std::string>& requestParam, std::function<void(const std::string&, bool)> callback)
{
//1.模型是否有效
if(!_is_available){
ERR("OllamaProvider sendMessageStream: model is not available");
return "";
}
//2.构建请求参数:温度值和最大token数
float temperature = 0.7;
int max_tokens = 1024;
if(requestParam.find("temperature") != requestParam.end()){
temperature = std::stof(requestParam.at("temperature"));
}
if(requestParam.find("max_tokens") != requestParam.end()){
max_tokens = std::stoi(requestParam.at("max_tokens"));
}
//3.构造历史消息数组
Json::Value messageArray(Json::arrayValue);
for(const auto& msg : messages){
Json::Value message;
message["role"] = msg._role;
message["content"] = msg._content;
messageArray.append(message);
}
//4.构造请求体,这里注意温度值和最大token数要放在一个options对象中
Json::Value options;
options["temperature"] = temperature;
options["num_ctx"] = max_tokens;
Json::Value requestBody;
requestBody["model"] = model_name;
requestBody["messages"] = messageArray;
requestBody["options"] = options;
requestBody["stream"] = true;
//5.对requestBody进行序列化
Json::StreamWriterBuilder writerbuilder;
writerbuilder["indentation"] = "";
std::string requestBodyStr = Json::writeString(writerbuilder, requestBody);
//6.创建客户端
httplib::Client client(_endpoint);
client.set_connection_timeout(30, 0); // 30秒连接超时
client.set_read_timeout(300, 0); // 60秒读取超时
//7.设置请求头
httplib::Headers headers = {
{"Content-Type", "application/json"}
};
//流式响应变量
std::string buffer; // 流式数据接收区
bool gotError = false; // 是否发生错误
std::string ErrorMsg; // 错误信息
int statusCode = 0; // HTTP状态码
bool isfinished = false; // 流式响应是否结束
std::string fulldata; // 完整响应内容
//创建请求对象
httplib::Request request;
request.method = "POST";
request.path = "/api/chat";
request.headers = headers;
request.body = requestBodyStr;
//响应头处理器
request.response_handler = [&](const httplib::Response& request) {
statusCode = request.status;
if (statusCode != 200) {
gotError = true;
ErrorMsg = "OllamaProvider sendMessageStream: request failed, status code: " + std::to_string(statusCode);
return false;
}
};
//内容接收处理器
request.content_receiver = [&](const char* data, size_t datalen, size_t offset, size_t totallen)->bool {
if (gotError)
return false;
//将数据追加到缓冲区
buffer.append(data, datalen);
INFO("OllamaProvider sendMessageStream: received data:{} ", buffer);
//数据块之间是以\n分隔
//此处接收到的数据块并不是模型返回的SSE格式数据,而是经过ollama处理过的数据: 字节流
size_t pos = 0;
while((pos = buffer.find("\n")) != std::string::npos)
{
std::string chunk = buffer.substr(0, pos);
buffer.erase(0, pos + 1);
if (chunk.empty())
continue;
//对字节流进行反序列化
Json::Value chunkJson;
Json::CharReaderBuilder readerBuilder;
std::istringstream iss(chunk);
std::string errors;
//解析JSON数据
if (!Json::parseFromStream(readerBuilder, iss, &chunkJson, &errors)) {
ERR("OllamaProvider sendMessageStream: JSON解析失败: {}", errors);
return false;
}
//get done字段: 如果为true,则表示响应结束
if (chunkJson.get("done", false).asBool()) {
isfinished = true;
callback("", true);
return true;
}
//解析成功 ------ 提取信息内容
if (chunkJson.isMember("message") && chunkJson["message"].isObject() && chunkJson["message"].isMember("content")) {
std::string delta = chunkJson["message"]["content"].asString();
fulldata += delta;
callback(delta, false);
}
}
return true;
};
//给ollama发送请求
auto result = client.send(request);
if (!result) {
ERR("OllamaProvider sendMessageStream: request failed, error: {}", to_string(result.error()));
return "";
}
if(!isfinished)
{
WARN("OllamaProvider sendMessageStream: response is not finished");
callback("", true);
}
return "";
}
🌏发送消息 --- 流式响应测试
cpp
//Ollama -- 流式返回测试
TEST(OllamaProvider, sendMessage)
{
//实例化OllamaProvider的对象
auto provider = std::make_shared<ai_chat_sdk::OllamaProvider>();
ASSERT_TRUE(provider != nullptr); //不报错就是成功
std::map<std::string, std::string> modelParams;
modelParams["model_name"] = "deepseek-r1:1.5b";
modelParams["model_desc"] = "本地部署deepseek-r1:1.5b模型, 采⽤专家混合架构,专注于深度理解与推理";
modelParams["_endpoint"] = "http://127.0.0.1:11434";
//初始化对象
provider->initModel(modelParams);
ASSERT_TRUE(provider->isAvailable()); //
//因为调用sendMessage方法需要传入参数:requestParam、message
const std::map<std::string, std::string> requestParam = {
{"temperature", "0.7"},
{"max_tokens", "2048"}
};
//调用sendMessage方法,发送消息
std::vector<ai_chat_sdk::Message> messages;
messages.push_back(ai_chat_sdk::Message("user", "你是谁?"));
//流式响应
auto writechunk = [&](const std::string& chunk, bool last) {
INFO("chunk: {}", chunk);
if (last) {
INFO("[Done]");
}
};
std::string fulldata = provider->sendMessageStream(messages, requestParam, writechunk);
ASSERT_FALSE(fulldata.empty()); //期待响应是false,如果fulldata是有数据的,没问题;反之数据为空会导致测试失败,报错
INFO("response: {}", fulldata);
}

📢写在最后
接下来登场的是 模型管理

