Bài 36.3: Memory và conversation history

Chào mừng các bạn đến với bài học tiếp theo trong chuỗi series lập trình web cùng AI!

Nếu bạn đã từng tương tác với các chatbot hay trợ lý ảo thông minh, bạn sẽ nhận thấy một điều đặc biệt: chúng dường như nhớ những gì bạn đã nói trước đó trong cùng một cuộc trò chuyện. Khả năng này không phải là "phép màu", mà là kết quả của việc quản lý Memoryconversation history. Đây là một khái niệm cực kỳ quan trọng khi xây dựng các ứng dụng AI đàm thoại (conversational AI), đặc biệt là trên nền tảng web.

Hãy cùng đào sâu hơn nhé!

Memory là gì trong AI đàm thoại?

Trong bối cảnh của các mô hình ngôn ngữ lớn (Large Language Models - LLMs) hay chatbot, Memory (Bộ nhớ) đề cập đến khả năng của hệ thống AI trong việc ghi nhớ thông tin từ các lượt tương tác trước đó trong cùng một cuộc trò chuyện.

Một mô hình AI "không có bộ nhớ" sẽ xử lý từng yêu cầu của bạn một cách độc lập. Nghĩa là, nếu bạn hỏi "Thời tiết hôm nay ở Hà Nội thế nào?", nó sẽ trả lời. Nhưng nếu bạn hỏi tiếp "Thế còn ở Hồ Chí Minh?", nó sẽ không hiểu "Thế còn ở" đang ám chỉ việc hỏi về thời tiết, vì nó đã "quên" câu hỏi trước đó. Trải nghiệm này sẽ rất khó chịu và kém hiệu quả.

Conversation History là gì?

Conversation History (Lịch sử cuộc trò chuyện) chính là dữ liệu cụ thể mà hệ thống AI sử dụng để tạo nên "bộ nhớ" đó. Nó là một chuỗi các tin nhắn đã trao đổi giữa người dùng và AI, thường được tổ chức dưới dạng một danh sách hoặc mảng các đối tượng, mỗi đối tượng đại diện cho một tin nhắn với vai trò (người dùng hoặc trợ lý/AI) và nội dung tương ứng.

Đây là dữ liệu "đầu vào" bổ sung mà chúng ta cần cung cấp cho mô hình AI cùng với câu hỏi hiện tại của người dùng, để mô hình có đủ ngữ cảnh để đưa ra phản hồi phù hợp.

Tại sao Memory và Conversation History lại quan trọng?

Khả năng ghi nhớ và sử dụng lịch sử cuộc trò chuyện mang lại nhiều lợi ích thiết yếu:

  1. Duy trì ngữ cảnh (Maintaining Context): Đây là lợi ích quan trọng nhất. Lịch sử giúp AI hiểu được chủ đề, các thông tin đã được thảo luận, và mối liên hệ giữa các câu hỏi liên tiếp của người dùng.
  2. Đảm bảo tính mạch lạc (Ensuring Coherence): Cuộc trò chuyện trở nên tự nhiên, liền mạch, giống như khi bạn nói chuyện với một con người. AI có thể tham chiếu đến các điều đã nói trước đó.
  3. Cá nhân hóa trải nghiệm (Personalizing Experience): Nếu AI ghi nhớ sở thích, thông tin cá nhân (mà người dùng cung cấp) từ các lượt trước, nó có thể cung cấp các phản hồi phù hợp và cá nhân hóa hơn.
  4. Tránh lặp lại (Avoiding Repetition): AI có thể tránh hỏi lại thông tin đã được cung cấp hoặc lặp lại các phản hồi trước đó.
Quản lý Conversation History trên Web

Khi xây dựng ứng dụng web có tích hợp AI đàm thoại (ví dụ: một chatbot hỗ trợ khách hàng trên website của bạn), bạn cần phải quản lý luồng dữ liệu lịch sử cuộc trò chuyện. Thông thường, lịch sử này sẽ được:

  1. Lưu trữ ở đâu đó:
    • Trên Frontend State: Tạm thời trong bộ nhớ trình duyệt (ví dụ: state của React/Vue/Angular component), chỉ tồn tại trong phiên làm việc hiện tại của người dùng. Phù hợp cho các cuộc trò chuyện ngắn, không cần duy trì khi refresh trang.
    • Trên Backend/Database: Lưu trữ vĩnh viễn hơn, cho phép người dùng quay lại và tiếp tục cuộc trò chuyện sau này. Đây là phương pháp phổ biến cho các ứng dụng chatbot đầy đủ tính năng.
    • Trong Browser Storage (localStorage, sessionStorage): Ít phổ biến hơn cho lịch sử trò chuyện phức tạp, nhưng có thể dùng để lưu trữ ID phiên hoặc các thông tin đơn giản.
  2. Gửi đến API của mô hình AI: Mỗi khi người dùng gửi một tin nhắn mới, ứng dụng frontend hoặc backend sẽ tổng hợp lịch sử cuộc trò chuyện hiện tại (bao gồm cả tin nhắn mới nhất của người dùng) và gửi nó như một phần của payload request đến API của mô hình AI (ví dụ: OpenAI API, Anthropic API...). Mô hình AI sẽ xử lý toàn bộ chuỗi tin nhắn này để tạo ra phản hồi tiếp theo, có tính đến ngữ cảnh.
Ví dụ minh họa (Frontend - React)

Chúng ta hãy xem một ví dụ đơn giản về cách quản lý lịch sử cuộc trò chuyện trong state của một component React.

Code Minh Họa 1: Lưu trữ lịch sử trong React State

import React, { useState } from 'react';

// Component giả định để hiển thị lịch sử và nhập tin nhắn
function ChatInterface() {
  // State để lưu trữ lịch sử cuộc trò chuyện
  // Mỗi tin nhắn có 'role' (user/assistant) và 'content'
  const [conversationHistory, setConversationHistory] = useState([
    { role: 'assistant', content: 'Chào bạn! Tôi có thể giúp gì?' },
  ]);
  const [inputMessage, setInputMessage] = useState(''); // State cho input của người dùng

  // Hàm xử lý khi người dùng gửi tin nhắn
  const handleSendMessage = async () => {
    if (!inputMessage.trim()) return; // Không gửi tin nhắn rỗng

    const newUserMessage = { role: 'user', content: inputMessage };

    // 1. Cập nhật lịch sử cuộc trò chuyện với tin nhắn của người dùng
    const updatedHistory = [...conversationHistory, newUserMessage];
    setConversationHistory(updatedHistory);

    // 2. Xóa nội dung trong input
    setInputMessage('');

    // 3. Gửi *toàn bộ* lịch sử cập nhật (bao gồm tin nhắn mới) đến API của AI
    // (Hàm sendToAIToGetReply sẽ được minh họa sau)
    const aiReply = await sendToAIToGetReply(updatedHistory);

    // 4. Cập nhật lịch sử cuộc trò chuyện với phản hồi của AI
    if (aiReply) {
      setConversationHistory(prevHistory => [...prevHistory, { role: 'assistant', content: aiReply }]);
    }
  };

  // Hàm giả định gọi API AI (sẽ minh họa cấu trúc sau)
  // Trong thực tế, bạn sẽ gọi API backend hoặc trực tiếp API của nhà cung cấp AI
  const sendToAIToGetReply = async (currentHistory) => {
      console.log('Gửi lịch sử đến AI:', currentHistory);
      // Ở đây bạn sẽ có logic gọi fetch hoặc axios
      // await fetch('/api/get-ai-reply', { method: 'POST', body: JSON.stringify({ messages: currentHistory }) });
      // ... xử lý phản hồi từ AI ...
      // Trả về phản hồi của AI (string) hoặc null nếu có lỗi
      return `Tôi đã nhận ${currentHistory.length} tin nhắn. Bạn vừa nói: "${currentHistory[currentHistory.length - 1].content}".`; // Phản hồi giả
  };


  return (
    <div style={{ border: '1px solid #ccc', padding: '10px', height: '400px', overflowY: 'scroll' }}>
      {/* Hiển thị lịch sử cuộc trò chuyện */}
      {conversationHistory.map((msg, index) => (
        <div key={index} style={{ margin: '5px 0', fontWeight: msg.role === 'user' ? 'bold' : 'normal' }}>
          **{msg.role === 'user' ? 'Bạn' : 'AI'}**: *{msg.content}*
        </div>
      ))}

      {/* Khu vực nhập tin nhắn */}
      <div style={{ marginTop: '10px' }}>
        <input
          type="text"
          value={inputMessage}
          onChange={(e) => setInputMessage(e.target.value)}
          onKeyPress={(e) => { if (e.key === 'Enter') handleSendMessage(); }}
          placeholder="Nhập tin nhắn của bạn..."
          style={{ width: 'calc(100% - 80px)', marginRight: '5px', padding: '5px' }}
        />
        <button onClick={handleSendMessage}>Gửi</button>
      </div>
    </div>
  );
}

export default ChatInterface;

Giải thích Code Minh Họa 1:

  • Chúng ta sử dụng useState để tạo một state conversationHistory là một mảng. Mỗi phần tử trong mảng là một object { role: 'user' | 'assistant', content: string }.
  • Khi người dùng gửi tin nhắn (handleSendMessage):
    • Tạo một object tin nhắn mới cho người dùng.
    • Quan trọng: Tạo một mảng mới (updatedHistory) bằng cách sao chép lịch sử cũ (...conversationHistory) và thêm tin nhắn mới của người dùng vào cuối. Điều này đảm bảo tính bất biến của state trong React.
    • Cập nhật state conversationHistory bằng mảng mới này.
    • Gọi hàm sendToAIToGetReplytruyền toàn bộ mảng updatedHistory vào đó.
    • Sau khi nhận được phản hồi từ AI, chúng ta lại cập nhật state conversationHistory một lần nữa bằng cách thêm phản hồi của AI vào cuối mảng (sử dụng prevHistory để lấy state mới nhất).
  • Phần hiển thị (.map) lặp qua mảng conversationHistory để hiển thị từng tin nhắn, phân biệt vai trò của người gửi bằng cách in đậm tên.

Code Minh Họa 2: Cấu trúc gửi lịch sử qua API

Đây là cấu trúc dữ liệu điển hình mà bạn sẽ gửi đến một API tích hợp mô hình AI (ví dụ: OpenAI Chat Completions API). Hàm sendToAIToGetReply từ ví dụ trước sẽ thực hiện việc này.

// Hàm giả định gọi API AI
async function sendToAIToGetReply(currentHistory) {
  const apiUrl = 'https://api.openai.com/v1/chat/completions'; // Ví dụ API OpenAI
  const apiKey = 'YOUR_OPENAI_API_KEY'; // Lấy từ biến môi trường!

  try {
    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
      },
      body: JSON.stringify({
        model: 'gpt-3.5-turbo', // Hoặc 'gpt-4', 'gpt-4o', ...
        messages: currentHistory // <-- Chính là mảng lịch sử cuộc trò chuyện của chúng ta
      })
    });

    if (!response.ok) {
      const errorData = await response.json();
      console.error('API error:', errorData);
      throw new Error(`API request failed with status ${response.status}`);
    }

    const data = await response.json();
    // Phản hồi từ OpenAI API có cấu trúc nhất định
    const aiMessageContent = data.choices[0].message.content;

    return aiMessageContent; // Trả về nội dung phản hồi của AI

  } catch (error) {
    console.error("Error sending message to AI:", error);
    return "Xin lỗi, tôi đang gặp vấn đề. Vui lòng thử lại sau."; // Thông báo lỗi cho người dùng
  }
}

Giải thích Code Minh Họa 2:

  • Hàm này nhận vào currentHistory, chính là mảng các tin nhắn từ state React của chúng ta.
  • Nó thực hiện một request POST đến endpoint của API AI.
  • Trong phần body của request, chúng ta gửi một JSON object chứa ít nhất model (chọn mô hình AI) và đặc biệt là mảng messages. Mảng messages này chính là currentHistory mà chúng ta truyền vào.
  • Mô hình AI sẽ đọc toàn bộ mảng messages, hiểu ngữ cảnh từ các tin nhắn trước đó, và tạo ra phản hồi cho tin nhắn cuối cùng (tin nhắn của người dùng hiện tại).
  • Chúng ta xử lý phản hồi từ API để lấy ra nội dung tin nhắn của AI và trả về.
Thách thức khi quản lý lịch sử dài

Một vấn đề phát sinh khi cuộc trò chuyện kéo dài là mảng lịch sử có thể trở nên rất lớn. Hầu hết các mô hình AI có giới hạn về số lượng token hoặc tin nhắn mà chúng có thể xử lý trong một lần gọi API (đây gọi là context window). Gửi lịch sử quá dài có thể vượt quá giới hạn này, dẫn đến lỗi hoặc chi phí tăng cao.

Để giải quyết vấn đề này, có một số chiến lược:

  1. Truncation (Cắt bớt): Chỉ giữ lại N tin nhắn gần nhất trong lịch sử. Đây là phương pháp đơn giản nhất. Bạn cần xác định N sao cho phù hợp với giới hạn context của mô hình AI bạn đang sử dụng.
  2. Summarization (Tóm tắt): Thay vì giữ toàn bộ tin nhắn cũ, định kỳ yêu cầu AI tóm tắt lại nội dung cuộc trò chuyện đến thời điểm đó, và sử dụng bản tóm tắt này cùng với các tin nhắn gần đây nhất làm context. Phương pháp này phức tạp hơn nhưng hiệu quả hơn trong việc duy trì ngữ cảnh cho các cuộc trò chuyện rất dài.

Trong phạm vi frontend, bạn thường sẽ xử lý Truncation trước khi gửi lịch sử lên backend hoặc API.

Code Minh Họa 3: Truncation đơn giản (Frontend)

Áp dụng vào handleSendMessage trong Code Minh Họa 1:

  // ... các import và useState như cũ ...

  const handleSendMessage = async () => {
    if (!inputMessage.trim()) return;

    const newUserMessage = { role: 'user', content: inputMessage };

    // 1. Thêm tin nhắn mới của người dùng vào lịch sử
    let updatedHistory = [...conversationHistory, newUserMessage];

    // --- Thêm logic Truncation tại đây ---
    const MAX_HISTORY_MESSAGES = 10; // Giới hạn 10 tin nhắn cuối cùng
    if (updatedHistory.length > MAX_HISTORY_MESSAGES) {
        // Bỏ đi các tin nhắn cũ nhất, chỉ giữ lại MAX_HISTORY_MESSAGES tin nhắn cuối
        updatedHistory = updatedHistory.slice(-MAX_HISTORY_MESSAGES);
        console.log(`Lịch sử đã được cắt bớt, chỉ giữ lại ${MAX_HISTORY_MESSAGES} tin nhắn cuối.`);
    }
    // --- Kết thúc logic Truncation ---


    // 2. Cập nhật state với lịch sử (có thể đã bị cắt bớt)
    setConversationHistory(updatedHistory);

    // 3. Xóa nội dung trong input
    setInputMessage('');

    // 4. Gửi lịch sử (đã xử lý truncation) đến API của AI
    const aiReply = await sendToAIToGetReply(updatedHistory); // Truyền updatedHistory đã xử lý

    // 5. Cập nhật lịch sử cuộc trò chuyện với phản hồi của AI (phản hồi của AI cũng được tính vào history)
     if (aiReply) {
       // Lưu ý: Khi thêm phản hồi của AI, lịch sử có thể lại vượt quá giới hạn.
       // Bạn có thể cần áp dụng truncation lại trước khi set state,
       // hoặc xử lý truncation chỉ trước khi gửi đi và giữ toàn bộ lịch sử trong state
       // tùy thuộc vào yêu cầu và cách bạn muốn hiển thị.
       // Cách đơn giản nhất là cứ thêm vào state và chỉ cắt bớt khi gửi đi.
       setConversationHistory(prevHistory => [...prevHistory, { role: 'assistant', content: aiReply }]);
       // Hoặc nếu muốn state luôn bị cắt:
       // let historyWithAIReply = [...updatedHistory, { role: 'assistant', content: aiReply }];
       // if (historyWithAIReply.length > MAX_HISTORY_MESSAGES) {
       //    historyWithAIReply = historyWithAIReply.slice(-MAX_HISTORY_MESSAGES);
       // }
       // setConversationHistory(historyWithAIReply);
     }
  };

  // ... rest of the component (sendToAIToGetReply and JSX) ...

Giải thích Code Minh Họa 3:

  • Chúng ta thêm một biến MAX_HISTORY_MESSAGES để định nghĩa số lượng tin nhắn tối đa muốn giữ lại.
  • Sau khi thêm tin nhắn mới nhất của người dùng vào updatedHistory, chúng ta kiểm tra xem độ dài của mảng có vượt quá giới hạn không.
  • Nếu có, chúng ta sử dụng phương thức .slice(-MAX_HISTORY_MESSAGES) để tạo một mảng mới chỉ chứa N phần tử cuối cùng.
  • Mảng updatedHistory (đã bị cắt bớt hoặc không) sau đó được dùng để cập nhật state và gửi đi API.
Kết nối với Next.js

Trong một ứng dụng Next.js, việc quản lý lịch sử trò chuyện thường diễn ra trên frontend (trong state của component hoặc Context API) để hiển thị cho người dùng. Tuy nhiên, cuộc gọi đến API của mô hình AI (như API OpenAI) nên được thực hiện ở backend (ví dụ: trong các API Routes của Next.js) thay vì gọi trực tiếp từ frontend.

Tại sao lại như vậy? Vì bạn không nên để API key của các dịch vụ AI lộ ra ở frontend code (sẽ bị người dùng xem được trong source code trình duyệt). API Route đóng vai trò như một "cửa ngõ" an toàn: frontend gửi tin nhắn và lịch sử đến API Route của bạn, API Route sử dụng API key an toàn trên server để gọi API của nhà cung cấp AI, và trả kết quả về cho frontend.

Comments

There are no comments at the moment.