Bỏ qua

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