作者:大洪聊AI
本章目标:实现会话与消息持久化,基于滑动窗口实现多轮上下文管理,让AI能够记住历史对话,支持自然的多轮交流
前置条件:第四章RAG知识库100%完成,文档上传、检索、防编造功能正常


前言

上一章我们完成了RAG知识库系统,AI已经可以基于私有知识库准确回答问题,不会胡说八道了。但这时的系统还有两个非常影响体验的问题:

  1. 没有记忆:每次对话都是独立的,AI完全记不住你上一句说了什么,只能一问一答
  2. 刷新就丢:聊天记录只存在浏览器内存里,页面一刷新全部消失,没有任何留存

这一章我们就彻底解决这两个问题:用MySQL做会话与消息持久化,基于滑动窗口策略实现多轮上下文管理,让AI真正拥有“记忆”,支持连贯的多轮对话。


一、为什么需要持久化 + 滑动窗口上下文?

1.1 纯前端记忆的痛点

  • 刷新丢失:页面刷新、浏览器关闭,聊天记录全部消失
  • 无法跨端:换个设备就看不到历史对话
  • 没有数据沉淀:无法做后续的数据分析、质检、优化
  • 容易超Token:把所有历史都塞给大模型,对话多了直接超出Token限制报错

1.2 滑动窗口策略

大模型的上下文长度是有限的(比如doubao-pro-32k是32K Token),不可能把所有历史对话都传进去。行业通用的最优解就是滑动窗口

只保留最近的N条对话作为上下文,更早的历史自动舍弃。既保证了AI能记住近期对话,又不会超出Token限制。

1.3 整体设计思路

用户发送消息
    ↓
保存用户消息到MySQL
    ↓
查询当前会话最近N条历史消息
    ↓
组装消息列表:系统提示 + 历史对话 + 当前问题
    ↓
调用大模型生成回答
    ↓
保存AI回答到MySQL
    ↓
流式返回给前端

二、数据库设计

2.1 表结构设计

我们需要两张核心表:

  • chat_session:会话表,记录每个会话的基本信息
  • chat_message:消息表,记录每条具体的聊天内容

2.2 执行建表SQL

在你的ai_kefu数据库中执行以下SQL:

-- 会话表
CREATE TABLE `chat_session` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `session_id` varchar(64) NOT NULL COMMENT '会话唯一标识',
  `title` varchar(255) DEFAULT '新对话' COMMENT '会话标题',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_session_id` (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话表';

-- 消息表
CREATE TABLE `chat_message` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `session_id` varchar(64) NOT NULL COMMENT '所属会话ID',
  `role` varchar(32) NOT NULL COMMENT '消息角色 user/assistant',
  `content` text NOT NULL COMMENT '消息内容',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
  PRIMARY KEY (`id`),
  KEY `idx_session_id` (`session_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天消息表';

字段说明

  • session_id:用UUID生成,前端传入,标识唯一会话
  • role:消息角色,user是用户,assistant是AI,和大模型接口要求一致
  • 统一逻辑删除、创建时间、更新时间,符合企业开发规范

三、后端持久化层开发

3.1 第一步:创建实体类

新建会话实体:src/main/java/org/example/model/entity/ChatSession.java

package org.example.model.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("chat_session")
public class ChatSession {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String sessionId;

    private String title;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    @TableLogic
    private Integer isDeleted;
}

新建消息实体:src/main/java/org/example/model/entity/ChatMessage.java

package org.example.model.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("chat_message")
public class ChatMessage {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String sessionId;

    private String role;

    private String content;

    private LocalDateTime createTime;

    @TableLogic
    private Integer isDeleted;
}

3.2 第二步:创建Mapper接口

新建:src/main/java/org/example/mapper/ChatSessionMapper.java

package org.example.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.model.entity.ChatSession;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ChatSessionMapper extends BaseMapper<ChatSession> {
}

新建:src/main/java/org/example/mapper/ChatMessageMapper.java

package org.example.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.model.entity.ChatMessage;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ChatMessageMapper extends BaseMapper<ChatMessage> {
}

3.3 第三步:会话服务SessionService

新建:src/main/java/org/example/service/SessionService.java

package org.example.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.example.mapper.ChatSessionMapper;
import org.example.model.entity.ChatSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class SessionService {

    @Autowired
    private ChatSessionMapper sessionMapper;

    /**
     * 创建新会话
     */
    public String createSession() {
        String sessionId = UUID.randomUUID().toString().replace("-", "");
        ChatSession session = new ChatSession();
        session.setSessionId(sessionId);
        session.setTitle("新对话");
        sessionMapper.insert(session);
        return sessionId;
    }

    /**
     * 检查会话是否存在
     */
    public boolean exists(String sessionId) {
        LambdaQueryWrapper<ChatSession> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ChatSession::getSessionId, sessionId);
        return sessionMapper.selectCount(wrapper) > 0;
    }
}

3.4 第四步:消息服务MessageService(核心)

这是本章最核心的服务,负责消息保存、历史查询、滑动窗口上下文组装。

新建:src/main/java/org/example/service/MessageService.java

package org.example.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import org.example.mapper.ChatMessageMapper;
import org.example.model.entity.ChatMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class MessageService {

    @Autowired
    private ChatMessageMapper messageMapper;

    /**
     * 保存单条消息
     */
    public void addMessage(String sessionId, String role, String content) {
        ChatMessage message = new ChatMessage();
        message.setSessionId(sessionId);
        message.setRole(role);
        message.setContent(content);
        messageMapper.insert(message);
    }

    /**
     * 获取会话的所有历史消息(按时间正序)
     */
    public List<ChatMessage> getHistoryMessages(String sessionId) {
        LambdaQueryWrapper<ChatMessage> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ChatMessage::getSessionId, sessionId)
                .orderByAsc(ChatMessage::getCreateTime);
        return messageMapper.selectList(wrapper);
    }

    /**
     * 构建滑动窗口上下文(转换为LangChain4j的ChatMessage格式)
     * @param sessionId 会话ID
     * @param limit 保留最近N条消息
     */
    public List<ChatMessage> buildSlidingWindowContext(String sessionId, int limit) {
        // 1. 查询历史消息
        List<ChatMessage> history = getHistoryMessages(sessionId);

        // 2. 截取最近N条
        int start = Math.max(0, history.size() - limit);
        List<ChatMessage> windowMessages = history.subList(start, history.size());

        // 3. 转换为LangChain4j的消息格式
        List<ChatMessage> context = new ArrayList<>();
        for (ChatMessage msg : windowMessages) {
            if ("user".equals(msg.getRole())) {
                context.add(UserMessage.from(msg.getContent()));
            } else if ("assistant".equals(msg.getRole())) {
                context.add(AiMessage.from(msg.getContent()));
            }
        }

        return context;
    }
}

核心逻辑说明

  1. buildSlidingWindowContext是滑动窗口的核心实现
  2. 先查出所有历史,再截取最后N条,保证只保留最新的对话
  3. 把数据库里的消息转换成大模型要求的UserMessage/AiMessage格式
  4. 默认建议保留最近10条,平衡记忆能力和Token消耗

四、业务层重构:抽取AiService

之前的聊天逻辑都写在Controller里,现在业务逻辑越来越多,我们按照MVC规范把业务逻辑抽到Service层,为后面第六章的意图识别重构打基础。

4.1 定义AiService接口

新建:src/main/java/org/example/service/AiService.java

package org.example.service;

import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

public interface AiService {
    /**
     * 流式生成回答
     * @param sessionId 会话ID
     * @param message 用户消息
     * @param emitter SSE发射器
     */
    void streamGenerate(String sessionId, String message, SseEmitter emitter);
}

4.2 实现豆包AI服务

新建:src/main/java/org/example/service/impl/DoubaoAiService.java

package org.example.service.impl;

import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.StreamingResponseHandler;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.output.Response;
import org.example.service.AiService;
import org.example.service.MessageService;
import org.example.service.RagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.List;

@Service("doubaoAiService")
public class DoubaoAiService implements AiService {

    @Autowired
    @Qualifier("doubaoStreamingChatModel")
    private StreamingChatLanguageModel streamingChatModel;

    @Autowired
    private MessageService messageService;

    @Autowired
    private RagService ragService;

    // 滑动窗口大小:保留最近10条消息
    private static final int CONTEXT_WINDOW_SIZE = 10;

    @Override
    public void streamGenerate(String sessionId, String message, SseEmitter emitter) {
        // 1. 保存用户消息到数据库
        messageService.addMessage(sessionId, "user", message);

        // 2. RAG检索知识库
        String relevantContent = ragService.retrieveRelevantContent(message);

        // 3. 无相关内容,直接返回固定话术,不调用大模型
        if (relevantContent.isEmpty()) {
            String reply = "抱歉,我没有找到相关信息,请您咨询人工客服。";
            try {
                emitter.send(reply, MediaType.TEXT_PLAIN);
                emitter.send("[DONE]", MediaType.TEXT_PLAIN);
                emitter.complete();
                // 保存AI回答
                messageService.addMessage(sessionId, "assistant", reply);
            } catch (IOException e) {
                emitter.completeWithError(e);
            }
            return;
        }

        // 4. 构建RAG增强后的用户问题
        String enhancedQuestion = ragService.buildRagPrompt(message, relevantContent);

        // 5. 获取滑动窗口上下文(去掉最后一条,因为刚存的用户消息和enhancedQuestion重复)
        List<ChatMessage> contextMessages = messageService.buildSlidingWindowContext(sessionId, CONTEXT_WINDOW_SIZE);
        if (!contextMessages.isEmpty() && contextMessages.get(contextMessages.size() - 1) instanceof UserMessage) {
            contextMessages.remove(contextMessages.size() - 1);
        }

        // 6. 组装完整消息列表:系统提示 + 历史上下文 + 增强后的当前问题
        contextMessages.add(0, SystemMessage.from("你是专业的AI客服,回答简洁、礼貌、专业。"));
        contextMessages.add(UserMessage.from(enhancedQuestion));

        // 7. 用于拼接完整的AI回复,最后存库
        StringBuilder fullResponse = new StringBuilder();

        // 8. 流式生成回答
        streamingChatModel.generate(contextMessages, new StreamingResponseHandler<AiMessage>() {
            @Override
            public void onNext(String token) {
                try {
                    emitter.send(token, MediaType.TEXT_PLAIN);
                    fullResponse.append(token);
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            }

            @Override
            public void onComplete(Response<AiMessage> response) {
                try {
                    emitter.send("[DONE]", MediaType.TEXT_PLAIN);
                    emitter.complete();
                    // 9. 保存AI回答到数据库
                    messageService.addMessage(sessionId, "assistant", fullResponse.toString());
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            }

            @Override
            public void onError(Throwable error) {
                emitter.completeWithError(error);
            }
        });
    }
}

关键细节

  • 去重处理:我们刚把用户原话存进了数据库,但传给大模型的是RAG增强后的问题,所以要把最后一条用户原话从上下文中去掉,避免重复
  • 系统提示:放在消息列表最前面,大模型最先看到规则
  • 完整回复拼接:流式生成是一个字一个字回调的,要用StringBuilder拼完整,最后存库

4.3 改造ChatController

Controller只负责参数校验和请求转发,不再写业务逻辑:

package org.example.controller;

import dev.langchain4j.model.chat.ChatLanguageModel;
import org.example.model.vo.AjaxResult;
import org.example.service.AiService;
import org.example.service.SessionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import lombok.Data;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    @Autowired
    @Qualifier("doubaoChatModel")
    private ChatLanguageModel chatModel;

    @Autowired
    @Qualifier("doubaoAiService")
    private AiService aiService;

    @Autowired
    private SessionService sessionService;

    @Data
    public static class ChatRequest {
        private String sessionId;
        private String message;
    }

    /**
     * 创建新会话
     */
    @PostMapping("/session")
    public AjaxResult<String> createSession() {
        String sessionId = sessionService.createSession();
        return AjaxResult.success(sessionId);
    }

    /**
     * SSE流式聊天接口(带上下文+RAG)
     */
    @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter streamChat(@RequestBody ChatRequest request) {
        SseEmitter emitter = new SseEmitter(60000L);
        aiService.streamGenerate(request.getSessionId(), request.getMessage(), emitter);
        return emitter;
    }

    /**
     * 同步聊天接口(保留用于测试)
     */
    @PostMapping("/simple")
    public AjaxResult<String> simpleChat(@RequestBody ChatRequest request) {
        String response = chatModel.generate(request.getMessage());
        return AjaxResult.success(response);
    }
}

五、前端改造:支持历史消息加载

5.1 核心改造点

  1. 页面加载时自动创建会话(如果没有)
  2. 进入页面拉取当前会话的历史消息并渲染
  3. 发送消息时带上sessionId
  4. 刷新页面后历史记录依然存在

5.2 完整改造后的App.vue

<template>
  <div class="app">
    <div class="chat-container">
      <div class="chat-header">
        <h1>AI智能客服</h1>
      </div>

      <div class="chat-content" ref="chatContentRef">
        <div
          v-for="(msg, index) in messageList"
          :key="index"
          :class="['message', msg.role]"
        >
          <div class="message-content">{{ msg.content }}</div>
        </div>
      </div>

      <div class="chat-input">
        <el-input
          v-model="inputMessage"
          placeholder="请输入您的问题..."
          @keyup.enter="handleSend"
          :disabled="isLoading"
        ></el-input>
        <el-button
          type="primary"
          @click="handleSend"
          :loading="isLoading"
        >
          发送
        </el-button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, nextTick, onMounted } from 'vue'
import { ElMessage } from 'element-plus'

const messageList = ref([])
const inputMessage = ref('')
const isLoading = ref(false)
const chatContentRef = ref(null)
const currentSessionId = ref('')

// 滚动到底部
const scrollToBottom = async () => {
  await nextTick()
  if (chatContentRef.value) {
    chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight
  }
}

// 加载历史消息
const loadHistory = async () => {
  if (!currentSessionId.value) return
  
  try {
    const res = await fetch(`/api/chat/history?sessionId=${currentSessionId.value}`)
    const data = await res.json()
    if (data.code === 200) {
      messageList.value = data.data
      await scrollToBottom()
    }
  } catch (error) {
    console.error('加载历史消息失败:', error)
  }
}

// 页面加载时初始化会话
onMounted(async () => {
  // 从localStorage取会话ID,没有就新建
  let sessionId = localStorage.getItem('currentSessionId')
  
  if (!sessionId) {
    const res = await fetch('/api/chat/session', { method: 'POST' })
    const data = await res.json()
    sessionId = data.data
    localStorage.setItem('currentSessionId', sessionId)
  }
  
  currentSessionId.value = sessionId
  // 加载历史消息
  await loadHistory()
})

// 发送消息(流式版本)
const handleSend = async () => {
  const content = inputMessage.value.trim()
  if (!content || isLoading.value) return

  inputMessage.value = ''
  isLoading.value = true

  // 添加用户消息
  messageList.value.push({ role: 'user', content })
  await scrollToBottom()

  // 添加AI空消息占位
  const aiIndex = messageList.value.length
  messageList.value.push({ role: 'ai', content: '' })
  await scrollToBottom()

  try {
    const response = await fetch('/api/chat/stream', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        sessionId: currentSessionId.value,
        message: content
      })
    })

    if (!response.ok || !response.body) {
      throw new Error(`请求失败,状态码:${response.status}`)
    }

    const reader = response.body.getReader()
    const decoder = new TextDecoder('utf-8')
    let buffer = ''

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      buffer += decoder.decode(value, { stream: true })

      if (buffer.includes('[DONE]')) {
        const realContent = buffer.replace('[DONE]', '')
        messageList.value[aiIndex].content = realContent
        buffer = ''
        break
      } else {
        messageList.value[aiIndex].content = buffer
      }

      await scrollToBottom()
    }

    if (buffer && buffer !== '[DONE]') {
      messageList.value[aiIndex].content = buffer.replace('[DONE]', '')
    }

    if (!messageList.value[aiIndex].content) {
      messageList.value[aiIndex].content = '未收到回复,请重试。'
    }

  } catch (error) {
    console.error('流式请求失败:', error)
    messageList.value[aiIndex].content = '抱歉,系统出错了,请稍后再试。'
    ElMessage.error('请求失败,请检查网络')
  } finally {
    isLoading.value = false
    await scrollToBottom()
  }
}
</script>

<style scoped>
/* 样式和第三章完全一致,直接复用 */
.app {
  width: 100vw;
  height: 100vh;
  background-color: #f5f7fa;
  display: flex;
  justify-content: center;
  align-items: center;
}

.chat-container {
  width: 800px;
  height: 80vh;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
}

.chat-header {
  padding: 20px;
  border-bottom: 1px solid #e6e6e6;
  text-align: center;
}

.chat-header h1 {
  margin: 0;
  font-size: 24px;
  color: #303133;
}

.chat-content {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
}

.message {
  margin-bottom: 16px;
  max-width: 70%;
  display: flex;
}

.message.user {
  margin-left: auto;
  justify-content: flex-end;
}

.message.ai {
  margin-right: auto;
  justify-content: flex-start;
}

.message-content {
  padding: 12px 16px;
  border-radius: 8px;
  line-height: 1.5;
  word-break: break-word;
}

.message.user .message-content {
  background-color: #409eff;
  color: white;
}

.message.ai .message-content {
  background-color: #f5f7fa;
  color: #303133;
}

.chat-input {
  padding: 20px;
  border-top: 1px solid #e6e6e6;
  display: flex;
  gap: 10px;
}

.chat-input .el-input {
  flex: 1;
}
</style>

5.3 补充历史消息查询接口

在ChatController中加一个查询历史消息的接口:

/**
 * 获取会话历史消息
 */
@GetMapping("/history")
public AjaxResult<List<ChatMessage>> getHistory(@RequestParam String sessionId) {
    List<ChatMessage> list = messageService.getHistoryMessages(sessionId);
    return AjaxResult.success(list);
}

注意:这里返回的是实体类,生产环境建议转成VO返回,避免把数据库字段全部暴露给前端。


六、完整功能验证

  1. 启动服务:确保MySQL、Chroma、后端、前端都正常启动
  2. 刷新页面:打开页面,会自动创建会话,消息列表为空
  3. 发送第一条消息:“你们产品多少钱?”,AI正常回答
  4. 发送第二条消息:“专业版有什么功能?”,AI应该能理解你问的是专业版产品的功能,说明上下文生效了
  5. 刷新页面:刷新浏览器,历史消息依然存在,说明持久化生效了
  6. 验证RAG:问知识库外的问题,依然返回找不到相关信息

✅ 全部验证通过,多轮对话和持久化功能正常。


七、调优与扩展建议

7.1 滑动窗口大小怎么调?

场景 建议窗口大小 说明
简单问答客服 6-8条 不需要太长记忆,节省Token
任务型对话(查订单) 10-15条 需要记住多轮追问的信息
深度咨询场景 20-30条 需要完整的上下文理解需求

原则:在满足业务需求的前提下,窗口越小越好,既省Token又快。

7.2 后续可扩展方向

  • 会话列表:左侧加会话列表,支持创建多个会话、切换会话
  • 会话标题自动生成:用大模型根据前几句话生成会话标题
  • 消息删除:支持删除单条消息、清空整个会话
  • Token计数:统计每次请求的Token消耗,做费用统计

八、常见问题排查

问题1:AI记不住之前的对话

  • 检查是否传了正确的sessionId
  • 检查消息是否成功保存到数据库
  • 检查滑动窗口大小是不是设太小了
  • 检查是不是RAG增强后的问题把上下文覆盖了

问题2:刷新页面历史消息不显示

  • 检查/api/chat/history接口是否正常返回
  • 检查前端onMounted里是否调用了加载历史的方法
  • 检查localStorage里的currentSessionId是否存在

问题3:提示Token超出限制

  • 调小滑动窗口大小
  • 调小文档切片大小和Top-K数量
  • 检查是不是系统提示写太长了

问题4:消息重复

  • 检查是不是没有去掉最后一条用户原话
  • 检查保存消息的逻辑是不是执行了两次

九、本章总结

本章我们完成了多轮对话与持久化的核心能力:

  1. ✅ 完成了数据库设计,建立了会话表和消息表
  2. ✅ 实现了消息的增删查和持久化存储
  3. ✅ 基于滑动窗口策略实现了多轮上下文管理
  4. ✅ 重构了代码架构,分层清晰,Controller只做转发
  5. ✅ 前端支持历史消息加载,刷新页面记录不丢失
  6. ✅ RAG和上下文完美结合,既懂知识库又有记忆

现在的系统已经是一个完整可用的智能客服了,有流式体验、有知识库、有记忆、有持久化。下一章我们将进入架构升级阶段,用策略模式+工厂模式实现意图识别,让系统支持任务型对话,具备真正的生产级扩展能力。


下章预告

下一章我们将实现意图识别与任务型对话,用经典的策略模式+工厂模式重构对话架构,支持查询订单、转人工等标准化任务,实现槽位填充和多轮追问,打造可无限扩展的对话系统。

关注我,第一时间收到更新通知!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐