2025년 6월, Andrej Karpathy(전 OpenAI, Tesla AI 디렉터)가 X에 짧은 글 하나를 올렸어요.
"프롬프트 엔지니어링이라는 말은 우리가 실제로 하는 일을 너무 사소하게 만든다. 더 정확한 표현은 컨텍스트 엔지니어링이다."
그리고 Shopify CEO 토비 뤼트케가 동의하며 이렇게 정의했어요.
"LLM이 그럴듯하게 문제를 풀 수 있도록 모든 컨텍스트를 제공하는 기술."
이 두 발언 이후 AI 개발 커뮤니티에서 컨텍스트 엔지니어링이 2026년 가장 중요한 개념으로 자리 잡았어요.
프롬프트 엔지니어링과 뭐가 다른가
먼저 LLM을 컴퓨터로 비유해볼게요.
LLM = CPU
컨텍스트 윈도우 = RAM
컨텍스트 엔지니어링 = 운영체제
운영체제는 CPU가 작업할 때 RAM에 딱 필요한 데이터만 올려요. 너무 많아도 안 되고, 너무 적어도 안 돼요.
프롬프트 엔지니어링은 "무슨 말을 어떻게 할까"예요. 배우한테 건네는 대본이에요.
컨텍스트 엔지니어링은 "그 말을 할 때 모델이 무엇을 알고 있어야 하는가"예요. 배우가 연기하기 전에 무대 배경, 소품, 상대 배우의 캐릭터 전부를 세팅하는 것이에요.
프롬프트 엔지니어링:
"당신은 전문 여행 에이전트입니다. 서울에서 도쿄 여행을 도와주세요."
컨텍스트 엔지니어링:
- 시스템 프롬프트: 역할과 규칙 정의
- 사용자 프로필: 과거 여행 기록, 선호 항공사
- 검색 결과: 오늘의 항공권 가격
- 대화 기록: 지난 3번의 대화
- 툴 목록: 예약 API, 날씨 API
- 출력 형식: JSON 구조
→ 이 모든 것을 최적으로 조합해서 컨텍스트 윈도우에 넣는 것
컨텍스트 윈도우 안에 들어가는 6가지
LLM이 응답을 생성할 때 보는 것들이에요. 컨텍스트 엔지니어링은 이 6가지를 어떻게 관리하느냐예요.

컨텍스트의 4가지 실패 모드
컨텍스트가 잘못되면 네 가지 방식으로 망가져요.
1. Context Poisoning (오염)
→ 잘못된 정보가 컨텍스트에 들어감
→ LLM이 잘못된 정보를 사실로 받아들여 답변
2. Context Distraction (산만함)
→ 관련 없는 정보가 너무 많음
→ LLM의 주의가 분산돼서 핵심을 놓침
3. Context Confusion (혼란)
→ 모순되는 정보가 동시에 존재
→ LLM이 어느 정보를 따를지 몰라서 일관성 없는 답변
4. Context Clash (충돌)
→ 시스템 프롬프트와 사용자 입력이 충돌
→ LLM이 규칙을 어기거나 이상한 답변 생성
그리고 중요한 연구 결과가 있어요. 많은 컨텍스트 = 더 좋은 답변이 아니에요.
2025년 Chroma 연구에서 GPT-4.1, Claude, Gemini 포함 18개 최신 모델을 테스트했는데, 모든 모델이 입력 길이가 늘어날수록 성능이 떨어졌어요. 어떤 모델은 95% 정확도에서 60%로 급락했어요.
그리고 "잃어버린 중간(Lost in the Middle)" 문제도 실재해요. 관련 정보를 컨텍스트 앞뒤에 놓으면 정확도가 높고, 중간에 넣으면 30% 이상 정확도가 떨어져요.
컨텍스트 내 위치별 LLM 주의도:
시작 부분 ████████████ (높음)
중간 부분 ████ (낮음)
끝 부분 ████████████ (높음)
→ 중요한 정보는 앞이나 뒤에 배치해야 함
컨텍스트 엔지니어링 4가지 핵심 전략
LangChain이 정리한 4가지 전략이에요.
전략 1: Write (외부 저장)
컨텍스트 윈도우가 꽉 차기 전에 중요한 정보를 외부에 저장해요.
from langchain_core.messages import AIMessage, HumanMessage
from supabase import create_client
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
class ConversationManager:
def save_to_external(self, conversation_id: str, messages: list):
"""대화가 길어지면 외부 DB에 저장"""
for msg in messages:
supabase.table("messages").insert({
"conversation_id": conversation_id,
"role": msg["role"],
"content": msg["content"]
}).execute()
def load_recent(self, conversation_id: str, limit: int = 10) -> list:
"""최근 N개 메시지만 컨텍스트에 로드"""
result = supabase.table("messages") \
.select("role, content") \
.eq("conversation_id", conversation_id) \
.order("created_at", desc=True) \
.limit(limit) \
.execute()
return list(reversed(result.data))
전략 2: Select (관련 정보만 검색)
모든 정보를 넣지 말고, 지금 질문과 관련된 것만 가져와요. RAG가 여기에 해당해요.
from langchain_community.vectorstores import Qdrant
from langchain_openai import OpenAIEmbeddings
class ContextSelector:
def __init__(self, vectorstore: Qdrant):
self.vectorstore = vectorstore
def select_relevant(
self,
query: str,
k: int = 3,
score_threshold: float = 0.7
) -> list[str]:
"""쿼리와 관련된 문서만 선택"""
results = self.vectorstore.similarity_search_with_score(
query, k=k
)
# 유사도 낮은 건 제외 (관련 없는 정보가 컨텍스트 오염시키는 걸 방지)
relevant = [
doc.page_content
for doc, score in results
if score >= score_threshold
]
return relevant
def build_context(self, query: str, history: list) -> str:
"""최적 컨텍스트 조합"""
relevant_docs = self.select_relevant(query)
context_parts = []
# 중요한 정보를 앞에 배치 (Lost in the Middle 방지)
if relevant_docs:
context_parts.append("### 관련 문서\n" + "\n\n".join(relevant_docs))
# 대화 기록은 마지막 몇 개만
if history:
recent = history[-4:] # 최근 4개만
history_text = "\n".join([
f"{msg['role']}: {msg['content']}" for msg in recent
])
context_parts.append("### 대화 기록\n" + history_text)
return "\n\n".join(context_parts)
전략 3: Compress (요약 압축)
대화가 길어지면 오래된 내용을 요약해서 토큰을 줄여요.
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-6")
class ContextCompressor:
MAX_MESSAGES = 20 # 이 이상이면 요약
def compress_history(self, messages: list) -> list:
if len(messages) <= self.MAX_MESSAGES:
return messages
# 오래된 메시지 요약
old_messages = messages[:-10] # 최근 10개 제외
recent_messages = messages[-10:]
summary_prompt = f"""
다음 대화 내용을 핵심 정보만 남겨서 3~5문장으로 요약하세요.
결정된 사항, 중요한 정보, 사용자 선호도를 포함하세요.
대화:
{self._format_messages(old_messages)}
"""
summary = llm.invoke(summary_prompt).content
# 요약을 시스템 메시지로 앞에 붙이고, 최근 대화는 유지
compressed = [
{"role": "system", "content": f"[이전 대화 요약]\n{summary}"}
] + recent_messages
return compressed
def _format_messages(self, messages: list) -> str:
return "\n".join([
f"{msg['role']}: {msg['content']}" for msg in messages
])
전략 4: Isolate (컨텍스트 격리)
멀티 에이전트 시스템에서 에이전트마다 독립적인 컨텍스트를 가져요. 서로의 컨텍스트가 오염되지 않게요.
class IsolatedAgentContext:
"""각 에이전트가 독립적인 컨텍스트를 가짐"""
def __init__(self):
self.contexts = {}
def create_agent_context(
self,
agent_id: str,
role: str,
tools: list
) -> dict:
"""에이전트별 격리된 컨텍스트 생성"""
self.contexts[agent_id] = {
"system_prompt": role,
"tools": tools,
"messages": [],
"memory": {}
}
return self.contexts[agent_id]
def get_context(self, agent_id: str) -> dict:
return self.contexts.get(agent_id, {})
def update_context(self, agent_id: str, key: str, value):
if agent_id in self.contexts:
self.contexts[agent_id][key] = value
# 사용 예시
context_manager = IsolatedAgentContext()
# 리서처 에이전트 — 검색 툴만 가짐
researcher_ctx = context_manager.create_agent_context(
"researcher",
role="당신은 정보 수집 전문가입니다.",
tools=["web_search", "document_search"]
)
# 작성자 에이전트 — 리서처 결과만 받음 (다른 히스토리 없음)
writer_ctx = context_manager.create_agent_context(
"writer",
role="당신은 글쓰기 전문가입니다.",
tools=["text_editor"]
)
컨텍스트 조립 파이프라인 — 실전 구현
매 LLM 호출 전에 실행되는 동적 컨텍스트 조립 시스템이에요.
from dataclasses import dataclass
from typing import Optional
@dataclass
class ContextConfig:
max_tokens: int = 4000
max_history_messages: int = 10
max_retrieved_docs: int = 3
critical_info_at_front: bool = True # 중요 정보 앞에 배치
class ContextEngine:
"""매 LLM 호출 전에 컨텍스트를 동적으로 조립"""
def __init__(self, config: ContextConfig = ContextConfig()):
self.config = config
self.selector = ContextSelector(vectorstore)
self.compressor = ContextCompressor()
self.token_counter = TokenCounter()
def assemble(
self,
query: str,
conversation_id: str,
user_profile: Optional[dict] = None,
system_role: str = "당신은 도움이 되는 어시스턴트입니다."
) -> list[dict]:
messages = []
# 1. 시스템 프롬프트 구성 (항상 앞에)
system_content = self._build_system_prompt(system_role, user_profile)
messages.append({"role": "system", "content": system_content})
# 2. 관련 문서 검색 및 추가
relevant_docs = self.selector.select_relevant(query, k=self.config.max_retrieved_docs)
if relevant_docs:
doc_context = "다음 정보를 참고해서 답변하세요:\n\n"
doc_context += "\n\n---\n\n".join(relevant_docs)
# 중요 정보 → 앞에 배치 (Lost in the Middle 방지)
messages.append({"role": "system", "content": doc_context})
# 3. 대화 기록 추가 (필요시 압축)
history = self._load_history(conversation_id)
compressed_history = self.compressor.compress_history(history)
# 토큰 예산 체크
remaining_tokens = self.config.max_tokens - self.token_counter.count(messages)
trimmed_history = self._trim_to_budget(compressed_history, remaining_tokens)
messages.extend(trimmed_history)
# 4. 현재 사용자 메시지 추가 (항상 마지막)
messages.append({"role": "user", "content": query})
return messages
def _build_system_prompt(self, role: str, user_profile: Optional[dict]) -> str:
prompt = role
if user_profile:
prompt += f"\n\n사용자 정보:\n"
prompt += f"- 이름: {user_profile.get('name', '알 수 없음')}\n"
prompt += f"- 선호도: {user_profile.get('preferences', '없음')}\n"
prompt += f"- 플랜: {user_profile.get('plan', 'free')}\n"
prompt += "\n\n중요:\n"
prompt += "- 제공된 정보 밖의 내용은 '모른다'고 답하세요.\n"
prompt += "- 불확실할 때는 확실한 것과 불확실한 것을 구분하세요.\n"
return prompt
def _trim_to_budget(self, messages: list, token_budget: int) -> list:
"""토큰 예산에 맞게 메시지 트리밍"""
result = []
used = 0
# 최근 메시지부터 역순으로 추가
for msg in reversed(messages):
msg_tokens = self.token_counter.count([msg])
if used + msg_tokens > token_budget:
break
result.insert(0, msg)
used += msg_tokens
return result
실전 팁 — 모델별 최적 컨텍스트 전략
모델마다 컨텍스트를 다루는 방식이 달라요.
Claude (Anthropic):
- XML 태그로 섹션 구분 효과적
- <context>, <instructions>, <examples> 구조 권장
- Few-shot 예시는 <example> 태그로 감싸기
- 긴 컨텍스트 처리 능력 우수
GPT-4o (OpenAI):
- 시스템 프롬프트 간결하게 유지
- "Step by step" 지시 효과적
- JSON 출력 요청 시 examples 포함
- 중간 길이 컨텍스트에서 최고 성능
Gemini (Google):
- Few-shot 예시 항상 포함 (zero-shot 비선호)
- 특정 질문은 데이터 컨텍스트 뒤에 배치
- 짧고 직접적인 프롬프트 선호
- 2M 토큰 활용 시 위치 전략 중요
# Claude 최적화 컨텍스트 구조
claude_system_prompt = """
<role>
당신은 전문 개발자 어시스턴트입니다.
</role>
<instructions>
- 코드 예시는 항상 포함하세요
- 알 수 없는 내용은 명시하세요
- 간결하고 실용적으로 답하세요
</instructions>
<examples>
<example>
Q: 파이썬에서 리스트 정렬 방법은?
A: sorted() 함수나 .sort() 메서드를 사용합니다.
sorted(list)는 새 리스트를 반환하고, list.sort()는 원본을 수정합니다.
</example>
</examples>
"""
컨텍스트 품질 측정
컨텍스트도 측정해야 개선할 수 있어요.
class ContextQualityMetrics:
def evaluate(self, context: list[dict], response: str) -> dict:
total_tokens = self.count_tokens(context)
system_tokens = self.count_tokens([m for m in context if m["role"] == "system"])
history_tokens = self.count_tokens([m for m in context if m["role"] != "system"])
return {
# 기본 지표
"total_tokens": total_tokens,
"token_utilization": total_tokens / 4096, # 컨텍스트 윈도우 대비
# 컨텍스트 구성 비율
"system_ratio": system_tokens / total_tokens,
"history_ratio": history_tokens / total_tokens,
# 품질 지표
"has_relevant_docs": any("참고" in m["content"] for m in context),
"response_length": len(response),
# 경고 플래그
"context_too_long": total_tokens > 3500, # 85% 이상 사용
"no_relevant_docs": not any("참고" in m["content"] for m in context),
}
# CI/CD에서 컨텍스트 품질 모니터링
def monitor_context_health(metrics: dict):
if metrics["context_too_long"]:
alert("컨텍스트 90% 이상 사용 중 — 압축 필요")
if metrics["no_relevant_docs"]:
alert("관련 문서 없음 — 검색 파이프라인 확인")
프롬프트 엔지니어링 vs 컨텍스트 엔지니어링 비교
항목 프롬프트 엔지니어링 컨텍스트 엔지니어링
| 초점 | 무슨 말을 어떻게 할까 | 모델이 무엇을 알고 있어야 하나 |
| 범위 | 단일 호출 최적화 | 전체 시스템 설계 |
| 성격 | 카피라이팅에 가까움 | 소프트웨어 아키텍처에 가까움 |
| 적합한 경우 | 단순 요약, 번역, 생성 | 에이전트, 멀티턴, 프로덕션 시스템 |
| 한계 | 컨텍스트 부족 시 해결 불가 | 복잡하고 유지보수 필요 |
| 프롬프트 엔지니어링의 관계 | - | 프롬프트 엔지니어링을 포함하는 상위 개념 |
마무리
컨텍스트 엔지니어링을 세 줄로 정리하면 이래요.
첫째, 더 많은 컨텍스트가 항상 좋은 게 아니다. 관련 있는 정보만, 적절한 위치에, 적절한 양으로 넣어야 해요.
둘째, 중요한 정보는 컨텍스트 앞이나 뒤에 배치해야 한다. 중간에 넣으면 30% 이상 성능이 떨어져요.
셋째, 프롬프트는 컨텍스트 엔지니어링의 일부일 뿐이다. 메모리 관리, 검색, 압축, 격리까지 설계해야 프로덕션에서 살아남아요.
"에이전트 전쟁의 승자는 가장 큰 컨텍스트 윈도우를 가진 팀이 아니라, 가장 신중하게 컨텍스트를 설계한 팀이다." 😄
'RAG' 카테고리의 다른 글
| RAG 청킹 전략 완전 정리 (0) | 2026.04.16 |
|---|---|
| RAG 데모는 잘 되는데 배포하면 망하는 이유 7가지 — 원인별 해결법, 프로덕션 RAG 완전 가이드 (2) | 2026.04.09 |
| 쿼리 재작성, 반복 검색, 멀티소스 라우팅 — Agentic RAG 동작 원리와 동적 검색 전략 완전 정리 (0) | 2026.03.25 |
| 벡터 검색 정확도 올리는 법 — 임베딩 모델 선택부터 HNSW 튜닝, Reranking까지 (0) | 2026.03.24 |
| 기존 RAG의 한계를 그래프로 돌파한다 — Graph RAG 동작 원리 완전 정리 (0) | 2026.03.24 |