之前已经实现了在项目中接入DeepSeek API并能够与其进行基础的辩论对话,但是我发现DeepSeek API并没有上下文记忆功能,缺乏对历史会话的记录与管理功能。这种限制使得DeepSeek在辩论中无法关联之前的会话内容进行辩论,也无法在后续的对话中基于历史数据进行更深入的记录与分析。为了解决这一问题,为辩论助手增加会话记录和上下文关联功能显得尤为重要。

本周的任务是实现辩论会话的记录与管理功能,记录完整辩论流程,将历史会话融入大模型Prompt,支持会话回溯与持续对话,查看辩论记录等功能。

目录

一、实现思路

二、辩论记录实现过程

1. 后端实现

2.前端实现

三、大模型上下文记忆功能实现

1.具体实现

2.效果展示

四、问题解决

1.content内容过长问题

2.保存的时间与实际时间不符

五、前端对话界面完善

1. 过滤去除思考过程

 2.输出内容格式美化


一、实现思路

在实现功能前,我首先设计了一套该功能的合理且可靠的实现流程,流程图如下图所示。

二、辩论记录实现过程

1. 后端实现

我之前在数据库模型建立的时候,创建了两个模型,一个是debate_session,用于存储一个完整的辩论过程,其具体属性如下表所示。

字段名 字段描述 数据类型
id 辩论会话唯一标识 uuid
user_id 发起辩论会话的用户标识 uuid
topic 辩论话题 string
position 用户在辩论中的立场 enum
result 辩论结果 enum
created_at 辩论会话创建时间 datetime

同时,我还创建了debate_turn,用于存储一个会话中的每一回合的内容,其具体属性如下表所示。

字段名 字段描述 数据类型
id 辩论轮次唯一标识 uuid
session_id 辩论轮次所属的会话标识 uuid
speaker 发言人(AI或user) string
content 该轮次的发言内容 longtext
turn_index 轮次序号 enum
timestamp 发言时间戳

datetime

首先,在保存辩论轮次之前,首先要创建一个辩论会话,并返回给前端,以便于前端同发送的内容一起发送给后端,在存储debate_turn的时候同时保存session_id。创建辩论的会话方法可以直接使用之前为数据库模型创建的CRUD接口方法:

@router.post("/", response_model=DebateSessionPublic)
def create_debate_session(
    *, session: SessionDep, current_user: CurrentUser, item_in: DebateSessionCreate
) -> Any:
    debate = DebateSession.model_validate(item_in, update={"user_id": current_user.id})
    session.add(debate)
    session.commit()
    session.refresh(debate)
    return debate

先前已经实现了websocket连接的对话接口方法,现在需要对方法进行改造,在每次用户发言和AI发言后,将二者的发言内容存入debate_turn中,具体实现如下。在api地址中,加入session_id,以便在前端发起请求的时候一同传回本会话的id,表明该轮次的对话属于哪个session_id。另一改动是在回复的过程中,由于是流式生成回复,因此需要对完整的回复内容进行收集,将最终的结果存入内容中。

@router.websocket("/stream/{session_id}")
async def websocket_chat(websocket: WebSocket, session_id: str, session: SessionDep):
    await websocket.accept()

    try:
        while True:
            data = await websocket.receive_text()
            message = json.loads(data)

            # 发送开始信号
            await websocket.send_json({
                "type": "start",
                "content": ""
            })
            ai_response = ""

            # 流式生成响应
            async for chunk in llm_streamer(message["message"]):
                ai_response += chunk  # 收集完整回复
                await websocket.send_json({
                    "type": "stream",
                    "content": chunk
                })

            # 发送结束信号
            await websocket.send_json({
                "type": "end",
                "content": ""
            })

在AI内容生成结束后,对对话记录进行保存。首先根据session_id查询目前的轮数,从而确定turn_index属性的值。在下面分别保存user_turn和ai_turn对象,实现辩论轮次的内容的保存。

with session.begin():
    stmt = select(func.count()).select_from(DebateTurn).where(DebateTurn.session_id == uuid.UUID(session_id))
    result = session.execute(stmt)
    turn_count = result.scalar_one()  # 当前已有的轮数
    next_index = turn_count  # 新的 user 是第 next_index 条

    # 保存聊天记录
    user_turn = DebateTurn(
        session_id=uuid.UUID(session_id),
        speaker="user",
        content=message["message"],
        turn_index=next_index
    )
    session.add(user_turn)

    ai_turn = DebateTurn(
        session_id=uuid.UUID(session_id),
        speaker="ai",
        content=ai_response,
        turn_index=next_index + 1
    )
    session.add(ai_turn)
    session.commit()

在数据库中成功保存用户和ai的会话记录 

2.前端实现

对于前端的改动有以下几点:

1. 加入用户身份验证:由于在对websocket接口的调用中无法直接使用之前在http协议中的身份,因此我单独在页面初始化的时候加入身份验证方法,向后端请求身份验证,如果处于未登录状态,则直接跳转道登录界面。

  const res = await getUserId();
  if (!res) {
    alert("未登录或 token 无效");
    router.push('/Login')
  }

2. 在之前的页面中添加了一个“开始辩论”的按钮,输入该次辩论会话的基本信息,点击之后即可调用后端接口创建一个会话,再在对websocket进行初始化,在请求中加入该会话的session_id,即可成功在后端实现辩论记录的保存。

const beginDebate = async () => {
  try {
    const postData = {
      topic: '',
      position: '',
      result: ''
    };
    const res = await createSession(postData)
    console.log(res)

    if (res) {
      session_id = res.id
      console.log("新会话创建成功:", session_id)

      // 初始化 WebSocket
      initWebSocket()
    } else {
      alert("会话创建失败")
    }
  } catch (err) {
    console.error("创建会话失败:", err)
    alert("创建会话失败")
  }
}

三、大模型上下文记忆功能实现

1.具体实现

再次对websocket_chat方法进行改进,本次主要进行了如下改进:

首先根据传入的session_id进行查询,查到所有属于本次会话的轮次,根据轮次序号进行排序以确定内容的顺序,遍历这些轮次,并将内容按照speaker进行区分,分为assistant和user身份加入到messages里,最后加上本轮用户的输入,一起传入到大语言模型中,即可实现大模型的上下文记录功能。

#根据session_id查询本次会话中的会话记录,并按顺序加入到messages中传入大模型
stmt = select(DebateTurn).where(
    DebateTurn.session_id == uuid.UUID(session_id)
).order_by(DebateTurn.turn_index)
result = session.exec(stmt)
history_turns = result.all()

messages = [{"role": "system", "content": "你是一个辩论教练,请对user内容进行深度点评,并结合上下文"}]
for turn in history_turns:
    role = "assistant" if turn.speaker == "ai" else "user"
    messages.append({
        "role": role,
        "content": turn.content
    })
#加入本轮用户的输入
messages.append({
    "role": "user",
    "content": message["message"]
})

2.效果展示

再次在前端进行对话测试,发现已经能够实现对于会话的上下文记忆功能,大模型可以根据我之前的所有会话内容来完成整体的评价。

四、问题解决

1.content内容过长问题

报错如下:

 Error: 处理错误: (pymysql.err.DataError) (1406, "Data too long for column 'content' at row 2")
[SQL: INSERT INTO debate_turns (id, session_id, speaker, content, turn_index, timestamp) VALUES (%(id)s, %(session_id)s, %(speaker)s, %(content)s, %(turn_index)s, %(timestamp)s)]
[parameters: [{'id': '0cb0a6e98ce84e63aecbb992a211907e', 'session_id': '8ebe527c0e744fc2a1cdcbb90b8e3ea0', 'speaker': 'user', 'content': '你好', 'turn_index': 0, 'timestamp': datetime.datetime(2025, 5, 9, 14, 32, 29, 619956)}, {'id': '659d3d8e53974545b898bf3c3c537662', 'session_id': '8ebe527c0e744fc2a1cdcbb90b8e3ea0', 'speaker': 'ai', 'content': '<think>\n好的,用户现在需要我作为辩论教练对他的内容进行全方位智能点评。首先,我要仔细分析他的需求。他可能希望得到专业的反馈,帮助他提升辩论技巧。但用户目前只发送了“你好”,可能还没有提供具体内容。这时候,我需要先确认他是否有具体的辩论内容需要点评,比如辩论稿、论点或者某个辩题的想法。\n ... (1038 characters truncated) ... 多元(统计数据、类比推理、归谬法、诉诸权威的平衡使用)  \n- **What层**:语言是否达到“锐度与温度并存”(例:用“数字难民”替代“弱势群体”增强画面感)  \n\n请提供具体内容,我将结合国际辩论赛评审标准(如WSDC/WUDC评分体系)为您生成带改进方案的诊断报告。期待您的详细论述!', 'turn_index': 1, 'timestamp': datetime.datetime(2025, 5, 9, 14, 32, 29, 620677)}]]
(Background on this error at: https://sqlalche.me/e/20/9h9h)
socket.onmessage	@	ChatPage.vue:152

根据报错内容不难看出,本错误是由于content属性data too long,即ai生成的内容已经超过了varchar(255)的长度限制,因此无法成功保存。

MySQL中字符串类型及最大长度

  1. CHAR: 固定长度的字符串类型。最大长度为255字节。如果使用多字节字符集(如UTF-8),实际可存储的字符数会减少。

  2. VARCHAR: 可变长度的字符串类型。最大长度为65535字节。同样,实际可存储的字符数取决于所使用的字符集。

  3. TEXT: 用于存储长文本数据。有三种类型:TINYTEXT(最大长度为255字节)、TEXT(最大长度为65535字节)和LONGTEXT(最大长度为4294967295字节)。

考虑到后续还需要加入RAG内容以及更多prompt工程,可能造成DeepSeek输出更多内容,在此直接使用LongText进行对话内容的存储。

2.保存的时间与实际时间不符

当前获取时间的语句如下

created_at: datetime = Field(default_factory=datetime.utcnow)

出现这一问题的原因是使用 datetime.utcnow() 作为默认工厂时会出现时区不匹配的问题,因为 utcnow() 返回的是UTC 时间(协调世界时),而不是本地时间。因此只需要将utcnow改为now即可成功保存当地当前时间。

datetime.utcnow():返回 当前的 UTC 时间

datetime.now():返回 当前本地时间(依赖操作系统时区)

五、前端对话界面完善

1. 过滤去除思考过程

DeepSeek-R1模型的特色是先思考后回答,辩论过程中也需要推理过程,但是冗长的思考过程输出会影响用户的辩论体验,因此我需要将思考部分过滤掉,只输出内容部分。

分析发现,思考过程使用<think></think>标签包围,且开始和结束标签在websocket传输时都独占一个token,因此我可以根据收到的token是否为<think>来控制思考过程的输出。

在代码中通过一个bool类型的inThinkBlock属性进行控制。

      // 检测 <think> 标签开始
      if (token.includes('<think')) {
        inThinkBlock = true
      }

      // 如果不在 think block 内则显示
      if (!inThinkBlock) {
        lastMessage.chunks.push(token)
        lastMessage.content = lastMessage.chunks.join('')
        scrollToBottom()
      }

      // 检测 </think> 标签结束
      if (token.includes('</think>')) {
        inThinkBlock = false
      }

 2.输出内容格式美化

在之前的对话效果中,AI输出的内容为markdown形式,而前端没有对形式的控制,导致各种markdown格式都被输出,输出显示十分混乱。为了提升 AI 回复内容在前端的可读性与美观性,我对原有的输出格式进行了优化。

为了解决这一问题,我引入了 marked 这一轻量级、功能强大的 Markdown 解析器库。该库能够将 Markdown 格式的文本可靠地转换为 HTML,从而便于在页面中以样式化形式渲染。

<div class="content">
    <div v-html="renderMarkdown(message.content)"></div>
    <span v-if="message.loading" class="cursor">▎</span>
</div>

美化后效果如下图所示,界面美观且富有条理。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐