1. 项目概述:LLM模型交互系统的设计与实现
在构建现代AI应用时,与大型语言模型(LLM)的高效交互是核心需求之一。本项目实现了一个灵活可扩展的LLM模型交互系统,采用策略模式设计抽象接口,支持DeepSeek、ChatGPT等多种模型的统一接入。系统包含三大核心模块:公共数据结构定义、日志系统封装和模型交互实现,其中特别针对流式响应(SSE)和全量返回两种模式进行了深度优化。
提示:本文代码示例基于C++17标准,使用spdlog 1.11.0、nlohmann/json 3.11.2和cpp-httplib 0.12.1等开源库实现。
2. 核心数据结构设计
2.1 模型配置数据结构
所有LLM模型交互都需要的基础配置参数通过ModelConfig结构体统一管理:
cpp复制// common.h
struct ModelConfig {
std::string modelName; // 模型标识如"deepseek-chat"
std::string apiKey; // 认证密钥
double temperature = 1.0; // 采样温度(0-2)
int maxTokens = 2048; // 生成最大token数
bool stream = false; // 是否启用流式响应
// 序列化为JSON字符串
std::string toJson() const {
nlohmann::json j;
j["model"] = modelName;
j["temperature"] = temperature;
j["max_tokens"] = maxTokens;
j["stream"] = stream;
return j.dump();
}
};
温度参数(temperature)的取值建议:
- 0.0:确定性输出(代码生成/数学解题)
- 1.0-1.3:平衡性输出(通用对话/翻译)
- 1.5以上:创造性输出(诗歌/故事生成)
2.2 会话消息结构
对话历史通过Message结构体管理,支持多轮对话上下文保持:
cpp复制struct Message {
std::string role; // "system"/"user"/"assistant"
std::string content; // 消息内容
nlohmann::json toJson() const {
return {{"role", role}, {"content", content}};
}
};
注意:DeepSeek等模型API采用无状态设计,客户端需自行维护完整的对话历史,每次请求携带全部上下文消息。
3. 日志系统封装实现
3.1 为什么需要专业日志库
对比std::cout,spdlog等专业日志库提供以下关键优势:
| 特性 | std::cout | spdlog |
|---|---|---|
| 日志级别管理 | 不支持 | 6级可调 |
| 线程安全 | 需手动实现 | 内置锁机制 |
| 输出目标 | 仅控制台 | 文件/网络/控制台 |
| 性能 | 同步阻塞 | 异步缓冲 |
| 格式化 | 手动拼接 | 自动添加时间戳 |
3.2 spdlog单例封装实现
cpp复制// mylog.h
class MyLogger {
public:
static MyLogger& instance() {
static MyLogger inst;
return inst;
}
void init(const std::string& filename = "app.log") {
try {
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
filename, 1024*1024*5, 3);
logger_ = std::make_shared<spdlog::logger>("multi_sink",
spdlog::sinks_init_list{console_sink, file_sink});
logger_->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [thread %t] %v");
logger_->set_level(spdlog::level::debug);
logger_->flush_on(spdlog::level::warn);
spdlog::register_logger(logger_);
} catch (const spdlog::spdlog_ex& ex) {
std::cerr << "Log init failed: " << ex.what() << std::endl;
}
}
std::shared_ptr<spdlog::logger> getLogger() { return logger_; }
private:
std::shared_ptr<spdlog::logger> logger_;
MyLogger() = default;
};
// 使用示例
#define LOG_TRACE(...) MyLogger::instance().getLogger()->trace(__VA_ARGS__)
#define LOG_DEBUG(...) MyLogger::instance().getLogger()->debug(__VA_ARGS__)
#define LOG_INFO(...) MyLogger::instance().getLogger()->info(__VA_ARGS__)
#define LOG_WARN(...) MyLogger::instance().getLogger()->warn(__VA_ARGS__)
#define LOG_ERROR(...) MyLogger::instance().getLogger()->error(__VA_ARGS__)
#define LOG_CRITICAL(...) MyLogger::instance().getLogger()->critical(__VA_ARGS__)
关键配置参数说明:
rotating_file_sink_mt:线程安全的滚动日志文件- 102410245:单个日志文件最大5MB
- 3:保留3个历史日志文件
%^%l%$:彩色日志级别显示%t:线程ID
4. 模型交互策略模式实现
4.1 策略模式类设计
cpp复制// LLMProvider.h
class LLMProvider {
public:
virtual ~LLMProvider() = default;
virtual bool initialize(const ModelConfig& config) = 0;
virtual bool isModelValid() const = 0;
virtual std::string sendMessage(const std::vector<Message>& messages) = 0;
virtual void streamMessage(
const std::vector<Message>& messages,
std::function<bool(const std::string&)> callback) = 0;
virtual std::string modelName() const = 0;
virtual std::string modelDescription() const = 0;
};
4.2 DeepSeekProvider实现要点
4.2.1 初始化配置
cpp复制// DeepSeekProvider.cpp
bool DeepSeekProvider::initialize(const ModelConfig& config) {
if (config.apiKey.empty()) {
LOG_ERROR("API key cannot be empty");
return false;
}
config_ = config;
if (config_.modelName.empty()) {
config_.modelName = "deepseek-chat";
}
// 验证温度参数范围
if (config_.temperature < 0 || config_.temperature > 2) {
LOG_WARN("Temperature {} out of range, clamping to [0,2]", config_.temperature);
config_.temperature = std::clamp(config_.temperature, 0.0, 2.0);
}
return true;
}
4.2.2 全量返回实现
cpp复制std::string DeepSeekProvider::sendMessage(const std::vector<Message>& messages) {
if (!isModelValid()) {
LOG_ERROR("Model not initialized or invalid");
return "";
}
httplib::Client cli("https://api.deepseek.com");
nlohmann::json reqJson;
// 构造请求体
reqJson["model"] = config_.modelName;
reqJson["temperature"] = config_.temperature;
reqJson["max_tokens"] = config_.maxTokens;
reqJson["stream"] = false;
for (const auto& msg : messages) {
reqJson["messages"].push_back(msg.toJson());
}
// 设置请求头
httplib::Headers headers = {
{"Content-Type", "application/json"},
{"Authorization", "Bearer " + config_.apiKey}
};
auto res = cli.Post("/v1/chat/completions", headers,
reqJson.dump(), "application/json");
if (!res || res->status != 200) {
LOG_ERROR("Request failed: {}", res ? res->status : -1);
return "";
}
try {
auto json = nlohmann::json::parse(res->body);
return json["choices"][0]["message"]["content"].get<std::string>();
} catch (const std::exception& e) {
LOG_ERROR("JSON parse error: {}", e.what());
return "";
}
}
4.2.3 流式响应实现
cpp复制void DeepSeekProvider::streamMessage(
const std::vector<Message>& messages,
std::function<bool(const std::string&)> callback)
{
httplib::Client cli("https://api.deepseek.com");
nlohmann::json reqJson;
reqJson["model"] = config_.modelName;
reqJson["temperature"] = config_.temperature;
reqJson["max_tokens"] = config_.maxTokens;
reqJson["stream"] = true;
for (const auto& msg : messages) {
reqJson["messages"].push_back(msg.toJson());
}
std::string accumulatedResponse;
bool firstChunk = true;
auto res = cli.Post("/v1/chat/completions",
{
{"Content-Type", "application/json"},
{"Authorization", "Bearer " + config_.apiKey},
{"Accept", "text/event-stream"}
},
reqJson.dump(),
"application/json",
[&](const char* data, size_t len, uint64_t /*offset*/, uint64_t /*total*/) {
std::string chunk(data, len);
accumulatedResponse += chunk;
// 处理SSE格式数据
size_t pos = 0;
while ((pos = accumulatedResponse.find("\n\n")) != std::string::npos) {
std::string event = accumulatedResponse.substr(0, pos);
accumulatedResponse.erase(0, pos + 2);
if (event.empty() || event == "data: [DONE]") {
continue;
}
if (event.substr(0, 6) == "data: ") {
try {
auto json = nlohmann::json::parse(event.substr(6));
if (json.contains("choices") &&
json["choices"][0].contains("delta") &&
json["choices"][0]["delta"].contains("content"))
{
std::string content = json["choices"][0]["delta"]["content"];
if (!content.empty()) {
if (!callback(content)) {
return false; // 用户请求停止
}
}
}
} catch (...) {
LOG_ERROR("Error parsing SSE chunk: {}", event);
}
}
}
return true;
});
if (!res || res->status != 200) {
LOG_ERROR("Stream request failed: {}", res ? res->status : -1);
callback("[ERROR] Request failed");
}
}
5. 关键技术深度解析
5.1 SSE协议处理机制
流式响应基于Server-Sent Events(SSE)协议,关键处理逻辑:
- 连接建立:客户端发送
Accept: text/event-stream头 - 数据格式:每个事件以
data:开头,双换行符分隔 - 消息处理:
- 过滤心跳包(空消息)
- 识别结束标记
[DONE] - 解析JSON增量内容
cpp复制// 典型SSE数据流示例
/*
data: {"id":"chat-123","choices":[{"delta":{"content":"Hello"}}]}
data: {"id":"chat-123","choices":[{"delta":{"content":" world"}}]}
data: [DONE]
*/
5.2 线程安全与性能优化
-
日志线程安全:
- 使用
spdlog::sinks::stdout_color_sink_mt(带锁控制台输出) - 文件写入使用
rotating_file_sink_mt的线程安全实现
- 使用
-
HTTP连接池:
cpp复制// 复用HTTP客户端实例 static httplib::Client& getClient() { static httplib::Client cli("https://api.deepseek.com"); cli.set_connection_timeout(10); cli.set_read_timeout(60); // 流式响应需要更长的超时 return cli; } -
异步日志刷新:
cpp复制logger_->flush_on(spdlog::level::warn); // 仅在WARN及以上级别触发同步刷新 spdlog::flush_every(std::chrono::seconds(3)); // 后台线程定期刷新
6. 完整测试示例
6.1 环境配置
bash复制# CMakeLists.txt关键配置
find_package(OpenSSL REQUIRED)
add_executable(test_llm testLLM.cpp)
target_link_libraries(test_llm PRIVATE
spdlog::spdlog
nlohmann_json::nlohmann_json
OpenSSL::SSL)
6.2 全量返回测试
cpp复制// testLLM.cpp
void testFullResponse() {
ModelConfig config;
config.apiKey = std::getenv("deepseek_apikey");
config.modelName = "deepseek-chat";
config.temperature = 0.7;
DeepSeekProvider provider;
if (!provider.initialize(config)) {
LOG_ERROR("Init failed");
return;
}
std::vector<Message> messages = {
{"user", "请用C++实现快速排序"}
};
auto response = provider.sendMessage(messages);
LOG_INFO("Full response:\n{}", response);
}
6.3 流式返回测试
cpp复制void testStreamResponse() {
ModelConfig config;
config.apiKey = std::getenv("deepseek_apikey");
config.modelName = "deepseek-chat";
config.stream = true;
DeepSeekProvider provider;
provider.initialize(config);
std::vector<Message> messages = {
{"user", "用Python实现二分查找算法"}
};
std::cout << "AI: ";
provider.streamMessage(messages, [](const std::string& chunk) {
std::cout << chunk << std::flush;
return true; // 继续接收
});
std::cout << std::endl;
}
7. 常见问题与解决方案
7.1 SSL连接失败
问题现象:
code复制[E] [2024-03-27 15:30:45.123] [thread 1402] HTTPS request failed: SSL connect error
解决方案:
- 确认CMake已正确链接OpenSSL
- 检查系统证书链是否完整
- 对于自签名证书,可临时禁用验证(不推荐生产环境):
cpp复制httplib::Client cli("https://api.deepseek.com"); cli.enable_server_certificate_verification(false);
7.2 流式响应中断
问题现象:连接提前关闭,接收不完整响应
排查步骤:
- 检查网络稳定性
- 增加读取超时时间:
cpp复制cli.set_read_timeout(300); // 5分钟超时 - 实现自动重连机制:
cpp复制int retry = 0; while (retry < 3) { if (doStreamRequest()) break; retry++; std::this_thread::sleep_for(std::chrono::seconds(1)); }
7.3 性能优化建议
- 连接复用:静态保持HTTP客户端实例
- 异步日志:使用
spdlog::async_logger - 内存池:对于高频创建的消息对象使用对象池
- JSON解析优化:
cpp复制// 使用json::parse的SAX接口处理大响应 nlohmann::json::parser_callback_t cb = [](int depth, nlohmann::json::parse_event_t event, nlohmann::json& parsed) { // 自定义解析逻辑 return true; }; auto json = nlohmann::json::parse(response, cb);
8. 扩展设计思路
8.1 会话管理增强
cpp复制class SessionManager {
public:
void startNewSession(const std::string& userId) {
sessions_[userId] = std::vector<Message>();
}
void addMessage(const std::string& userId, const Message& msg) {
sessions_[userId].push_back(msg);
// 实现token计数和截断
if (calculateTokens(sessions_[userId]) > MAX_TOKENS) {
truncateSession(userId);
}
}
private:
std::unordered_map<std::string, std::vector<Message>> sessions_;
size_t calculateTokens(const std::vector<Message>& messages) {
// 实现token估算
return 0;
}
};
8.2 多模型负载均衡
cpp复制class LoadBalancer {
public:
void addProvider(std::shared_ptr<LLMProvider> provider) {
providers_.push_back(provider);
}
std::string sendMessage(const std::vector<Message>& messages) {
// 基于轮询/延迟/错误的策略选择provider
auto provider = selectProvider();
return provider->sendMessage(messages);
}
private:
std::vector<std::shared_ptr<LLMProvider>> providers_;
size_t currentIndex_ = 0;
};
在实际项目中使用时,建议将API密钥等敏感信息通过环境变量或配置中心获取,避免硬编码在代码中。对于生产环境部署,还需要考虑实现请求限流、故障熔断等机制保障系统稳定性。