一.介绍

本博客记录在开发智能面试平台过程中,基于Redis实现对话中间状态缓存,并持久化Postgresq

l中,实现断点续答。

在知识库问答场景下,用户可能进行多轮连续对话,每轮对话都需要保存上下文。如果对话记录只存在内存中,用户刷新页面或重新进入就会丢失历史记录,体验很差。

为此,本模块采用冷热分离的存储架构:

Redis热缓存:对话的中间状态实时缓存到Redis,响应速度快

PostgreSQL持久化:对话消息持久化存入数据库,用户下次进入能看到完整的历史记录,实现断点续答

二:设计思路

对话持久化的核心是两张表:rag_chat_sessions(会话表)和rag_chat_messages(消息表)。

会话表负责记录每一次对话的元信息,一个会话可以关联多个知识库。

消息表负责记录每一条具体的消息,消息分两种类型:USER(用户提问)和ASSISTANT(AI回答)。每条消息都有messageOrder字段保证顺序,有completed字段标记流式响应是否完成。

两张表的关系如下:
rag_chat_session->(一对多)rag_chat_message(消息:USER问题+ASSISTANT回答交替存储)

流式响应的处理为:AI回答是流式输出的,不能等全部输出完再存库,所以采用占位符机制:AI开始回答时先创建一条completed=false的空消息占位,等流式输出全部完成后再把完整内容更新进去,同时把completed改为true

二.主要实现

1.会话实体的设计

@Entity
@Table(name = "rag_chat_sessions")
public class RagChatSessionEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 会话标题(自动生成或用户自定义)
    private String title;

    // 多对多:一个会话可关联多个知识库
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "rag_session_knowledge_bases")
    private Set<KnowledgeBaseEntity> knowledgeBases = new HashSet<>();

    // 消息数量(冗余字段,避免每次COUNT查询)
    private Integer messageCount = 0;

    // 是否置顶
    private Boolean isPinned = false;
}

2.消息实体设计

@Entity
@Table(name = "rag_chat_messages")
public class RagChatMessageEntity {

    // USER 或 ASSISTANT
    @Enumerated(EnumType.STRING)
    private MessageType type;

    // 消息内容
    @Column(columnDefinition = "TEXT")
    private String content;

    // 消息顺序,保证展示顺序正确
    private Integer messageOrder;

    // 流式响应是否完成
    private Boolean completed = true;
}

3.流式响应占位机制

// 流式回答开始前:保存用户消息 + 创建AI消息占位
public Long prepareStreamMessage(Long sessionId, String question) {
    // 保存用户消息(completed=true)
    RagChatMessageEntity userMessage = new RagChatMessageEntity();
    userMessage.setType(MessageType.USER);
    userMessage.setContent(question);
    userMessage.setCompleted(true);
    messageRepository.save(userMessage);

    // 创建AI消息占位(completed=false,内容为空)
    RagChatMessageEntity assistantMessage = new RagChatMessageEntity();
    assistantMessage.setType(MessageType.ASSISTANT);
    assistantMessage.setContent("");
    assistantMessage.setCompleted(false);
    return messageRepository.save(assistantMessage).getId();
}

// 流式回答完成后:更新AI消息内容
public void completeStreamMessage(Long messageId, String content) {
    message.setContent(content);
    message.setCompleted(true);
    messageRepository.save(message);
}

4.Redis缓存封装

// 设置缓存(带过期时间)
public <T> void set(String key, T value, Duration ttl) {
    RBucket<T> bucket = redissonClient.getBucket(key);
    bucket.set(value, ttl);
}

// 获取缓存,不存在则通过loader加载并缓存
public <T> T getOrLoad(String key, Duration ttl, Function<String, T> loader) {
    T value = redissonClient.getBucket(key).get();
    if (value == null) {
        value = loader.apply(key);
        if (value != null) {
            redissonClient.getBucket(key).set(value, ttl);
        }
    }
    return value;
}

三.验证结果

1.知识库问答界面

在前端问答界面,选择“机器学习导论”知识库,提问后AI给出了结构化的问答,历史消息也正常展示。

2.会话持久化验证

在DataGrip中查询rag_chat_sessions表,可以看到每次对话都生成了对应的会话记录,包含会话标题、消息数量和创建时间。

SELECT id, title, message_count, created_at FROM rag_chat_sessions LIMIT 5;

3.消息持久化验证

查询rag_chat_messages表,可以看到USER和ASSISTANT的消息交替存储,消息内容、类型、创建时间均正确记录。

SELECT id, type, LEFT(content, 50) as content, created_at FROM rag_chat_messages LIMIT 5;

四.总结

本文实现了知识库问答对话的缓存与持久化功能。

在数据存储层面,设计了rag_chat_sessionsrag_chat_messages两张核心表,会话与消息一对多关联,消息通过messageOrder字段保证顺序,USER和ASSISTANT消息交替存储,结构清晰。

在流式响应处理上,采用占位符机制解决了流式输出的持久化问题。AI开始回答时先创建一条completed=false的空消息,等流式输出全部完成后再一次性更新内容并标记完成。这样既保证了数据的完整性,又避免了每个token都写一次数据库带来的性能问题。

在缓存设计上,通过RedisService对Redisson进行统一封装,提供setgetgetOrLoad等通用操作,所有缓存均设置TTL自动过期,避免内存无限增长。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐