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 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ình | Context window |
|---|---|
| Claude Sonnet 4 | 200K tokens (1M in beta) |
| GPT-4.1 | 1M tokens |
| Gemini 2.5 Pro | 1M 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 đề:
- 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ì.
- 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.
- 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ại | Nó là gì |
|---|---|
| In-context Memory | Tất cả nội dung bên trong Context Window sẽ được gửi cho API của LLM |
| External Memory | Cá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ần | Kích thước phổ biến | Chú thích |
|---|---|---|
| System prompt | 500 - 2K token | Thông tin agent, hướng dẫn, quy tắc |
| Các định nghĩa tool | 2K - 100K+ token | Tỷ lệ thuận với số lượng tool. |
| Context được truy xuất | 1K - 10K token | Kết quả RAG, user memories, dữ liệu ngoài được đưa vào request |
| Lịch sử trò chuyện | Tích luỹ dần | Thành phần dễ dẫn đến tràn Context Window |
| Message hiện tại | 100 - 4K token | Phụ thuộc vào độ dài yêu cầu của người dùng |
| Dự trù cho response | 4K - 16K token | Bắ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 tool và lị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áp | Cách hoạt động | Tính chất |
|---|---|---|
| Truncation | Xó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 Summary | Tóm tắt các message cũ, giữ nguyên văn các message gần đây | Giữ 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ữ và 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ợp | Cách hoạt động |
|---|---|
| Code lưu + code truy xuất | Logic 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ất | Agent 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ất | Agent 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ược | Nén khi | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Đếm số token | Khi vượt quá N% của Context Window (ví dụ: 70%) | Phát hiện được | Phải đếm được số token |
| Đếm số message | Số lượng messages vượt quá N (ví dụ: 30) | Đơn giản nhất | Lượng token của mỗi message là khác nhau |
| Luôn luôn nén | Chạy mỗi lần gọi API, không cần điều kiện | Không bao giờ tràn | Né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