
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一、LLMManager 类设计初衷](#一、LLMManager 类设计初衷)
[1.1 当前已完成的模型接入能力](#1.1 当前已完成的模型接入能力)
[1.2 现有使用方式暴露出来的问题](#1.2 现有使用方式暴露出来的问题)
[1.3 LLMManager 管理层的设计目标](#1.3 LLMManager 管理层的设计目标)
[1.4 核心实现思路](#1.4 核心实现思路)
[二、LLMManager 类架构设计](#二、LLMManager 类架构设计)
[2.1 LLMManager 核心组件定义](#2.1 LLMManager 核心组件定义)
[2.1.1 LLMManager 类定位与职责](#2.1.1 LLMManager 类定位与职责)
[2.1.2 头文件基础保护与依赖引入](#2.1.2 头文件基础保护与依赖引入)
[2.1.3 私有成员变量设计](#2.1.3 私有成员变量设计)
[2.2 ModelInfo 元信息结构体](#2.2 ModelInfo 元信息结构体)
[2.2.1 结构体作用](#2.2.1 结构体作用)
[2.2.2 结构体定义与字段解析](#2.2.2 结构体定义与字段解析)
[2.3 公有接口声明与功能说明](#2.3 公有接口声明与功能说明)
[2.3.1 模型生命周期接口](#2.3.1 模型生命周期接口)
[registerProvider 注册接口](#registerProvider 注册接口)
[initModel 初始化接口](#initModel 初始化接口)
[2.3.2 模型查询接口](#2.3.2 模型查询接口)
[getAvailableModels 获取可用列表](#getAvailableModels 获取可用列表)
[isModelAvailable 可用性检查](#isModelAvailable 可用性检查)
[2.3.3 消息交互接口](#2.3.3 消息交互接口)
[sendMessage 同步非流式调用](#sendMessage 同步非流式调用)
[sendMessageStream 流式调用](#sendMessageStream 流式调用)
[三、智能指针选型优化:unique_ptr 设计分析](#三、智能指针选型优化:unique_ptr 设计分析)
[3.1 方案对比:从 shared_ptr 到 unique_ptr](#3.1 方案对比:从 shared_ptr 到 unique_ptr)
[3.1.1 初始方案:shared_ptr 的短板](#3.1.1 初始方案:shared_ptr 的短板)
[3.1.2 优化选型:unique_ptr 独占所有权](#3.1.2 优化选型:unique_ptr 独占所有权)
[3.2 架构层面的思考总结](#3.2 架构层面的思考总结)
[3.2.1 业务上的唯一映射关系](#3.2.1 业务上的唯一映射关系)
[3.2.2 语义与安全层面的优势](#3.2.2 语义与安全层面的优势)
[3.2.3 性能差异要点](#3.2.3 性能差异要点)
[4.1 注册与初始化逻辑](#4.1 注册与初始化逻辑)
[4.1.1 registerProvider 实现](#4.1.1 registerProvider 实现)
[核心要点:为什么必须用 std::move?](#核心要点:为什么必须用 std::move?)
[4.1.2 initModel 实现](#4.1.2 initModel 实现)
[4.2 查询逻辑实现](#4.2 查询逻辑实现)
[4.2.1 getAvailableModels 实现](#4.2.1 getAvailableModels 实现)
[4.2.2 isModelAvailable 实现](#4.2.2 isModelAvailable 实现)
[4.3 消息转发机制](#4.3 消息转发机制)
[4.3.1 sendMessage(全量响应)](#4.3.1 sendMessage(全量响应))
[4.3.2 sendMessageStream(流式响应)](#4.3.2 sendMessageStream(流式响应))
前言
到目前为止,我们已经完成了多款大模型 Provider 的独立开发,先后实现了 DeepSeek、ChatGPT、Gemini 以及 Ollama 本地模型的接入逻辑,每一个 Provider 都继承自 LLMProvider 抽象基类,对外统一暴露了非流式、流式两套消息交互接口。
在之前的开发模式里,我们都是直接手动创建各个 Provider 实例,自行维护模型配置、调用模型接口。这种方式在模型数量少、测试场景简单的时候足够直观好用,但随着接入的模型越来越多,我们会发现缺少一层统一的调度管理层:模型实例的创建、查找、状态管理、调用分发都需要业务代码自己处理,业务侧需要感知每一个 Provider 的类型,模型的管理逻辑散落在业务代码中,不利于统一管控、扩展和维护。
为了解决这个问题,本章我们将设计并实现
LLMManager管理模块。它将作为整个 SDK 的调度中枢,负责统一管理所有注册进来的模型 Provider,对外提供一套统一的调用入口。业务代码只需要向 LLMManager 传入模型标识和请求参数,不用关心底层是云端模型还是本地 Ollama、是哪一家厂商的 API,由 LLMManager 完成模型查找、接口转发。本章我们会从设计初衷入手,一步步完成 LLMManager 的架构定义、数据结构设计、接口声明,同时针对智能指针选型做所有权层面的优化分析,最后落地全部接口的实现代码。在完成这一层封装之后,我们这套手写大模型接入 SDK 的整体架构会更加清晰,真正把 "多模型适配、统一调度" 的能力落地。
一、LLMManager 类设计初衷
1.1 当前已完成的模型接入能力
我们已经基于LLMProvider抽象基类,完成了多套模型接入子类的开发,覆盖云端 API 与本地推理两条路线:
- **云端模型接入:**通过 HTTP API 对接 ChatGPT、Gemini、DeepSeek 等大模型服务,分别实现非流式、流式两套消息接口;
- **本地模型接入:**基于 Ollama 实现本地大模型的调用,适配 Ollama 独特的换行 JSON 分片流式协议;
- **基础扩展逻辑:**新增模型时,只需要派生新的 Provider 子类、实现基类纯虚接口,即可完成模型适配,底层调用逻辑由子类自行实现。
1.2 现有使用方式暴露出来的问题
虽然抽象基类已经解决了不同模型接口行为的差异化封装,但在上层业务调用侧,仍然存在明显的使用痛点:
- 实例化逻辑重复: 调用任意模型,都需要手动创建对应 Provider 智能指针、手动调用
initModel初始化、再调用消息接口;不同模型的调用样板代码高度雷同; - 业务层强依赖具体子类: 业务代码需要显式引入
DeepSeekProvider、ChatGPTProvider等具体类型,切换模型时,需要修改实例化代码,模型越多,切换和维护成本越高; - 缺少统一的模型注册与查找机制: 业务无法仅通过模型名称字符串来选择目标模型,没有集中的容器存放、管理所有已加载的模型实例;
- **模型信息无法统一对外暴露:**各个模型的描述信息、基础参数分散在各个子类内部,缺少统一容器给上层界面、日志模块读取展示模型列表。
1.3 LLMManager 管理层的设计目标
在 Provider 抽象层之上,再封装一层LLMManager统一管理器,借助哈希表完成模型名称到 Provider 实例的映射,实现下面核心目标:
- 屏蔽各个 Provider 子类的实例化细节,上层业务只需要传入模型名字符串,即可获取对应的模型实例,不再直接依赖子类头文件;
- 提供统一的模型注册、实例管理入口,用
unordered_map保存模型名称和 Provider 实例的映射关系,支持按名字快速查找模型; - 配套维护模型信息哈希表,保存各个模型对应的
ModelInfo描述,方便给前端、日志模块展示可用模型清单; - 遵循开闭原则:新增模型只需要新增 Provider 子类,管理器本身不需要改动,上层业务切换模型仅修改模型名字符串即可。
1.4 核心实现思路
管理器内部维护两张哈希表:
std::unordered_map<std::string, std::unique_ptr<LLMProvider>> _providers:key 为模型名称,value 为 Provider 实例,用来根据模型名拿到模型对象,调用对话接口;std::unordered_map<std::string, ModelInfo> _modelInfos:key 为模型名称,value 为模型描述信息,用于查询、展示模型基础信息。
上层业务不再手动make_shared创建各个 Provider,全部交给 LLMManager 完成注册、管理、查找,业务仅通过模型名称完成交互。

二、LLMManager 类架构设计
2.1 LLMManager 核心组件定义
2.1.1 LLMManager 类定位与职责
LLMManager 是整套 SDK 的顶层统一调度入口 ,负责托管所有 LLMProvider 实例的生命周期,完成模型注册、初始化状态维护、可用性校验、消息转发 等工作。
它在 Provider 抽象层之上,隔离上层业务与各个具体模型子类,业务侧只需要通过模型名称字符串完成交互。
2.1.2 头文件基础保护与依赖引入
cpp
#ifndef LLM_MANAGER_H
#define LLM_MANAGER_H
#include "LLMProvider.h"
#include "common.h"
namespace ai_chat_sdk
{
class LLMManager
{
// 成员声明
};
}
#endif // LLM_MANAGER_H
- 使用
#ifndef / #define / #endif传统头文件保护方案,规避头文件重复包含问题; - 仅依赖基类头文件
LLMProvider.h和公共定义头文件common.h,不引入任何具体 Provider 子类头文件,实现业务层与模型实现类的解耦。
2.1.3 私有成员变量设计
cpp
private:
// 模型实例映射表:key=模型名称,value=Provider独占智能指针
std::unordered_map<std::string, std::unique_ptr<LLMProvider>> _providers;
// 模型元信息映射表:key=模型名称,value=ModelInfo元数据
std::unordered_map<std::string, ModelInfo> _modelInfos;
_providers:持有全部模型实例的所有权,依托基类指针 + C++ 多态特性统一调用子类接口;采用unique_ptr自动管理内存,生命周期由管理器控制;_modelInfos:和_providers一一配对,存放模型对外展示的元信息;上层只能读取这份元数据,不会直接拿到 Provider 实例,实现内部对象隔离;- 容器选用
unordered_map哈希表,相比有序 map,模型名称查找的平均访问效率更高。
2.2 ModelInfo 元信息结构体
2.2.1 结构体作用
ModelInfo 是标准化的模型元数据载体,用来统一描述模型基础属性 ,专门用于给上层展示模型列表、查看模型基础信息,不保存 API Key 这类敏感配置。
2.2.2 结构体定义与字段解析
cpp
struct ModelInfo
{
std::string _model_name; // 模型唯一标识名称
std::string _model_desc; // 模型功能描述文本
std::string _base_url; // 模型服务端点地址
std::string _provider; // 模型厂商类型,如deepseek、openai、ollama
bool _isAvailable = false; // 模型可用标记,true代表初始化完成可调用
// 构造函数,一次性填充基础元信息
ModelInfo(const std::string &model_name = "", const std::string &model_desc = "",
const std::string &base_url = "", const std::string &provider = "")
: _model_name(model_name), _model_desc(model_desc), _base_url(base_url), _provider(provider)
{
}
};
_model_name:和管理器哈希表的 key 保持一致,是模型查找匹配的唯一标识;_isAvailable:状态标记,和 Provider 内部IsAvailable()状态对齐,用来快速给前端展示模型是否就绪;- 构造函数支持默认参数,方便注册模型时快速构造元信息对象。
2.3 公有接口声明与功能说明
2.3.1 模型生命周期接口
registerProvider 注册接口
cpp
bool registerProvider(const std::string &modelName, std::unique_ptr<LLMProvider> provider);
- 功能:将具体模型 Provider 实例注册到管理器容器,是模型接入统一管理的入口;
- 参数:
modelName:模型唯一标识名称,作为后续调用查找的索引键;provider:模型提供者实例独占指针,必须是LLMProvider子类实例;
initModel 初始化接口
cpp
bool initModel(const std::string &modelName, const std::unordered_map<std::string, std::string> &modelParams);
- 功能:根据模型名称查找对应 Provider 实例,调用底层
initModel完成配置加载与可用性校验; - 参数:
modelName:目标模型注册名称;modelParams:模型初始化参数字典,包含 api-key、baseUrl 等配置项;
- 返回值:初始化成功返回 true,失败返回 false;
- 约束:传入的模型名称未注册时,直接返回失败。
2.3.2 模型查询接口
getAvailableModels 获取可用列表
cpp
std::vector<ModelInfo> getAvailableModels();
- 功能:返回所有已注册、且状态为可用的模型元信息集合;
- 应用场景:客户端新建会话时的模型选择界面展示;
- 返回值:仅收集
_isAvailable = true的模型信息。
isModelAvailable 可用性检查
cpp
bool isModelAvailable(const std::string &modelName);
- 功能:校验指定名称的模型是否存在、并且初始化完成处于可用状态;
- 返回值:模型不存在或者未就绪,均返回 false;可用则返回 true;
- 作用:消息发送前的前置校验,避免无效的模型调用。
2.3.3 消息交互接口
sendMessage 同步非流式调用
cpp
std::optional<std::string> sendMessage(const std::string &modelName,
const std::vector<Message> &messages,
const std::unordered_map<std::string, std::string> &requestParams);
- 功能:向指定模型同步发送对话消息,阻塞等待完整回复生成后返回;
- 参数:
modelName:目标模型名称;messages:对话历史消息列表,包含 role 角色与 content 内容;requestParams:单次请求参数,如 temperature、max_tokens 等;
- 返回值:
std::optional,调用成功存放完整回复文本,失败为空。
sendMessageStream 流式调用
cpp
std::optional<std::string> sendMessageStream(const std::string &modelName,
const std::vector<Message> &messages,
const std::unordered_map<std::string, std::string> &requestParams,
std::function<void(const std::string &, bool)> callback);
- 功能:向指定模型发送流式请求,通过回调函数逐块接收模型返回的增量文本;
- 回调参数:
- 第一个参数:当前返回的文本片段 chunk;
- 第二个 bool 参数:是否是最后一块数据,true 代表流式响应结束;
- 返回值:
std::optional,保存拼接完成的完整回复文本; - 统一契约:所有对外接口第一个入参均为
modelName,管理器根据名字自动匹配对应的 Provider 实例;消息接口参数和底层LLMProvider保持一致,上层业务不需要关心底层是哪一个模型;返回值、回调契约完全复用基类定义,保证调用风格统一。
三、智能指针选型优化:unique_ptr 设计分析
3.1 方案对比:从 shared_ptr 到 unique_ptr
3.1.1 初始方案:shared_ptr 的短板
早期设计考虑使用 std::shared_ptr<LLMProvider> 托管 Provider 实例,放到本项目的场景下并不适配,存在多处不合理之处:
- 所有权场景不匹配: 所有
LLMProvider实例只在LLMManager内部使用,业务侧不会直接持有 Provider 对象,不存在多地方共享实例的业务需求。 - 额外性能开销:
shared_ptr内部维护引用计数,拷贝、销毁时会触发原子操作,带来内存与运行时的额外损耗。 - **存在安全隐患:**共享指针允许外部拷贝持有,一旦外部代码保留指针,有可能延长对象生命周期,甚至多线程并发操作同一个 Provider,引发状态竞争、逻辑错乱等难以复现的 bug,违背管理器独占管控实例的设计意图。
3.1.2 优化选型:unique_ptr 独占所有权
最终选用 std::unique_ptr<LLMProvider>,和当前 SDK 架构的设计目标高度契合:
- 遵循 RAII 独占语义:
LLMManager是 Provider 实例唯一持有者,实例的创建、初始化、销毁全部由管理器统一管控。 - **几乎零运行开销:**编译后的运行效率和裸指针接近,没有引用计数、控制块的额外负担。
- 编译期防护: C++ 语法层面直接禁用拷贝,从源头杜绝多处共享同一个实例的可能性,规避意外共享带来的生命周期风险。
3.2 架构层面的思考总结
3.2.1 业务上的唯一映射关系
在管理器的实例映射表中,一个 modelName 键 固定对应唯一的 Provider 实例 。哪怕是同一个厂商下不同模型,也会注册成互相独立的 Provider 对象,各自持有独立的配置、会话状态,互不干扰。
上层业务只和 LLMManager 交互,通过模型名字调用接口,不直接触碰底层 Provider,天然不需要共享所有权。
3.2.2 语义与安全层面的优势
unique_ptr 代表独占所有权 ,刚好匹配 LLMManager 作为模型资源 "大管家" 的定位。
对比 shared_ptr,它直接从语法上阻断外部随意持有实例的可能,避免因为外部代码误操作,引发对象生命周期失控、多线程竞争问题,降低后期调试难度。
3.2.3 性能差异要点
shared_ptr 需要维护控制块,包含强引用、弱引用计数,为保证线程安全,增减计数要执行原子指令,持续带来开销。
而 unique_ptr没有引用计数机制,编译后没有额外运行时负担,在大量模型频繁注册销毁的场景下收益更明显。

四、公有接口定义实现细节
4.1 注册与初始化逻辑
4.1.1 registerProvider 实现
cpp
bool LLMManager::registerProvider(const std::string &modelName, std::unique_ptr<LLMProvider> provider)
{
// 判断模型提供器是否为空
if (!provider)
{
ERR("can not register nullptr provider, modelName: {}", modelName);
return false;
}
if (_providers.find(modelName) == _providers.end())
{
// 模型未注册,注册模型提供器
// unique_ptr防拷贝,只能通过move将provider的资源转移到_providers中
_providers[modelName] = std::move(provider); // 移动语义,将provider的所有权转移到_providers中
}
if (_modelInfos.find(modelName) == _modelInfos.end())
{
// 模型信息未注册,注册模型信息
// 这里只注册模型名称,其他信息如模型描述、模型是否可用等参数需要在initModel中初始化
_modelInfos[modelName] = ModelInfo(model_name: modelName);
}
// 打印模型注册成功信息
INFO("register provider success, modelName: {}", modelName);
return true;
}
- 实现要点:
- 空指针校验: 优先判断传入的
unique_ptr是否为空,空则直接打错误日志并返回 false,避免后续非法访问。 - 容器判重: 分别对
_providers、_modelInfos两个容器做查找判断,避免重复插入。 - 移动语义重点 :
unique_ptr禁止拷贝,不能直接赋值传递,必须使用std::move把 provider 的资源所有权转移到_providers容器内。std::move本质是把左值强制转为右值引用,触发移动赋值,转移之后原 provider 会变成nullptr,后续不能再使用。 - 数据分离: 注册阶段只完成 Provider 实例存入、ModelInfo 基础占位,不会执行模型初始化 ,可用状态、模型描述等信息,延后到
initModel阶段填充。 - **返回值:**使用 bool 返回值,方便上层判断注册是否成功。
- 空指针校验: 优先判断传入的
核心要点:为什么必须用 std::move?
1. 为什么直接"传"(拷贝)不行?
-
左值特性 :参数
provider本身是一个具名的左值(lvalue)。 -
unique_ptr的设计原则 :它的名字叫独占(unique),设计初衷就是禁止拷贝 。如果允许拷贝,就会出现两个指针指向同一块堆内存,析构时会导致双重释放(Double Free),程序直接崩溃。 -
因此,标准库源码中,
unique_ptr的拷贝构造函数和拷贝赋值运算符被显式标记为= delete。直接赋值会引发编译错误。
2. 为什么"变成右值"就可以?
-
移动语义 :C++11 引入了移动语义,允许"转移"资源所有权。
-
std::move的本质 :它仅仅是一个**static_cast<T&&>** ,把左值强转成了右值 。它本身不移动任何数据,但告知编译器:"我保证以后不再使用这个变量了,你可以放心大胆地把它掏空"。 -
函数匹配 :转为右值后,代码会匹配到
unique_ptr的移动赋值运算符(接收右值引用的函数),从而合法、高效地完成指针所有权的交接。
3. 被 Move 之后会发生什么?
-
底层操作 :移动赋值内部会执行
provider.release(),将原指针交出,并把原对象(provider)置为nullptr。 -
后续使用 :如果你在
std::move(provider)之后又使用了它(比如provider->init()),会触发空指针解引用,导致程序直接崩溃(Segmentation Fault)。 -
C++ 标准保证 :被移动后的
unique_ptr必定是nullptr。如果需要再用,只能重新给它赋值。
进阶思考:右值的生命周期特性
关于"右值(临时对象)生命很短,转瞬即逝"的误区:
-
真正的临时对象(纯右值) :比如
std::make_unique<...>(),它的生命周期由 C++ 语言语法规定,在当前行(完整表达式)结束后自动销毁。移动构造只是在这个对象临死前,把它的资源"合法地抢劫"出来。 -
被
std::move的具名变量(将亡值) :它的生命周期并没有变短 ,内存依然在栈上 ,直到离开作用域 才释放 。只是内部的资源 (指针)被移动构造转移走了。 -
结论 :不是移动构造让右值变短命,而是移动构造专门为即将销毁或主动放弃资源的对象 设计的高效接管机制。移动操作改变的是对象的状态(State) ,而非对象的生命周期(Lifetime)。
补充:shared_ptr 的循环引用回顾

4.1.2 initModel 实现
cpp
bool LLMManager::initModel(const std::string &modelName, const std::unordered_map<std::string, std::string> &modelParams)
{
auto it = _providers.find(modelName);
if (it == _providers.end())
{
ERR("model provider not found, must register provider first, modelName: {}", modelName);
return false;
}
// 多态调用子类模型提供器的初始化接口,初始化模型
bool ret = it->second->initModel(modelParams);
if (ret)
{
// 初始化模型成功,更新模型信息
_modelInfos[modelName]._model_desc = it->second->getModelDesc();
_modelInfos[modelName]._isAvailable = it->second->IsAvailable();
INFO("init model success, modelName: {}", modelName);
}
return ret;
}
- 多态原理: 基类指针指向子类实例,调用子类重写的
initModel虚函数,实现同一个接口适配 DeepSeek、ChatGPT、Ollama 等不同模型。 - 状态同步: 初始化成功之后,主动从 Provider 中读取模型描述、可用性标记,同步更新到
_modelInfos元信息容器;初始化失败则不修改状态,保留不可用标记。 - **容错设计:**找不到 provider 直接打错误日志返回 false,不抛出异常,上层可以自主处理失败场景。
4.2 查询逻辑实现
4.2.1 getAvailableModels 实现
cpp
std::vector<ModelInfo> LLMManager::getAvailableModels()
{
std::vector<ModelInfo> availableModels;
// 遍历所有模型信息,筛选出所有可用模型,初始化失败的模型无需展示
for(auto &pair : _modelInfos)
{
if(pair.second._isAvailable)
{
availableModels.push_back(pair.second);
}
}
return availableModels;
}
- 实现逻辑: 遍历
_modelInfos元信息容器,过滤出_isAvailable=true的模型,返回模型信息列表,一般用于前端界面展示模型选择列表。 - 设计原则: 只读取
_modelInfos,完全不访问_providers,做到信息隔离,对外只暴露模型元数据,隐藏底层 Provider 实例。
4.2.2 isModelAvailable 实现
cpp
bool LLMManager::isModelAvailable(const std::string &modelName)
{
// 检查模型信息是否已注册
if(_modelInfos.find(modelName) == _modelInfos.end())
{
return false;
}
// 检查模型是否已初始化
return _modelInfos[modelName]._isAvailable;
}
- **容错处理:**先判断模型是否注册,不存在直接返回 false;存在则返回记录的可用性标记。
- 用途: 作为消息发送前的前置校验工具,统一收口可用性判断逻辑,减少重复代码。
4.3 消息转发机制
LLMManager本身不处理 HTTP 请求、JSON 封装、SSE 解析等业务逻辑,只做路由转发、前置校验,所有和模型相关的业务逻辑下沉到各个 Provider 子类。
4.3.1 sendMessage(全量响应)
cpp
std::optional<std::string> LLMManager::sendMessage(const std::string &modelName,
const std::vector<Message> &messages,
const std::unordered_map<std::string, std::string> &requestParams)
{
// 检查模型是否可用(包括了检测模型是否已注册以及初始化)
if(!isModelAvailable(modelName))
{
ERR("model not available, modelName: {}", modelName);
return std::nullopt;
}
return _providers[modelName]->sendMessage(messages, requestParams);
}
- 职责边界:
- 调用
isModelAvailable做前置校验,模型不可用直接返回std::nullopt,并打印错误日志; - 校验通过后,直接转发调用对应 provider 的
sendMessage; - 请求参数、消息列表原样透传给 provider,LLMManager 不感知不同厂商 API 的参数差异。
- 调用
4.3.2 sendMessageStream(流式响应)
cpp
std::optional<std::string> LLMManager::sendMessageStream(const std::string &modelName,
const std::vector<Message> &messages,
const std::unordered_map<std::string, std::string> &requestParams,
std::function<void(const std::string &, bool)> callback)
{
// 检查模型是否可用(包括了检测模型是否已注册以及初始化)
if(!isModelAvailable(modelName))
{
ERR("model not available, modelName: {}", modelName);
return std::nullopt;
}
return _providers[modelName]->sendMessageStream(messages, requestParams, callback);
}
- **实现逻辑:**和全量消息接口框架一致,额外把回调函数透传给底层 Provider;
- **回调作用:**底层 Provider 收到流式分片内容时,通过这个回调把片段逐层抛给上层业务,实现逐字输出效果。
结束语
本章我们完成了
LLMManager管理层的完整设计与代码落地,从最初发现多模型直接调用带来的管理痛点出发,搭建起 SDK 的统一调度中枢。我们梳理了
LLMManager的定位职责、配套的ModelInfo元信息结构,并且针对智能指针的所有权模型做了选型优化,选用unique_ptr明确 Provider 实例的独占语义,规避共享指针带来的生命周期隐患。随后依次实现了模型注册、初始化、状态查询、消息转发等全部对外接口,所有底层 Provider 的差异都被 Manager 层屏蔽,业务侧只需要面向统一接口进行开发。到这里,我们 SDK 的整体分层架构就完整成型了:抽象基类
LLMProvider定义接入契约,各个厂商 Provider 负责对接各自的 API 规范,LLMManager统一管控所有模型实例并分发请求。后续新增模型时,我们只需要新增对应的 Provider 子类,注册到 Manager 中即可直接使用,不需要改动上层业务调用代码,充分体现了策略模式带来的扩展性优势。