1. LLMProvider的设计与实现
1.1 策略模式的应用
在设计LLMProvider时,我们采用了策略模式来封装不同大模型的调用方式。策略模式的核心思想是将算法(或行为)封装起来,使它们可以相互替换,而不用在代码中写一堆if-else/switch来决定使用哪个算法。
举个生活中的例子:假设你要从宿舍去图书馆,有三种方式可选:
- 走路(最省钱但慢)
- 骑自行车(中等速度,中等花销)
- 坐校内公交车(最快但贵)
在代码中,我们可以这样实现:
cpp复制class TransportStrategy {
public:
virtual void go() = 0;
};
class WalkStrategy : public TransportStrategy {
public:
virtual void go() override { cout << "走路去机房🚶"; }
};
class BikeStrategy : public TransportStrategy {
public:
virtual void go() override { cout << "骑车去机房🚴"; }
};
class BusStrategy : public TransportStrategy {
public:
virtual void go() override { cout << "打车去机房🚕"; }
};
class Student {
private:
TransportStrategy* strategy;
public:
void setStrategy(TransportStrategy* s) { strategy = s; }
void goToLab() { strategy->go(); }
};
这种设计的美妙之处在于,使用时只需和TransportStrategy打交道,不需要知道背后具体是哪种策略。如果想更换模式,只需要更换一个具体的策略对象即可,程序基本不需要改动。
1.2 LLMProvider抽象基类设计
我们将策略模式的思想应用到LLMProvider的设计中。首先定义一个抽象基类LLMProvider,包含所有大模型提供者都需要实现的接口:
cpp复制class LLMProvider {
public:
virtual bool initModel(const std::map<std::string, std::string>& modelConfig) = 0;
virtual std::string getModelName() const = 0;
virtual std::string getModelDesc() const = 0;
virtual bool isAvailable() const = 0;
virtual std::string sendMessage(const std::vector<Message>& message,
const std::map<std::string, std::string>& requestParam) = 0;
virtual void sendMessageStream(const std::vector<Message>& message,
const std::map<std::string, std::string>& requestParam,
std::function<void(const std::string&, bool)> callback) = 0;
protected:
bool _isAvailable = false;
std::string _apikey;
std::string _endpoint;
};
这个设计的关键点:
- 将LLMProvider设为抽象类(接口类),所有方法都是纯虚函数
- 使用C++的多态机制,基类指针可以指向子类对象
- 公共部分(如apiKey和endpoint)放在protected区域,子类可以直接访问
- 支持两种消息发送方式:全量返回和流式返回
1.3 多态机制的实现
通过C++的多态机制,我们可以在运行时决定使用哪个具体的大模型提供者。例如:
cpp复制std::shared_ptr<LLMProvider> provider;
// 根据配置决定使用哪个提供者
if(config.model == "deepseek") {
provider = std::make_shared<DeepSeekProvider>();
} else if(config.model == "gemini") {
provider = std::make_shared<GeminiProvider>();
}
// 统一接口调用
provider->sendMessage(messages, params);
这种设计避免了代码重复,新增一个大模型支持时,只需要实现一个新的Provider子类即可,不需要修改现有代码。
2. DeepSeek模型接入实现
2.1 DeepSeek API基础
DeepSeek提供了标准的HTTP API接口,主要参数包括:
- model:模型名称
- messages:对话消息列表
- temperature:控制生成文本的随机性
- max_tokens:最大生成token数
- stream:是否启用流式响应
API调用需要注意几个关键点:
- 无状态服务原则:每次请求视为独立会话,需要客户端主动管理对话上下文
- 系统提示:如需保持角色设定,每次请求必须包含系统级指令
- 对话历史:模型仅处理当前请求中的上下文,无法关联前序对话
2.2 初始化实现
DeepSeekProvider的初始化主要包括:
- 从配置中获取api_key和endpoint
- 验证配置有效性
- 设置模型可用状态
cpp复制bool DeepSeekProvider::initModel(const std::map<std::string, std::string>& modelConfig) {
// 检查api_key
auto it = modelConfig.find("api_key");
if(it == modelConfig.end()) {
ERR("DeepSeekProvider initModel failed: api_key not found");
return false;
}
_apikey = it->second;
// 检查endpoint
it = modelConfig.find("endpoint");
if(it == modelConfig.end()) {
ERR("DeepSeekProvider initModel failed: endpoint not found");
return false;
}
_endpoint = it->second;
_isAvailable = true;
return true;
}
安全提示:在实际项目中,api_key这样的敏感信息不应该硬编码在代码中,应该通过环境变量或配置中心获取。
2.3 全量返回实现
全量返回的实现相对简单,主要步骤:
- 构造请求参数
- 发送HTTP请求
- 解析响应
cpp复制std::string DeepSeekProvider::sendMessage(const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam) {
if(!isAvailable()) {
ERR("Model not available");
return "";
}
// 构造请求体
Json::Value requestBody;
requestBody["model"] = getModelName();
// 构造messages数组...
requestBody["temperature"] = 0.7; // 默认值
// 设置其他参数...
// 发送请求
httplib::Client client(_endpoint.c_str());
auto res = client.Post("/v1/chat/completions", requestBody.toStyledString(), "application/json");
if(!res || res->status != 200) {
ERR("Request failed");
return "";
}
// 解析响应
Json::Value response;
Json::Reader reader;
if(!reader.parse(res->body, response)) {
ERR("Parse response failed");
return "";
}
// 提取content
if(response.isMember("choices") &&
response["choices"].isArray() &&
!response["choices"].empty() &&
response["choices"][0].isMember("message") &&
response["choices"][0]["message"].isMember("content")) {
return response["choices"][0]["message"]["content"].asString();
}
return "";
}
2.4 流式返回实现
流式返回的实现更为复杂,需要使用SSE(Server-Sent Events)协议。SSE的特点:
- 单向通信:服务器可以主动推送数据到客户端
- 基于HTTP协议:使用标准HTTP,无需额外协议或端口
- 自动重连:连接断开会自动尝试重新连接
- 支持事件类型:可以发送不同类型的事件
实现步骤:
- 设置stream=true
- 设置Accept头为text/event-stream
- 实现content_receiver处理增量数据
cpp复制void DeepSeekProvider::sendMessageStream(const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam,
std::function<void(const std::string&, bool)> callback) {
// ...初始化检查
// 设置流式参数
requestBody["stream"] = true;
// 设置请求头
httplib::Headers headers = {
{"Authorization", "Bearer " + _apikey},
{"Content-Type", "application/json"},
{"Accept", "text/event-stream"}
};
// 创建客户端
httplib::Client client(_endpoint.c_str());
client.set_read_timeout(300, 0); // 设置长超时
// 定义处理变量
std::string buffer;
bool streamFinish = false;
// 设置请求
httplib::Request req;
req.method = "POST";
req.path = "/v1/chat/completions";
req.headers = headers;
req.body = Json::writeString(writer, requestBody);
// 设置响应处理器
req.response_handler = [&](const httplib::Response& res) {
if(res.status != 200) {
ERR("HTTP error: {}", res.status);
return false;
}
return true;
};
// 设置数据接收处理器
req.content_receiver = [&](const char* data, size_t len, uint64_t offset, uint64_t total) {
buffer.append(data, len);
// 处理SSE格式数据
size_t pos = 0;
while((pos = buffer.find("\n\n")) != std::string::npos) {
std::string chunk = buffer.substr(0, pos);
buffer.erase(0, pos + 2);
if(chunk.empty() || chunk[0] == ':') continue;
if(chunk.compare(0, 6, "data: ") == 0) {
std::string modelData = chunk.substr(6);
if(modelData == "[DONE]") {
streamFinish = true;
callback("", true); // 通知结束
return true;
}
// 解析JSON
Json::Value modelDataJson;
Json::CharReaderBuilder reader;
std::string errors;
std::istringstream modelDataStream(modelData);
if(Json::parseFromStream(reader, modelDataStream, &modelDataJson, &errors)) {
// 提取content
if(modelDataJson.isMember("choices") &&
modelDataJson["choices"].isArray() &&
!modelDataJson["choices"].empty() &&
modelDataJson["choices"][0].isMember("delta") &&
modelDataJson["choices"][0]["delta"].isMember("content")) {
std::string content = modelDataJson["choices"][0]["delta"]["content"].asString();
callback(content, false); // 传递增量数据
}
}
}
}
return true;
};
// 发送请求
auto result = client.send(req);
if(!result) {
ERR("Request failed");
callback("", true);
}
if(!streamFinish) {
WARN("Stream ended without [DONE] marker");
callback("", true);
}
}
3. 测试与问题排查
3.1 测试环境搭建
我们使用Google Test框架进行单元测试,测试代码结构如下:
cpp复制TEST(DeepSeekProviderTest, sendMessage) {
auto provider = std::make_shared<DeepSeekProvider>();
ASSERT_TRUE(provider != nullptr);
// 初始化模型
std::map<std::string, std::string> modelParam;
const char* apiKey = std::getenv("DEEPSEEK_API_KEY");
modelParam["api_key"] = apiKey ? apiKey : "sk-your-api-key";
modelParam["endpoint"] = "https://api.deepseek.com";
provider->initModel(modelParam);
ASSERT_TRUE(provider->isAvailable());
// 测试全量返回
std::map<std::string, std::string> requestParam = {
{"temperature", "0.7"},
{"max_tokens", "2048"}
};
std::vector<Message> messages = {{"user", "你是谁?"}};
std::string response = provider->sendMessage(messages, requestParam);
ASSERT_FALSE(response.empty());
// 测试流式返回
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());
}
3.2 常见问题与解决方案
-
API Key泄露风险
- 问题:将API Key硬编码在代码中
- 解决方案:通过环境变量获取,如
std::getenv("DEEPSEEK_API_KEY")
-
流式返回不工作
- 可能原因:
- 没有设置
stream=true - 没有正确设置
Accept: text/event-stream头 - 读取超时时间太短
- 没有设置
- 解决方案:
cpp复制requestBody["stream"] = true; headers["Accept"] = "text/event-stream"; client.set_read_timeout(300, 0); // 5分钟超时
- 可能原因:
-
SSE数据解析错误
- 问题:没有正确处理SSE的数据块分隔符
\n\n - 解决方案:
cpp复制while((pos = buffer.find("\n\n")) != std::string::npos) { std::string chunk = buffer.substr(0, pos); buffer.erase(0, pos + 2); // 处理chunk... }
- 问题:没有正确处理SSE的数据块分隔符
-
JSON解析失败
- 问题:模型返回的JSON格式不正确
- 解决方案:添加严格的格式检查
cpp复制if(!modelDataJson.isMember("choices") || !modelDataJson["choices"].isArray() || modelDataJson["choices"].empty()) { WARN("Invalid response format"); continue; }
-
HTTP客户端问题
- 问题:某些HTTP客户端默认不支持HTTP协议
- 解决方案:明确设置协议版本
cpp复制client.set_http_version(httplib::HttpVersion::v1_1);
4. 性能优化与最佳实践
4.1 连接池管理
频繁创建和销毁HTTP连接会影响性能,建议实现连接池:
cpp复制class HttpClientPool {
public:
httplib::Client* getClient(const std::string& endpoint) {
std::lock_guard<std::mutex> lock(_mutex);
if(_pool[endpoint].empty()) {
return new httplib::Client(endpoint);
}
auto client = _pool[endpoint].back();
_pool[endpoint].pop_back();
return client;
}
void releaseClient(const std::string& endpoint, httplib::Client* client) {
std::lock_guard<std::mutex> lock(_mutex);
_pool[endpoint].push_back(client);
}
private:
std::map<std::string, std::vector<httplib::Client*>> _pool;
std::mutex _mutex;
};
4.2 异步处理
对于高并发场景,可以使用异步方式发送请求:
cpp复制std::future<std::string> DeepSeekProvider::sendMessageAsync(
const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam) {
return std::async(std::launch::async, [=]() {
return this->sendMessage(messages, requestParam);
});
}
4.3 重试机制
网络请求可能会失败,实现自动重试机制:
cpp复制std::string sendMessageWithRetry(const std::vector<Message>& messages,
const std::map<std::string, std::string>& requestParam,
int maxRetry = 3) {
int retry = 0;
while(retry < maxRetry) {
try {
return sendMessage(messages, requestParam);
} catch(const std::exception& e) {
WARN("Request failed ({}), retrying...", e.what());
retry++;
std::this_thread::sleep_for(std::chrono::seconds(1 << retry)); // 指数退避
}
}
throw std::runtime_error("Max retry count exceeded");
}
4.4 日志记录
完善的日志记录有助于问题排查:
cpp复制void DeepSeekProvider::logRequest(const Json::Value& request) {
Json::StreamWriterBuilder writer;
writer["indentation"] = "";
std::string requestStr = Json::writeString(writer, request);
// 脱敏处理
size_t pos = requestStr.find(_apikey);
if(pos != std::string::npos) {
requestStr.replace(pos, _apikey.length(), "***");
}
INFO("Request: {}", requestStr);
}
void DeepSeekProvider::logResponse(const std::string& response) {
INFO("Response: {}", response);
}
5. 安全注意事项
-
API Key保护
- 永远不要将API Key提交到代码仓库
- 使用环境变量或密钥管理服务存储敏感信息
- 在日志中自动脱敏API Key
-
输入验证
- 验证所有输入参数
- 限制消息长度和参数范围
cpp复制void validateMessages(const std::vector<Message>& messages) {
if(messages.empty()) {
throw std::invalid_argument("Messages cannot be empty");
}
for(const auto& msg : messages) {
if(msg._content.empty()) {
throw std::invalid_argument("Message content cannot be empty");
}
if(msg._content.length() > 4096) {
throw std::invalid_argument("Message too long");
}
}
}
- HTTPS加密
- 确保所有通信使用HTTPS
- 验证服务器证书
cpp复制client.enable_server_certificate_verification(true);
- 速率限制
- 实现请求速率限制,避免被服务商限制
cpp复制class RateLimiter {
public:
bool allowRequest() {
auto now = std::chrono::steady_clock::now();
std::lock_guard<std::mutex> lock(_mutex);
// 移除过期记录
_timestamps.erase(std::remove_if(_timestamps.begin(), _timestamps.end(),
[now](auto& t) { return now - t > _interval; }), _timestamps.end());
if(_timestamps.size() >= _maxRequests) {
return false;
}
_timestamps.push_back(now);
return true;
}
private:
std::vector<std::chrono::steady_clock::time_point> _timestamps;
std::chrono::seconds _interval{60};
size_t _maxRequests{60};
std::mutex _mutex;
};
6. 扩展性与维护性
6.1 配置化管理
将硬编码的参数改为可配置的:
yaml复制providers:
deepseek:
endpoint: "https://api.deepseek.com"
rate_limit: 60/60s
default_params:
temperature: 0.7
max_tokens: 2048
6.2 插件化架构
支持动态加载新的Provider实现:
cpp复制class ProviderFactory {
public:
static std::shared_ptr<LLMProvider> create(const std::string& name) {
auto it = _creators.find(name);
if(it == _creators.end()) {
throw std::runtime_error("Unknown provider: " + name);
}
return it->second();
}
static void registerProvider(const std::string& name,
std::function<std::shared_ptr<LLMProvider>()> creator) {
_creators[name] = creator;
}
private:
static std::map<std::string, std::function<std::shared_ptr<LLMProvider>()>> _creators;
};
// 注册Provider
ProviderFactory::registerProvider("deepseek", []() {
return std::make_shared<DeepSeekProvider>();
});
6.3 监控与指标
添加性能监控指标:
cpp复制class ProviderMetrics {
public:
void recordRequest(const std::string& provider, bool success,
std::chrono::milliseconds duration) {
std::lock_guard<std::mutex> lock(_mutex);
_requestsTotal[provider]++;
if(!success) _requestErrors[provider]++;
_requestDuration[provider].push_back(duration);
}
double getErrorRate(const std::string& provider) const {
std::lock_guard<std::mutex> lock(_mutex);
if(_requestsTotal.count(provider) == 0) return 0.0;
return static_cast<double>(_requestErrors.at(provider)) / _requestsTotal.at(provider);
}
private:
mutable std::mutex _mutex;
std::map<std::string, int> _requestsTotal;
std::map<std::string, int> _requestErrors;
std::map<std::string, std::vector<std::chrono::milliseconds>> _requestDuration;
};
7. 实际应用中的经验分享
- 上下文管理
- 大模型API通常是无状态的,需要客户端维护对话历史
- 实现一个简单的上下文管理器:
cpp复制class Conversation {
public:
void addMessage(const std::string& role, const std::string& content) {
_messages.push_back({role, content});
// 限制上下文长度
while(getTokenCount() > _maxTokens && _messages.size() > 1) {
_messages.erase(_messages.begin() + 1); // 保留系统消息
}
}
const std::vector<Message>& getMessages() const { return _messages; }
private:
size_t getTokenCount() const {
// 简单实现:按字数估算
size_t count = 0;
for(const auto& msg : _messages) {
count += msg._content.length() / 4; // 粗略估算
}
return count;
}
std::vector<Message> _messages;
size_t _maxTokens = 4096;
};
- 流式返回的优化处理
- 对于流式返回,可以添加缓冲区减少回调频率
- 实现自动拼接和格式化
cpp复制class StreamBuffer {
public:
void append(const std::string& chunk,
std::function<void(const std::string&)> callback) {
_buffer += chunk;
// 按句子分割处理
size_t pos = 0;
while((pos = _buffer.find_first_of("。.!?\n")) != std::string::npos) {
std::string sentence = _buffer.substr(0, pos + 1);
_buffer.erase(0, pos + 1);
if(!sentence.empty()) {
callback(sentence);
}
}
}
void flush(std::function<void(const std::string&)> callback) {
if(!_buffer.empty()) {
callback(_buffer);
_buffer.clear();
}
}
private:
std::string _buffer;
};
- 多模型降级策略
- 当主模型不可用时,自动降级到备用模型
cpp复制std::string getResponseWithFallback(const std::vector<Message>& messages,
const std::vector<std::string>& providers) {
for(const auto& providerName : providers) {
try {
auto provider = ProviderFactory::create(providerName);
if(provider->isAvailable()) {
return provider->sendMessage(messages, {});
}
} catch(const std::exception& e) {
WARN("Provider {} failed: {}", providerName, e.what());
}
}
throw std::runtime_error("All providers failed");
}
- 测试中的经验教训
- 单元测试要覆盖各种边界情况
- 模拟网络故障和API限流场景
- 测试长时间运行的流式连接
cpp复制TEST(DeepSeekProviderTest, streamLongRunning) {
// 测试长时间运行的流式连接
auto provider = createProvider();
std::vector<Message> messages;
messages.push_back({"user", "请生成一篇1000字以上的文章"});
std::string fullResponse;
auto callback = [&](const std::string& chunk, bool last) {
if(!last) {
fullResponse += chunk;
}
};
provider->sendMessageStream(messages, {{"max_tokens", "2048"}}, callback);
EXPECT_GT(fullResponse.length(), 1000);
}
在实际项目中,这种设计模式已经被证明能够很好地适应不同大模型的接入需求。通过抽象接口和具体实现的分离,我们可以轻松支持新的模型提供者,而不会影响现有代码的稳定性。