LLMManager 类如何用 unique_ptr 管理大模型提供者
LLMManager 的核心职责
LLMManager 是 AI 大模型 SDK 里的中枢组件,主要干一件事:统一托管所有大语言模型提供者的生命周期。它不直接和模型对话,而是负责把外部请求准确地转发给对应的底层实现(也就是 LLMProvider),同时确保这些底层实例被安全地创建、使用和销毁。

它的内部结构很清晰,主要靠两个私有成员变量撑起来:
_providers:一个std::map,键是模型的唯一名称(比如 "gpt-4o" 或 "qwen-max"),值是std::unique_ptr<LLMProvider>。这个 map 拥有所有 LLMProvider 实例的完全所有权。_modelInfos:另一个std::map,同样以模型名为键,但值是一个叫ModelInfo的结构体。这个结构体用来存储模型的元数据,比如描述、厂商、API 端点,还有一个关键的_isAvailable标记,用来指示这个模型是否已经成功初始化并可以对外服务。
这种设计把“干活的”(Provider)和“管信息的”(ModelInfo)分开,职责分明,也方便上层应用查询模型状态而不必触碰底层实例。
ModelInfo:模型的身份证
ModelInfo 结构体是 SDK 对外暴露模型信息的标准格式。它的定义很简单:
struct ModelInfo {
std::string _modelName; // 模型唯一标识名称
std::string _modelDesc; // 模型功能描述
std::string _provider; // 模型提供厂商
std::string _endpoint; // API服务基础端点
bool _isAvailable = false; // 模型初始化完成、可服务状态标记
ModelInfo(const std::string& modelName,
const std::string& modelDesc,
const std::string& provider,
const std::string& endpoint)
: _modelName(modelName),
_modelDesc(modelDesc),
_provider(provider),
_endpoint(endpoint) {}
};注意,_isAvailable 默认是 false。只有当 initModel 接口被成功调用后,这个标志才会被置为 true。这样,任何想使用模型的代码都可以先通过 isModelAvailable 快速检查一下,避免在未就绪的模型上浪费时间或引发错误。
为什么选 unique_ptr 而不是 shared_ptr?
在最初的草稿里,作者考虑过用 std::shared_ptr 来管理 _providers 里的实例。这看起来是个“万金油”方案,但仔细一想,问题就来了。
首先,LLMProvider 实例的生命周期完全由 LLMManager 控制。SDK 内部没有任何其他地方需要共享同一个 Provider 实例。这意味着 shared_ptr 引以为傲的引用计数机制在这里完全是多余的开销。每次拷贝、赋值都要去原子地增减那个计数器,对于高频调用的 SDK 来说,这笔性能账不能不算。

其次,也是更重要的,shared_ptr 的语义是“共享所有权”。但在我们的场景里,所有权应该是“独占”的。LLMManager 是这些资源唯一的主人。使用 shared_ptr 会打开一个危险的口子:万一某个地方不小心拷贝了一份 shared_ptr 出去,就可能导致 LLMManager 销毁时,因为还有外部引用存在,资源无法被释放,造成内存泄漏。或者更糟,在 LLMManager 不知情的情况下,外部代码提前释放了资源。

std::unique_ptr 完美地解决了这些问题。它强制执行独占所有权,没有引用计数,性能开销几乎为零。更重要的是,它的拷贝构造函数和拷贝赋值运算符在 C++11 标准下是被显式删除的(= delete)。这意味着编译器会在你试图拷贝一个 unique_ptr 时直接报错,从源头上杜绝了意外共享的可能性。你只能通过移动语义(std::move)来转移所有权,这正是我们向 _providers 这个 map 里插入新元素时需要的操作。
向容器里塞 unique_ptr 的正确姿势
既然不能拷贝,那怎么把一个 unique_ptr 放进 std::map 呢?答案就是 std::move。
看 registerProvider 接口的实现:
bool LLMManager::registerProvider(const std::string& modelName,
std::unique_ptr<LLMProvider> provider) {
if (!provider) {
ERR("cannot register nullptr provider, modelName={}", modelName);
return false;
}
// 关键就在这行
_providers[modelName] = std::move(provider);
_modelInfos[modelName] = ModelInfo(modelName);
INFO("register provider success, modelName={}", modelName);
return true;
}函数参数 provider 本身就是一个 unique_ptr。当我们执行 std::move(provider) 时,实际上是把 provider 所指向的资源的所有权,从函数参数这个临时变量,转移到了 _providers map 中对应键的值上。操作完成后,函数参数 provider 自己就变成了一个空指针,而 _providers[modelName] 则拥有了这个资源。整个过程清晰、高效,且在编译期就保证了安全性。
模型的完整生命周期管理
有了 unique_ptr 打下的坚实基础,LLMManager 的其他接口实现起来就顺理成章了。
注册 (registerProvider):如上所述,接收一个独占指针,通过 move 存入 map,并初始化对应的 ModelInfo。
初始化 (initModel):这个接口负责让一个已注册的模型真正“活”起来。它首先在 _providers 里查找模型,如果找不到就报错返回。如果找到了,就调用该 Provider 的 initModel 方法,并传入用户提供的配置参数(比如 API key、超时时间等)。只有当底层初始化成功返回 true 后,LLMManager 才会去更新 _modelInfos 里的描述信息,并把 _isAvailable 标记设为 true。
bool LLMManager::initModel(const std::string& modelName,
const std::map<std::string, std::string>& modelParam) {
auto it = _providers.find(modelName);
if (it == _providers.end()) {
ERR("model provider not found, modelName={}", modelName);
return false;
}
bool isSuccess = it->second->initModel(modelParam);
if (!isSuccess) {
ERR("init model failed, modelName={}", modelName);
return false;
}
// 初始化成功才更新状态
INFO("init model success, modelName={}", modelName);
_modelInfos[modelName]._modelDesc = it->second->getModelDesc();
_modelInfos[modelName]._isAvailable = true;
return isSuccess;
}查询与检测:getAvailableModels 方法会遍历 _modelInfos,只把 _isAvailable 为 true 的 ModelInfo 对象收集起来返回。isModelAvailable 则是一个轻量级的检查,直接返回指定模型的状态标记。
消息发送:无论是全量同步的 sendMessage 还是流式的 sendMessageStream,LLMManager 都扮演着一个严格的守门人角色。它们都会先做两步校验:1. 模型是否已注册;2. 模型是否可用。只有两项都通过,才会把请求原封不动地转发给底层的 LLMProvider 去处理。LLMManager 自己不参与任何实际的网络通信或数据解析,保持了极高的内聚性和可维护性。
这种基于 unique_ptr 的独占式管理,配合清晰的接口职责划分,使得整个 SDK 的资源管理既安全又高效,为上层应用提供了一个稳定可靠的接入层。