Cách quản lý Memory

Context Window, các mẫu Memory, và cách triển khai Memory cho agent

Các mô hình LLM là stateless. Mỗi lần gọi API là độc lập - mô hình không lưu giữ bất kỳ thông tin nào từ các lần gọi trước đó. Nếu agent cần "ghi nhớ" bất kỳ điều gì, ứng dụng AI của bạn phải nhắc nhở nó.

Memory không phải là tính năng của LLM. Đó là logic mà bạn viết trong ứng dụng xung quanh LLM. Nhiệm vụ của bạn là đảm bảo các thông tin quan trọng sẽ nằm trong Context Window.

Context Engineering

Context Window là gì?

Mỗi lần gọi API của LLM đều có đầu vào là một danh sách các message. Danh sách này — bao gồm system prompt, lịch sử trò chuyện, các định nghĩa tool và message hiện tại của người dùng — được gọi là Context Window. Đây là tất cả những gì mô hình có thể "nhìn thấy" cho lần gọi đó.

Context Window có giới hạn kích thước cố định được đo bằng token. Các mô hình khác nhau có giới hạn khác nhau, ví dụ như:

Mô hìnhContext window
Claude Sonnet 4200K tokens (1M in beta)
GPT-4.11M tokens
Gemini 2.5 Pro1M tokens

Vấn đề

Các con số của Context Window rất lớn, nhưng giới hạn lớn đó không giải quyết được vấn đề Memory — nó chỉ xảy ra muộn hơn. Cuối cùng, bạn sẽ phải đối mặt với ba vấn đề:

  1. Cuộc trò chuyện vượt quá giới hạn Context Window. Bạn phải quyết định giữ lại gì và loại bỏ gì.
  2. Context dài làm giảm độ chính xác. Một thông tin quan trọng bị ẩn trong giữa một cuộc trò chuyện dài có thể bị bỏ qua.
  3. Nhiều token hơn đồng nghĩa với chi phí cao hơn và phản hồi chậm hơn. Gửi toàn bộ lịch sử mỗi lần gọi là lãng phí khi phần lớn trong đó không liên quan đến message hiện tại.

Memory hiệu quả không phải là việc truyền hết mọi thứ đi — mà là việc nhắc nhở mô hình về thông tin cần thiết vào đúng thời điểm.

Phân loại Memory

Không có hệ thống phân loại tiêu chuẩn cho các loại Memory. Quản lý Memory là trách nhiệm của ứng dụng AI, không phải của mô hình — vì vậy bạn có thể thiết kế sao cho phù hợp với sản phẩm của mình.

Với team AI, chúng tôi chia Memory thành hai loại dựa trên vị trí của nó:

LoạiNó là gì
In-context MemoryTất cả nội dung bên trong Context Window sẽ được gửi cho API của LLM
External MemoryCác thông tin được lưu bên ngoài - database, vector store, file

In-Context Memory

Đây là thứ duy nhất mà mô hình thực sự "nhìn thấy". Nó bao gồm:

Thành phầnKích thước phổ biếnChú thích
System prompt500 - 2K tokenThông tin agent, hướng dẫn, quy tắc
Các định nghĩa tool2K - 100K+ tokenTỷ lệ thuận với số lượng tool.
Context được truy xuất1K - 10K tokenKết quả RAG, user memories, dữ liệu ngoài được đưa vào request
Lịch sử trò chuyệnTích luỹ dầnThành phần dễ dẫn đến tràn Context Window
Message hiện tại100 - 4K tokenPhụ thuộc vào độ dài yêu cầu của người dùng
Dự trù cho response4K - 16K tokenBắt buộc phải có để mô hình phản hồi ra kết quả

Kích thước thay đổi tùy theo ứng dụng, nhưng thông thường, định nghĩa toollịch sử trò chuyện là hai thành phần tiêu tốn nhiều tài nguyên nhất.

External Memory

Tất cả dữ liệu mà ứng dụng của bạn lưu trữ bên ngoài Context Window có thể bao gồm: lịch sử trò chuyện trong database, tùy chọn người dùng trong key-value store, kiến thức chuyên môn trong vector store hoặc knowledge graph.

Bộ nhớ ngoài có dung lượng có thể coi là không giới hạn, nhưng sẽ vô dụng đối với LLM cho đến khi ứng dụng của bạn truy xuất nó và đưa vào Context Window. Bài toán Memory xoay quanh loại logic ứng dụng bạn xây dựng để quyết định những gì được đưa vào Context Window trước mỗi lần gọi API.

┌─ External Memory ────────────────────────────────────────┐
│  Database, vector store, key-value store                 │
│  [tuỳ chọn người dùng] [tóm tắt] [kiến thức chuyên môn]  │
└───────────────────────────┬──────────────────────────────┘
┌─ Context Window ─────────────────────────────────────────┐
│  System prompt                                           │
│  Context được truy xuất  ◄── từ external memory          │
│  Lịch sử trò chuyện                                      │
│  Message hiện tại                                        │
└───────────────────────────┬──────────────────────────────┘
                    Response của LLM

Các mẫu Memory

Có nhiều cách để quản lý Memory — phương pháp phù hợp phụ thuộc vào sản phẩm của bạn. Dưới đây là các mẫu thiết kế mà chúng tôi đã nghiên cứu và áp dụng:

Nén gọn

Vấn đề: Cuộc trò chuyện vượt quá giới hạn của Context Window.

Giải pháp: Giảm kích thước lịch sử cuộc trò chuyện. Hai phương pháp phổ biến:

Phương phápCách hoạt độngTính chất
TruncationXóa các message cũ nhất khi lịch sử vượt quá giới hạnĐơn giản nhưng mất mát context cũ
Rolling SummaryTóm tắt các message cũ, giữ nguyên văn các message gần đâyGiữ nguyên context cũ với độ chính xác giảm. Cần thêm bước gọi LLM.
Trước khi nén gọn:
  [msg 1] [msg 2] [msg 3] ... [msg 47] [msg 48] [msg 49] [msg 50]
  ◄──────── vượt quá Context Window ──────────►

Sau khi Truncation:
  [msg 36] [msg 37] ... [msg 49] [msg 50]
  ◄ context cũ bị mất ►

Sau khi Rolling Summary:
  [summary of msg 1-35] [msg 36] [msg 37] ... [msg 49] [msg 50]
  ◄ context cũ vẫn còn với độ chính xác ít hơn ►

Rolling Summary là một quá trình tái diễn — lần sau khi message lại tràn, bản tóm tắt cũ sẽ được tổng hợp lại cùng với các message mới cần bị loại bỏ. Context cũ dần mất chi tiết nhưng không bao giờ bị mất hoàn toàn.

Lưu trữ và truy xuất

Vấn đề: Thông tin từ các cuộc trò chuyện trước đây (hoặc kiến thức bên ngoài) là cần thiết nhưng không có trong Context Window hiện tại.

Giải pháp: Lưu trữ thông tin bên ngoài, truy xuất và chèn các phần thông tin liên quan trước mỗi lần gọi LLM.

Đây là mô hình RAG được áp dụng cho Memory của agent. Ứng dụng của bạn lưu trữ thông tin quan trọng — tuỳ chọn của người dùng, tóm tắt cuộc trò chuyện, kiến thức chuyên môn — vào bộ nhớ ngoài. Trước mỗi lần gọi LLM, ứng dụng của bạn truy xuất các ký ức liên quan và thêm chúng vào Context Window.

Mô hình này có hai phần — lưu trữtruy xuất — và mỗi phần có thể được xử lý bởi logic trong code của bạn hoặc bởi agent thông qua các gọi tool.

Lưu trữ

Cách thông tin được lưu trữ vào External Memory.

Bằng code của bạn: Bạn viết logic trích xuất thông tin chạy vào các thời điểm cụ thể — sau khi mỗi cuộc trò chuyện kết thúc, với mỗi message hoặc theo các mốc cụ thể. Bạn kiểm soát chính xác những gì được lưu trữ. Phương pháp này hoạt động tốt khi bạn biết những gì đáng lưu trữ (ví dụ: thông tin của người dùng).

Bằng tool của agent: Bạn cung cấp cho agent tool như memory_save. Agent tự quyết định những gì cần ghi nhớ. Phương pháp này phù hợp cho các cuộc trò chuyện mở, nơi bạn không thể dự đoán trước những gì quan trọng.

Truy xuất

Cách thông tin được truy xuất quay trở lại Context Window.

Bằng code của bạn: Ứng dụng của bạn truy xuất các thông tin và thêm chúng vào Context Window trước mỗi lần gọi API — như một phần của system prompt, như các message thêm hoặc bất kỳ cách nào bạn thiết kế. LLM luôn nhìn thấy chúng, phù hợp cho các thông tin luôn cần nắm (ví dụ: tên người dùng, sở thích).

Bằng tool của agent: Bạn cung cấp cho agent tool như memory_search. Agent quyết định khi nào tìm kiếm và sử dụng kết quả đó. Phù hợp cho các thông tin mang tính nhất thời — chỉ tiêu tốn token khi cần truy xuất.

Kết hợp lại

Việc lưu trữ và truy xuất là hai quá trình độc lập - bạn có thể kết hợp chúng một cách tự do. Dưới đây là một số cách kết hợp trong thực tế:

Sự kết hợpCách hoạt động
Code lưu + code truy xuấtLogic của bạn lưu trữ dữ liệu, code của bạn tải chúng vào context trước mỗi lần gọi API
Agent lưu + code truy xuấtAgent lưu thông tin qua gọi tool, code của bạn tải chúng vào context trước mỗi trong các lần tới
Agent lưu + agent truy xuấtAgent tự quản lý Memory qua gọi tool — lưu trữ những gì nó học được, tìm kiếm lại khi cần

Triển khai nén gọn

Nén gọn dữ liệu là phương pháp mà nên áp dụng đầu tiên. Các phương pháp khác là tùy thuộc vào nhu cầu sản phẩm của bạn. Dưới đây là hướng dẫn cho hai phương pháp: Truncation và Rolling Summary.

Truncation

Đếm số token. Khi cuộc trò chuyện vượt quá giới hạn, xóa các message cũ nhất.

def truncate_messages(messages, max_tokens):
    """Giữ lại các message gần nhất trong ngưỡng cho phép."""
    total = 0
    kept = []
    # Duyệt ngược từ message mới nhất
    for msg in reversed(messages):
        msg_tokens = count_tokens(msg["content"])
        if total + msg_tokens > max_tokens:
            break
        kept.append(msg)
        total += msg_tokens
    return list(reversed(kept))

Nếu người dùng đã đề cập đến tên của họ cách đây rất nhiều message, thông tin đó sẽ biến mất.

Rolling Summary

Thay vì xóa hoàn toàn các message cũ, hãy biến chúng thành một bản tóm tắt, đặt làm message đầu tiên trong Context Window. Agent vẫn biết những gì đã xảy ra trước đó, chỉ là không còn chính xác từng từ.

async def compact_messages(messages, summary, token_threshold):
    """Tóm tắt message cũ khi hội thoại vượt ngưỡng."""
    total = count_tokens(messages)
    if total <= token_threshold:
        return messages, summary  # Chưa cần compact

    # Giữ nguyên 15 message gần nhất
    keep_recent = 15
    recent = messages[-keep_recent:]
    to_summarize = messages[:-keep_recent]

    # Yêu cầu LLM cập nhật bản tóm tắt trước đó
    prompt = (
        f"Previous summary:\n{summary}\n\n"
        f"New messages:\n{format_messages(to_summarize)}\n\n"
        "Write an updated summary. Capture: user preferences, "
        "decisions, key facts, and current task state. "
        "Be concise but do not lose important details."
    )

    new_summary = await llm_call(
        system="You are a conversation summarizer.",
        messages=[{"role": "user", "content": prompt}],
    )

    # Đặt bản tóm tắt làm message đầu tiên
    summary_message = {
        "role": "system",
        "content": f"Summary of earlier conversation:\n"
                   f"{new_summary}",
    }
    return [summary_message] + recent, new_summary

Prompt để yêu cầu LLM tóm tắt nên:

  • Bảo toàn: thông tin người dùng và tuỳ chọn, trạng thái hiện tại của tác vụ, các quyết định quan trọng đã đưa ra và các sự kiện quan trọng...
  • Loại bỏ: lời chào, thông tin lặp lại, quá trình suy luận trung gian và chi tiết các thông tin gọi tool (chỉ giữ lại kết quả)...

Tích hợp Rolling Summary

Quá trình kiểm tra và xử lý nén được thực hiện trước mỗi lần gọi LLM. Câu hỏi là điều kiện nào kích hoạt quá trình nén thật sự:

Chiến lượcNén khiƯu điểmNhược điểm
Đếm số tokenKhi vượt quá N% của Context Window (ví dụ: 70%)Phát hiện đượcPhải đếm được số token
Đếm số messageSố lượng messages vượt quá N (ví dụ: 30)Đơn giản nhấtLượng token của mỗi message là khác nhau
Luôn luôn nénChạy mỗi lần gọi API, không cần điều kiệnKhông bao giờ trànNén quá nhiều

Khuyến nghị sử dụng ngưỡng token 70% để đảm bảo có đủ không gian cho response của LLM, bối cảnh được truy xuất và định nghĩa tool.

# Ví dụ: Context Window 200K
CONTEXT_WINDOW = 200_000
RESERVED_FOR_RESPONSE = 16_000
RESERVED_FOR_RETRIEVED_CONTEXT = 10_000
RESERVED_FOR_TOOLS = 30_000

# 70% phần còn lại sau khi trừ các khoản dự trữ
MESSAGE_THRESHOLD = int(0.7 * (
    CONTEXT_WINDOW -
    RESERVED_FOR_RESPONSE -
    RESERVED_FOR_RETRIEVED_CONTEXT -
    RESERVED_FOR_TOOLS
))

async def handle_message(user_message, conversation):
    # 1. Thêm message của user vào lịch sử
    conversation.messages.append({"role": "user", "content": user_message})

    # 2. Compact nếu cần
    conversation.messages, conversation.summary = (
        await compact_messages(
            conversation.messages,
            conversation.summary,
            token_threshold=MESSAGE_THRESHOLD,
        )
    )

    # 3. Gọi LLM
    response = await client.messages.create(
        model="claude-sonnet-4-20250514",
        system=conversation.system_prompt,
        messages=conversation.messages,
    )

    # 4. Thêm response vào lịch sử
    conversation.messages.append({
        "role": "assistant",
        "content": response.content,
    })

    return response.content

Triển khai lưu trữ và truy xuất

Dưới đây là hướng dẫn cho hai phương pháp lưu trữ External Memory: Memory Block (luôn được giữ trong context) và Memory Store (được truy xuất theo yêu cầu). Bạn có thể sử dụng một trong hai hoặc cả hai.

Memory Block

Các Memory Block là các phần nhỏ, có nhãn, tồn tại trong system prompt. Chúng luôn được thấy bởi LLM — không cần tìm kiếm. Agent có thể chỉnh sửa chúng thông qua tool như memory_update.

Mỗi khối nên ngắn gọn (500-2000 ký tự) vì mỗi lần gọi API đều tiêu tốn token cho chúng.

Định nghĩa tên khối dưới dạng enum để agent có thể ghi vào các khối đã được định nghĩa trước — điều này giúp duy trì cấu trúc bộ nhớ và ngăn chặn các khối trùng lặp hoặc không nhất quán:

from enum import Enum

class MemoryBlock(str, Enum):
    HUMAN = "human"      # tên, vai trò, sở thích của user
    PERSONA = "persona"  # danh tính, hành vi của agent
    TASK = "task"        # task hiện tại, tiến độ, quyết định

# Load blocks từ database một lần khi bắt đầu hội thoại
memory_blocks = load_memory_blocks(user_id)
# e.g. {
#   "human": "Name: Alex\nRole: SEO specialist",
#   "persona": "Helpful SEO assistant",
#   "task": "Working on keyword clustering",
# }

BASE_SYSTEM_PROMPT = "You are a helpful assistant."

def build_system_prompt():
    """Build lại trước mỗi API call."""
    blocks = "\n".join(
        f"<memory_block name=\"{name}\">\n"
        f"{content}\n"
        f"</memory_block>"
        for name, content in memory_blocks.items()
    )
    return f"{BASE_SYSTEM_PROMPT}\n\n{blocks}"

Tool memory_update sử dụng enum cho block_name:

def handle_memory_update(
    block_name: MemoryBlock,
    new_content: str,
) -> str:
    memory_blocks[block_name.value] = new_content
    save_block_to_db(
        user_id, block_name.value, new_content
    )
    return "Memory updated."

Ví dụ: người dùng nói "Actually my name is Bob, not Alex." Agent sẽ gọi:

memory_update(
    block_name="human",
    new_content="Name: Bob\nRole: SEO specialist",
)

Trong lần gọi API tiếp theo, build_system_prompt() sẽ chứa block với thông tin mới chính xác.

Memory Store

Memory Store là không gian lưu trữ gần như không giới hạn mà agent có thể truy vấn theo yêu cầu. Thông tin chỉ được đưa vào context khi agent gọi tool như memory_search — do đó, nó không tốn chi phí khi không được sử dụng.

# Tool: lưu vào memory store
def handle_memory_save(content: str) -> str:
    embedding = get_embedding(content)
    db.insert({
        "user_id": user_id,
        "content": content,
        "embedding": embedding,
        "created_at": now()
    })
    return "Saved to memory."

# Tool: tìm kiếm trong memory store
def handle_memory_search(query: str) -> str:
    query_embedding = get_embedding(query)
    results = db.vector_search(
        user_id=user_id,
        embedding=query_embedding,
        limit=5
    )
    if not results:
        return "No relevant memories found."
    return "\n---\n".join(r["content"] for r in results)

Agent quyết định khi nào lưu và khi nào tìm kiếm. Các định nghĩa tool nên hướng dẫn rõ điều này — mô tả khi nào sử dụng chúng.

Tích hợp lưu trữ và truy xuất

Kết hợp với phương pháp nén gọn để có hiệu quả tốt hơn:

async def handle_message(user_message, conversation):
    conversation.messages.append({
        "role": "user",
        "content": user_message,
    })

    # Compact hội thoại nếu cần
    conversation.messages, conversation.summary = (
        await compact_messages(
            conversation.messages,
            conversation.summary,
            token_threshold=MESSAGE_THRESHOLD,
        )
    )

    # Agent loop — gọi LLM cho đến khi trả text response
    while True:
        response = await client.messages.create(
            model="claude-sonnet-4-20250514",
            system=build_system_prompt(),
            messages=conversation.messages,
            tools=memory_tools + other_tools,
        )

        if response.stop_reason == "tool_use":
            tool_results = execute_tools(response.tool_calls)

            conversation.messages.append({
                "role": "assistant",
                "content": response.content,
            })
            conversation.messages.append({
                "role": "user",
                "content": tool_results,
            })
        else:
            conversation.messages.append({
                "role": "assistant",
                "content": response.content,
            })
            break

    return response.content