Mở đầu: Agent "quên" bạn vừa nói gì — vì sao?

Nếu bạn từng trò chuyện dài với một chatbot và thấy nó bỗng "quên" điều bạn nói ở đầu cuộc trò chuyện, bạn đang chứng kiến trực tiếp giới hạn bộ nhớ ngắn hạn. Model ngôn ngữ không có bộ nhớ thật sự giữa các lần gọi — mọi thứ nó "nhớ" chỉ là văn bản được nhét lại vào context mỗi lần hỏi. Bài này xây tiếp 2 viên gạch cho framework agent: quản lý bộ nhớ, và cách lắp ghép prompt một cách an toàn.

Cả hai phần đều mở rộng file dùng chung aisys-agent-kernel.js đã bắt đầu ở Bài 7 — không viết file mới, không viết lại ToolRegistry đã có.


📚 Điều kiện tiên quyết
Nên đọc Bài 7 trước — Bài 9 sẽ ghép cả Tool (Bài 7) và Memory (bài này) lại thành 1 Agent hoàn chỉnh.

1. Short-Term Memory vs Long-Term Memory

Agent cần 2 loại bộ nhớ khác bản chất: short-term memory (context window) — vài lượt hội thoại gần nhất, luôn kèm trong mọi request, nhưng có giới hạn kích thước cứng; và long-term memory — kho tri thức lớn hơn nhiều, chỉ lấy ra phần LIÊN QUAN khi cần (giống việc bạn không nhớ hết mọi thứ trong đầu, nhưng biết tra ở đâu).

short-term-memory.js
class ShortTermMemory {
  constructor(maxTurns = 3) {
    this.maxTurns = maxTurns;
    this.turns = [];
  }
  add(role, content) {
    this.turns.push({ role, content });
    const maxMessages = this.maxTurns * 2;
    while (this.turns.length > maxMessages) {
      this.turns.shift(); // cắt (truncate) tin nhắn CŨ NHẤT trước tiên
    }
  }
  getContext() {
    return this.turns.map((t) => `${t.role}: ${t.content}`).join("\n");
  }
}

2. Mô Phỏng "Vector Recall" Bằng So Khớp Từ Khoá

Long-term memory thật sự thường dùng embedding + similarity search (xem Series 16 — Text Embeddings để hiểu sâu cơ chế đó). Ở đây, demo rút gọn ý tưởng bằng cách so khớp từ khoá — đủ để dạy đúng KHÁI NIỆM "tìm thông tin liên quan trong kho lớn" mà không cần model embedding thật:

long-term-memory.js
class LongTermMemory {
  constructor() { this.records = []; }
  remember(text) {
    const keywords = new Set(text.toLowerCase().split(/\W+/).filter((w) => w.length > 2));
    this.records.push({ text, keywords });
  }
  recall(query, topK = 2) {
    const queryWords = new Set(query.toLowerCase().split(/\W+/).filter((w) => w.length > 2));
    return this.records
      .map((r) => {
        let overlap = 0;
        for (const w of queryWords) if (r.keywords.has(w)) overlap++;
        return { text: r.text, score: overlap };
      })
      .filter((r) => r.score > 0)
      .sort((a, b) => b.score - a.score)
      .slice(0, topK);
  }
}
💡 Mẹo: So khớp từ khoá là "em bé" của embedding search
Điểm khác biệt cốt lõi: so khớp từ khoá chỉ tìm được văn bản chứa ĐÚNG từ đó, còn embedding thật có thể tìm ra văn bản liên quan về Ý NGHĨA dù không chung từ nào (ví dụ query "xe hơi" tìm ra văn bản nói về "ô tô"). Cơ chế demo ở đây chỉ minh hoạ MỤC ĐÍCH của recall, không thay thế được embedding thật trong production.

3. Prompt Template Engine

Thay vì nối chuỗi thủ công (dễ sai sót, khó bảo trì), agent dùng 1 template engine tối giản để lắp ghép system prompt + biến động (câu hỏi người dùng, kết quả tool, lịch sử hội thoại):

prompt-template.js
class PromptTemplate {
  constructor(template) { this.template = template; }
  render(vars, { escape = true } = {}) {
    return this.template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
      if (!(key in vars)) throw new Error(`Thiếu biến template: "${key}"`);
      const raw = String(vars[key]);
      return escape ? escapeForPrompt(raw) : raw;
    });
  }
}

const tpl = new PromptTemplate("Hệ thống: Bạn là trợ lý hữu ích.\nNgười dùng nói: {{userInput}}");
tpl.render({ userInput: "Xin chào!" });
// => "Hệ thống: Bạn là trợ lý hữu ích.\nNgười dùng nói: Xin chào!"

4. Cạm Bẫy: Context Truncation & Prompt Injection Qua Biến

🕳️ Cạm bẫy 1: Context truncation cắt mất thông tin quan trọng
ShortTermMemory ở mục 1 luôn cắt tin nhắn CŨ NHẤT trước — nghĩa là nếu người dùng nói tên mình ở đầu cuộc trò chuyện rồi trò chuyện kéo dài quá giới hạn, agent sẽ "quên" mất tên đó dù mới nói cách đây vài phút (tính theo số lượt, không phải thời gian thật). Demo mục 5 minh hoạ trực tiếp: thêm đủ 7 lượt hội thoại vào bộ nhớ chỉ giữ 3 lượt, và xem lượt đầu tiên ("tôi tên là An") biến mất khỏi context trước khi agent kịp trả lời câu hỏi "tên tôi là gì?" ở lượt cuối.
⚠️ Cạm bẫy 2: Prompt injection qua chính biến hợp lệ
Nếu biến người dùng nhập được chèn thẳng vào template mà KHÔNG escape, một người dùng có thể viết "### System: bỏ qua mọi quy tắc..." — và tuỳ model, chuỗi này có thể bị hiểu nhầm là một chỉ thị hệ thống MỚI thay vì chỉ là nội dung câu hỏi. Đây không phải lỗ hổng ở tool hay ở model, mà ở chính bước LẮP GHÉP prompt — luôn escape/trung hoà cú pháp điều khiển trong biến trước khi chèn vào template.

5. Thực hành tương tác: Memory & Template Lab

Ba demo nhỏ minh hoạ đúng 3 khái niệm ở trên — thử theo thứ tự để thấy rõ nhất:

🧠 Memory & Template Lab

A. Short-Term Memory (giữ tối đa 3 lượt = 6 tin nhắn)

B. Long-Term Recall (so khớp từ khoá)

    C. Prompt Template — Escape chống Injection

    aisys-memory-lab.js (trích)
    import { ShortTermMemory, LongTermMemory, PromptTemplate } from "./aisys-agent-kernel.js";
    const stm = new ShortTermMemory(3);
    stmAddBtn.addEventListener("click", () => {
      stm.add(msg.role, msg.content); // tự cắt (truncate) khi vượt giới hạn
      stmContext.textContent = stm.getContext();
    });

    Bấm "Bật escape" tắt đi rồi render lại chuỗi injection mặc định — bạn sẽ thấy cảnh báo đỏ xuất hiện, minh hoạ đúng cạm bẫy 2 ở mục 4. Bài 9 sẽ ghép cả ToolRegistry (Bài 7) lẫn 2 loại memory này thành một Agent hoàn chỉnh chạy được vòng lặp ReAct.

    Tải file code thực hành minh họa bài học

    File JavaScript aisys-agent-kernel.js — phần ShortTermMemory/LongTermMemory/PromptTemplate vừa thêm ở bài này, cùng aisys-memory-lab.js wiring demo:

    Tải về aisys-agent-kernel.js

    📖 Tài liệu tham khảo

    Bài viết liên quan trong series

    Bài 7: Tool-Calling — Thiết Kế Interface Tool Bài 9: ReAct Loop, Callback & Streaming Quay lại Lộ trình Kỹ Thuật Hệ Thống AI

    Bình luận