基于 cpp-httplib 客户端实现 LLM 模型下载与切换:Hugging Face 模型管理实战
基于 cpp-httplib 客户端实现 LLM 模型下载与切换:Hugging Face 模型管理实战
导读
前几章利用 httplib::Server 完成了 REST 翻译 API 与 SSE 流式输出,但模型文件仍是第 1 章手动下载的单个 GGUF 文件。本章将启用 cpp-httplib 的客户端能力,从应用内部直接连接 Hugging Face,实现模型的一键下载、进度上报与运行时切换。读完本章你将掌握:TLS 客户端的构建配置、基于回调的大文件流式下载、通过 SSE 向客户端实时上报下载进度,以及在多线程服务器中使用互斥锁安全地热切换 LLM 模型。
目标效果:通过 HTTP 接口管理模型
本章完成后,模型管理功能会以两个新端点呈现。首先是用 GET /models 查询可用模型及其下载/选中状态:
curl http://localhost:8080/models
{
"models": [
{"name": "gemma-2-2b-it", "params": "2B", "size": "1.6 GB", "downloaded": true, "selected": true},
{"name": "gemma-2-9b-it", "params": "9B", "size": "5.8 GB", "downloaded": false, "selected": false},
{"name": "Llama-3.1-8B-Instruct", "params": "8B", "size": "4.9 GB", "downloaded": false, "selected": false}
]
}
其次是 POST /models/select 切换模型。若目标模型尚未下载,服务端会立即开始下载,并通过 SSE 持续推送进度(curl -N 让进度不被缓冲):
curl -N -X POST http://localhost:8080/models/select \
-H "Content-Type: application/json" \
-d '{"model": "gemma-2-9b-it"}'
data: {"status":"downloading","progress":0}
data: {"status":"downloading","progress":12}
...
data: {"status":"downloading","progress":100}
data: {"status":"loading"}
data: {"status":"ready"}
4.1 httplib::Client 基础:从只做服务端到同时发起 HTTPS 请求
此前代码只用到了 httplib::Server,而 cpp-httplib 本身是头文件库,同时内置了客户端能力(见 httplib.h 中 ClientImpl/Client 的定义)。由于 Hugging Face 走 HTTPS,客户端必须启用 TLS。cpp-httplib 的写法非常简洁——URL 携带 https:// 协议时,客户端会自动选择 SSL 实现:
#include <httplib.h>
// URL 含协议时自动使用 SSL 客户端
httplib::Client cli("https://huggingface.co");
// 自动跟随重定向(Hugging Face 会重定向到 CDN)
cli.set_follow_location(true);
auto res = cli.Get("/api/models");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
其中 set_follow_location(true) 与 set_read_timeout(...) 在 httplib.h 中有完整声明:set_read_timeout 同时支持秒级参数和 std::chrono::duration 重载,本章的下载函数正是利用 chrono 重载把读超时放宽到 1 小时。
构建期启用 TLS:CMake 中的 OpenSSL 配置
HTTPS 需要编译期开启 OpenSSL 支持,在 CMakeLists.txt 中加入:
find_package(OpenSSL REQUIRED)
target_link_libraries(translate-server PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(translate-server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
# macOS: 读取系统证书链所需
if(APPLE)
target_link_libraries(translate-server PRIVATE "-framework CoreFoundation" "-framework Security")
endif()
CPPHTTPLIB_OPENSSL_SUPPORT 是仓库约定的 TLS 开关:在 httplib.h 中,该宏控制 #include <openssl/ssl.h> 等头文件、SSL 相关类型与 SSLClient/SSLServer 的编译;CMakeLists.txt 也确认了项目自身构建时 HTTPLIB_IS_USING_OPENSSL 与 CPPHTTPLIB_OPENSSL_SUPPORT 的对应关系。macOS 上 OpenSSL 需要访问 Keychain 系统证书,因此还要链接 CoreFoundation 与 Security 框架(httplib.h 中的 CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN 逻辑就是为此准备的)。完整配置见 4.8 节。
4.2 定义模型清单:以元数据驱动的多模型管理
应用可处理的模型由一个全局静态表定义,便于后续所有端点统一遍历:
struct ModelInfo {
std::string name; // 显示名
std::string params; // 参数量
std::string size; // GGUF Q4 量化后大小
std::string repo; // Hugging Face 仓库
std::string filename; // GGUF 文件名
};
const std::vector<ModelInfo> MODELS = {
{
.name = "gemma-2-2b-it",
.params = "2B",
.size = "1.6 GB",
.repo = "bartowski/gemma-2-2b-it-GGUF",
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
},
{
.name = "gemma-2-9b-it",
.params = "9B",
.size = "5.8 GB",
.repo = "bartowski/gemma-2-9b-it-GGUF",
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
},
{
.name = "Llama-3.1-8B-Instruct",
.params = "8B",
.size = "4.9 GB",
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
},
};
repo 与 filename 两列被 download_model 用来拼装 Hugging Face 的下载 URL;name/params/size 则直接进入 GET /models 的 JSON 响应。这样模型清单、下载逻辑与列表接口由同一份数据驱动,新增模型只需在此追加一条记录。
4.3 模型存储位置:从项目目录迁移到用户目录
第 3 章之前模型放在项目内的 models/。管理多个模型时,改放到应用专用目录更合理:macOS/Linux 使用 ~/.translate-app/models/,Windows 使用 %APPDATA%\translate-app\models\:
std::filesystem::path get_models_dir() {
#ifdef _WIN32
auto env = std::getenv("APPDATA");
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
return base / "translate-app" / "models";
#else
auto env = std::getenv("HOME");
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
return base / ".translate-app" / "models";
#endif
}
环境变量未设置时回退到当前目录。目录在应用启动时通过 std::filesystem::create_directories 自动创建——该函数对已存在的目录不会报错,因此可安全地幂等调用。
4.4 重写模型初始化:启动即自动补齐默认模型
模型初始化从硬编码路径改为“目录 + 文件名 + 互斥锁”的组合。selected_model 记录当前加载的文件名,启动时加载 MODELS 首项;GET /models 与 POST /models/select 都会读写该变量:
int main() {
auto models_dir = get_models_dir();
std::filesystem::create_directories(models_dir);
std::string selected_model = MODELS[0].filename;
auto path = models_dir / selected_model;
// 默认模型未下载时启动自动拉取
if (!std::filesystem::exists(path)) {
std::cout << "Downloading " << selected_model << "..." << std::endl;
if (!download_model(MODELS[0], [](int pct) {
std::cout << "\r" << pct << "%" << std::flush;
return true;
})) {
std::cerr << "\nFailed to download model." << std::endl;
return 1;
}
std::cout << std::endl;
}
auto llm = llamalib::Llama{path};
std::mutex llm_mutex; // 保护模型切换期间的访问
// ...
}
关键设计考量:cpp-httplib 的 Server 使用线程池并发执行处理器,若在另一线程正在执行 llm.chat() 时重写 llm,将导致未定义行为甚至崩溃。因此用 std::mutex 对 llm 的构造/重写与所有推理入口统一加锁。这样用户在首次启动时无需再用 curl 手动下载模型,控制台会实时显示进度(\r 回车刷新同一行)。
4.5 GET /models 处理器:标注下载与选中状态
该端点遍历 MODELS,为每个模型附加 downloaded(文件是否存在于模型目录)与 selected(文件名是否等于 selected_model)两个状态字段:
svr.Get("/models",
& {
auto arr = json::array();
for (const auto &m : MODELS) {
auto path = get_models_dir() / m.filename;
arr.push_back({
{"name", m.name},
{"params", m.params},
{"size", m.size},
{"downloaded", std::filesystem::exists(path)},
{"selected", m.filename == selected_model},
});
}
res.set_content(json{{"models", arr}}.dump(), "application/json");
});
downloaded 直接由文件系统事实推导(std::filesystem::exists),天然与 download_model 的落盘结果保持一致,无需额外状态登记。
4.6 大文件下载:回调式流式写入与原子替换
GGUF 模型动辄数 GB,无法整体装入内存。httplib::Client::Get 接受两个回调:content_receiver(逐块接收数据)与 progress(进度通知),返回 false 即可中止下载:
// content_receiver: 接收数据块的回调
// progress: 下载进度回调
cli.Get(url,
& { // content_receiver
ofs.write(data, len);
return true; // 返回 false 则中断下载
},
& { // progress
int pct = total ? (int)(current * 100 / total) : 0;
std::cout << pct << "%" << std::endl;
return true; // 返回 false 则中断下载
});
这两个回调类型在 httplib.h 中有明确定义:ContentReceiver 为 bool(const char* data, size_t data_length),DownloadProgress 为 bool(size_t current, size_t total)(httplib.h)。Get 也提供带 Headers、Params、ResponseHandler 的多个重载(httplib.h),可按需选用。
基于此实现 Hugging Face 模型下载函数:
#include <filesystem>
#include <fstream>
// 下载模型并以 progress_cb 上报进度;progress_cb 返回 false 则中止
bool download_model(const ModelInfo &model,
std::function<bool(int)> progress_cb) {
httplib::Client cli("https://huggingface.co");
cli.set_follow_location(true);
cli.set_read_timeout(std::chrono::hours(1));
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
auto path = get_models_dir() / model.filename;
auto tmp_path = std::filesystem::path(path).concat(".tmp");
std::ofstream ofs(tmp_path, std::ios::binary);
if (!ofs) { return false; }
auto res = cli.Get(url,
& {
ofs.write(data, len);
return ofs.good();
},
& {
return progress_cb(total ? (int)(current * 100 / total) : 0);
});
ofs.close();
if (!res || res->status != 200) {
std::filesystem::remove(tmp_path);
return false;
}
// 先写 .tmp 再重命名,避免下载中断的半成品被当作模型使用
std::filesystem::rename(tmp_path, path);
return true;
}
要点拆解:
- URL 拼装:
/+repo+/resolve/main/+filename对应 Hugging Face 的resolve/main直链格式; - 超时:
set_read_timeout(std::chrono::hours(1))为 GB 级文件预留充足读超时; - 原子性:数据先写入
<模型名>.gguf.tmp,仅当 HTTP 状态为 200 且完整接收后才rename为正式文件名。任何中断(超时、断连、回调返回 false)都会删除.tmp,绝不产生“看似可用”的残缺模型; - 进度节流:完整版代码中进度回调使用
last_pct记录上一次值,相同进度直接跳过,避免刷屏与无谓的 SSE 推送。
4.7 POST /models/select 处理器:SSE 贯穿下载→加载→就绪
选择请求一律以 SSE(text/event-stream)响应,无论目标模型是否已下载都保持同一格式,依次推送 downloading(下载进度)、loading、ready 事件:
svr.Post("/models/select",
& {
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded() || !input.contains("model")) {
res.status = 400;
res.set_content(json{{"error", "'model' is required"}}.dump(),
"application/json");
return;
}
auto name = input["model"].get<std::string>();
// 从模型清单中查找
auto it = std::find_if(MODELS.begin(), MODELS.end(),
& { return m.name == name; });
if (it == MODELS.end()) {
res.status = 404;
res.set_content(json{{"error", "Unknown model"}}.dump(),
"application/json");
return;
}
const auto &model = *it;
// 无论已下载与否,一律以 SSE 响应
res.set_chunked_content_provider(
"text/event-stream",
&, model {
// SSE 事件发送辅助函数
auto send = & {
sink.os << "data: " << event.dump() << "\n\n";
};
// 未下载则先下载(进度经 SSE 上报)
auto path = get_models_dir() / model.filename;
if (!std::filesystem::exists(path)) {
bool ok = download_model(model, & {
send({{"status", "downloading"}, {"progress", pct}});
return sink.os.good(); // 客户端断开时中止下载
});
if (!ok) {
send({{"status", "error"}, {"message", "Download failed"}});
sink.done();
return true;
}
}
// 加载模型并切换
send({{"status", "loading"}});
{
std::lock_guard<std::mutex> lock(llm_mutex);
llm = llamalib::Llama{path};
selected_model = model.filename;
}
send({{"status", "ready"}});
sink.done();
return true;
});
});
这段代码值得注意的几个联动点:
- 进度直通 SSE:
download_model的进度回调内部直接调用send发送 SSE 事件,是第 3 章set_chunked_content_provider+sink.os技巧的直接应用(set_chunked_content_provider在 httplib.h 声明、httplib.h 实现,底层以Transfer-Encoding: chunked输出); - 断连即取消:进度回调返回
sink.os.good(),客户端一旦断开连接,sink.os失效,download_model随即中止下载——第 5 章 Web UI 的取消按钮正是利用这一机制; - 状态联动:
selected_model更新后,GET /models返回的selected标志自动刷新; - 加锁切换:
llm的覆盖写操作由llm_mutex保护,而/translate与/translate/stream处理器内部也持有同一把锁(见完整代码),因此模型切换期间绝不会穿插推理调用。
4.8 完整代码
以下为在第 3 章代码基础上加入模型管理功能的完整工程。
完整代码(CMakeLists.txt)
cmake_minimum_required(VERSION 3.20)
project(translate-server CXX)
set(CMAKE_CXX_STANDARD 20)
include(FetchContent)
# llama.cpp
FetchContent_Declare(llama
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp
GIT_TAG master
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(llama)
# cpp-httplib
FetchContent_Declare(httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib
GIT_TAG master
)
FetchContent_MakeAvailable(httplib)
# nlohmann/json
FetchContent_Declare(json
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)
FetchContent_MakeAvailable(json)
# cpp-llamalib
FetchContent_Declare(cpp_llamalib
GIT_REPOSITORY https://github.com/yhirose/cpp-llamalib
GIT_TAG main
)
FetchContent_MakeAvailable(cpp_llamalib)
find_package(OpenSSL REQUIRED)
add_executable(translate-server src/main.cpp)
target_link_libraries(translate-server PRIVATE
httplib::httplib
nlohmann_json::nlohmann_json
cpp-llamalib
OpenSSL::SSL OpenSSL::Crypto
)
target_compile_definitions(translate-server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
if(APPLE)
target_link_libraries(translate-server PRIVATE
"-framework CoreFoundation"
"-framework Security"
)
endif()
完整代码(main.cpp)
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <cpp-llamalib.h>
#include <algorithm>
#include <csignal>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <mutex>
using json = nlohmann::json;
// -------------------------------------------------------------------------
// 模型定义
// -------------------------------------------------------------------------
struct ModelInfo {
std::string name;
std::string params;
std::string size;
std::string repo;
std::string filename;
};
const std::vector<ModelInfo> MODELS = {
{
.name = "gemma-2-2b-it",
.params = "2B",
.size = "1.6 GB",
.repo = "bartowski/gemma-2-2b-it-GGUF",
.filename = "gemma-2-2b-it-Q4_K_M.gguf",
},
{
.name = "gemma-2-9b-it",
.params = "9B",
.size = "5.8 GB",
.repo = "bartowski/gemma-2-9b-it-GGUF",
.filename = "gemma-2-9b-it-Q4_K_M.gguf",
},
{
.name = "Llama-3.1-8B-Instruct",
.params = "8B",
.size = "4.9 GB",
.repo = "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF",
.filename = "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
},
};
// -------------------------------------------------------------------------
// 模型存储目录
// -------------------------------------------------------------------------
std::filesystem::path get_models_dir() {
#ifdef _WIN32
auto env = std::getenv("APPDATA");
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
return base / "translate-app" / "models";
#else
auto env = std::getenv("HOME");
auto base = env ? std::filesystem::path(env) : std::filesystem::path(".");
return base / ".translate-app" / "models";
#endif
}
// -------------------------------------------------------------------------
// 模型下载
// -------------------------------------------------------------------------
// progress_cb 返回 false 则中止下载
bool download_model(const ModelInfo &model,
std::function<bool(int)> progress_cb) {
httplib::Client cli("https://huggingface.co");
cli.set_follow_location(true); // Hugging Face 会重定向到 CDN
cli.set_read_timeout(std::chrono::hours(1)); // 大模型预留长超时
auto url = "/" + model.repo + "/resolve/main/" + model.filename;
auto path = get_models_dir() / model.filename;
auto tmp_path = std::filesystem::path(path).concat(".tmp");
std::ofstream ofs(tmp_path, std::ios::binary);
if (!ofs) { return false; }
auto res = cli.Get(url,
// content_receiver: 逐块接收并写入文件
& {
ofs.write(data, len);
return ofs.good();
},
// progress: 上报进度(返回 false 则中止)
&, last_pct = -1 mutable {
int pct = total ? (int)(current * 100 / total) : 0;
if (pct == last_pct) return true; // 相同进度则跳过
last_pct = pct;
return progress_cb(pct);
});
ofs.close();
if (!res || res->status != 200) {
std::filesystem::remove(tmp_path);
return false;
}
// 下载完成后重命名
std::filesystem::rename(tmp_path, path);
return true;
}
// -------------------------------------------------------------------------
// 服务器
// -------------------------------------------------------------------------
httplib::Server svr;
void signal_handler(int sig) {
if (sig == SIGINT || sig == SIGTERM) {
std::cout << "\nReceived signal, shutting down gracefully...\n";
svr.stop();
}
}
int main() {
// 创建模型存储目录
auto models_dir = get_models_dir();
std::filesystem::create_directories(models_dir);
// 默认模型未下载则自动拉取
std::string selected_model = MODELS[0].filename;
auto path = models_dir / selected_model;
if (!std::filesystem::exists(path)) {
std::cout << "Downloading " << selected_model << "..." << std::endl;
if (!download_model(MODELS[0], [](int pct) {
std::cout << "\r" << pct << "%" << std::flush;
return true;
})) {
std::cerr << "\nFailed to download model." << std::endl;
return 1;
}
std::cout << std::endl;
}
auto llm = llamalib::Llama{path};
std::mutex llm_mutex; // 保护模型切换期间的访问
// LLM 推理耗时较长,放宽超时(默认 5 秒)
svr.set_read_timeout(300);
svr.set_write_timeout(300);
svr.set_logger([](const auto &req, const auto &res) {
std::cout << req.method << " " << req.path << " -> " << res.status
<< std::endl;
});
svr.Get("/health", [](const httplib::Request &, httplib::Response &res) {
res.set_content(json{{"status", "ok"}}.dump(), "application/json");
});
// --- 翻译端点(第 2 章)--------------------------------------------------
svr.Post("/translate",
& {
// JSON 解析与校验(详见第 2 章)
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
"application/json");
return;
}
if (!input.contains("text") || !input["text"].is_string() ||
input["text"].get<std::string>().empty()) {
res.status = 400;
res.set_content(json{{"error", "'text' is required"}}.dump(),
"application/json");
return;
}
auto text = input["text"].get<std::string>();
auto target_lang = input.value("target_lang", "ja");
auto prompt = "Translate the following text to " + target_lang +
". Output only the translation, nothing else.\n\n" + text;
try {
std::lock_guard<std::mutex> lock(llm_mutex);
auto translation = llm.chat(prompt);
res.set_content(json{{"translation", translation}}.dump(),
"application/json");
} catch (const std::exception &e) {
res.status = 500;
res.set_content(json{{"error", e.what()}}.dump(), "application/json");
}
});
// --- SSE 流式翻译(第 3 章)---------------------------------------------
svr.Post("/translate/stream",
& {
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded()) {
res.status = 400;
res.set_content(json{{"error", "Invalid JSON"}}.dump(),
"application/json");
return;
}
if (!input.contains("text") || !input["text"].is_string() ||
input["text"].get<std::string>().empty()) {
res.status = 400;
res.set_content(json{{"error", "'text' is required"}}.dump(),
"application/json");
return;
}
auto text = input["text"].get<std::string>();
auto target_lang = input.value("target_lang", "ja");
auto prompt = "Translate the following text to " + target_lang +
". Output only the translation, nothing else.\n\n" + text;
res.set_chunked_content_provider(
"text/event-stream",
&, prompt {
std::lock_guard<std::mutex> lock(llm_mutex);
try {
llm.chat(prompt, & {
sink.os << "data: "
<< json(std::string(token)).dump(
-1, ' ', false, json::error_handler_t::replace)
<< "\n\n";
return sink.os.good(); // 断开则中止推理
});
sink.os << "data: [DONE]\n\n";
} catch (const std::exception &e) {
sink.os << "data: " << json({{"error", e.what()}}).dump() << "\n\n";
}
sink.done();
return true;
});
});
// --- 模型列表(第 4 章)--------------------------------------------------
svr.Get("/models",
& {
auto models_dir = get_models_dir();
auto arr = json::array();
for (const auto &m : MODELS) {
auto path = models_dir / m.filename;
arr.push_back({
{"name", m.name},
{"params", m.params},
{"size", m.size},
{"downloaded", std::filesystem::exists(path)},
{"selected", m.filename == selected_model},
});
}
res.set_content(json{{"models", arr}}.dump(), "application/json");
});
// --- 模型选择(第 4 章)--------------------------------------------------
svr.Post("/models/select",
& {
auto input = json::parse(req.body, nullptr, false);
if (input.is_discarded() || !input.contains("model")) {
res.status = 400;
res.set_content(json{{"error", "'model' is required"}}.dump(),
"application/json");
return;
}
auto name = input["model"].get<std::string>();
auto it = std::find_if(MODELS.begin(), MODELS.end(),
& { return m.name == name; });
if (it == MODELS.end()) {
res.status = 404;
res.set_content(json{{"error", "Unknown model"}}.dump(),
"application/json");
return;
}
const auto &model = *it;
// 无论已下载与否,一律以 SSE 响应
res.set_chunked_content_provider(
"text/event-stream",
&, model {
// SSE 事件发送辅助函数
auto send = & {
sink.os << "data: " << event.dump() << "\n\n";
};
// 未下载则先下载(进度经 SSE 上报)
auto path = get_models_dir() / model.filename;
if (!std::filesystem::exists(path)) {
bool ok = download_model(model, & {
send({{"status", "downloading"}, {"progress", pct}});
return sink.os.good(); // 客户端断开时中止下载
});
if (!ok) {
send({{"status", "error"}, {"message", "Download failed"}});
sink.done();
return true;
}
}
// 加载模型并切换
send({{"status", "loading"}});
{
std::lock_guard<std::mutex> lock(llm_mutex);
llm = llamalib::Llama{path};
selected_model = model.filename;
}
send({{"status", "ready"}});
sink.done();
return true;
});
});
// 支持 `Ctrl+C`(SIGINT)与 `kill`(SIGTERM)优雅停机
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
std::cout << "Listening on http://127.0.0.1:8080" << std::endl;
svr.listen("127.0.0.1", 8080);
}
4.9 运行验证
CMakeLists.txt 新增 OpenSSL 配置后,需重新执行 CMake 再构建:
cmake -B build
cmake --build build -j
./build/translate-server
检查模型列表
curl http://localhost:8080/models
第 1 章下载的 gemma-2-2b-it 应显示 downloaded: true 与 selected: true。
切换到其他模型
curl -N -X POST http://localhost:8080/models/select \
-H "Content-Type: application/json" \
-d '{"model": "gemma-2-9b-it"}'
SSE 中会持续流出下载进度,完成后收到 "ready"。
跨模型对比翻译效果
用不同模型翻译同一例句:
# 用 gemma-2-9b-it 翻译(刚切换的模型)
curl -X POST http://localhost:8080/translate \
-H "Content-Type: application/json" \
-d '{"text": "The quick brown fox jumps over the lazy dog.", "target_lang": "ja"}'
# 切回 gemma-2-2b-it
curl -N -X POST http://localhost:8080/models/select \
-H "Content-Type: application/json" \
-d '{"model": "gemma-2-2b-it"}'
# 翻译同一句话
curl -X POST http://localhost:8080/translate \
-H "Content-Type: application/json" \
-d '{"text": "The quick brown fox jumps over the lazy dog.", "target_lang": "ja"}'
可以看到:同样的代码、同样的提示词,不同模型会产出不同的翻译结果。由于 cpp-llamalib 会自动为每个模型应用对应的聊天模板,业务代码无需任何改动,这正是“元数据驱动模型管理”带来的收益。
小结与下一步
至此,服务器已具备完整的功能闭环:REST API(/translate)、SSE 流式输出(/translate/stream)、模型下载与热切换(/models、/models/select)。其中模型管理这一环,本质上是把 cpp-httplib 的客户端能力(TLS 连接、重定向跟随、回调式流式下载、超时控制)与服务端能力(线程池并发、set_chunked_content_provider + DataSink、SSE)在同一进程内组合使用,再辅以互斥锁保证模型热切换的线程安全。下一步将添加静态文件服务,构建可在浏览器中直接操作的 Web UI(对应 ch05-web-ui),届时本节实现的进度上报与断连取消机制将成为前端交互的基础。