Pendahuluan: Kenapa Context Engineering Jadi Kunci di 2026?
Kalau kamu sudah berkecimpung di dunia AI dan Large Language Models (LLM) sejak 2023-2024, pasti familiar dengan istilah prompt engineering — seni merangkai kata agar model AI memberikan respons yang kita mau. Tapi di 2026, lanskap ini sudah berubah drastis. Prompt engineering saja tidak lagi cukup untuk membangun aplikasi LLM yang benar-benar production-ready.
Masuk ke era context engineering — sebuah paradigma baru yang tidak hanya fokus pada apa yang kamu tulis ke model, tapi seluruh lingkungan informasi yang kamu sediakan saat model memproses permintaan. Bayangkan begini: prompt engineering itu seperti menulis satu email yang sempurna, sedangkan context engineering itu merancang seluruh ruang kerja — lengkap dengan dokumen referensi, tools yang tersedia, catatan meeting sebelumnya, dan konteks organisasi.
Istilah ini dipopulerkan oleh Andrej Karpathy (co-founder OpenAI) yang menyatakan bahwa membangun aplikasi LLM modern pada dasarnya adalah "mengisi context window dengan informasi yang tepat pada waktu yang tepat." Tobi Lutke, CEO Shopify, bahkan lebih tegas: context engineering adalah skill paling penting yang harus dikuasai developer di era AI.
Nah, di artikel ini kamu akan belajar apa itu context engineering secara mendalam, bagaimana perbedaannya dengan prompt engineering, 9 komponen utamanya, dan — yang paling penting — 7 teknik praktis yang bisa langsung kamu terapkan di aplikasi LLM produksi. Semua disertai contoh kode Python yang bisa kamu jalankan (dan percayalah, ini game-changer). Mari kita mulai.
Perbedaan Context Engineering vs Prompt Engineering
Sebelum masuk lebih dalam, penting untuk memahami perbedaan mendasar antara keduanya. Banyak developer yang masih mencampuradukkan kedua konsep ini, padahal scope-nya sangat berbeda.
Prompt Engineering: Fokus pada Instruksi
Prompt engineering adalah proses merancang dan mengoptimalkan instruksi teks yang dikirim ke LLM. Teknik-tekniknya termasuk few-shot prompting, chain-of-thought, role-playing, dan lain-lain.
Intinya, kamu mengontrol bagaimana kamu bertanya ke model. Cukup straightforward.
Context Engineering: Mendesain Seluruh Lingkungan Informasi
Context engineering melangkah jauh lebih luas. Kamu tidak hanya merancang prompt, tapi mendesain seluruh pipeline informasi yang masuk ke context window model. Ini mencakup:
- System prompt yang dinamis dan berubah berdasarkan state aplikasi
- Memory management — apa yang diingat dan dilupakan oleh sistem
- Retrieval strategy — dokumen apa yang di-fetch dan kapan
- Tool definitions — tools apa yang tersedia dan bagaimana deskripsinya
- State management — data global yang mempengaruhi behavior model
- Output structuring — format respons yang diharapkan
Simpelnya, prompt engineering adalah subset dari context engineering. Kalau prompt engineering itu menulis naskah aktor, context engineering itu mendesain seluruh panggung, lighting, props, dan skenario sekaligus.
Analogi yang paling sering dipakai: bayangkan kamu mau briefing seorang konsultan baru di perusahaan. Prompt engineering itu memberikan instruksi tertulis yang bagus. Context engineering itu menyiapkan seluruh onboarding experience — dokumen perusahaan, akses ke tools yang relevan, catatan meeting sebelumnya, kontak tim terkait, dan prosedur kerja yang jelas. Beda tipis tapi impactnya huge.
9 Komponen Context Engineering
Setiap kali kamu mengirim request ke LLM dalam aplikasi produksi, ada banyak komponen yang membentuk "konteks" yang diterima model. Berikut 9 komponen utamanya:
- System Prompt — Instruksi dasar yang mendefinisikan persona, aturan, dan batasan model. Di context engineering, ini bersifat dinamis, bukan statis.
- User Input — Pesan atau query dari pengguna. Bisa di-augment dengan metadata seperti timestamp, lokasi, atau preferensi user.
- Short-term Memory (Conversation History) — Riwayat percakapan dalam sesi saat ini. Perlu di-manage karena context window terbatas.
- Long-term Memory — Informasi yang persist antar sesi — preferensi user, fakta penting, keputusan sebelumnya. Biasanya disimpan di database eksternal.
- Knowledge Base Retrieval (RAG) — Dokumen atau data yang di-retrieve dari vector database atau knowledge base berdasarkan query user. Ini komponen RAG (Retrieval-Augmented Generation).
- Tool Definitions — Daftar tools/functions yang tersedia untuk dipanggil model. Termasuk nama, deskripsi, dan parameter schema.
- Tool Responses — Hasil dari pemanggilan tools yang sudah dieksekusi. Bisa berupa data API, hasil query database, atau output komputasi.
- Structured Output Schema — Format output yang diharapkan, biasanya dalam bentuk JSON schema. Membantu model menghasilkan respons yang konsisten dan parseable.
- Global State — Metadata aplikasi seperti waktu saat ini, user role, feature flags, konfigurasi tenant, atau status workflow yang mempengaruhi behavior model.
Tugas context engineer adalah merancang bagaimana ke-9 komponen ini dikomposisi, diprioritaskan, dan di-manage dalam batasan context window yang tersedia. Inilah seni dan science dari context engineering.
Teknik 1: Dynamic System Prompts
System prompt tradisional itu statis — ditulis sekali, dipakai terus. Tapi di aplikasi produksi, kebutuhan berubah-ubah. User yang baru onboarding butuh instruksi berbeda dengan power user.
Percakapan yang sudah panjang butuh handling berbeda dengan percakapan baru (serius, ini sering banget terjadi).
Dynamic system prompts memungkinkan kamu menyesuaikan instruksi model secara real-time berdasarkan state aplikasi, preferensi user, dan konteks percakapan.
Kapan Menggunakan Dynamic System Prompts?
- User dengan role berbeda (admin vs regular user) butuh behavior model yang berbeda
- Percakapan panjang perlu instruksi untuk lebih ringkas
- Fitur tertentu hanya aktif untuk user tertentu (feature flags)
- Bahasa atau tone perlu disesuaikan berdasarkan preferensi user
Implementasi dengan Middleware Pattern
from datetime import datetime
from typing import Any
class DynamicSystemPromptBuilder:
"""Builder untuk membuat system prompt yang dinamis
berdasarkan state aplikasi dan user context."""
def __init__(self, base_prompt: str):
self.base_prompt = base_prompt
self.middleware_stack: list = []
def add_middleware(self, middleware_fn):
"""Tambahkan middleware function yang memodifikasi prompt."""
self.middleware_stack.append(middleware_fn)
return self
def build(self, context: dict[str, Any]) -> str:
"""Build final system prompt dengan menjalankan semua middleware."""
prompt = self.base_prompt
for middleware in self.middleware_stack:
prompt = middleware(prompt, context)
return prompt
# --- Middleware Functions ---
def add_user_role_context(prompt: str, context: dict) -> str:
"""Sesuaikan instruksi berdasarkan role user."""
role = context.get("user_role", "user")
role_instructions = {
"admin": "\n\nUser ini adalah admin. Berikan akses penuh ke semua fitur dan data.",
"developer": "\n\nUser ini adalah developer. Boleh tampilkan detail teknis dan kode.",
"user": "\n\nUser ini pengguna biasa. Gunakan bahasa sederhana tanpa jargon teknis.",
}
return prompt + role_instructions.get(role, role_instructions["user"])
def add_conversation_length_handler(prompt: str, context: dict) -> str:
"""Tambahkan instruksi berdasarkan panjang percakapan."""
msg_count = context.get("message_count", 0)
if msg_count > 20:
prompt += (
"\n\nPercakapan sudah panjang. Berikan respons yang lebih ringkas "
"dan to-the-point. Hindari pengulangan informasi sebelumnya."
)
elif msg_count > 10:
prompt += (
"\n\nPercakapan sudah cukup panjang. Referensikan konteks "
"sebelumnya tanpa mengulangi secara detail."
)
return prompt
def add_time_awareness(prompt: str, context: dict) -> str:
"""Tambahkan kesadaran waktu ke prompt."""
now = datetime.now()
prompt += f"\n\nWaktu saat ini: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}."
if now.hour < 6 or now.hour > 22:
prompt += " User mungkin sedang bekerja di luar jam normal."
return prompt
def add_feature_flags(prompt: str, context: dict) -> str:
"""Aktifkan/nonaktifkan fitur berdasarkan feature flags."""
flags = context.get("feature_flags", {})
if flags.get("enable_code_execution"):
prompt += "\n\nKamu boleh menulis dan menjalankan kode jika diminta."
if flags.get("enable_web_search"):
prompt += "\n\nKamu bisa melakukan web search untuk informasi terkini."
if not flags.get("enable_data_export", True):
prompt += "\n\nJANGAN pernah membantu user mengekspor data dalam bentuk apapun."
return prompt
# --- Penggunaan ---
builder = DynamicSystemPromptBuilder(
base_prompt="Kamu adalah asisten AI yang membantu pengguna platform e-commerce."
)
builder.add_middleware(add_user_role_context)
builder.add_middleware(add_conversation_length_handler)
builder.add_middleware(add_time_awareness)
builder.add_middleware(add_feature_flags)
# Build prompt berdasarkan konteks saat ini
system_prompt = builder.build({
"user_role": "developer",
"message_count": 25,
"feature_flags": {
"enable_code_execution": True,
"enable_web_search": False,
"enable_data_export": True,
},
})
print(system_prompt)
Pattern middleware ini sangat powerful karena setiap komponen bisa ditambah, dihapus, atau di-reorder tanpa mengubah yang lain. Kamu juga bisa menulis unit test untuk setiap middleware secara independen.
Teknik 2: Manajemen Context Window
Context window LLM itu terbatas — bahkan model dengan 128K atau 200K token pun punya batasan. Dan semakin banyak token yang kamu masukkan, semakin lambat (dan mahal) inference-nya.
Selain itu, ada fenomena yang disebut "lost in the middle" — model cenderung kurang memperhatikan informasi di tengah-tengah context window yang sangat panjang. Dari pengalaman saya, ini salah satu masalah paling subtle tapi impactful di production.
Manajemen context window yang baik memastikan kamu selalu punya ruang untuk informasi yang paling relevan, sambil membuang atau meringkas yang kurang penting.
Sliding Window Manager
import tiktoken
from dataclasses import dataclass, field
@dataclass
class Message:
role: str # "system", "user", "assistant", "tool"
content: str
token_count: int = 0
priority: int = 0 # Higher = more important
is_pinned: bool = False # Pinned messages never get removed
timestamp: float = 0.0
class SlidingWindowManager:
"""Mengelola context window dengan sliding window strategy.
Fitur utama:
- Token counting otomatis pakai tiktoken
- Pinned messages (system prompt, instruksi penting)
- Priority-based eviction (pesan kurang penting dihapus duluan)
- Reserved space untuk respons model
"""
def __init__(
self,
max_tokens: int = 16000,
reserved_for_response: int = 4000,
model: str = "gpt-4o",
):
self.max_tokens = max_tokens
self.reserved_for_response = reserved_for_response
self.available_tokens = max_tokens - reserved_for_response
self.encoder = tiktoken.encoding_for_model(model)
self.messages: list[Message] = []
def count_tokens(self, text: str) -> int:
"""Hitung jumlah token dalam sebuah teks."""
return len(self.encoder.encode(text))
def add_message(
self,
role: str,
content: str,
priority: int = 0,
is_pinned: bool = False,
) -> Message:
"""Tambahkan pesan baru ke context window."""
import time
msg = Message(
role=role,
content=content,
token_count=self.count_tokens(content),
priority=priority,
is_pinned=is_pinned,
timestamp=time.time(),
)
self.messages.append(msg)
self._enforce_window()
return msg
def _enforce_window(self):
"""Pastikan total token tidak melebihi batas yang tersedia."""
while self._total_tokens() > self.available_tokens:
# Cari pesan non-pinned dengan priority terendah
removable = [m for m in self.messages if not m.is_pinned]
if not removable:
break # Semua pesan di-pin, tidak bisa hapus lagi
# Sort by priority (ascending), lalu timestamp (oldest first)
removable.sort(key=lambda m: (m.priority, m.timestamp))
oldest_lowest = removable[0]
self.messages.remove(oldest_lowest)
def _total_tokens(self) -> int:
"""Hitung total token dari semua pesan."""
return sum(m.token_count for m in self.messages)
def get_messages(self) -> list[dict]:
"""Dapatkan messages dalam format yang siap dikirim ke API."""
return [
{"role": m.role, "content": m.content}
for m in self.messages
]
def get_stats(self) -> dict:
"""Dapatkan statistik penggunaan context window."""
total = self._total_tokens()
return {
"total_tokens": total,
"available_tokens": self.available_tokens,
"utilization_pct": round(total / self.available_tokens * 100, 1),
"message_count": len(self.messages),
"pinned_count": sum(1 for m in self.messages if m.is_pinned),
}
# --- Penggunaan ---
manager = SlidingWindowManager(max_tokens=8000, reserved_for_response=2000)
# System prompt selalu di-pin
manager.add_message(
role="system",
content="Kamu adalah asisten coding Python yang helpful.",
priority=100,
is_pinned=True,
)
# Simulasi percakapan panjang
for i in range(50):
manager.add_message(
role="user",
content=f"Pertanyaan ke-{i}: Bagaimana cara implementasi fitur {i}?",
priority=1,
)
manager.add_message(
role="assistant",
content=f"Untuk fitur {i}, kamu bisa menggunakan pendekatan berikut...",
priority=1,
)
stats = manager.get_stats()
print(f"Token terpakai: {stats['total_tokens']}/{stats['available_tokens']}")
print(f"Jumlah pesan: {stats['message_count']}")
print(f"Utilisasi: {stats['utilization_pct']}%")
Kunci dari sliding window management yang baik adalah menentukan apa yang boleh dihapus dan apa yang harus dipertahankan. System prompt dan instruksi penting harus di-pin, sementara pesan percakapan lama bisa di-evict berdasarkan usia dan relevansi.
Teknik 3: Dynamic Tool Selection
Dalam aplikasi LLM modern, model biasanya punya akses ke berbagai tools — API calls, database queries, file operations, dan lain-lain. Tapi memasukkan semua tool definitions ke context window sekaligus itu wasteful dan bisa membingungkan model.
Dynamic tool selection memungkinkan kamu memfilter tools yang tersedia berdasarkan konteks saat ini — siapa user-nya, apa yang sedang dikerjakan, dan tools apa yang paling relevan.
Kenapa Ini Penting?
- Menghemat token — Setiap tool definition bisa memakan 200-500 token. Kalau punya 50 tools, itu 10K-25K token hanya untuk definisi tools.
- Mengurangi confusion — Model yang diberi terlalu banyak tools cenderung memilih tool yang salah atau memanggil tools yang tidak perlu.
- Security — User dengan role tertentu tidak boleh mengakses tools sensitif.
from dataclasses import dataclass
from typing import Callable
@dataclass
class ToolDefinition:
name: str
description: str
parameters: dict
required_roles: list[str]
required_auth_scopes: list[str]
complexity_level: str # "basic", "intermediate", "advanced"
category: str # "data", "communication", "admin", "analysis"
class DynamicToolSelector:
"""Memilih tools yang relevan berdasarkan konteks user dan task."""
def __init__(self):
self.tools: list[ToolDefinition] = []
self.selection_rules: list[Callable] = []
def register_tool(self, tool: ToolDefinition):
"""Daftarkan tool baru ke registry."""
self.tools.append(tool)
def add_selection_rule(self, rule_fn: Callable):
"""Tambahkan rule untuk filtering tools."""
self.selection_rules.append(rule_fn)
def select_tools(self, context: dict) -> list[dict]:
"""Pilih tools yang relevan berdasarkan konteks."""
available = list(self.tools)
# Filter berdasarkan role
user_role = context.get("user_role", "user")
available = [
t for t in available
if user_role in t.required_roles or "all" in t.required_roles
]
# Filter berdasarkan auth scopes
user_scopes = set(context.get("auth_scopes", []))
available = [
t for t in available
if all(scope in user_scopes for scope in t.required_auth_scopes)
]
# Filter berdasarkan complexity level
task_complexity = context.get("task_complexity", "basic")
complexity_order = {"basic": 0, "intermediate": 1, "advanced": 2}
max_complexity = complexity_order.get(task_complexity, 0)
available = [
t for t in available
if complexity_order.get(t.complexity_level, 0) <= max_complexity
]
# Filter berdasarkan kategori task
relevant_categories = context.get("relevant_categories", [])
if relevant_categories:
available = [
t for t in available
if t.category in relevant_categories
]
# Apply custom rules
for rule in self.selection_rules:
available = rule(available, context)
# Convert ke format OpenAI function calling
return [
{
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": t.parameters,
},
}
for t in available
]
# --- Setup ---
selector = DynamicToolSelector()
# Daftarkan tools
selector.register_tool(ToolDefinition(
name="search_products",
description="Cari produk berdasarkan keyword, kategori, atau filter harga",
parameters={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Kata kunci pencarian"},
"category": {"type": "string", "description": "Kategori produk"},
},
"required": ["query"],
},
required_roles=["all"],
required_auth_scopes=[],
complexity_level="basic",
category="data",
))
selector.register_tool(ToolDefinition(
name="export_analytics",
description="Ekspor data analytics dalam format CSV atau JSON",
parameters={
"type": "object",
"properties": {
"date_range": {"type": "string"},
"format": {"type": "string", "enum": ["csv", "json"]},
},
"required": ["date_range"],
},
required_roles=["admin", "analyst"],
required_auth_scopes=["analytics:read"],
complexity_level="intermediate",
category="analysis",
))
selector.register_tool(ToolDefinition(
name="delete_user_data",
description="Hapus semua data user (GDPR compliance)",
parameters={
"type": "object",
"properties": {
"user_id": {"type": "string"},
"confirmation": {"type": "boolean"},
},
"required": ["user_id", "confirmation"],
},
required_roles=["admin"],
required_auth_scopes=["admin:write", "data:delete"],
complexity_level="advanced",
category="admin",
))
# User biasa hanya dapat 1 tool
user_tools = selector.select_tools({
"user_role": "user",
"auth_scopes": [],
"task_complexity": "basic",
})
print(f"Tools untuk user biasa: {len(user_tools)}") # 1
# Admin dengan full scope dapat semua
admin_tools = selector.select_tools({
"user_role": "admin",
"auth_scopes": ["analytics:read", "admin:write", "data:delete"],
"task_complexity": "advanced",
})
print(f"Tools untuk admin: {len(admin_tools)}") # 3
Dengan dynamic tool selection, kamu bisa punya ratusan tools di registry tapi hanya mengirim 5-10 yang paling relevan ke model pada setiap request. Ini dramatically mengurangi token usage dan meningkatkan akurasi tool calling.
Teknik 4: Injeksi Konteks Just-in-Time
Salah satu kesalahan paling umum dalam membangun aplikasi LLM adalah pre-loading terlalu banyak data ke context window. Misalnya, langsung memasukkan seluruh profil user, semua dokumen terkait, dan seluruh riwayat transaksi — padahal belum tentu semua data itu dibutuhkan.
Just-in-Time (JIT) context injection artinya kamu hanya memasukkan data saat benar-benar dibutuhkan. Strateginya adalah menyimpan lightweight identifiers (ID, nama, summary singkat) di context, lalu meng-fetch detail lengkap hanya ketika model atau user membutuhkannya.
Strategi Hybrid: Lightweight References + On-Demand Loading
import json
from dataclasses import dataclass
@dataclass
class ContextReference:
"""Referensi ringan ke data yang bisa di-load on demand."""
ref_id: str
ref_type: str # "document", "user_profile", "transaction", "ticket"
summary: str # Summary singkat untuk context
token_estimate: int # Estimasi token jika di-load penuh
class JustInTimeContextManager:
"""Mengelola injeksi konteks secara on-demand."""
def __init__(self, max_injected_tokens: int = 4000):
self.max_injected_tokens = max_injected_tokens
self.references: list[ContextReference] = []
self.loaded_data: dict[str, str] = {}
self.data_loaders: dict[str, callable] = {}
self.total_injected_tokens: int = 0
def register_loader(self, ref_type: str, loader_fn):
"""Daftarkan fungsi loader untuk tipe referensi tertentu."""
self.data_loaders[ref_type] = loader_fn
def add_reference(self, ref: ContextReference):
"""Tambahkan referensi ringan ke konteks."""
self.references.append(ref)
def get_context_summary(self) -> str:
"""Dapatkan summary dari semua referensi yang tersedia.
Ini yang selalu ada di context window — ringan dan informatif."""
lines = ["Data yang tersedia untuk di-akses:"]
for ref in self.references:
status = "[LOADED]" if ref.ref_id in self.loaded_data else "[available]"
lines.append(f" - {status} [{ref.ref_type}] {ref.ref_id}: {ref.summary}")
lines.append(
"\nGunakan fungsi load_context(ref_id) untuk mengambil detail lengkap."
)
return "\n".join(lines)
def load_context(self, ref_id: str) -> str | None:
"""Load data lengkap untuk sebuah referensi (dipanggil on-demand)."""
if ref_id in self.loaded_data:
return self.loaded_data[ref_id]
ref = next((r for r in self.references if r.ref_id == ref_id), None)
if not ref:
return None
# Cek apakah masih cukup ruang
if self.total_injected_tokens + ref.token_estimate > self.max_injected_tokens:
# Evict data yang sudah di-load tapi kurang penting
self._evict_oldest()
# Load data menggunakan registered loader
loader = self.data_loaders.get(ref.ref_type)
if not loader:
return None
data = loader(ref_id)
self.loaded_data[ref_id] = data
self.total_injected_tokens += ref.token_estimate
return data
def _evict_oldest(self):
"""Hapus data yang paling lama di-load untuk membuat ruang."""
if self.loaded_data:
oldest_key = next(iter(self.loaded_data))
ref = next((r for r in self.references if r.ref_id == oldest_key), None)
if ref:
self.total_injected_tokens -= ref.token_estimate
del self.loaded_data[oldest_key]
def build_injected_context(self) -> str:
"""Build konteks yang sudah di-load untuk dimasukkan ke prompt."""
if not self.loaded_data:
return ""
sections = ["\n--- Data Detail (Loaded) ---"]
for ref_id, data in self.loaded_data.items():
ref = next((r for r in self.references if r.ref_id == ref_id), None)
label = ref.ref_type if ref else "unknown"
sections.append(f"\n[{label}: {ref_id}]\n{data}")
return "\n".join(sections)
# --- Penggunaan ---
ctx_manager = JustInTimeContextManager(max_injected_tokens=4000)
# Register data loaders (simulasi)
def load_document(ref_id: str) -> str:
# Di produksi, ini bisa query ke database atau vector store
return f"Isi lengkap dokumen {ref_id}... (paragraf detail tentang topik)"
def load_user_profile(ref_id: str) -> str:
return json.dumps({
"user_id": ref_id,
"name": "Budi Santoso",
"plan": "premium",
"preferences": {"language": "id", "notifications": True},
}, indent=2)
ctx_manager.register_loader("document", load_document)
ctx_manager.register_loader("user_profile", load_user_profile)
# Tambahkan referensi ringan (ini yang selalu ada di context)
ctx_manager.add_reference(ContextReference(
ref_id="DOC-001",
ref_type="document",
summary="Panduan integrasi API pembayaran v3.2",
token_estimate=1500,
))
ctx_manager.add_reference(ContextReference(
ref_id="USR-42",
ref_type="user_profile",
summary="Budi Santoso, Premium plan, bergabung sejak 2024",
token_estimate=200,
))
# Context summary selalu tersedia (hemat token)
print(ctx_manager.get_context_summary())
# Load detail hanya saat dibutuhkan
ctx_manager.load_context("USR-42")
print(ctx_manager.build_injected_context())
Nah, keuntungan dari pendekatan JIT ini sangat signifikan di produksi. Daripada memasukkan 10.000 token data yang mungkin tidak dipakai, kamu cukup memasukkan 200 token summary. Data detail hanya di-load ketika model benar-benar membutuhkannya — menghemat token dan biaya secara dramatis.
Teknik 5: Structured Note-Taking (Memori Agentik)
Untuk task yang berjalan lama (long-horizon tasks), model butuh cara untuk "mengingat" progress dan keputusan penting yang sudah dibuat. Tapi context window punya batasan.
Solusinya? Structured note-taking — kemampuan agent untuk menulis catatan terstruktur ke storage eksternal, yang kemudian bisa di-retrieve kembali.
Ini beda dari conversation history biasa. Notes ini bersifat deliberate — agent secara sadar memutuskan apa yang penting untuk dicatat, daripada menyimpan semua percakapan verbatim. Jujur, teknik ini game-changing untuk aplikasi agent yang kompleks.
Implementasi Memori Agentik
import json
import time
from dataclasses import dataclass, field
from enum import Enum
class NoteCategory(Enum):
DECISION = "decision" # Keputusan arsitektur / desain
PROGRESS = "progress" # Progress tracking
FACT = "fact" # Fakta penting yang ditemukan
TODO = "todo" # Task yang perlu dikerjakan
BLOCKER = "blocker" # Hambatan yang ditemui
USER_PREFERENCE = "preference" # Preferensi user yang terungkap
@dataclass
class AgentNote:
category: NoteCategory
content: str
context_key: str # Key untuk grouping (e.g., project name, task ID)
created_at: float = field(default_factory=time.time)
is_active: bool = True # False jika sudah resolved/outdated
class AgenticMemory:
"""Sistem note-taking terstruktur untuk agent dengan long-horizon tasks."""
def __init__(self, storage_path: str = "agent_memory.json"):
self.storage_path = storage_path
self.notes: list[AgentNote] = []
def take_note(
self,
category: NoteCategory,
content: str,
context_key: str,
) -> AgentNote:
"""Agent menulis catatan baru."""
note = AgentNote(
category=category,
content=content,
context_key=context_key,
)
self.notes.append(note)
self._persist()
return note
def recall(
self,
context_key: str | None = None,
category: NoteCategory | None = None,
active_only: bool = True,
) -> list[AgentNote]:
"""Recall catatan berdasarkan filter."""
results = self.notes
if context_key:
results = [n for n in results if n.context_key == context_key]
if category:
results = [n for n in results if n.category == category]
if active_only:
results = [n for n in results if n.is_active]
return results
def build_memory_context(self, context_key: str) -> str:
"""Bangun konteks memori untuk diinjeksi ke prompt.
Format ini ringkas tapi informatif."""
notes = self.recall(context_key=context_key)
if not notes:
return "Belum ada catatan untuk konteks ini."
sections: dict[str, list[str]] = {}
for note in notes:
cat_name = note.category.value.upper()
if cat_name not in sections:
sections[cat_name] = []
sections[cat_name].append(f" - {note.content}")
lines = [f"=== Catatan Agent untuk '{context_key}' ==="]
for cat, items in sections.items():
lines.append(f"\n[{cat}]")
lines.extend(items)
return "\n".join(lines)
def resolve_note(self, note: AgentNote):
"""Tandai catatan sebagai resolved/tidak aktif."""
note.is_active = False
self._persist()
def _persist(self):
"""Simpan notes ke file (di produksi bisa pakai database)."""
data = [
{
"category": n.category.value,
"content": n.content,
"context_key": n.context_key,
"created_at": n.created_at,
"is_active": n.is_active,
}
for n in self.notes
]
with open(self.storage_path, "w") as f:
json.dump(data, f, indent=2)
# --- Penggunaan dalam Agent Loop ---
memory = AgenticMemory()
# Agent menemukan informasi penting selama task
memory.take_note(
NoteCategory.DECISION,
"Menggunakan PostgreSQL sebagai primary database karena kebutuhan JSONB support",
context_key="project-alpha",
)
memory.take_note(
NoteCategory.PROGRESS,
"Schema database sudah dibuat: users, products, orders (3 tabel utama)",
context_key="project-alpha",
)
memory.take_note(
NoteCategory.BLOCKER,
"API payment gateway belum bisa ditest — sandbox credentials belum dikirim tim finance",
context_key="project-alpha",
)
memory.take_note(
NoteCategory.USER_PREFERENCE,
"User lebih suka menggunakan async/await daripada callback pattern",
context_key="project-alpha",
)
# Saat agent butuh konteks, recall dari memori
context_for_prompt = memory.build_memory_context("project-alpha")
print(context_for_prompt)
Dengan structured note-taking, agent kamu bisa menangani task yang berlangsung berjam-jam atau bahkan berhari-hari. Notes ini bertindak sebagai "otak kedua" di luar context window — bisa di-recall kapan saja tanpa memakan space di context window secara permanen.
Teknik 6: Context Compaction dan Summarization
Ketika percakapan semakin panjang, tidak semua pesan lama masih relevan. Tapi menghapusnya begitu saja bisa menghilangkan konteks penting. Solusinya adalah context compaction — meringkas pesan-pesan lama menjadi summary yang padat tanpa kehilangan informasi krusial.
Teknik ini berbeda dari sliding window biasa. Daripada langsung membuang pesan lama, kamu mengkompres-nya menjadi summary yang jauh lebih hemat token tapi tetap mempertahankan informasi esensial.
Summarization Middleware
from openai import OpenAI
class SummarizationMiddleware:
"""Middleware yang meringkas pesan-pesan lama untuk menghemat context window.
Strategi:
1. Pesan terbaru (recent_count) tetap utuh
2. Pesan lama diringkas menjadi satu blok summary
3. Keputusan penting dan fakta kunci dipertahankan
"""
def __init__(
self,
client: OpenAI | None = None,
recent_count: int = 10,
summary_max_tokens: int = 500,
model: str = "gpt-4o-mini",
):
self.client = client or OpenAI()
self.recent_count = recent_count
self.summary_max_tokens = summary_max_tokens
self.model = model
self.cached_summary: str | None = None
self.summarized_up_to: int = 0 # Index pesan terakhir yang sudah di-summary
def process_messages(self, messages: list[dict]) -> list[dict]:
"""Proses messages — ringkas yang lama, pertahankan yang baru."""
if len(messages) <= self.recent_count + 1: # +1 untuk system prompt
return messages # Belum perlu summarize
system_msg = messages[0] if messages[0]["role"] == "system" else None
non_system = messages[1:] if system_msg else messages
# Pisahkan pesan lama dan baru
old_messages = non_system[:-self.recent_count]
recent_messages = non_system[-self.recent_count:]
# Summarize pesan lama jika ada yang baru
if len(old_messages) > self.summarized_up_to:
self.cached_summary = self._summarize(old_messages)
self.summarized_up_to = len(old_messages)
# Bangun messages baru: system + summary + recent
result = []
if system_msg:
result.append(system_msg)
if self.cached_summary:
result.append({
"role": "system",
"content": (
f"[Ringkasan percakapan sebelumnya]\n{self.cached_summary}"
),
})
result.extend(recent_messages)
return result
def _summarize(self, messages: list[dict]) -> str:
"""Buat ringkasan dari serangkaian pesan."""
conversation_text = "\n".join(
f"{m['role'].upper()}: {m['content']}" for m in messages
)
response = self.client.chat.completions.create(
model=self.model,
max_tokens=self.summary_max_tokens,
messages=[
{
"role": "system",
"content": (
"Ringkas percakapan berikut menjadi poin-poin utama. "
"Pertahankan: 1) Keputusan yang sudah dibuat, "
"2) Fakta penting yang disebutkan, "
"3) Konteks yang diperlukan untuk melanjutkan percakapan. "
"Buang: basa-basi, pengulangan, detail yang tidak relevan. "
"Tulis dalam format bullet points yang ringkas."
),
},
{
"role": "user",
"content": f"Ringkas percakapan ini:\n\n{conversation_text}",
},
],
)
return response.choices[0].message.content
def clear_tool_results(self, messages: list[dict]) -> list[dict]:
"""Bersihkan tool results yang sudah diproses.
Tool results sering sangat besar tapi hanya perlu sekali."""
cleaned = []
for msg in messages:
if msg["role"] == "tool" and msg in messages[:-self.recent_count]:
# Ganti tool result lama dengan placeholder ringkas
cleaned.append({
"role": "tool",
"content": "[Tool result sudah diproses - lihat ringkasan di atas]",
"tool_call_id": msg.get("tool_call_id", ""),
})
else:
cleaned.append(msg)
return cleaned
# --- Penggunaan ---
summarizer = SummarizationMiddleware(recent_count=6, summary_max_tokens=300)
# Simulasi percakapan panjang
messages = [
{"role": "system", "content": "Kamu adalah asisten pemrograman."},
]
for i in range(20):
messages.append({"role": "user", "content": f"Pertanyaan tentang topik {i}"})
messages.append({"role": "assistant", "content": f"Jawaban detail tentang topik {i}"})
print(f"Sebelum compaction: {len(messages)} pesan")
compacted = summarizer.process_messages(messages)
print(f"Setelah compaction: {len(compacted)} pesan")
Hal yang perlu diperhatikan saat melakukan summarization: pastikan keputusan arsitektur dan fakta kunci tidak hilang. Misalnya, kalau di awal percakapan user bilang "Saya pakai Python 3.11 dan FastAPI," informasi ini harus tetap ada di summary meskipun pesannya sudah di-compress.
Kamu juga bisa membersihkan tool results yang sudah lama — ini sering jadi sumber "token bloat" terbesar (dan trust me, bisa bikin context window penuh dengan cepat).
Teknik 7: Sub-Agent Architecture untuk Konteks Bersih
Salah satu pattern paling powerful di context engineering adalah sub-agent architecture. Idenya simpel: ketika sebuah task kompleks perlu dikerjakan, daripada menambahkan semua informasi ke context window agent utama, delegasikan ke sub-agent yang punya context window bersih dan fokus.
Sub-agent menyelesaikan task spesifik dengan konteks yang relevan saja, lalu mengembalikan ringkasan hasil — bukan seluruh proses. Ini menjaga context window agent utama tetap bersih dan efisien.
Implementasi Sub-Agent Pattern
from openai import OpenAI
from dataclasses import dataclass
@dataclass
class SubAgentResult:
"""Hasil dari eksekusi sub-agent — ringkas dan terstruktur."""
task_id: str
status: str # "success", "partial", "failed"
summary: str # Ringkasan hasil untuk dikembalikan ke parent agent
key_findings: list[str] # Temuan utama
artifacts: dict # Data terstruktur yang dihasilkan
class SubAgentOrchestrator:
"""Orkestrator yang mengelola delegasi task ke sub-agents."""
def __init__(self, client: OpenAI | None = None, model: str = "gpt-4o"):
self.client = client or OpenAI()
self.model = model
self.sub_agent_configs: dict[str, dict] = {}
def register_sub_agent(
self,
agent_type: str,
system_prompt: str,
tools: list[dict] | None = None,
max_tokens: int = 2000,
):
"""Daftarkan tipe sub-agent dengan konfigurasinya."""
self.sub_agent_configs[agent_type] = {
"system_prompt": system_prompt,
"tools": tools or [],
"max_tokens": max_tokens,
}
def delegate(
self,
agent_type: str,
task_description: str,
task_context: str = "",
task_id: str = "",
) -> SubAgentResult:
"""Delegasikan task ke sub-agent dan dapatkan hasil ringkas."""
config = self.sub_agent_configs.get(agent_type)
if not config:
return SubAgentResult(
task_id=task_id,
status="failed",
summary=f"Sub-agent tipe '{agent_type}' tidak ditemukan.",
key_findings=[],
artifacts={},
)
# Sub-agent mendapat context window bersih
messages = [
{"role": "system", "content": config["system_prompt"]},
{
"role": "user",
"content": (
f"Task: {task_description}\n\n"
f"Konteks tambahan:\n{task_context}\n\n"
"Selesaikan task ini dan berikan hasil dalam format:\n"
"SUMMARY: (ringkasan 1-2 kalimat)\n"
"FINDINGS: (poin-poin temuan utama)\n"
"RESULT: (data atau output yang dihasilkan)"
),
},
]
response = self.client.chat.completions.create(
model=self.model,
max_tokens=config["max_tokens"],
messages=messages,
tools=config["tools"] if config["tools"] else None,
)
result_text = response.choices[0].message.content or ""
# Parse hasil sub-agent
return self._parse_result(task_id, result_text)
def _parse_result(self, task_id: str, result_text: str) -> SubAgentResult:
"""Parse output sub-agent menjadi format terstruktur."""
summary = ""
findings = []
artifacts = {}
lines = result_text.split("\n")
current_section = None
for line in lines:
stripped = line.strip()
if stripped.startswith("SUMMARY:"):
summary = stripped[8:].strip()
current_section = "summary"
elif stripped.startswith("FINDINGS:"):
current_section = "findings"
elif stripped.startswith("RESULT:"):
current_section = "result"
elif current_section == "findings" and stripped.startswith("- "):
findings.append(stripped[2:])
elif current_section == "result" and stripped:
artifacts["raw_result"] = artifacts.get("raw_result", "") + stripped + "\n"
return SubAgentResult(
task_id=task_id,
status="success" if summary else "partial",
summary=summary or result_text[:200],
key_findings=findings,
artifacts=artifacts,
)
def build_delegation_summary(self, results: list[SubAgentResult]) -> str:
"""Bangun summary dari semua hasil sub-agent untuk parent agent."""
lines = ["=== Hasil Delegasi Sub-Agent ==="]
for r in results:
lines.append(f"\n[Task: {r.task_id}] Status: {r.status}")
lines.append(f" Summary: {r.summary}")
if r.key_findings:
lines.append(" Temuan:")
for f in r.key_findings:
lines.append(f" - {f}")
return "\n".join(lines)
# --- Penggunaan ---
orchestrator = SubAgentOrchestrator()
# Daftarkan sub-agent untuk code review
orchestrator.register_sub_agent(
agent_type="code_reviewer",
system_prompt=(
"Kamu adalah code reviewer berpengalaman. "
"Analisis kode yang diberikan dan berikan feedback "
"tentang kualitas, potensi bug, dan saran perbaikan. "
"Selalu format output dengan SUMMARY:, FINDINGS:, dan RESULT:."
),
max_tokens=1500,
)
# Daftarkan sub-agent untuk research
orchestrator.register_sub_agent(
agent_type="researcher",
system_prompt=(
"Kamu adalah researcher yang menganalisis dokumentasi teknis. "
"Cari informasi spesifik yang diminta dan rangkum temuanmu. "
"Selalu format output dengan SUMMARY:, FINDINGS:, dan RESULT:."
),
max_tokens=1000,
)
# Parent agent mendelegasikan tasks
# (di produksi, ini dipanggil oleh agent utama)
# result = orchestrator.delegate(
# agent_type="code_reviewer",
# task_description="Review fungsi authenticate() untuk security issues",
# task_context="def authenticate(user, pwd): return db.check(user, pwd)",
# task_id="review-001",
# )
# print(orchestrator.build_delegation_summary([result]))
Keuntungan utama sub-agent architecture:
- Context window agent utama tetap bersih — tidak terpenuhi dengan detail task spesifik
- Setiap sub-agent punya konteks yang fokus — meningkatkan kualitas output
- Parallel execution — beberapa sub-agent bisa dijalankan bersamaan
- Isolation — kegagalan satu sub-agent tidak merusak konteks agent utama
Pattern ini sangat berguna untuk aplikasi seperti coding assistant yang perlu review kode, search dokumentasi, dan generate test secara bersamaan. Setiap task ditangani sub-agent yang terpisah, dan agent utama hanya menerima summary yang ringkas.
Best Practices dan Anti-Pattern
Setelah membahas 7 teknik utama, sekarang kita rangkum best practices dan kesalahan umum yang harus kamu hindari saat menerapkan context engineering.
Best Practices
-
Mulai statis, tambahkan dinamis secara bertahap
Jangan langsung membangun sistem context engineering yang super kompleks. Mulai dengan system prompt statis dan conversation history biasa. Baru ketika kamu mengidentifikasi bottleneck spesifik — misalnya context window penuh, atau model memberikan respons yang tidak konsisten — tambahkan komponen dinamis secara incremental.
-
Bedakan data transient vs persistent
Tidak semua informasi punya lifetime yang sama. Tool results biasanya transient — hanya relevan untuk request saat ini. Keputusan arsitektur bersifat persistent — harus tetap tersedia sepanjang sesi. Rancang context management kamu berdasarkan lifetime data ini.
-
Monitor token usage secara aktif
Implementasikan logging dan metrics untuk token usage di setiap komponen. Kamu perlu tahu berapa persen context window dihabiskan oleh system prompt, berapa oleh tool definitions, berapa oleh conversation history. Ini membantu kamu mengidentifikasi dan mengoptimalkan komponen yang paling "boros."
-
Test dengan skenario percakapan panjang
Banyak bug context engineering baru muncul setelah 20-30 turn percakapan. Buat automated test yang mensimulasikan percakapan panjang dan verifikasi bahwa informasi penting masih tersedia di akhir.
-
Gunakan structured formats untuk data dalam konteks
Data yang diinjeksi ke context window sebaiknya dalam format yang terstruktur — JSON, Markdown tables, atau bullet points. Hindari memasukkan teks panjang tanpa struktur karena model lebih sulit mengekstrak informasi darinya.
Anti-Pattern yang Harus Dihindari
-
Context Rot (Pembusukan Konteks)
Ini terjadi ketika informasi yang sudah tidak relevan atau outdated masih menumpuk di context window. Masalahnya bukan hanya token waste — transformer models menggunakan quadratic attention (O(n²)), jadi setiap token tambahan membuat model memproses SEMUA token lebih lambat. Context rot secara eksponensial memperlambat inference dan meningkatkan biaya.
Solusi: implementasikan mekanisme "garbage collection" untuk context — bersihkan data yang sudah tidak relevan secara berkala.
-
Tool Bloat (Kembung Tools)
Memasukkan 30+ tool definitions ke setiap request, padahal user hanya butuh 3-5 tools pada satu waktu. Selain membuang token, ini membuat model bingung dan sering memilih tool yang salah.
Solusi: gunakan dynamic tool selection (Teknik 3) untuk hanya menyajikan tools yang relevan.
-
Premature Retrieval (Pengambilan Data Dini)
Langsung memasukkan semua data dari vector database ke context sebelum tahu apa yang sebenarnya dibutuhkan. Ini common di implementasi RAG yang naif.
Solusi: gunakan JIT context injection (Teknik 4) — berikan summary dulu, load detail saat dibutuhkan.
-
Ignoring Token Economics (Mengabaikan Ekonomi Token)
Di production, setiap token punya biaya. Jangan hanya fokus pada kualitas respons tanpa mempertimbangkan efisiensi. Sebuah sistem yang menghasilkan respons sedikit lebih baik tapi menggunakan 3x token mungkin tidak worth it secara bisnis.
-
Monolithic Context (Konteks Monolitik)
Memasukkan semua informasi dalam satu blok besar tanpa pemisahan atau prioritas. Model kesulitan menemukan informasi yang relevan dalam "lautan teks" yang tidak terstruktur.
Solusi: segmentasikan context ke bagian-bagian yang jelas dengan header dan delimiter. Gunakan XML tags atau Markdown headers untuk membuat struktur yang mudah dipahami model.
Checklist Token Budget
Berikut panduan alokasi token budget yang bisa kamu jadikan starting point:
- System prompt: 5-15% dari context window
- Tool definitions: 5-10% (setelah dynamic filtering)
- Conversation history: 30-40%
- Retrieved knowledge: 15-25%
- Reserved for response: 20-30%
Persentase ini bervariasi tergantung use case, tapi intinya: selalu sisakan cukup ruang untuk respons model. Kesalahan paling umum adalah mengisi context window hingga 95% dan hanya menyisakan 5% untuk respons — ini menghasilkan jawaban yang terpotong dan tidak lengkap.
Observabilitas dan Debugging Context
Satu aspek yang sering dilupakan developer adalah observabilitas. Ketika model memberikan respons yang buruk, kamu perlu bisa melihat persis apa yang ada di context window saat itu.
Tanpa observabilitas yang baik, debugging aplikasi LLM itu seperti debugging kode tanpa log — hampir mustahil.
Beberapa hal yang perlu kamu log:
- Full context window yang dikirim ke model (termasuk system prompt, tools, dan semua messages)
- Token count per komponen — berapa token dipakai system prompt, tools, history, retrieved docs
- Keputusan context management — pesan mana yang di-evict, di-summarize, atau di-pin
- Latency per komponen — berapa lama retrieval, summarization, dan inference
Tools seperti LangSmith, Helicone, atau Braintrust sangat membantu untuk ini. Mereka memungkinkan kamu melihat trace lengkap dari setiap request, termasuk semua intermediate steps dan context composition.
Pola Integrasi: Menggabungkan Semua Teknik
Dalam aplikasi produksi yang nyata, kamu tidak menggunakan satu teknik saja — kamu menggabungkan beberapa teknik menjadi pipeline yang kohesif. Berikut contoh bagaimana semua teknik bisa bekerja sama:
from dataclasses import dataclass
@dataclass
class ContextPipelineConfig:
"""Konfigurasi untuk context engineering pipeline."""
max_context_tokens: int = 16000
reserved_for_response: int = 4000
max_tool_definitions: int = 8
summarize_after_n_messages: int = 15
enable_jit_loading: bool = True
enable_sub_agents: bool = True
class ContextEngineeringPipeline:
"""Pipeline lengkap yang menggabungkan semua teknik context engineering."""
def __init__(self, config: ContextPipelineConfig):
self.config = config
def compose_context(
self,
user_message: str,
conversation_history: list[dict],
user_context: dict,
available_tools: list[dict],
knowledge_base_results: list[str],
) -> dict:
"""Komposisi context window lengkap menggunakan semua teknik."""
# 1. Dynamic System Prompt (Teknik 1)
system_prompt = self._build_dynamic_system_prompt(user_context)
# 2. Context Window Management (Teknik 2)
managed_history = self._manage_conversation_history(
conversation_history
)
# 3. Dynamic Tool Selection (Teknik 3)
filtered_tools = self._select_relevant_tools(
available_tools, user_context
)
# 4. JIT Context Injection (Teknik 4)
injected_context = self._inject_relevant_context(
user_message, knowledge_base_results
)
# 5. Agentic Memory Recall (Teknik 5)
memory_context = self._recall_agent_notes(user_context)
# 6. Context Compaction (Teknik 6)
if len(managed_history) > self.config.summarize_after_n_messages:
managed_history = self._compact_history(managed_history)
# Compose final context
final_system = system_prompt
if memory_context:
final_system += f"\n\n{memory_context}"
if injected_context:
final_system += f"\n\n{injected_context}"
return {
"messages": [
{"role": "system", "content": final_system},
*managed_history,
{"role": "user", "content": user_message},
],
"tools": filtered_tools,
"token_budget": {
"system": self._estimate_tokens(final_system),
"history": sum(
self._estimate_tokens(m["content"])
for m in managed_history
),
"tools": len(filtered_tools) * 300, # estimate
"available_for_response": self.config.reserved_for_response,
},
}
def _build_dynamic_system_prompt(self, user_context: dict) -> str:
base = "Kamu adalah asisten AI yang helpful dan knowledgeable."
role = user_context.get("role", "user")
if role == "admin":
base += " User memiliki akses admin penuh."
return base
def _manage_conversation_history(
self, history: list[dict]
) -> list[dict]:
max_messages = 20
if len(history) > max_messages:
return history[-max_messages:]
return history
def _select_relevant_tools(
self, tools: list[dict], context: dict
) -> list[dict]:
return tools[: self.config.max_tool_definitions]
def _inject_relevant_context(
self, query: str, kb_results: list[str]
) -> str:
if not kb_results:
return ""
return "Informasi relevan:\n" + "\n".join(
f"- {r}" for r in kb_results[:5]
)
def _recall_agent_notes(self, context: dict) -> str:
return "" # Integrasi dengan AgenticMemory
def _compact_history(self, history: list[dict]) -> list[dict]:
return history[-10:] # Simplified; gunakan SummarizationMiddleware
def _estimate_tokens(self, text: str) -> int:
return len(text) // 4 # Rough estimate
# --- Penggunaan ---
pipeline = ContextEngineeringPipeline(ContextPipelineConfig())
result = pipeline.compose_context(
user_message="Bagaimana cara optimasi query database yang lambat?",
conversation_history=[
{"role": "user", "content": "Halo, saya butuh bantuan"},
{"role": "assistant", "content": "Tentu! Ada yang bisa saya bantu?"},
],
user_context={"role": "developer", "plan": "premium"},
available_tools=[
{"type": "function", "function": {"name": "run_query", "description": "Execute SQL"}},
{"type": "function", "function": {"name": "explain_plan", "description": "Show query plan"}},
],
knowledge_base_results=[
"Gunakan EXPLAIN ANALYZE untuk melihat query plan",
"Index pada kolom yang sering di-WHERE dan JOIN",
"Hindari SELECT * — pilih kolom spesifik saja",
],
)
print(f"Total messages: {len(result['messages'])}")
print(f"Tools provided: {len(result['tools'])}")
print(f"Token budget: {result['token_budget']}")
Pipeline ini menunjukkan bagaimana semua teknik bekerja bersama dalam satu alur yang kohesif. Di production, setiap method akan lebih kompleks — menggunakan kelas-kelas yang sudah kita bangun sebelumnya.
Tapi struktur dasarnya tetap sama: compose, manage, optimize, deliver. Sesimpel itu konsepnya (meski implementasinya bisa jadi cukup kompleks).
Kesimpulan: Context Engineering sebagai Pembeda di 2026
Kalau kamu cuma mengambil satu takeaway dari artikel sepanjang ini, ambil yang ini: di 2026, kualitas aplikasi LLM kamu ditentukan bukan oleh seberapa pintar model yang kamu pakai, tapi seberapa baik kamu merancang konteks yang diberikan ke model tersebut.
Context engineering telah berevolusi dari "nice-to-have" menjadi skill wajib bagi setiap developer yang membangun aplikasi AI untuk produksi. Perbedaannya terasa nyata:
- Aplikasi dengan context engineering yang baik memberikan respons yang konsisten, akurat, dan efisien — bahkan dalam percakapan panjang dan task kompleks.
- Aplikasi tanpa context engineering yang baik mulai "halusinasi" setelah beberapa turn, menggunakan token secara boros, dan memberikan pengalaman yang inkonsisten.
Mari kita recap 7 teknik utama yang sudah kita bahas:
- Dynamic System Prompts — Sesuaikan instruksi model berdasarkan state, role, dan konteks
- Manajemen Context Window — Kelola token budget dengan sliding window dan priority-based eviction
- Dynamic Tool Selection — Sajikan hanya tools yang relevan untuk mengurangi noise dan token waste
- Injeksi Konteks Just-in-Time — Load data on-demand, bukan pre-load semuanya
- Structured Note-Taking — Berikan agent kemampuan mencatat untuk long-horizon tasks
- Context Compaction — Ringkas pesan lama sambil mempertahankan informasi krusial
- Sub-Agent Architecture — Delegasikan task ke sub-agent dengan context bersih
Jadi, mulai dari mana? Saran saya: mulai dari yang paling sederhana. Implementasikan token monitoring dulu untuk memahami bagaimana context window kamu terpakai. Kemudian tambahkan sliding window management.
Setelah itu, baru bergerak ke teknik yang lebih advanced seperti dynamic system prompts dan sub-agent architecture. Step by step, nothing fancy.
Yang terpenting, perlakukan context window bukan sebagai "tempat buang semua data" tapi sebagai resource yang berharga dan terbatas — seperti RAM di komputer. Setiap byte (token) yang masuk harus punya alasan yang jelas. Dengan mindset ini, kamu sudah selangkah lebih maju dibanding kebanyakan developer yang masih hanya fokus pada prompt engineering.
Context engineering bukan hanya tentang teknik — ini tentang cara berpikir. Berpikir tentang informasi apa yang model butuhkan, kapan membutuhkannya, dan bagaimana cara paling efisien untuk menyediakannya. Kuasai cara berpikir ini, dan kamu akan bisa membangun aplikasi LLM yang benar-benar production-grade di 2026 dan seterusnya.