사내에서 LLM 에이전트를 프로덕션에 올려본 사람이면 한 번쯤 겪는 상황이 있다. 로컬에서 프롬프트 몇 개 던져보면 기가 막히게 잘 답하는데, 실제 서비스에 붙여놓고 세션이 길어지면 갑자기 바보가 된다. 그러면 팀에서 나오는 얘기는 대체로 이렇다. "모델을 더 좋은 걸로 바꾸자", "프롬프트를 더 잘 써보자".
Dev.to에 올라온 "Your Agent Doesn't Have a Reasoning Problem, It Has a Memory Problem"은 정확히 이 지점을 찌른다. 저자가 만든 멀티 에이전트 게임(Horcrux Hunt)에서 에이전트에게 사람이 직접 다듬은 완벽한 컨텍스트를 주면 95% 이상 최적 판단을 했는데, 실제 게임 승률은 23%였다는 것이다. 추론 능력은 멀쩡했고, 문제는 그 모델에게 무엇을 기억시켜서 넣어주느냐였다.
왜 지금 이 얘기가 중요한가
인프라 하는 입장에서 보면 이건 사실 익숙한 문제다. 애플리케이션은 멀쩡한데 캐시 무효화를 잘못해서 stale 데이터를 뿌리는 상황, 로그가 너무 많아서 정작 필요한 에러 라인을 못 찾는 상황과 구조가 똑같다. 다만 LLM에서는 이 문제가 돈과 레이턴시로 즉시 환산된다는 게 차이다.
원문에서 관찰된 실패 유형은 네 가지로 정리된다. 이 분류가 꽤 실용적이라 그대로 옮긴다.
- Stale Memory — 12턴에 받은 긍정 신호를 25턴에도 믿고 행동. 그 사이 상대가 위치를 바꿔놨다. 캐시 TTL 안 걸어둔 것과 같다.
- Retrieval Failure — 8턴에 "여긴 비었음"이라는 신호가 컨텍스트 안에 분명히 있었는데, 그 뒤 4,800토큰에 파묻혀서 모델이 못 찾음. 6,000토큰 안에서 20토큰짜리 신호는 사실상 없는 것과 같았다.
- Memory Overload — 40턴쯤 컨텍스트가 6,000토큰이 되니 생성에 25초. Lambda 타임아웃 30초에 걸려서 게임이 통째로 죽음.
- Decay Problem — 3턴의 중요한 신호가 40턴에도 유효했는데, 시간 가중치나 관련도 스코어링 없이 그냥 시간순으로 쌓기만 하니 노이즈에 묻힘.
네 개 다 "추론 실패로 위장한 메모리 아키텍처 문제"다. 그리고 세 번째 항목 — Lambda 타임아웃 — 은 완전히 인프라 담당자 티켓으로 떨어지는 종류의 장애다.
핵심: 컨텍스트를 3계층으로 쪼갠다
원문이 제시하는 해법은 메모리를 비용이 다른 세 계층으로 분리하는 것이다. 인프라 하는 사람한테는 L1 캐시 / 애플리케이션 연산 / 영구 스토리지 3단 구조로 보면 바로 이해된다.
Layer 3 Event Store (DynamoDB) — 전부 저장, 원가 ¢ → LLM에 절대 안 들어감
↓ 계산
Layer 2 Computation (Python) — 확률/엔트로피 계산, 0 토큰
↓ 압축 주입
Layer 1 Context Window — 지금 이 판단에 필요한 것만, $$$
포인트는 Layer 3는 절대 컨텍스트 윈도우에 직접 들어가지 않는다는 것. 전체 이력은 DynamoDB에 TTL 걸어서 쌓아두되, LLM한테는 Layer 2가 계산해낸 결과값만 준다.
Layer 2의 핵심은 신념(belief) 갱신 로직이다. 원문 코드를 최소한으로 다듬으면 이렇다.
class BeliefMap:
def __init__(self, locations):
self.beliefs = {loc: 1.0 / len(locations) for loc in locations}
self.last_updated = {loc: 0 for loc in locations}
def update(self, location, signal, turn):
if signal == "positive":
self.beliefs[location] *= 3.0
elif signal == "negative":
self.beliefs[location] *= 0.1
elif signal == "destroyed":
self.beliefs[location] = 0.0
self.last_updated[location] = turn
# 오래된 신념은 감쇠 — 그 사이 상황이 바뀌었을 수 있다
for loc in self.beliefs:
if self.last_updated[loc] < turn - 10:
self.beliefs[loc] *= 0.7
self._normalize()
여기서 *= 0.7 감쇠가 바로 캐시 TTL의 확률 버전이다. Stale Memory 문제를 코드 두 줄로 막는다. 이 계산은 파이썬으로 수십 마이크로초, 토큰 비용 0원이다. 같은 판단을 "50턴치 서사를 읽고 추론해줘"로 LLM에 맡기면 토큰과 레이턴시를 다 지불하게 된다.
그리고 Layer 1에 주입되는 건 이렇게 압축된다.
# Before: 50턴 서사 나열 → 2,000+ 토큰, $0.015, 8초
# After: 55 토큰, $0.0002, 1초
"Horcrux likely at: Hogwarts (34%), Azkaban (22%), Ministry (18%).
Entropy: 1.4 bits (medium confidence). Budget: 3 actions remaining.
Ron available in 2 turns. Last signal: negative @ Godric's Hollow."
97% 압축인데 왜 손실 압축이 통하느냐. 원문의 설명이 정확하다. 확률분포가 충분통계량(sufficient statistic)이기 때문이다. Hogwarts가 34%라면, 그게 3턴의 긍정 신호에서 왔든 20턴간 부정 신호가 없어서 왔든 판단에는 영향이 없다. 의사결정에 필요한 정보는 이미 확률 안에 다 들어있다.
엔트로피로 호출 자체를 게이팅한다
여기서 한 발 더 나간 게 재밌다. 모든 판단이 같은 비용을 쓸 필요는 없다는 것.
def decide(belief_map):
h = calculate_entropy(belief_map) # Shannon entropy, bits
if h < 1.0: # 확신 있음 → LLM 안 부름
return heuristic(belief_map.top_target()) # 0 토큰
elif h < 2.5: # 애매함 → 압축 컨텍스트
return llm_decide(compress_to_55_tokens(belief_map))
else: # 진짜 어려움 → 투자할 가치 있음
return llm_decide(build_full_context(belief_map)) # 200-500 토큰
원문 기준으로 전체 판단의 약 35%가 엔트로피 1.0 미만 구간에 들어가서 토큰 0원으로 처리됐다. 45%는 55토큰, 나머지 20%만 200~500토큰을 쓴다. 확률이 이미 한 곳에 쏠려 있는데 "그래도 LLM한테 한번 물어볼까"는 그냥 낭비라는 얘기다.
인프라 용어로 번역하면 이건 불확실성 기반 캐시 히트다. 우리가 CDN 앞단에서 "정적 리소스는 오리진까지 안 간다"고 판단하는 것과 같은 결정을, LLM 호출에 대해 하는 것. 참고로 엔트로피가 낮아도 10%는 일부러 다른 선택을 하는 ε-greedy 탐색을 넣어서 패턴 고착을 막았다고 한다.
실무 관점: 도입할 때 실제로 부딪히는 것들
1. "요약 압축"과 "상태 압축"을 헷갈리지 마라
가장 흔한 착각이다. 대화가 길어지면 보통 "지난 대화를 LLM으로 요약해서 넣자"로 간다. 그런데 이건 Layer 1으로 Layer 1 문제를 푸는 것이라 비용이 안 줄고, 요약할 때마다 정보가 비가역적으로 뭉개진다. 요약의 요약의 요약을 5번 하면 원본과 무관한 텍스트가 된다.
원문 방식은 다르다. 자연어를 요약하는 게 아니라 구조화된 상태(확률분포, 카운터, 플래그)를 코드로 유지하고, 그걸 렌더링해서 넣는다. 상태는 언제든 Layer 3에서 재계산 가능하니까 손실이 누적되지 않는다. 이게 핵심 차이다.
실무 적용 예를 들면 — 고객상담 에이전트라면 대화 전문 대신 이런 걸 유지한다.
{
"intent": "refund_request", "intent_confidence": 0.82,
"verified_identity": true,
"order_ids": ["A-1024"],
"attempted_solutions": ["reship_declined"],
"escalation_score": 0.4,
"turns": 14
}
14턴 대화 원문이 3,000토큰이라면 이건 60토큰 남짓이다. 그리고 intent가 바뀌면 attempted_solutions를 감쇠시키는 로직(위의 *= 0.7)을 넣으면 stale 문제도 같이 해결된다.
2. 흔한 함정: 타임아웃은 항상 컨텍스트가 최대일 때 터진다
원문의 3번 실패 모드가 바로 이거다. 컨텍스트가 커질수록 생성 시간이 늘어나는데, 로컬 테스트는 짧은 세션으로만 하니까 절대 안 걸린다. 프로덕션에서 장시간 세션이 쌓이는 시점에 갑자기 터진다.
Lambda에서는 이런 식으로 나온다.
$ aws logs tail /aws/lambda/agent-orchestrator --since 10m
2024-XX-XX 14:22:31 START RequestId: 8f3a... Version: $LATEST
2024-XX-XX 14:23:01 2024-XX-XXT14:23:01.442Z 8f3a... Task timed out after 30.02 seconds
2024-XX-XX 14:23:01 END RequestId: 8f3a...
2024-XX-XX 14:23:01 REPORT RequestId: 8f3a... Duration: 30020.11 ms
Billed Duration: 30000 ms Memory Size: 512 MB Max Memory Used: 214 MB
여기서 Max Memory Used: 214 MB가 함정이다. 메모리는 널널하니까 "메모리 문제 아니네" 하고 넘어가는데, 실제 원인은 컨텍스트 토큰 수에 비례한 LLM 응답 지연이다. Lambda 메모리를 올려도 안 고쳐진다. 컨텍스트를 줄여야 고쳐진다.
API Gateway를 앞에 뒀다면 그쪽 상한(기본 29초로 알려져 있으나 최신 설정값은 AWS 공식 문서 확인 필요)에 먼저 걸려서 이렇게 나오기도 한다.
{"message": "Endpoint request timed out"}
HTTP/1.1 504 Gateway Timeout
대응은 두 갈래다. 근본 대응은 컨텍스트 압축, 임시 대응은 동기 요청을 끊고 SQS/Step Functions로 비동기 전환 + 스트리밍 응답. 다만 비동기로 도망치면 토큰 비용은 그대로 남는다는 걸 잊지 말자.
3. 벡터 스토어를 만능으로 보지 마라
"컨텍스트가 넘친다 → RAG 붙이자 → 벡터 DB"는 반사신경처럼 나오는 답인데, 원문의 2번(Retrieval Failure)과 4번(Decay) 실패 모드는 벡터 검색만으로는 안 풀린다.
- 임베딩 유사도는 최신성을 모른다. 12턴의 신호와 25턴의 신호가 의미상 비슷하면 둘 다 똑같이 잘 검색된다. 시간 가중치를 스코어에 직접 섞거나, 메타데이터 필터로 turn/timestamp 범위를 걸어야 한다.
- 애초에 계산으로 얻을 수 있는 값은 검색할 이유가 없다. 확률분포처럼 결정론적으로 산출되는 상태를 벡터로 저장하고 유사도 검색으로 꺼내오는 건, RDB에 있는 값을 Elasticsearch 전문검색으로 찾는 것만큼 어색하다.
정리하면 계층별 도구 선택은 이렇게 나뉜다. 확정적 상태(카운터, 플래그, 확률, 잔여 예산)는 Redis 같은 세션 스토어, 전체 감사 로그와 재계산 소스는 DynamoDB/RDB에 TTL, 비정형 참조 지식(문서, 과거 유사 케이스)만 벡터 스토어. 하나로 다 하려다 셋 다 어정쩡해진다.
4. 계측 없이 압축하면 사고 난다
손실 압축은 "무엇을 버려도 되는지" 확신이 있을 때만 안전하다. 원문 사례에서는 확률분포가 충분통계량이라는 근거가 명확했다. 우리 서비스에서 그게 성립하는지는 별개 문제다.
그래서 도입 순서는 반드시 이래야 한다. 먼저 턴별 토큰 수와 레이턴시를 로깅해서 어디서 폭발하는지 본다. 그다음 압축 버전과 원본 버전을 같은 입력으로 돌려 판단이 갈리는 케이스를 세어본다(shadow 비교). 마지막에 전환한다. 순서를 뒤집으면 "왜 이번 달부터 에이전트가 이상하죠?" 소리를 듣는다.
'Tech_News' 카테고리의 다른 글
| OAuth, 인가 코드와 토큰을 왜 두 번에 나눠 받나 — 실무자용 정리 (0) | 2026.08.25 |
|---|---|
| MCP 로드맵 뜯어보기 — 장시간 에이전트, 권한 위임, 수천 개 Tool을 인프라 관점에서 (0) | 2026.08.24 |
| Kagi가 페이월 링크를 검색 결과에서 빼주기 시작했다 — 검색 품질 필터를 인프라 관점에서 뜯어보기 (0) | 2026.08.23 |
| 알리익스프레스 탭이 내 블루투스 멀티포인트를 죽인 이유: 무음 WebAudio 핑거프린팅 (0) | 2026.08.22 |
| [블로그 초안] 장난으로 산 도메인이 전쟁에 끌려간 이야기 — SondeHub로 보는 공개 API 운영의 현실 (0) | 2026.08.21 |
