🔥小叶-duck:个人主页

 ❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、LLM 会话管理模块整体介绍

  1.1 业务上需要解决的几个核心问题

  1.2 会话管理完整业务需求

  1.3 线程安全约束

二、核心数据结构定义

  2.1 Message 消息结构体(common.h)

  2.2 Session 会话结构体(common.h)

三、会话管理类头文件设计(SessionManager.h)

  3.1 头文件依赖说明

  3.2 完整头文件代码

  3.3 关键设计要点解析

    3.3.1 存储结构:unordered_map + shared_ptr

    3.3.2 mutable std::mutex _mutex

    3.3.3 std::atomic _sessionCounter

    3.3.4 接口设计原则:全部基于 sessionId 操作

    3.3.5 私有辅助函数

四、源文件实现(SessionManager.cpp)

  4.1 私有辅助函数:ID 生成函数

    4.1.1 generateSessionId () 生成会话 ID

    4.1.2 generateMessageId (size_t messageCount) 生成消息 ID

  4.2 createSession 创建会话

  4.3 getSession 获取会话智能指针

  4.4 addMessage 向指定会话追加消息

  4.5 getSessionMessages 获取会话全部历史消息

  4.6 getSessionLists 获取排序后的会话 ID 列表

  4.7 updateSessionTimestamp 更新会话时间戳

  4.8 deleteSession 删除指定会话

  4.9 deleteAllSessions 清空全部会话

  4.10 getSessionCount 获取当前会话总数

  4.11 会话管理类完整源代码

  4.12 整体业务调用流程汇总

  4.13 线程安全方案总结

五、SessionManager 单元测试

  5.1 测试用例代码

  5.2 测试入口函数

结束语


前言

      在前面章节中,我们已经完成了多厂商大模型 Provider 的抽象封装,实现了 DeepSeek、ChatGPT、Gemini、Ollama 等模型的全量与流式接口调用。但当前 SDK 还缺少会话上下文的统一管理能力:每次调用模型都需要外部手动组装消息数组、维护会话标识,会话的创建、消息追加、会话排序、过期清理等逻辑散落在业务代码里,复用性差,也很难保证多线程并发访问时的数据安全。

      本章我们将独立实现一套SessionManager会话管理模块,专门负责对话会话的生命周期管理。我们会从业务需求出发,先梳理会话模块需要解决的核心问题,再定义消息与会话基础结构体,接着完成头文件设计、逐一实现每一个成员接口,同时重点讲解存储结构、互斥锁、原子变量等线程安全相关设计。最后基于 GTest 编写单元测试,完整验证会话创建、消息保存、时间戳更新、会话排序、删除等全部能力。

      会话管理器是 SDK 向上层业务交付的重要组件,它和前面的 LLMProvider、LLMManager 配合,让上层业务只需要关心会话 ID,不用再手动维护对话上下文,让整个 SDK 的使用体验更加贴近真实对话产品。

一、LLM 会话管理模块整体介绍

      会话定义:会话代表用户和大模型之间一组连续交互的集合,它承载本轮对话的全部消息、会话基础状态,保证多轮对话的连贯性,让模型能够读懂历史对话内容。

      大模型侧的本质特性:所有我们对接的 DeepSeek、Gemini、Ollama 等大模型 API 都是无状态服务。模型服务端不会自动记住上一轮对话内容,上下文记忆完全由调用方 SDK 自行维护,每次发起对话请求,都必须把完整历史消息一并传给模型。

  1.1 业务上需要解决的几个核心问题

  1. 多轮对话产生的多条消息,应该以什么样的结构存储;
  2. 模型本身没有记忆,每次请求都必须携带完整历史上下文,缺少统一管理就无法实现连续对话;
  3. 多用户、多对话场景下,如何区分一条消息归属哪一次对话;
  4. 在大量会话数据中,可以根据会话标识快速取出某次对话的全部历史消息,组装成请求参数传给 LLMManager。

      之前我们在单元测试里,是手动往messages数组追加 user、assistant 消息来模拟多轮对话,这种写法只适合简单验证接口,业务代码不能这样写。历史消息应当归属到会话对象里,由会话管理器统一维护,而不是上层业务手动拼接消息列表。

  1.2 会话管理完整业务需求

基于 Session 结构体,我们的会话管理器需要对外提供一套完整能力,覆盖会话全生命周期:

  1. 创建会话:生成全局唯一session_id,新建会话对象,绑定该会话使用的模型名称;
  2. 根据session_id查询对应的会话对象;
  3. 向指定会话追加消息,支持 user 角色、assistant 角色消息写入;
  4. 获取指定会话内全部历史消息,直接组装成请求参数,交给 LLMManager 发送给大模型;
  5. 获取全部会话 ID 列表,用于前端 UI 展示历史会话列表;
  6. 根据 session_id 删除指定会话;
  7. 每次会话新增消息时,自动更新会话的最后更新时间戳;
  8. 根据session_id删除指定的会话;
  9. 支持清空全部会话;
  10. 查询当前管理器内一共存在多少个会话;
  11. 提供工具方法,生成全局唯一的会话标识session_id;
  12. 提供工具方法,生成每个会话中唯一的消息标识message_id;
  13. 保证线程安全:支持多线程并发对会话进行增、删、改、查操作,通过互斥机制保护共享资源。

  1.3 线程安全约束

并发风险场景:

      业务场景中经常会出现多用户同时访问 SDK,多个线程并行执行创建会话、追加消息、删除会话等操作。会话管理器内部保存的会话容器属于共享临界资源,并发访问会带来安全隐患。

会出现的风险:

  • 并发生成 ID 时,出现session_id或者message_id重复冲突;
  • 容器并发读写,迭代器失效、内部数据错乱;
  • 内存数据损坏,程序出现崩溃、会话数据丢失。

对应的解决方案:

  1. 使用互斥锁std::mutex保护内部存储会话的容器,所有读写容器的操作都要加锁;
  2. ID 计数使用原子变量std::atomic,保证 ID 自增过程无竞争;
  3. 对于const成员函数(比如查询会话、读取会话列表这类只读接口),锁成员变量需要加上mutable修饰,保证在 const 函数内部依然可以上锁解锁。

二、核心数据结构定义

      本节我们将会使用之前在common.h中定义会话模块所需要的基础结构体,这两个结构体是整个会话管理模块的数据载体,和前面 SDK 的消息结构保持统一,同时适配会话上下文存储需求。

  2.1 Message 消息结构体(common.h)

      Message 代表一条独立的聊天消息,和各大模型 API 中消息单元一一对应,用来承载单条对话的全部元数据与内容。

// 单条对话消息结构体,对应LLM接口message单元
struct Message
{
    std::string _message_id;   // 消息唯一ID
    std::string _role;         // 角色,如"user"、"assistant"、"system"
    std::string _content;      // 消息文本内容
    std::time_t _timestamp;    // 消息发送时间戳

    // 构造函数,业务层传入角色与内容
    Message(const std::string &role = "", const std::string &content = "")
        : _role(role), _content(content)
    {
    }
};

结构说明

  1. _message_id:每条消息独立唯一标识,方便后续检索、定位单条消息;
  2. _role:角色字段,对齐 OpenAI 风格 API 规范,支持 user 用户消息、assistant 模型回复、system 系统提示词;
  3. _content:消息正文,就是用户提问或者模型返回的回答文本;
  4. _timestamp:消息生成时间,用于消息排序、时间展示、日志追溯;
  5. 构造函数提供默认参数,方便快速构造一条消息实例。

  2.2 Session 会话结构体(common.h)

      Session 代表一次完整的对话会话,一个会话绑定一个模型,内部有序保存本轮对话所有 Message 消息,用来维护多轮对话上下文。

// 会话信息:保存一轮对话的上下文与历史消息
struct Session
{
    std::string _session_id;           // 会话唯一ID
    std::string _model_name;           // 会话绑定使用的模型名称
    std::vector<Message> _messages;     // 会话内全部消息记录,有序存储
    std::time_t _created_at;            // 会话创建时间戳
    std::time_t _updated_at;            // 会话最后更新时间戳

    // 构造函数:绑定本次会话使用的模型
    Session(const std::string &model_name = "")
        : _model_name(model_name)
    {
    }
};

结构说明

  1. _session_id:会话全局唯一编号,是外部访问、查找、删除会话的主键;
  2. _model_name:记录该会话绑定的模型,一次会话全程使用同一个模型;
  3. _messages:消息数组,按对话先后顺序存放所有消息,发送给 LLM 时直接取出这个数组组装请求上下文;
  4. _created_at:会话创建时刻,用于会话列表展示、会话生命周期统计;
  5. _updated_at:最后一次追加消息的时间,每次新增消息后更新,用来排序会话列表(最近会话优先);
  6. 构造函数支持在创建会话时直接指定模型名称。

三、会话管理类头文件设计(SessionManager.h)

      前面我们已经定义好 Message、Session 基础数据结构,现在需要一个管理器统一负责会话的创建、查找、消息追加、删除等生命周期管理。SessionManager 是整个会话模块的核心调度类,负责维护所有会话实例,同时保证多线程环境下访问安全。

  3.1 头文件依赖说明

#include <memory>      // shared_ptr 智能指针,管理Session对象生命周期
#include <mutex>       // 互斥锁,多线程保护会话共享资源
#include <unordered_map>// 哈希表存储会话,平均O(1)查询,适合sessionId检索
#include <atomic>      // 原子变量,用于会话ID序号无锁自增
#include <algorithm>   // sort排序,用于会话列表按更新时间排序
#include <sstream>     // 字符串流,拼接格式化会话ID、消息ID
#include <iomanip>     // 格式化输出辅助,配合sstream使用
#include "common.h"    // 引入Message、Session结构体定义
  • unordered_map:采用哈希表,对比map红黑树,查询速度更快,我们的业务不需要会话有序存储,优先选择哈希结构;
  • mutex:多线程场景下,多个线程同时读写会话容器,需要互斥锁保证线程安全;
  • memory:std::shared_ptr 托管堆上的 Session 对象,自动管理内存,避免内存泄漏;
  • atomic:原子计数器,用于生成会话自增序号,支持无锁读取;
  • algorithm:会话列表接口返回时,支持按照更新时间倒序排序;
  • sstream + iomanip:字符串格式化工具,用来拼接生成唯一的sessionId、messageId;
  • common.h:依赖我们在上一节定义的Message、Session结构体。

  3.2 完整头文件代码

#ifndef SESSION_MANAGER_H
#define SESSION_MANAGER_H

#include <algorithm>
#include <atomic>
#include <iomanip>
#include <memory>
#include <mutex>
#include <sstream>
#include <unordered_map>

#include "common.h"

namespace ai_chat_sdk
{
class SessionManager
{
  public:
    // 创建会话:根据模型名称创建一个新的会话
    std::string createSession(const std::string &modelName);
    // 获取会话:根据会话ID获取会话对象的智能指针
    std::shared_ptr<Session> getSession(const std::string &sessionId) const;
    // 添加消息:将一条消息添加到指定会话的历史消息中
    bool addMessage(const std::string &sessionId, const Message &message);
    // 获取会话历史消息:根据会话ID获取指定会话的所有历史消息
    std::vector<Message> getSessionMessages(const std::string &sessionId) const;
    // 获取所有会话ID列表:返回所有当前存在的会话ID
    std::vector<std::string> getSessionLists() const;
    // 更新会话时间戳:根据会话ID更新指定会话的更新时间戳
    void updateSessionTimestamp(const std::string &sessionId);
    // 删除会话:根据会话ID删除指定会话
    bool deleteSession(const std::string &sessionId);
    // 删除所有会话:删除所有当前存在的会话
    void deleteAllSessions();
    // 获取会话总数:返回当前存在的会话总数
    size_t getSessionsCount() const;

  private:
    // 生成会话唯一ID
    std::string generateSessionId();
    // 生成消息唯一ID
    std::string generateMessageId(size_t messageCount);

  private:
    // 会话映射表:key:会话ID,value:会话对象的智能指针
    std::unordered_map<std::string, std::shared_ptr<Session>> _sessions;
    // 这里value也可以是Session类对象,但是使用智能指针可以自动管理内存,避免内存泄漏,
    // 并且因为对于一个会话的历史消息越来越多,直接使用Session类对象会导致空间占用过高,而指针所占用的空间是固定的

    // mutable关键字:允许在const成员函数中修改指定成员变量,确保const成员函数下也能线程安全
    // 会话映射表的互斥锁:确保在多线程环境下对会话相关操作是安全访问的
    mutable std::mutex _mutex;

    // 记录会话的总数:atomic:确保在多线程环境下对会话总数的操作是线程安全的
    std::atomic<int64_t> _sessionsCounter = {0};
};
} // namespace ai_chat_sdk

#endif

  3.3 关键设计要点解析

    3.3.1 存储结构:unordered_map + shared_ptr

std::unordered_map<std::string, std::shared_ptr<Session>> _sessions;
  1. 不直接在容器中存放Session对象:Session 内部会持续追加大量消息,对象体积会不断变大,直接存储会带来高额拷贝开销;放到堆上由shared_ptr管理,指针本身占用空间固定,拷贝代价极低;
  2. 以sessionId字符串作为 key,哈希表查询平均复杂度 O (1),适合高频的会话查找场景;
  3. 智能指针自动管理生命周期,当没有任何地方引用这个 Session 时,自动释放内存,规避内存泄漏。

    3.3.2 mutable std::mutex _mutex

mutable关键字是这里的重点:

  1. 被const修饰的成员函数,语义上承诺不会修改类的业务数据;
  2. 但是mutex的 lock/unlock 本身会修改互斥锁内部状态,普通 mutex 无法在 const 函数里面调用;
  3. mutable专门用来突破这个限制,标记该成员不受 const 语义约束;
  4. 典型场景:getSession、getSessionMessages这类 const 查询接口,依然需要加锁保护共享容器,防止其他线程并发修改。

    3.3.3 std::atomic<int64_t> _sessionCounter

      这里有一个很容易产生疑问的点:
      既然在创建会话过程中我们会进行加锁保护,那么生成会话id也就是修改计数器的操作已经是在锁保护范围内,普通 int64_t 理论上也可以保证数据安全,为什么还要选择 atomic?

  1. 无锁读取(核心优势):getSessionCount()是一个 const 查询接口,如果使用普通变量,读取时也必须加锁;而atomic支持无锁读取,高并发查询场景下可以减少锁竞争,提升查询性能。
  2. 防御性编程:如果后续代码迭代,不小心把计数器自增逻辑移出锁保护范围,普通变量会直接引发数据竞争、诡异 bug;atomic无论是否在锁内,都能保证基础的原子语义,提升代码健壮性。
  3. 语义自解释:atomic向阅读代码的开发者传递明确信息:这个变量是一个用于并发计数的变量,本身就具备并发访问的语义,可读性更强。

      补充:锁内的原子自增相比普通变量自增,性能损耗很小,工程上优先选择 atomic,换取更好的并发查询能力与代码鲁棒性。


    3.3.4 接口设计原则:全部基于 sessionId 操作

      对外所有接口都只传递sessionId字符串,不会直接把Session裸指针或者 shared_ptr 随意暴露给外部模块:

  1. 降低模块耦合;
  2. 为后续持久化(SQLite 存储会话)预留扩展空间,数据库 CURD 天然使用字符串 ID 作为主键;
  3. 避免跨模块传递内存指针带来的生命周期风险。

    3.3.5 私有辅助函数

  • generateSessionId():生成全局唯一会话编号;
  • generateMessageId():为会话内每一条消息生成独立消息 ID;ID 生成逻辑封装在管理器内部,外部业务不需要关心 ID 生成规则。

四、源文件实现(SessionManager.cpp)

  4.1 私有辅助函数:ID 生成函数

    4.1.1 generateSessionId () 生成会话 ID

std::string SessionManager::generateSessionId()
{
    // 会话总数自增
    _sessionCounter.fetch_add(1);
    // 获取当前时间戳
    std::time_t time = std::time(nullptr);
    std::ostringstream oss;
    // 格式化输出:session_时间戳_8位序号,不足8位前置补0
    oss << "session_" << time << "_" << std::setw(8) << std::setfill('0') << _sessionCounter;
    return oss.str();
}

设计说明:
    
会话 ID 格式:session_<unix时间戳>_<8位十进制序号>

  • unix 时间戳保证跨时间、大范围场景下的唯一性;
  • 原子计数器_sessionCounter解决同一秒内批量创建会话时 ID 冲突问题;
  • std::setw(8) + std::setfill('0')固定宽度补零,ID 格式统一,方便检索展示。

    4.1.2 generateMessageId (size_t messageCount) 生成消息 ID

std::string SessionManager::generateMessageId(size_t messageCount)
{
    // 获取当前时间戳
    std::time_t time = std::time(nullptr);
    std::ostringstream oss;
    // 格式化输出:msg_时间戳_当前会话内消息序号
    oss << "msg_" << time << "_" << std::setw(8) << std::setfill('0') << messageCount;
    return oss.str();
}

设计说明:
    
消息 ID 格式:msg_<unix时间戳>_<会话内消息序号>

  • 入参messageCount取自目标会话_messages数组当前 size,代表本条是该会话内第 N 条消息;
  • 唯一性约束是会话内唯一,不同会话允许出现相同序号的消息 ID,搭配 sessionId 一起使用就可以全局定位一条消息。

      两个 ID 生成函数作为私有方法,外部完全不需要关心 ID 生成规则,由管理器内部统一控制。

  4.2 createSession 创建会话

std::string SessionManager::createSession(const std::string &modelName)
{
    // RAII锁,出作用域自动解锁
    std::lock_guard<std::mutex> lock(&_mutex);
    // 在堆上创建Session对象,shared_ptr托管生命周期
    std::shared_ptr<Session> session = std::make_shared<Session>(modelName);
    // 生成唯一sessionId
    std::string sessionId = generateSessionId();
    // 绑定会话ID
    session->_session_id = sessionId;
    // 创建时,创建时间、更新时间戳都初始化为当前时间
    session->_created_at = session->_updated_at = std::time(nullptr);
    // 存入哈希映射表
    _sessions[sessionId] = session;
    return sessionId;
}

逻辑拆解:

  1. 加锁:所有操作共享容器_sessions的地方,都用lock_guard,RAII 机制,函数 return、异常抛出都会自动释放锁,杜绝忘记 unlock、死锁;
  2. 构造 Session 智能指针对象,绑定本次会话使用的模型名称;
  3. 生成 sessionId 并赋值,初始化创建、更新时间;
  4. 把sessionId -> shared_ptr<Session>存入 unordered_map;
  5. 返回 sessionId 给上层业务,后续所有操作都拿着这个字符串 ID 访问会话。

  4.3 getSession 获取会话智能指针

std::shared_ptr<Session> SessionManager::getSession(const std::string &sessionId) const
{
    std::lock_guard<std::mutex> lock(&_mutex);
    auto it = _sessions.find(sessionId);
    if (it != _sessions.end())
    {
        return it->second;
    }
    return nullptr;
}

重点:
      
这是const成员函数,因为我们标记了_mutex为mutable,所以可以在 const 函数内上锁;
      只做查询,找到返回会话 shared_ptr,找不到返回空指针。上层拿到 shared_ptr 之后,可以安全读取会话数据。

  4.4 addMessage 向指定会话追加消息

bool SessionManager::addMessage(const std::string &sessionId, const Message &message)
{
    std::lock_guard<std::mutex> lock(&_mutex);
    auto it = _sessions.find(sessionId);
    if (it == _sessions.end())
    {
        return false;
    }
    // 构造消息对象
    Message msg(message._role, message._content);
    // 基于当前会话已有消息数量,生成消息ID
    std::string messageId = generateMessageId(it->second->_messages.size());
    msg._message_id = messageId;
    // 消息时间戳赋值
    msg._timestamp = std::time(nullptr);
    // 追加到会话消息列表
    it->second->_messages.push_back(msg);
    // 每次新增消息,自动刷新会话的最后更新时间
    it->second->_updated_at = std::time(nullptr);
    INFO("add message success, messageId: {}, messageContent: {}", messageId, msg._content);
    return true;
}

业务流程说明:
    
这是支撑多轮对话的核心:

  1. 业务传入 sessionId 和消息(user 提问 /assistant 回答);
  2. 校验会话是否存在,不存在直接返回 false;
  3. 生成 messageId、填充消息时间戳;
  4. push_back 进入会话内部_messages数组;
  5. 自动更新_updated_at,这一步很关键:后续会话列表排序 “最近会话置顶” 依靠这个字段;
  6. 打印日志,方便调试,返回 true 代表消息保存成功。

      业务侧使用习惯:用户发消息调用一次 addMessage 存 user 消息,拿到 LLM 返回结果之后,再调用一次 addMessage 保存 assistant 消息,上下文自动累积。

  4.5 getSessionMessages 获取会话全部历史消息

std::vector<Message> SessionManager::getSessionMessages(const std::string &sessionId) const
{
    std::lock_guard<std::mutex> lock(&_mutex);
    auto it = _sessions.find(sessionId);
    if (it == _sessions.end())
    {
        return {};
    }
    return it->second->_messages;
}

用途:
     拿到有序的完整消息数组,直接传给LLMManager::sendMessage,作为对话上下文,不再需要业务手动维护消息列表。找不到会话返回空 vector。

  4.6 getSessionLists 获取排序后的会话 ID 列表

std::vector<std::string> SessionManager::getSessionLists() const
{
    std::lock_guard<std::mutex> lock(&_mutex);
    std::vector<std::pair<std::string, std::time_t>> temp;
    for (auto &pair : _sessions)
    {
        temp.emplace_back(pair.first, pair.second->_updated_at);
    }
    // 按照更新时间降序排序,最新会话排在前面
    std::sort(temp.begin(), temp.end(), [](const auto &a, const auto &b)
    {
        return a.second > b.second;
    });
    // 提取排序后的sessionId
    std::vector<std::string> sessionIds;
    for (const auto &pair : temp)
    {
        sessionIds.push_back(pair.first);
    }
    return sessionIds;
}

排序逻辑讲解:
      
unordered_map本身是哈希表,遍历顺序是无序的,UI 展示历史对话要求最近交互的会话置顶:

  1. 先遍历 unordered_map,把sessionId + updated_at存入临时 vector;
  2. std::sort配合 lambda 比较器,按时间戳从大到小降序;
  3. 从排好序的临时数组提取 sessionId 返回给上层 UI;上层拿到有序 sessionId 列表后,循环调用getSession拿到会话元信息渲染界面。

  4.7 updateSessionTimestamp 更新会话时间戳

void SessionManager::updateSessionTimestamp(const std::string &sessionId)
{
    std::lock_guard<std::mutex> lock(&_mutex);
    auto it = _sessions.find(sessionId);
    if (it == _sessions.end())
    {
        return;
    }
    it->second->_updated_at = std::time(nullptr);
}

      独立接口,适用于不需要新增消息、但是需要刷新会话更新时间的场景(例如:只查看会话,不发消息,也想让会话上浮);正常发消息的场景,addMessage内部已经自动更新时间戳,不需要额外调用。

  4.8 deleteSession 删除指定会话

bool SessionManager::deleteSession(const std::string &sessionId)
{
    std::lock_guard<std::mutex> lock(&_mutex);
    auto it = _sessions.find(sessionId);
    if (it == _sessions.end())
    {
        return false;
    }
    // 从哈希表擦除会话
    _sessions.erase(it);
    return true;
}
  • 找到会话,执行 erase;
  • 因为存储的是 shared_ptr,erase 之后,如果没有其他地方持有这个会话的 shared_ptr,Session 对象自动析构释放内存;
  • 返回 true 代表删除成功,false 代表会话不存在。

  4.9 deleteAllSessions 清空全部会话

void SessionManager::deleteAllSessions()
{
    std::lock_guard<std::mutex> lock(&_mutex);
    _sessions.clear();
    _sessionCounter = 0;
}

      清空整个哈希容器,同时重置原子计数器,一般用于重置 SDK、批量清理场景。

  4.10 getSessionCount 获取当前会话总数

size_t SessionManager::getSessionCount() const
{
    return _sessionCounter;
}

      _sessionCounter 是 std::atomic 类型,支持无锁读取,高并发查询会话数量时性能更好,不需要加锁。

  4.11 会话管理类完整源代码

#include "../include/SessionManager.h"
#include "../include/util/MyLog.h"

using namespace ply_Log;

namespace ai_chat_sdk
{
// 生成会话ID:会话id格式:session_时间戳_当前会话总数
std::string SessionManager::generateSessionId()
{
    // 会话总数自增
    _sessionsCounter.fetch_add(1);
    // 获取当前时间戳
    std::time_t time = std::time(nullptr);
    // 生成会话ID
    std::ostringstream oss;
    // 格式化输出:session_12345678_00000001
    oss << "session_" << time << "_" << std::setw(8) << std::setfill('0') << _sessionsCounter;
    //std::setw(8):设置输出宽度为8个字符,不足的部分用'0'填充(std::setfill('0'))
    return oss.str();
}

// 生成消息ID:消息id格式:msg_时间戳_当前会话中的消息总数
std::string SessionManager::generateMessageId(size_t messageCount)
{
    // 消息总数自增
    messageCount++;
    // 获取当前时间戳
    std::time_t time = std::time(nullptr);
    // 生成消息ID
    std::ostringstream oss;
    // 格式化输出:msg_12345678_00000001
    oss << "msg_" << time << "_" << std::setw(8) << std::setfill('0') << messageCount;
    return oss.str();
}

// 创建会话:根据模型名称创建一个新的会话
std::string SessionManager::createSession(const std::string &modelName)
{
    // RAII加锁保护,确保在多线程环境下对创建会话是线程安全的,并且在函数退出时自动释放锁
    std::lock_guard<std::mutex> lock(_mutex);

    // 创建会话对象指针
    std::shared_ptr<Session> session = std::make_shared<Session>(modelName);
    // 创建会话ID
    std::string sessionId = generateSessionId();
    // 将会话ID和会话对象指针进行绑定
    session->_session_id = sessionId;
    // 设置会话的创建时间戳和更新时间戳为当前时间
    session->_created_at = session->_updated_at = std::time(nullptr);
    
    // 将会话对象指针添加到会话映射表中
    _sessions[sessionId] = session;
    return sessionId;
}

// 获取会话:根据会话ID获取会话对象的智能指针
std::shared_ptr<Session> SessionManager::getSession(const std::string &sessionId) const
{
    // 加锁保护,确保在多线程环境下对获取会话是线程安全的,并且在函数退出时自动释放锁
    std::lock_guard<std::mutex> lock(_mutex);

    auto it = _sessions.find(sessionId);
    // 检查会话映射表中是否存在指定会话ID,如果存在则返回会话对象指针,否则返回空指针
    if (it != _sessions.end())
    {
        return it->second;
    }
    return nullptr;
}

// 添加消息:将一条消息添加到指定会话的历史消息中
bool SessionManager::addMessage(const std::string &sessionId, const Message &message)
{
    // 加锁保护,确保在多线程环境下对获取会话是线程安全的,并且在函数退出时自动释放锁
    std::lock_guard<std::mutex> lock(_mutex);

    auto it = _sessions.find(sessionId);
    // 检查会话映射表中是否存在指定会话ID,如果不存在则返回false
    if (it == _sessions.end())
    {
        return false;
    }

    // 创建消息
    Message msg(message._role, message._content);
    // 生成消息ID
    std::string messageId = generateMessageId(it->second->_messages.size());
    // 将消息ID添加到消息对象中
    msg._message_id = messageId;
    // 将消息对象添加到会话的历史消息中
    it->second->_messages.push_back(msg);

    // 添加信息:更新会话的更新时间戳为当前时间
    it->second->_updated_at = std::time(nullptr);
    INFO("add message success, messageId: {}, messageContent: {}", messageId, msg._content);
    return true;
}

// 获取会话历史消息:根据会话ID获取指定会话的所有历史消息
std::vector<Message> SessionManager::getSessionMessages(const std::string &sessionId) const
{
    // 加锁保护,确保在多线程环境下对获取会话是线程安全的,并且在函数退出时自动释放锁
    std::lock_guard<std::mutex> lock(_mutex);

    auto it = _sessions.find(sessionId);
    // 检查会话映射表中是否存在指定会话ID,如果不存在则返回空向量
    if (it == _sessions.end())
    {
        return {};
    }
    // 返回会话的所有历史消息
    return it->second->_messages;
}

// 获取所有会话ID列表:返回所有当前存在的会话ID
std::vector<std::string> SessionManager::getSessionLists() const
{
    // 加锁保护,确保在多线程环境下对获取会话列表是线程安全的,并且在函数退出时自动释放锁
    std::lock_guard<std::mutex> lock(_mutex);

    // 根据更新时间戳对会话ID进行降序排序,保证最新的会话ID在列表的最前面
    std::vector<std::pair<std::string, std::time_t>> temp(_sessions.size());
    for (auto pair : _sessions)
    {
        temp.emplace_back(pair.first, pair.second->_updated_at);
    }
    std::sort(temp.begin(), temp.end(), [](const auto &a, const auto &b)
              { return a.second > b.second; });

    // 从排序后的会话ID-时间戳对中提取排完序的会话ID,构建会话ID列表
    std::vector<std::string> sessionIds(temp.size());
    for (const auto &pair : temp)
    {
        sessionIds.push_back(pair.first);
    }
    return sessionIds;
}

// 更新会话时间戳:根据会话ID更新指定会话的更新时间戳
void SessionManager::updateSessionTimestamp(const std::string &sessionId)
{
    // 加锁保护,确保在多线程环境下对更新会话时间戳是线程安全的,并且在函数退出时自动释放锁
    std::lock_guard<std::mutex> lock(_mutex);

    auto it = _sessions.find(sessionId);
    // 检查会话映射表中是否存在指定会话ID,如果不存在则返回
    if (it == _sessions.end())
    {
        return;
    }
    // 更新会话的更新时间戳为当前时间
    it->second->_updated_at = std::time(nullptr);
}

// 删除会话:根据会话ID删除指定会话
bool SessionManager::deleteSession(const std::string &sessionId)
{
    // 加锁保护,确保在多线程环境下对删除会话是线程安全的,并且在函数退出时自动释放锁
    std::lock_guard<std::mutex> lock(_mutex);

    auto it = _sessions.find(sessionId);
    // 检查会话映射表中是否存在指定会话ID,如果不存在则返回false
    if (it == _sessions.end())
    {
        return false;
    }
    // 从会话映射表中删除指定会话ID
    _sessions.erase(it);
    // 修改会话总数,会话总数减1
    _sessionsCounter.fetch_sub(1);
    return true;
}

// 删除所有会话:删除所有当前存在的会话
void SessionManager::deleteAllSessions()
{
    // 加锁保护,确保在多线程环境下对删除会话是线程安全的,并且在函数退出时自动释放锁
    std::lock_guard<std::mutex> lock(_mutex);

    // 清空会话映射表,删除所有会话
    _sessions.clear();
    // 重置会话总数为0
    _sessionsCounter = 0;
}

// 获取会话总数:返回当前存在的会话总数
size_t SessionManager::getSessionsCount() const
{
    // 无需加锁,因为_sessionsCounter是原子操作,所以不会出现线程安全问题
    return _sessionsCounter;
}

} // namespace ai_chat_sdk

  4.12 整体业务调用流程汇总

  1. 新建对话:createSession(modelName)拿到 sessionId;
  2. 用户提问:addMessage(sessionId, userMsg)保存用户消息;
  3. 取上下文:getSessionMessages(sessionId)拿到完整消息 vector;
  4. 调用大模型:把消息 vector 传给LLMManager::sendMessage;
  5. 模型返回结果:addMessage(sessionId, assistantMsg)保存回答,自动刷新会话更新时间;
  6. UI 加载历史会话:getSessionLists()拿到有序会话 ID 列表,逐个getSession渲染;
  7. 用户删除对话:deleteSession(sessionId)。

  4.13 线程安全方案总结

  1. 共享容器_sessions全部读写都使用lock_guard RAII 锁,自动管理锁生命周期;
  2. mutable std::mutex 解决 const 只读查询函数内部上锁的语法限制;
  3. 会话计数器_sessionCounter使用std::atomic,支持无锁读取,并发计数安全;
  4. 锁只保护管理器内部的哈希表,会话本身的数据在 shared_ptr 托管下,生命周期安全。

五、SessionManager 单元测试

      基于 GTest 框架编写SessionManager的功能冒烟测试,结合我们上面实现的会话管理器,验证会话创建、消息追加、会话排序、时间戳更新、删除等整套接口逻辑,同时展示主测试入口与运行输出结果。

  5.1 测试用例代码

TEST(SessionManagerTest, operSession)
{
    // 1、创建SessionManager对象
    SessionManager sessions;
    // 2、创建多个会话
    std::string deepseek_id = sessions.createSession("deepseek-flash");
    sleep(1);
    std::string gemini_id = sessions.createSession("gemini-3.6-flash");
    std::cout << deepseek_id << std::endl;
    std::cout << gemini_id << std::endl;
    std::cout << "sessionsCount: " << sessions.getSessionsCount() << std::endl;

    sleep(1);
    // 3、添加消息到会话
    bool res;
    res = sessions.addMessage(deepseek_id, {"user", "hello"});
    ASSERT_TRUE(res);
    res = sessions.addMessage(deepseek_id, {"user", "world"});
    ASSERT_TRUE(res);

    // 4、获取会话消息
    std::cout << "-----------------获取指定会话及会话消息-----------------" << std::endl;
    auto session_ptr = sessions.getSession(deepseek_id);
    ASSERT_TRUE(session_ptr != nullptr);
    std::cout << session_ptr->_model_name << ": " << session_ptr->_session_id << std::endl;
    std::vector<Message> messages = session_ptr->_messages;
    for (auto &message : messages)
    {
        std::cout << message._role << ": " << message._content << std::endl;
    }

    // 5、获取所有会话ID列表
    std::cout << "-----------------获取所有会话ID列表-----------------" << std::endl;
    std::vector<std::string> sessions_id = sessions.getSessionLists();
    for (auto &session_id : sessions_id)
    {
        std::cout << session_id << std::endl;
    }

    // 6、更新会话时间戳
    sleep(5);
    std::cout << "-----------------更新指定会话更新时间戳-----------------" << std::endl;
    sessions.updateSessionTimestamp(gemini_id);
    std::cout << "deepseek: " << sessions.getSession(deepseek_id)->_updated_at << std::endl;
    std::cout << "gemini: " << sessions.getSession(gemini_id)->_updated_at << std::endl;

    // 7、更新会话时间戳后获取所有会话ID列表:会话ID列表按更新时间戳重新进行降序排序
    std::cout << "-----------------更新会话时间戳后获取所有会话ID列表-----------------" << std::endl;
    sessions_id = sessions.getSessionLists();
    for (auto &session_id : sessions_id)
    {
        std::cout << session_id << std::endl;
    }

    // 8、删除所有会话
    std::cout << "-----------------删除所有会话-----------------" << std::endl;
    sessions.deleteAllSessions();
    std::cout << "sessionsCount: " << sessions.getSessionsCount() << std::endl;
}

  5.2 测试入口函数

int main(int argc, char **argv)
{
    // 初始化Google Test框架
    testing::InitGoogleTest(&argc, argv);

    // 初始化日志系统
    Logger::logger_init("testLLM", "stdout", spdlog::level::info);

    // 运行所有测试用例
    return RUN_ALL_TESTS();
}

      这个测试属于集成冒烟测试,不是细分的单元测试,目的是串行跑通 SessionManager 整套业务链路:

  • 覆盖 ID 生成、会话 CRUD、消息存储、时间戳、会话排序核心能力;
  • 使用 GTest 断言捕获异常分支,控制台打印方便我们直观观察数据;
  • sleep 用来人为制造时间差,专门用来测试基于updated_at的排序逻辑;
  • 没有单独写多线程并发测试,后续可以额外补充并发读写的压力测试用例。

运行结果展示:

结束语

      到这里,SessionManager会话管理模块的设计、编码与测试就全部完成了。我们从业务痛点出发,一步步完成结构体定义、头文件接口设计、各个成员函数的落地实现,同时针对多线程场景做了锁、原子变量等线程安全保障,最后通过 GTest 测试用例跑通了整套会话生命周期流程,验证了会话创建、消息追加、时间戳更新、会话排序、会话删除等核心功能。

      会话管理器补齐了 SDK 里对话上下文管理的短板,它和前面的 LLMProvider、LLMManager 共同构成完整的调用链路:上层业务依靠 SessionManager 维护对话历史,再交由 LLMManager 调度对应的模型 Provider 完成请求。

Logo

更多推荐