14. Memory - Quản lý ngữ cảnh ngắn hạn và dài hạn

Nam

Nam Hoang / Sep 14, 2025

9 min read

Trong các bài học trước, chúng ta đã dùng Checkpointer để Agent có thể nhớ những gì vừa xảy ra trong một phiên hội thoại. Tuy nhiên, nếu người dùng quay lại vào ngày mai với một thread_id mới, Agent sẽ hoàn toàn "mất trí nhớ". Để giải quyết vấn đề này, chúng ta cần hiểu về hai tầng bộ nhớ: Short-term Memory (Bộ nhớ ngắn hạn) và Long-term Memory (Bộ nhớ dài hạn).

I. Phân biệt hai loại bộ nhớ

Đặc điểmShort-term Memory (Checkpointer)Long-term Memory (Store)
Phạm viChỉ trong một Thread (phiên chat).Xuyên suốt nhiều Thread của cùng một User.
Dữ liệu lưuToàn bộ State, lịch sử tin nhắn.Thông tin cô đọng (sở thích, tên, thói quen).
Cơ chếTự động lưu sau mỗi bước chạy.Phải chủ động lưu/đọc qua API store.
Ví dụ"Bạn vừa hỏi gì ở câu trước?""Bạn tên là Nam (từ cuộc trò chuyện tuần trước)."

II. Bộ nhớ dài hạn với Store

Store là một giao diện trong LangGraph cho phép bạn lưu trữ dữ liệu theo namespace (ví dụ: [userId, 'memories']). Khác với Checkpointer lưu lại toàn bộ snapshot của đồ thị, Store được dùng để lưu trữ những mẩu thông tin có tính chất bền vững cao.

Các thao tác cơ bản:

  • put: Lưu một mẩu thông tin.
  • search: Tìm kiếm thông tin liên quan (hỗ trợ tìm kiếm theo từ khóa hoặc ngữ nghĩa).

III. Mã nguồn: 14-memory-context.ts

Ví dụ dưới đây minh họa một Agent có thể nhớ tên người dùng ngay cả khi chúng ta khởi tạo một cuộc hội thoại (Thread) mới hoàn toàn.

// path: 14-memory-context.ts
import {
  InMemoryStore,
  MemorySaver,
  START,
  StateGraph,
} from '@langchain/langgraph';
import { v4 as uuidv4 } from 'uuid';
import z from 'zod';
import { generateImage } from './utils/image';

// 1. Define State
const State = z.object({
  messages: z.array(z.any()),
  userId: z.string(),
});

// 2. Node logic using Store
async function chatNode(state: any, runtime: any) {
  const userId = state.userId;
  const store = runtime.store; // Access long-term memory
  const namespace = [userId, 'memories'];

  // Check whether the user's name is already known in the long-term store
  const memories = await store.search(namespace, { query: 'User name' });
  const userName = memories.length > 0 ? memories[0].value.name : 'stranger';

  console.log(`--- Agent knows you as: ${userName} ---`);

  const lastMsg = state.messages.at(-1).content;

  // Logic: If the user introduces their name, store it globally for this userId
  if (lastMsg.includes('My name is')) {
    const name = lastMsg.split('My name is ')[1];
    await store.put(namespace, uuidv4(), { name: name });
    return {
      messages: [
        {
          role: 'assistant',
          content: `Hello ${name}, I will remember your name!`,
        },
      ],
    };
  }

  return {
    messages: [
      { role: 'assistant', content: `Hello ${userName}, how can I help you?` },
    ],
  };
}

// 3. Initialization
const memory = new MemorySaver(); // Short-term (per thread)
const longTermStore = new InMemoryStore(); // Long-term (cross-thread)

const workflow = new StateGraph(State)
  .addNode('chat', chatNode)
  .addEdge(START, 'chat')
  .compile({
    checkpointer: memory,
    store: longTermStore,
  });

// 4. Execution across 2 different Threads
async function run() {
  await generateImage(workflow, 'graph-ignore/scripts-14-memory-context.jpg');

  // Thread 1: Introduction
  const config1 = { configurable: { thread_id: 'thread_1' } };
  console.log('--- Conversation 1 (Thread 1) ---');
  await workflow.invoke(
    { messages: [{ content: 'My name is Nam' }], userId: 'user_007' },
    config1,
  );

  // Thread 2: New context, but same User
  const config2 = { configurable: { thread_id: 'thread_2' } };
  console.log('\n--- Conversation 2 (Completely new Thread 2) ---');
  const result = await workflow.invoke(
    { messages: [{ content: 'Hello there' }], userId: 'user_007' },
    config2,
  );

  console.log('AI response:', result.messages.at(-1).content);
}

run();

IV. Giải thích cơ chế

  1. Isolated Threads: thread_1thread_2 là hai phiên làm việc độc lập. Nếu chỉ dùng Checkpointer, ở Thread 2 Agent sẽ không biết "Nam" là ai.
  2. Shared Store: Cả hai Node trong hai Thread đều truy cập vào cùng một longTermStore thông qua userId: 'user_007'.
  3. Namespace: Việc sử dụng [userId, 'memories'] đảm bảo dữ liệu của người dùng này không bị lẫn sang người dùng khác, đồng thời giúp tổ chức dữ liệu khoa học.

Sơ đồ hoạt động:

Sơ đồ Memory

V. Tổng kết

  • Checkpointer lưu giữ trạng thái chi tiết của một cuộc hội thoại đang diễn ra (Short-term).
  • Store lưu giữ các thông tin quan trọng về thực thể hoặc sở thích người dùng xuyên suốt thời gian (Long-term).
  • Việc kết hợp cả hai loại bộ nhớ giúp bạn xây dựng những Agent thực sự "có chiều sâu", mang lại cảm giác cá nhân hóa mạnh mẽ cho người dùng.

Trong bài học tiếp theo, chúng ta sẽ học về Subgraphs - cách chia nhỏ những Graph khổng lồ thành các module dễ quản lý và tái sử dụng!

👉 Bài tiếp theo: 15. Subgraphs - Xây dựng hệ thống Agent Modular