目录
前言
一、我们要做的不是“大模型”,而是“大模型应用”
二、项目最终能做到什么
三、项目整体架构
3.1 Web UI:用户真正操作的界面
3.2 ChatServer:把 SDK 变成 HTTP 服务
四、ChatSDK:整个项目真正的核心
五、SDK 内部为什么还要继续拆模块
5.1 LLMManager:统一管理不同模型
5.2 SessionManager:管理聊天上下文
5.3 DataManager:把会话保存到 SQLite
六、一条消息到底是怎么走的
七、项目使用了哪些技术
八、当前源码目录先有个印象
九、为什么这个项目值得从 SDK 开始做
写到最后
前言
系列:从零实现 C++ AI 大模型接入 SDK,第一篇
项目源码:
AI-Chat-SDK
https://gitee.com/kuang-zhenting/my_ai_cpp_project
现在我们已经可以直接在网页或客户端中使用各种大模型,但对于开发人员来说,只会“打开网页聊天”还不够。真正把大模型接入自己的程序时,我们还需要处理 HTTP 请求、JSON 数据、不同厂商的接口差异、流式响应、上下文管理以及聊天记录持久化等问题。
这个系列要做的,就是从 C++ 项目的角度把这些问题一步一步解决。
最终我们会实现一套自己的ChatSDK:上层只需要面对统一接口,就可以接入 DeepSeek、ChatGPT、Gemini 以及 Ollama 本地模型。在 SDK 之上,再实现一个带网页前端的 AI 聊天助手,把模型接入、会话管理、流式输出和历史记录真正串成一个完整项目。
这篇作为整个系列的开篇,我们先不急着进入具体代码,而是先把三个问题搞清楚:
这个项目最终要做成什么样?
为什么需要自己封装一层 SDK?
整个项目由哪些模块组成,它们之间是什么关系?
一、我们要做的不是“大模型”,而是“大模型应用”
首先要明确一个边界:这个项目并不是让我们自己训练一个大语言模型。
DeepSeek、ChatGPT、Gemini 这类模型本身已经由厂商训练完成,我们要解决的是如何在 C++ 程序中使用这些模型提供的能力。
最直接的方法当然是:针对某一家模型写一段 HTTP 请求代码,把用户输入发送过去,再解析返回结果,这样确实能跑。
但当项目继续往下做,很快就会遇到新的问题:
不同模型的请求地址不同;
请求 JSON 格式不同;
返回 JSON 结构不同;
流式响应格式和解析方式也可能不同;
有的模型走云端 API,有的模型通过 Ollama 在本地运行;
聊天不能只发送当前一句话,还要维护之前的上下文;
程序重启后,我们还希望历史会话能够继续存在。
如果所有这些逻辑都堆在业务代码里,后面每增加一种模型,聊天业务都会跟着一起改。
因此,这个项目真正要解决的问题不是“怎么调用一次 API”,而是:
怎么把不同模型的接入细节封装起来,对上层提供一套统一、稳定的 C++ 接口。
这就是 ChatSDK 存在的意义。
二、项目最终能做到什么
按照当前项目源码,我们最后得到的是一个完整的网页 AI 聊天助手,而不只是几段 API 测试代码。
目前项目的主要能力包括:
接入 DeepSeek、ChatGPT、Gemini 等云端模型;
通过 Ollama 接入本地模型;
创建并管理多个聊天会话;
每个会话绑定自己的模型;
支持普通完整回复;
支持基于 SSE 的流式回复;
使用 SQLite 保存会话和历史消息;
支持会话重命名;
支持会话置顶和取消置顶;
提供 HTTP 接口供前端调用;
提供网页聊天界面;
AI 回复支持 Markdown 渲染、代码高亮和代码复制。
从使用者的角度看,它就是一个聊天应用;但从项目实现的角度看,真正值得学习的是背后的分层设计。
三、项目整体架构
先来看最终项目的大致结构:
整个项目可以从上到下理解成几层。
3.1 Web UI:用户真正操作的界面
最上面是网页前端。
用户可以在这里:
查看历史会话;
创建新会话并选择模型;
切换不同会话;
发送消息;
查看流式回复;
重命名、置顶或删除会话。
前端本身并不直接访问 DeepSeek、ChatGPT 或 Gemini,而是统一请求我们自己的ChatServer。
这样做以后,浏览器不需要知道不同模型 API 的具体差异,也不需要保存云端模型的 API Key。
3.2 ChatServer:把 SDK 变成 HTTP 服务
ChatServer位于应用层,它基于cpp-httplib提供 HTTP 接口,例如:
POST /api/sessions GET /api/sessions GET /api/models DELETE /api/sessions/{session_id} PATCH /api/sessions/{session_id}/rename PATCH /api/sessions/{session_id}/pin GET /api/sessions/{session_id}/history POST /api/message POST /api/message/async这里最重要的一点是:
ChatServer 负责 HTTP,ChatSDK 负责 AI 聊天业务。
也就是说,服务器知道怎么接收浏览器请求、怎么返回 JSON、怎么通过 SSE 往前端持续推送数据,但它不需要自己实现某个大模型的请求协议。
真正的大模型调用会继续交给下面的 ChatSDK。
四、ChatSDK:整个项目真正的核心
ChatSDK是 SDK 对外的统一入口。
从当前源码来看,上层主要通过它完成模型初始化、会话管理以及消息发送:
class ChatSDK { public: bool initModels(const std::vector<std::shared_ptr<Config>> &configs); std::string createSession(const std::string &modelName); std::shared_ptr<Session> getSession(const std::string &sessionId); std::vector<std::string> getSessionList() const; bool deleteSession(const std::string &sessionId); bool renameSession(const std::string &sessionId, const std::string &title); bool setSessionPinned(const std::string &sessionId, bool pinned); std::vector<ModelInfo> getAvailableModels() const; std::string sendMessage( const std::string &sessionId, const std::string &message); std::string sendMessageStream( const std::string &sessionId, const std::string &message, std::function<void(const std::string &, bool)> callback); };这里暂时不用研究每个函数内部是怎么实现的。
我们现在只需要建立一个认识:
对于 SDK 的使用者来说,不需要直接操作某个具体 Provider,也不需要关心消息最后发给了哪一家模型。
上层只面对ChatSDK,底下的模型选择、会话历史、请求转发和数据保存由 SDK 内部继续分工。
这也是整个项目后面所有设计的主线。
五、SDK 内部为什么还要继续拆模块
如果把所有事情都塞进ChatSDK,这个类很快也会变得非常庞大。
所以当前项目继续把职责拆成了几个核心模块。
5.1 LLMManager:统一管理不同模型
LLMManager负责管理所有 Provider。
项目定义了统一的ILLMProvider接口,然后让不同模型分别实现自己的 Provider:
ILLMProvider ├── DeepSeekProvider ├── ChatGPTProvider ├── GeminiProvider └── OllamaLLMProvider这样,LLMManager在发送消息时只需要根据模型名称找到对应 Provider,再调用统一接口即可。
至于某个 Provider 内部使用什么 URL、请求体怎么组织、响应怎么解析,都由它自己负责。
这解决的是:
多模型之间的差异问题。
5.2 SessionManager:管理聊天上下文
大模型聊天和普通的一次 HTTP 请求不同。
如果用户连续问:
我:推荐一部科幻电影 AI:…… 我:为什么推荐它?第二个问题要想回答正确,就必须把前面的聊天内容一起提供给模型。
因此项目中使用SessionManager管理会话。每个 Session 会记录:
会话 ID;
当前使用的模型;
历史消息;
创建和更新时间;
会话标题;
置顶状态等信息。
这解决的是:
一次次独立请求如何组成连续聊天的问题。
5.3 DataManager:把会话保存到 SQLite
如果历史消息只存在内存中,一旦服务器退出,之前的聊天记录就全部丢失了。
所以SessionManager后面还有DataManager,负责使用 SQLite 保存:
会话信息;
消息记录;
标题;
置顶状态;
时间信息。
这样服务器重新启动以后,历史会话仍然可以恢复。
六、一条消息到底是怎么走的
有了上面的模块划分以后,一条消息从网页发送出去,大致会经过下面这条链路:
以一次流式聊天为例:
用户在网页输入消息;
前端调用
ChatServer的流式消息接口;ChatServer把session_id和用户消息交给ChatSDK;ChatSDK根据 Session 找到当前会话绑定的模型;用户消息先加入会话历史;
LLMManager找到对应的 Provider;Provider 按当前模型的协议发起请求;
模型返回的数据通过回调逐段向上交付;
ChatServer使用 SSE 把片段持续推送给网页;本轮结束后,完整的助手回复再写入会话,并持久化到 SQLite。
这一条数据流其实就是整个项目最核心的主线。
后面的很多代码看起来模块很多,但只要始终记住:
Web UI ↓ ChatServer ↓ ChatSDK ├── LLMManager → Provider → LLM └── SessionManager → DataManager → SQLite再去看具体类时就不会容易迷路。
七、项目使用了哪些技术
这个项目主体使用 C++17,整体依赖并不算特别夸张,但基本覆盖了一个完整网络应用会碰到的几个方向。
| 技术 / 库 | 在项目中的作用 |
|---|---|
| C++17 | SDK 和 ChatServer 的主要开发语言 |
| CMake | SDK 与服务器的构建、安装 |
| cpp-httplib | HTTP 客户端和 HTTP Server |
| OpenSSL | HTTPS 请求支持 |
| jsoncpp | JSON 请求与响应解析 |
| SQLite3 | 会话和消息持久化 |
| spdlog + fmt | 日志输出与格式化 |
| gflags | ChatServer 命令行参数与配置 |
| HTML / CSS / JavaScript | 网页聊天界面 |
| SSE | 将模型生成内容流式推送到浏览器 |
前端还使用了marked、highlight.js和DOMPurify,分别用于 Markdown 解析、代码高亮以及 HTML 内容清理。
大家不需要在第一篇就把这些库全部学会。它们会随着项目推进逐渐出现,到真正需要时再理解,反而更容易建立联系。
八、当前源码目录先有个印象
当前项目的主要目录可以先简化理解成这样:
my_ai_cpp_project ├── SDK/ │ ├── include/ │ └── src/ │ ├── ChatServer/ │ ├── www/ │ ├── ChatServer.h │ ├── ChatServer.cpp │ └── main.cpp │ ├── TEST/ ├── SQL_TEST/ ├── create_ollama_service.sh └── README.md其中:
SDK/是整个系列最核心的部分;ChatServer/把 SDK 封装成可以供网页调用的 HTTP 服务;ChatServer/www/保存前端页面;TEST/、SQL_TEST/用于项目开发过程中的测试;README.md记录最终项目的构建、配置和使用方式。
第一眼看到这些目录时不用急着逐个研究。
我们后面会按照项目真正的依赖顺序,一层一层把它们搭起来。
九、为什么这个项目值得从 SDK 开始做
如果我们的目标只是“让 C++ 调一次 DeepSeek”,几十行代码就可能完成。
但这种代码很难继续扩展。
这个项目真正有价值的地方,是在一次次功能增加的过程中逐渐建立出:
统一接口 ↓ 不同 Provider ↓ 模型统一管理 ↓ 会话与上下文管理 ↓ SQLite 持久化 ↓ ChatSDK 对外封装 ↓ HTTP Server ↓ 网页聊天应用也就是说,我们最后得到的不只是“会调用大模型 API”,而是能够理解:
如何把一个外部 AI 能力,逐渐封装成自己项目里可以长期使用的模块。
这也是我整理这个系列时最想保留下来的主线。
写到最后
到这里,我们已经知道这个项目最终要做什么,也知道了各个核心模块之间的大致关系。
接下来不需要立刻钻进 Provider 的 HTTP 代码。
在真正开始接入模型之前,我们还需要先补齐少量和大模型调用直接相关的基础概念,并把开发环境准备好。等这些准备完成以后,再正式进入 ChatSDK 的实现。
后面的文章里,我们会从最底层开始,一步一步把上面的架构真正写出来。