서론
어느 날 오후, 복잡한 리팩토링 작업을 위해 AI 코딩 어시스턴트와 긴 페어 프로그래밍 세션을 진행하고 있었습니다. 초반에는 정확하고 날카로운 제안을 주던 AI가, 한 시간이 지나자 갑자기 초기에 합의했던 아키텍처 결정을 잊어버리고 엉뚱한 코드를 제안하기 시작했습니다. “왜 이렇게 멍청해졌지?“라는 의문이 들었고, 결국 새 채팅창을 열어 처음부터 모든 맥락을 다시 설명해야 했습니다.
이 경험은 결코 특별한 것이 아닙니다. LLM 기반 애플리케이션을 개발하고 운영하는 엔지니어라면 누구나 겪는 일상적인 문제입니다. 2023년 Stanford 연구진이 발표한 “Lost in the Middle” 논문(Liu et al., 2023)은 이 현상을 수학적으로 증명했습니다. 컨텍스트가 길어질수록 모델은 중간에 위치한 정보를 효과적으로 검색하지 못하며, 이는 곧 응답 품질의 저하로 이어집니다.
특히 Claude Code나 Copilot 같은 대화형 개발 도구에서 이 문제는 치명적입니다. 대화가 20-30턴을 넘어가면 불필요한 이전 디버깅 과정, 오류 메시지, 임시 코드 조각들이 컨텍스트를 채우고, 정작 중요한 아키텍처 결정이나 비즈니스 로직은 Attention의 사각지대로 밀려납니다. 기존의 /compact 명령어는 극단적인 압축으로 중요한 맥락까지 날려버리는 부작용이 있었습니다.
Contexty는 바로 이 지점에서 출발합니다. 개발자가 AI의 컨텍스트 윈도우를 직접 들여다보고, 무엇을保留하고 무엇을 버릴지 선택적으로 제어할 수 있게 해주는 도구입니다. 단순한 편의 기능이 아니라, LLM의 근본적인 아키텍처적 한계를 실무적으로 완화하는 접근법입니다.
본론
컨텍스트 누적 문제: 기술적 원인
Transformer 기반 LLM에서 컨텍스트는 단순히 “대화 기록"이 아닙니다. 매 추론 시점마다 전체 시퀀스에 대해 Self-Attention을 계산하는 비용이 발생하며, KV Cache 메모리는 시퀀스 길이에 선형적으로 증가합니다.
1
2
3
4
5
6
7
8
9
| graph TD
A[사용자 질문 + 전체 컨텍스트] --> B[Self-Attention 계산]
B --> C{컨텍스트 길이}
C -->|짧음 < 4K tokens| D[높은 Attention 집중도]
C -->|중간 4K-16K tokens| E[점진적 정보 분산]
C -->|김 > 16K tokens| F[Lost in the Middle 발생]
D --> G[정확한 응답]
E --> H[간헐적 환각]
F --> I[핵심 맥락 상실 및 환각 증가]
|
문제의 핵심은 Attention 분포의 왜곡입니다. 모델이 처리해야 할 정보량이 늘어나면, 각 정보 항목에 할당되는 Attention 가중치는 희석됩니다. 특히 최근 정보와 매우 오래된 정보는 상대적으로 높은 Attention을 받지만, 중간에 위치한 정보는 간과되는 경향이 강합니다. 이것이 바로 “Lost in the Middle” 현상입니다.
기존 해결책의 한계와 Contexty의 차별성
대화형 AI 도구들은 전통적으로 두 가지 방식으로 이 문제에 접근했습니다.
| 접근 방식 | 장점 | 단점 | 적용 사례 | | :— | :— | :— | :— | | 전체 컨텍스트 유지 | 정보 손실 없음 | 토큰 비용 증가, 품질 저하, 속도 지연 | 기본 채팅 모드 | | /compact 명령어 | 토큰 절약, 속도 개선 | 과도한 압축, 중요 맥락 상실 | Claude Code, Cursor | | Sliding Window | 일정한 토큰 유지, 예측 가능 | 오래된 핵심 정보 단절 | 일부 오픈소스 LLM | | Contexty (선택적 유지) | 중요 맥락 보존, 토큰 효율 | 수동 관리 오버헤드 | 새로운 패러다임 |
/compact 명령어의 근본적 한계는 압축의 기준이 모델의 판단에 전적으로 의존한다는 점입니다. LLM이 스스로 “이 정보는 중요하다"고 판단하여 요약하지만, 이 판단 자체가 불완전합니다. 개발자가 명시적으로 중요하다고 생각하는 아키텍처 결정이나 코딩 컨벤션은 요약 과정에서 사라질 수 있습니다.
Contexty 작동 원리: 시각화와 선택적 보존
Contexty는 컨텍스트를 구조화된 단위로 분할하여 시각화합니다. 각 발화, 코드 블록, 시스템 메시지가 독립된 노드로 표시되며, 개발자는 이를 직접 확인하고 제어할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| from dataclasses import dataclass, field
from typing import List, Optional
import tiktoken
@dataclass
class ContextNode:
"""컨텍스트의 개별 단위를 나타내는 클래스"""
id: str
role: str # 'user', 'assistant', 'system', 'tool'
content: str
token_count: int
timestamp: float
importance: float = 0.0 # 0.0 ~ 1.0
is_pinned: bool = False # 사용자가 명시적으로 보존 지정
tags: List[str] = field(default_factory=list)
class ContextManager:
"""Contexty 스타일의 컨텍스트 관리자 구현체"""
def __init__(self, max_tokens: int = 128000, model: str = "gpt-4o"):
self.nodes: List[ContextNode] = []
self.max_tokens = max_tokens
self.model = model
self.encoding = tiktoken.encoding_for_model(model)
def add_node(self, node: ContextNode) -> None:
"""새로운 컨텍스트 노드 추가"""
node.token_count = len(self.encoding.encode(node.content))
self.nodes.append(node)
self._auto_manage_context()
def pin_node(self, node_id: str) -> None:
"""특정 노드를 보존 목록에 고정"""
for node in self.nodes:
if node.id == node_id:
node.is_pinned = True
break
def unpin_node(self, node_id: str) -> None:
"""고정 해제"""
for node in self.nodes:
if node.id == node_id:
node.is_pinned = False
break
def get_active_context(self) -> List[ContextNode]:
"""현재 활성화된 컨텍스트 반환 (PIN된 것 + 최근 것들)"""
pinned = [n for n in self.nodes if n.is_pinned]
unpinned = [n for n in self.nodes if not n.is_pinned]
# PIN되지 않은 노드 중 최근 것들을 토큰 한계 내에서 포함
available_tokens = self.max_tokens - sum(n.token_count for n in pinned)
active_unpinned = []
for node in reversed(unpinned): # 가장 최근 것부터
if available_tokens >= node.token_count:
active_unpinned.insert(0, node)
available_tokens -= node.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| token_count
else:
break
return pinned + active_unpinned
def get_context_stats(self) -> dict:
"""컨텍스트 통계 정보 반환"""
pinned_nodes = [n for n in self.nodes if n.is_pinned]
active_nodes = self.get_active_context()
return {
"total_nodes": len(self.nodes),
"pinned_nodes": len(pinned_nodes),
"active_nodes": len(active_nodes),
"total_tokens": sum(n.token_count for n in self.nodes),
"active_tokens": sum(n.token_count for n in active_nodes),
"token_utilization": sum(n.token_count for n in active_nodes) / self.max_tokens * 100
}
def _auto_manage_context(self) -> None:
"""내부적으로 토큰 한계를 초과하면 오래된 비핀 노드 제거"""
while True:
active = self.get_active_context()
active_tokens = sum(n.token_count for n in active)
if active_tokens <= self.max_tokens:
break
# 가장 오래된 비핀 노드 찾아서 제거
for i, node in enumerate(self.nodes):
if not node.is_pinned:
self.nodes.pop(i)
break
else:
break # 모든 노드가 PIN된 상태
# 사용 예시
if __name__ == "__main__":
manager = ContextManager(max_tokens=8000)
# 초기 아키텍처 결정 (PIN 필요)
arch_node = ContextNode(
id="arch_001",
role="user",
content="우리 프로젝트는 Clean Architecture를 사용하며, "
"의존성 주입은 Hilt를 사용합니다. "
"Repository 패턴을 적용하고 UseCase 계층을 분리합니다.",
token_count=0,
timestamp=1000.0,
importance=1.0,
tags=["architecture", "convention"]
)
manager.add_node(arch_node)
manager.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| pin_node("arch_001") # 핵심 맥락 고정!
# 일반 디버깅 대화 (여러 턴)
for i in range(15):
debug_node = ContextNode(
id=f"debug_{i:03d}",
role="user",
content=f"이 오류 로그를 확인해줘: NullPointerException at line {i*10}",
token_count=0,
timestamp=1000.0 + i * 100,
)
manager.add_node(debug_node)
# 통계 확인
stats = manager.get_context_stats()
print(f"활성 노드: {stats['active_nodes']} / {stats['total_nodes']}")
print(f"토큰 사용률: {stats['token_utilization']:.1f}%")
print(f"고정된 노드: {stats['pinned_nodes']}")
|
이 코드는 Contexty의 핵심 아이디어를 구현한 것입니다. is_pinned 속성을 통해 개발자가 명시적으로 보존할 맥락을 지정하고, 나머지는 최근 것 위주로 자동 관리됩니다.
Step-by-Step: Contexty 실무 적용 가이드
Step 1: 컨텍스트 시각화로 현황 파악
Contexty를 처음 실행하면 현재 대화의 컨텍스트 구성을 시각적으로 보여줍니다. 어떤 정보가 얼마나 많은 토큰을 차지하는지, 각 발화의 상대적 위치는 어떻게 되는지 직관적으로 확인 가능합니다.
Step 2: 핵심 맥락 식별 및 PIN 지정
프로젝트의 아키텍처 결정, 코딩 컨벤션, 중요한 비즈니스 규칙이 포함된 대화 턴을 식별합니다. 이들을 PIN하여 어떤 상황에서도 컨텍스트에서 제거되지 않도록 보호합니다.
Step 3: 불필요한 컨텍스트 정리
이전 디버깅 과정, 이미 해결된 트러블슈팅 대화, 임시 테스트 코드 등은 UNPIN하여 자동 정리 대상으로 만듭니다. 이들은 새로운 대화가 추가될 때 자연스럽게 밀려나갑니다.
Step 4: 지속적인 모니터링
대화가 진행되면서 주기적으로 컨텍스트 통계를 확인합니다. 토큰 사용률이 80%를 넘어가면 PIN되지 않은 오래된 노드를 수동으로 정리하여 여유 공간을 확보합니다.
토큰 효율성 비교 분석
실제 프로젝트 시나리오에서 기존 방식과 Contexty 방식의 효율성을 비교해봅니다.
| 시나리오 | 전체 유지 토큰 | /compact 후 토큰 | Contexty 관리 토큰 | 핵심 정보 보존율 | | :— | :— | :— | :— | :— | | 초기 설계 (10턴) | 4,200 | 1,800 | 3,800 | 95% | | 디버깅 세션 (30턴) | 28,500 | 3,200 | 12,400 | 92% | | 리팩토링 (50턴) | 45,000 | 4,100 | 18,600 | 88% | | 전체 개발 (100턴) | 95,000 | 6,500 | 24,300 | 85% |
Contexty는 /compact보다 약 3-4배 많은 토큰을 사용하지만, 핵심 정보 보존율에서 20-30% 포인트 앞섭니다. 특히 장기 대
출처: https://news.hada.io/topic?id=28660