Bài 3: Agent Memory & Context Engineering¶
Tổng quan¶
Context window có giới hạn, nhưng agent cần hoạt động qua hàng trăm lượt hội thoại và hàng nghìn tool call. Bài này phân loại các loại bộ nhớ agent cần, chiến lược quản lý context khi nó vượt giới hạn, cách nén thông tin mà không mất ý nghĩa quan trọng, và context rot - hiện tượng chất lượng output suy giảm ngay cả khi context còn xa mới đầy.
1. Memory Types¶
Con người có nhiều loại trí nhớ khác nhau cho các mục đích khác nhau - agent cũng vậy.
graph TD
M[Agent Memory] --> ST[Short-term<br/>Trong 1 session/task]
M --> LT[Long-term<br/>Xuyên suốt nhiều session]
LT --> EP[Episodic<br/>Sự kiện cụ thể đã xảy ra]
LT --> SEM[Semantic<br/>Kiến thức/sự thật chung]
LT --> PROC[Procedural<br/>Cách thực hiện task]
| Loại | Định nghĩa | Ví dụ | Nơi lưu trữ điển hình |
|---|---|---|---|
| Short-term | Ngữ cảnh của session/task hiện tại | Lịch sử hội thoại trong 1 conversation | Context window / state của graph |
| Long-term | Thông tin tồn tại qua nhiều session | "User tên Minh, thích nhận báo cáo dạng bullet point" | Database / vector store |
| Episodic | Ghi nhớ về các sự kiện/trải nghiệm cụ thể đã xảy ra | "Lần trước agent gọi tool X và bị lỗi timeout" | Log có timestamp, vector store |
| Semantic | Kiến thức, sự thật chung không gắn với 1 sự kiện | "Công ty TNHH tối đa 50 thành viên" | Knowledge base / RAG (Module I) |
| Procedural | Quy trình, cách thực hiện một loại task | "Khi user hỏi về hoá đơn, luôn check trạng thái thanh toán trước" | System prompt, few-shot examples, learned rules |
Episodic vs Semantic - phân biệt dễ nhầm
Episodic nhớ "chuyện gì đã xảy ra" (gắn với thời điểm cụ thể). Semantic nhớ "sự thật là gì" (không gắn thời điểm). "Hôm qua user phàn nàn về giao hàng chậm" là episodic. "User ở Hà Nội" là semantic.
Ánh xạ vào LangGraph¶
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages] # Short-term: hội thoại hiện tại
user_profile: dict # Long-term/Semantic: nạp từ DB khi bắt đầu session
task_history: list # Episodic: các task đã hoàn thành trong session
Long-term/Semantic/Procedural memory thường không sống trong graph state - chúng được đọc từ external store (database, vector store) vào đầu session, và ghi lại (nếu cần) sau khi session kết thúc.
2. Management Strategies¶
Khi hội thoại càng dài, messages trong state càng phình to → vượt context window, tăng chi phí, giảm độ chính xác (lost-in-the-middle). Cần chiến lược quản lý.
Sliding Window¶
Chỉ giữ N message gần nhất, bỏ phần cũ hơn.
def trim_messages(messages: list, max_messages: int = 20) -> list:
if len(messages) <= max_messages:
return messages
# Luôn giữ system message (nếu có) + N message gần nhất
system_msgs = [m for m in messages if m.get("role") == "system"]
recent = messages[-max_messages:]
return system_msgs + recent
Phù hợp khi: Hội thoại casual, thông tin cũ không còn liên quan sau vài lượt (vd: chatbot hỏi đáp đơn giản).
Nhược điểm: Mất hoàn toàn thông tin bị cắt - nếu user hỏi lại điều đã nói ở đầu hội thoại dài, agent "quên" thật sự.
Summarization¶
Định kỳ tóm tắt phần hội thoại cũ thành 1 đoạn ngắn, thay thế toàn bộ message chi tiết.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def format_messages(messages: list) -> str:
"""Chuyển list message (dict hoặc BaseMessage) thành text để đưa vào prompt."""
lines = []
for m in messages:
role = m.get("role") if isinstance(m, dict) else m.type
content = m.get("content") if isinstance(m, dict) else m.content
lines.append(f"{role}: {content}")
return "\n".join(lines)
def summarize_old_messages(messages: list, keep_recent: int = 6) -> list:
if len(messages) <= keep_recent + 1:
return messages
old_messages = messages[:-keep_recent]
recent_messages = messages[-keep_recent:]
summary_prompt = f"""Tóm tắt cuộc hội thoại sau thành 3-5 câu, giữ lại thông tin quan trọng
(tên, số liệu, quyết định đã chốt, task đang làm dở):
{format_messages(old_messages)}"""
summary = llm.invoke(summary_prompt).content
return [{"role": "system", "content": f"[Tóm tắt hội thoại trước]: {summary}"}] + recent_messages
Phù hợp khi: Hội thoại dài nhưng có thông tin quan trọng rải rác cần giữ lại (vd: agent hỗ trợ khách hàng qua nhiều lượt).
Kết hợp trigger theo độ dài, không chạy mỗi lượt
Summarization tốn 1 lời gọi LLM - chỉ nên chạy khi len(messages) vượt ngưỡng (vd: > 20 message), không chạy sau mỗi lượt hội thoại.
Vector Memory¶
Lưu toàn bộ lịch sử vào vector store, chỉ retrieve phần liên quan đến câu hỏi hiện tại - giống RAG nhưng áp dụng cho memory thay vì tài liệu.
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embedding = OpenAIEmbeddings(model="text-embedding-3-small")
memory_store = Chroma(collection_name="agent_memory", embedding_function=embedding)
def store_memory(text: str, metadata: dict):
memory_store.add_texts([text], metadatas=[metadata])
def retrieve_relevant_memory(query: str, k: int = 3) -> list[str]:
results = memory_store.similarity_search(query, k=k)
return [doc.page_content for doc in results]
# Sau mỗi lượt hội thoại quan trọng, lưu lại
store_memory(
"User Minh làm việc tại công ty ABC, quan tâm đến báo cáo tài chính quý.",
metadata={"user_id": "minh", "type": "semantic", "timestamp": "2026-07-15"},
)
# Khi cần, retrieve theo ngữ cảnh hiện tại thay vì load toàn bộ lịch sử
relevant = retrieve_relevant_memory("Minh cần loại báo cáo gì?")
Phù hợp khi: Long-term memory xuyên suốt nhiều session, lượng thông tin lớn, chỉ cần phần liên quan đến truy vấn hiện tại.
Hybrid¶
Thực tế production thường kết hợp cả ba: sliding window cho short-term, summarization khi window đầy, vector memory cho long-term.
graph TD
NEW[Message mới] --> STATE[Thêm vào state.messages]
STATE --> CHECK{len messages > threshold?}
CHECK -->|Không| CONT[Tiếp tục hội thoại]
CHECK -->|Có| SUM[Summarize phần cũ]
SUM --> TRIM[Giữ summary + N message gần nhất]
TRIM --> STORE[Lưu message quan trọng vào Vector Memory]
STORE --> CONT
| Chiến lược | Chi phí | Mất thông tin? | Phù hợp |
|---|---|---|---|
| Sliding window | Thấp (không cần LLM call) | Có, hoàn toàn | Session ngắn, casual |
| Summarization | Trung bình (1 LLM call/lần trigger) | Một phần (nén, giữ ý chính) | Session dài, cần giữ ngữ cảnh |
| Vector memory | Trung bình (embedding + storage) | Không, nhưng cần retrieve đúng | Long-term, nhiều session |
| Hybrid | Cao nhất | Thấp nhất | Production agent phức tạp |
3. Context Compression¶
Ngoài quản lý lịch sử hội thoại, cần nén cả kết quả tool call - đây thường là nguồn "phình" context lớn nhất (vd: search trả về 5000 từ nhưng chỉ 2 câu liên quan).
Nén tool output¶
def compress_tool_result(raw_result: str, query: str, max_tokens: int = 300) -> str:
"""Tóm tắt kết quả tool trước khi đưa vào context, giữ phần liên quan đến query."""
if len(raw_result) < max_tokens * 4: # Ước lượng thô: 1 token ≈ 4 ký tự
return raw_result
compress_prompt = f"""Trích xuất thông tin liên quan đến câu hỏi từ kết quả sau.
Giữ số liệu, tên riêng chính xác. Bỏ phần không liên quan.
Câu hỏi: {query}
Kết quả thô: {raw_result[:4000]}
Thông tin liên quan (ngắn gọn):"""
return llm.invoke(compress_prompt).content
Structured over raw¶
Khi có thể, yêu cầu tool trả về dữ liệu có cấu trúc (JSON các trường cần thiết) thay vì raw text - giảm token và tăng độ chính xác khi model đọc lại.
@tool
def search_product(query: str) -> dict:
"""Tìm sản phẩm - trả về CHỈ các trường cần thiết, không trả nguyên trang HTML."""
raw = call_search_api(query)
return {
"name": raw["title"],
"price": raw["price"],
"in_stock": raw["stock"] > 0,
# Bỏ: mô tả dài, review, ảnh, metadata không cần thiết
}
Đừng nén quá sớm
Nén mất thông tin không thể phục hồi. Chỉ nén khi thực sự cần (gần chạm giới hạn context) hoặc khi biết chắc phần bị cắt không quan trọng cho task hiện tại. Nén quá tay là nguyên nhân phổ biến khiến agent "quên" chi tiết quan trọng.
4. Context Rot¶
Đây là "kẻ giết agent thầm lặng". Compaction (mục 2-3) giải quyết bài toán context tràn (vượt giới hạn window). Nhưng có một vấn đề tinh vi hơn: chất lượng output suy giảm ngay cả khi window chưa đầy.
Context Rot vs Context Overflow¶
| Context Overflow | Context Rot | |
|---|---|---|
| Là gì | Context vượt giới hạn window | Chất lượng giảm dù còn trong giới hạn |
| Khi nào | Ở 100% dung lượng | Bắt đầu từ ~25-40% dung lượng |
| Biểu hiện | Lỗi / crash | Suy giảm chất lượng khó nhận ra |
| Nhìn thấy? | Ngay lập tức | Thường vô hình đến khi gây lỗi nghiêm trọng |
| Cách xử lý | Compaction (cắt/tóm tắt) | Context engineering (cấu trúc) |
Model có window 200K token vẫn có thể suy giảm ở 50K
Nghiên cứu của Chroma (2025) test 18 model frontier (GPT-4.1, Claude Opus 4, Gemini 2.5...) và phát hiện mọi model đều bị context rot ở mọi mức độ dài input được test. Window cho biết bao nhiêu token vừa, không cho biết model dùng hiệu quả được bao nhiêu.
Ba cơ chế gây rot¶
| Cơ chế | Mô tả |
|---|---|
| Lost-in-the-middle | Model chú ý tốt phần đầu và cuối context, kém phần giữa. Thông tin quan trọng nằm giữa hội thoại dài gần như vô hình. |
| Attention dilution | Attention là bậc hai: càng nhiều token, "spotlight" của model càng loãng. Ở 5K token là chùm sáng tập trung; ở 100K là ánh sáng tản ra 20x diện tích. |
| Distractor interference | Nội dung tương tự về mặt ngữ nghĩa nhưng không liên quan chủ động đánh lạc hướng reasoning. Nhiễu "giống signal" tệ hơn nhiễu ngẫu nhiên. |
Ba cơ chế này cộng hưởng nhân lên, nên context rot là siêu tuyến tính: gấp đôi context gây suy giảm nhiều hơn gấp đôi.
Instruction Fade-Out & Agent Drift¶
Khi hội thoại dài ra, model dần quên system prompt ban đầu - các log thực thi đẩy system prompt ra ngoài rìa vùng chú ý hiệu quả. Hệ quả là agent drift với 3 dạng:
- Goal drift: agent dần đi lệch mục tiêu gốc (từ "fix bug login" → refactor auth → redesign toàn bộ kiến trúc).
- Reasoning drift: chuỗi suy luận kém mạch lạc dần, xây trên kết luận sai ở bước trước.
- Context drift: các lần gọi tool lỗi, traceback dài, reasoning đã lỗi thời chồng chất, lấn át signal.
Nguyên tắc 40-60%¶
Đóng góp thực tiễn nhất từ HumanLayer: giữ mức sử dụng context trong khoảng 40-60% mọi lúc.
# Compact chủ động, không đợi đến khi gần đầy
if context_usage > 0.40:
consider_compaction() # Cân nhắc nén
if context_usage > 0.60:
definitely_compact() # Chắc chắn nén
if context_usage > 0.85:
emergency_compact() # Nén khẩn cấp
Điều này quyết liệt hơn chuẩn ngành (đa số agent nén ở 80-95%), nhưng giảm đáng kể context rot.
Các cách phòng thủ chính¶
| Phòng thủ | Cách làm |
|---|---|
| Compact sớm & chủ động | Áp dụng nguyên tắc 40-60%, đừng đợi window đầy |
| Re-inject instructions | Chèn lại chỉ dẫn quan trọng ở cuối context (vị trí attention cao), không chỉ dựa vào system prompt ở đầu - đây là cách hiệu quả nhất chống instruction fade-out |
| Sub-agent cho việc "nhiễu" | Giao search/exploration cho sub-agent có context sạch riêng; agent chính chỉ nhận kết quả tinh gọn (Anthropic: cải thiện 90.2% nhờ context isolation) |
| Strip nội dung phục hồi được | Nội dung file có thể đọc lại từ disk → thay bằng ghi chú "[đã đọc file X, đọc lại nếu cần]" thay vì giữ toàn bộ |
| Structured sections | Đặt thông tin vào các mục có header rõ ràng (## MỤC TIÊU, ## QUYẾT ĐỊNH, ## KHÔNG LÀM) để model dễ chú ý |
| Task anchoring | Chèn định kỳ mục tiêu gốc + phạm vi (in/out of scope) để chống goal drift |
Re-inject ở cuối context: cách hiệu quả nhất
Lost-in-the-middle nghĩa là chỉ dẫn ở đầu system prompt mất dần ảnh hưởng khi hội thoại dài. Đặt lại chỉ dẫn quan trọng ở cuối context (vị trí gần nhất = attention cao nhất) là cách rẻ và hiệu quả nhất để agent không "quên" quy tắc. Đây chính là lý do Claude Code re-inject CLAUDE.md sau mỗi lần compaction.
Context Engineering là bước tiến hoá của Prompt Engineering
Prompt engineering (Module I) tập trung vào nội dung system prompt. Context engineering tập trung vào toàn bộ token trong window (system + messages + tools + memory) và duy trì chất lượng theo thời gian, không chỉ ở lần gọi đầu. Với agent chạy dài, đây mới là kỹ năng quyết định.
5. Hands-on: Multi-turn Agent + Vector Memory¶
Dùng lại hàm format_messages đã định nghĩa ở phần Summarization.
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
# ====== LONG-TERM MEMORY STORE ======
embedding = OpenAIEmbeddings(model="text-embedding-3-small")
long_term_memory = Chroma(collection_name="user_memory", embedding_function=embedding)
def save_to_long_term(user_id: str, fact: str):
long_term_memory.add_texts([fact], metadatas=[{"user_id": user_id}])
def recall_long_term(user_id: str, query: str, k: int = 3) -> list[str]:
results = long_term_memory.similarity_search(query, k=k, filter={"user_id": user_id})
return [d.page_content for d in results]
# ====== STATE ======
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
user_id: str
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
# ====== NODES ======
def recall_node(state: AgentState) -> dict:
"""Truy xuất long-term memory liên quan trước khi trả lời."""
last_user_msg = state["messages"][-1].content
memories = recall_long_term(state["user_id"], last_user_msg)
if memories:
context = "Thông tin đã biết về user:\n" + "\n".join(f"- {m}" for m in memories)
return {"messages": [{"role": "system", "content": context}]}
return {}
def agent_node(state: AgentState) -> dict:
# Sliding window: chỉ giữ 15 message gần nhất khi gọi LLM
trimmed = state["messages"][-15:]
response = llm.invoke(trimmed)
return {"messages": [response]}
def extract_and_store_node(state: AgentState) -> dict:
"""Sau mỗi lượt, kiểm tra xem có thông tin đáng nhớ dài hạn không."""
last_exchange = state["messages"][-2:]
extract_prompt = f"""Trích xuất 1 sự thật đáng nhớ dài hạn về user từ đoạn hội thoại sau
(sở thích, thông tin cá nhân, quyết định quan trọng). Nếu không có, trả về "NONE".
{format_messages(last_exchange)}"""
fact = llm.invoke(extract_prompt).content
if fact.strip() != "NONE":
save_to_long_term(state["user_id"], fact)
return {}
# ====== BUILD GRAPH ======
graph = StateGraph(AgentState)
graph.add_node("recall", recall_node)
graph.add_node("agent", agent_node)
graph.add_node("store", extract_and_store_node)
graph.add_edge(START, "recall")
graph.add_edge("recall", "agent")
graph.add_edge("agent", "store")
graph.add_edge("store", END)
checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)
# ====== CHẠY QUA NHIỀU SESSION ======
config = {"configurable": {"thread_id": "session-1"}}
# Session hôm nay
app.invoke(
{"messages": [{"role": "user", "content": "Tôi bị dị ứng hải sản, nhớ giúp tôi khi gợi ý món ăn."}],
"user_id": "minh"},
config=config,
)
# Session khác (thread_id mới → short-term memory không còn, nhưng long-term vẫn nhớ)
config_new_session = {"configurable": {"thread_id": "session-2"}}
result = app.invoke(
{"messages": [{"role": "user", "content": "Gợi ý cho tôi món ăn tối nay."}], "user_id": "minh"},
config=config_new_session,
)
print(result["messages"][-1].content) # Sẽ tránh gợi ý hải sản nhờ long-term memory
Tóm tắt¶
graph TD
Q{Thông tin cần nhớ?} --> A[Chỉ trong task/session hiện tại]
Q --> B[Xuyên suốt nhiều session]
A --> ST[Short-term: state.messages]
B --> C{Loại thông tin?}
C --> EP[Sự kiện cụ thể] --> EPS[Episodic memory]
C --> SEM[Sự thật chung] --> SEMS[Semantic memory - Vector store]
C --> PROC[Quy trình xử lý] --> PROCS[Procedural - System prompt]
ST --> SIZE{Context quá dài?}
SIZE -->|Có, casual| SW[Sliding window]
SIZE -->|Có, cần giữ ý chính| SUM[Summarization]
SIZE -->|Đã tràn?| ROT{Chưa tràn nhưng<br/>output kém đi?}
ROT -->|Có| RULE[Context Rot -<br/>áp dụng nguyên tắc 40-60%]
| Khái niệm | Ghi nhớ |
|---|---|
| Short-term | Context của session hiện tại, sống trong graph state |
| Long-term | Xuyên session, cần external store (DB/vector store) |
| Episodic | "Chuyện gì đã xảy ra" - gắn thời điểm |
| Semantic | "Sự thật là gì" - không gắn thời điểm |
| Procedural | "Cách làm task X" - system prompt / learned rules |
| Sliding window | Rẻ, mất thông tin hoàn toàn |
| Summarization | Vừa phải, nén nhưng giữ ý chính |
| Vector memory | Tốt cho long-term, cần retrieve đúng |
| Context compression | Áp dụng cho tool output, không chỉ hội thoại |
| Context Rot | Chất lượng giảm dù chưa tràn window - bắt đầu từ ~25-40% |
| Nguyên tắc 40-60% | Compact chủ động, đừng đợi đến khi gần đầy |
| Re-injection | Chèn lại chỉ dẫn quan trọng ở cuối context, chống fade-out |