AI 에이전트가 자꾸 기억을 잃어버리는 이유 (그리고 우리가 해결한 방법)
당신의 AI 에이전트가 방금 사용자와 30분 동안 대화를 나눴습니다. 프로젝트 요구 사항을 논의하고, 선호도를 공유하며, 결정을 내렸습니다. 그런 다음 사용자가 새로운 세션을 시작하기 위해 /new를 입력합니다.
에이전트는 해당 대화 내용을 장기 기억으로 통합(consolidate)하려고 시도합니다. 하지만 LLM 호출이 실패합니다. Rate limit(속도 제한)이 걸리거나, 타임아웃이 발생하거나, 모델이 필요한 도구(tool)를 호출하는 대신 일반 텍스트를 반환합니다.
기억은 사라졌습니다. 30분간의 컨텍스트가 증발해 버린 것입니다.
이런 일은 생각보다 자주 발생합니다. 우리는 LemonClaw 인스턴스 전반에서 이를 추적해 보았습니다. 단일 모델에서의 메모리 통합 실패율은 약 15%에 달했습니다. 보이지 않는 인프라 역할을 해야 하는 기능 치고는 받아들일 수 없는 수치입니다.
다른 프레임워크들이 이를 처리하는 방식 (사실 처리하지 않습니다)
대부분의 AI 에이전트 프레임워크는 메모리 통합을 단순한 LLM 호출로 취급합니다. 작동하면 다행이고, 작동하지 않으면 기억은 소실됩니다.
가장 인기 있는 오픈 소스 에이전트 프레임워크인 OpenClaw는 대화에 사용하는 것과 동일한 모델을 통합 작업에도 사용합니다. 사용자가 절대 볼 일 없는 채팅 요약을 위해 $0.003의 비용과 8초 이상의 시간이 소요되는 Claude Sonnet 호출을 수행합니다. 이 호출이 실패하면(Rate limit, 타임아웃, 모델 오류 등) 프레임워크는 경고 로그만 남기고 다음으로 넘어갑니다. 사용자의 컨텍스트는 사라집니다.
또 다른 인기 프레임워크인 nanobot도 동일한 아키텍처를 가지고 있습니다. 하나의 모델, 한 번의 시도, 폴백(fallback) 없음. 통합 함수에는 타임아웃조차 설정되어 있지 않습니다. 업스트림 응답이 느려지면(Cloudflare 524 오류가 흔함) 연결이 끊길 때까지 전체 세션이 차단됩니다.
두 프레임워크 모두 통합 작업을 메인 모델과 분리하지 않습니다. 메모리 작업을 위한 폴백 로직도 없으며, "API 호출 실패"와 "API 호출은 성공했지만 모델이 요청한 대로 수행하지 않음"을 구분하지도 않습니다.
이것들은 특이 케이스가 아닙니다. 단일 모델 실패율이 15%라면, 하루에 100번의 통합을 수행하는 프레임워크는 그중 15번의 기억을 잃게 됩니다. 일주일이면 에이전트가 모든 것을 잊어버리는 대화가 105건이나 발생하는 셈입니다.
문제는 재시도(Retry) 로직보다 더 깊은 곳에 있습니다
명백한 해결책은 지수 백오프(exponential backoff)를 적용한 재시도입니다. 우리도 이를 적용했었고, 일시적인 HTTP 오류는 잘 처리했습니다.
# 재시도 루프: 1초 → 2초 → 4초 백오프
for attempt in range(3):
try:
response = await acompletion(**kwargs)
return await self._collect_stream(response)
except (RateLimitError, APIConnectionError) as e:
await asyncio.sleep(RETRY_DELAYS[attempt])
이 방식은 429 오류나 네트워크 일시 중단은 잡아냅니다. 하지만 두 가지 실패 모드는 빠져나갑니다.
실패 모드 1: 모델이 도구 호출(tool calling)을 수행하지 못함. 일부 모델, 특히 빠른 추론 엔진에서 실행되는 소규모 모델들은 복잡한 프롬프트에서 유효한 함수 호출을 생성하지 못할 때가 있습니다. API는 MidStreamFallbackError를 포함한 ServiceUnavailableError와 함께 200 응답을 반환합니다. 재시도 로직은 이를 예외로 인식하고 동일한 모델에 다시 시도하지만, 똑같은 오류를 얻게 됩니다.
실패 모드 2: 모델이 "성공"했지만 도구를 호출하지 않음. LLM이 완벽하게 유효한 응답을 반환합니다. HTTP 200. 오류 없음. 하지만 구조화된 데이터와 함께 save_memory를 호출하는 대신, 평문 텍스트 요약을 작성합니다. 재시도 엔진은 이를 성공으로 간주합니다. 통합 함수는 도구 호출 여부를 확인하고, 호출이 없으므로 포기합니다.
두 번째 실패 모드가 정말 교활합니다. 전송 레이어는 모든 것이 잘 작동했다고 생각합니다. 비즈니스 레이어는 그렇지 않다는 것을 압니다. 모델이 도구 스키마를 이해하지 못하는 상황에서는 아무리 HTTP 레벨에서 재시도를 해도 소용이 없습니다.
이중 레이어 폴백 아키텍처 (Dual-Layer Fallback Architecture)
우리는 서로 다른 레벨에서 작동하는 두 개의 독립적인 폴백 루프로 이 문제를 해결했습니다.
사용자가 /new 전송
│
▼
consolidate() ─── 비즈니스 레이어 폴백
│ "모델이 save_memory를 호출했는가?"
│ 아니오 → 체인의 다음 모델 시도
│
▼
_chat_with_retry() ─── 전송 레이어 폴백
│ HTTP 오류 → 지수 백오프
│ 모든 재시도 소진 → 폴백 체인 순회
│
▼
MODEL_MAP 폴백 체인:
llama-3.3-70b ─$0.59/M─→ qwen3-32b ─$0.29/M─→ llama-4-scout ─$0.11/M─→ gpt-4.1-mini ─→ claude-haiku
(394 TPS) (662 TPS) (594 TPS) (안정적임) (최후의 수단)
레이어 1은 전송 실패를 처리합니다. 레이어 2는 비즈니스 로직 실패를 처리합니다. 폴백 체인은 두 레이어 사이에서 공유되며 중앙 카탈로그에 한 번만 정의됩니다.
이는 '동일한 모델 재시도'와는 근본적으로 다른 접근 방식입니다. 모델이 도구 호출에 실패했을 때, 같은 프롬프트로 다시 시도하는 것은 거의 도움이 되지 않습니다. 다른 가중치와 다른 도구 호출 동작을 가진 다른 모델로 전환하는 것이 답입니다.
모델 카탈로그: 단일 진실 공급원 (One Source of Truth)
카탈로그의 모든 모델에는 다음에 시도할 모델을 가리키는 선택적 fallback 필드가 있습니다.
@dataclass(frozen=True)
class ModelEntry:
id: str
label: str
tier: str
description: str
fallback: str | None = None
hidden: bool = False # 사용자용 /model 리스트에서 숨김
MODEL_CATALOG = [
# 사용자에게 보이는 모델 (사용자가 전환 가능한 16개 모델)
ModelEntry("claude-sonnet-4-6", "Claude Sonnet 4.6", "standard",
"Recommended", fallback="claude-sonnet-4-5"),
ModelEntry("gpt-4.1-mini", "GPT-4.1 Mini", "economy",
"Stable tool calling", fallback="claude-haiku-4-5"),
# 숨겨진 통합 전용 모델 (내부용)
ModelEntry("llama-3.3-70b-versatile", "Llama 3.3 70B (Groq)", "economy",
"394 TPS", fallback="qwen3-32b", hidden=True),
ModelEntry("qwen3-32b", "Qwen3 32B (Groq)", "economy",
"662 TPS", fallback="llama-4-scout-17b-16e-instruct", hidden=True),
# ...
]
hidden=True 플래그는 내부 모델을 사용자용 /model 명령어 결과에서 제외하면서도 폴백 체인에는 참여하게 합니다. 사용자는 전환 가능한 16개의 모델을 보지만, 시스템은 19개를 사용합니다. 세 개의 숨겨진 모델은 대화의 품질보다 속도와 비용이 중요한 메모리 통합과 같은 백그라운드 작업만을 위해 존재합니다.
이 카탈로그는 모든 모델 라우팅의 단일 진실 공급원입니다. 폴백 체인에 새 모델을 추가하는 것은 코드 한 줄을 추가하는 것과 같습니다. 동기화할 설정 파일도, 업데이트할 환경 변수도, 수정할 배포 스크립트도 없습니다.
전송 레이어: 순환 감지가 포함된 체인 폴백
재시도 엔진은 무한 루프를 방지하기 위해 '방문한 모델 세트'를 사용하여 폴백 체인을 순회합니다.
async def _chat_with_retry(self, kwargs, original_model):
# 1단계: 기본 모델에 대한 지수 백오프
for attempt in range(3):
try:
response = await acompletion(**kwargs)
return await self._collect_stream(response)
except (RateLimitError, APIConnectionError, APIError) as e:
await asyncio.sleep(RETRY_DELAYS[attempt])
except AuthenticationError:
return LLMResponse(content="API key invalid.", finish_reason="error")
# 2단계: 폴백 체인 순회
visited = {original_model}
current = original_model
while True:
entry = MODEL_MAP.get(current)
if not entry or not entry.fallback or entry.fallback in visited:
break
current = entry.fallback
visited.add(current)
# 이 모델에 맞는 올바른 게이트웨이 확인
gw = self._resolve_gateway_for_model(current)
resolved = self._resolve_model(current, gateway=gw)
fb_kwargs = {**kwargs, "model": resolved}
# 대상 모델 프로토콜에 맞게 api_base 수정
if gw and gw.default_api_base:
fb_kwargs["api_base"] = gw.default_api_base
try:
response = await acompletion(**fb_kwargs)
return await self._collect_stream(response)
except Exception:
continue # 체인의 다음 모델 시도
return LLMResponse(content="Service unavailable.", finish_reason="error")
visited 세트는 매우 중요합니다. 이것이 없다면 A→B→A 같은 체인에서 영원히 루프를 돌게 될 것입니다. 이를 통해 엔진은 각 모델을 정확히 한 번씩만 시도합니다.
게이트웨이 확인도 중요합니다. 모델마다 서로 다른 API 형식이 필요합니다. Claude 모델은 Anthropic 형식 게이트웨이(/v1 접미사 없음)를 통해 라우팅됩니다. GPT 모델은 OpenAI 호환 게이트웨이(/v1 포함)를 사용합니다. Groq 모델은 또 다른 엔드포인트를 사용합니다. 폴백 엔진은 체인의 각 모델에 맞는 올바른 게이트웨이를 결정하여, OpenAI 엔드포인트에 Anthropic 요청을 보내는 것과 같은 프로토콜 불일치를 방지합니다.
이는 대부분의 프레임워크가 완전히 무시하는 디테일입니다. 그들은 모든 모델이 동일한 프로토콜을 사용한다고 가정합니다. 하지만 4가지 서로 다른 API 형식에 걸쳐 19개의 모델을 사용하는 프로덕션 환경에서는 그 가정이 즉시 깨집니다.
비즈니스 레이어: 도구 호출 검증
통합 함수는 그 위에 자신만의 폴백 루프를 추가합니다.
async def consolidate(self, session, provider, model, **kwargs):
visited = set()
current_model = model
while current_model and current_model not in visited and len(visited) <= 3:
visited.add(current_model)
response = await asyncio.wait_for(
provider.chat(messages=messages, tools=SAVE_MEMORY_TOOL, model=current_model),
timeout=30,
)
if response.has_tool_calls:
# 성공: 메모리 추출 및 저장
args = response.tool_calls[0].arguments
self.write_long_term(args["memory_update"])
self.append_history(args["history_entry"])
return True
# 모델이 도구를 호출하지 않음 — 체인의 다음 모델 시도
entry = MODEL_MAP.get(current_model)
next_model = entry.fallback if entry else None
if next_model and next_model not in visited:
current_model = next_model
continue
return False # 더 이상의 폴백 없음
return False
이 코드는 _chat_with_retry가 성공적인 응답(HTTP 200, 유효한 내용)을 반환했지만 모델이 도구를 사용하지 않은 경우를 잡아냅니다. 통합 함수는 has_tool_calls를 확인하고, 호출이 없으면 체인의 다음 모델로 이동합니다.
타임아웃 래퍼(asyncio.wait_for) 또한 폴백을 트리거합니다. 모델이 30초 이상 걸리는 경우(느린 업스트림에서 Cloudflare 524 오류가 발생할 때 흔함), 함수는 TimeoutError를 잡아서 사용자의 세션을 무한정 차단하는 대신 다음 모델을 시도합니다.
통합 작업에 Groq를 사용하는 이유
메모리 통합은 백그라운드 작업입니다. 사용자는 결과물을 보지 않습니다. 그저 제대로 작동하기만 하면 됩니다. 그렇기에 빠르고 저렴한 모델을 사용하기에 완벽한 대상입니다.
대부분의 프레임워크는 모든 작업에 동일한 비싼 모델을 사용합니다. 대화에 Claude Sonnet을 사용하고 있다면 메모리 통합에도 Claude Sonnet을 사용하는 식입니다. 이는 사람이 읽지도 않을 결과물을 만드는 작업에 입력 토큰 100만 개당 $3의 비용과 8초 이상의 시간을 쓰는 셈입니다.
우리는 통합 작업을 대화 모델과 완전히 분리했습니다. 대화는 사용자가 선택한 모델을 사용합니다. 통합 작업은 Groq에서 호스팅되는 전용 모델 체인을 사용합니다.
| 모델 | 속도 | 입력 비용 (1M) | 출력 비용 (1M) |
|---|---|---|---|
| llama-3.3-70b-versatile | 394 TPS | $0.59 | $0.79 |
| qwen3-32b | 662 TPS | $0.29 | $0.59 |
| llama-4-scout-17b-16e | 594 TPS | $0.11 | $0.34 |
| gpt-4.1-mini (기존) | ~150 TPS | $0.40 | $1.60 |
기본 모델(llama-3.3-70b)은 60개의 메시지 세션을 약 5초 만에 통합합니다. 이전 기본값이었던 gpt-4.1-mini는 8초 이상 걸렸습니다. 통합당 비용은 약 $0.003에서 약 $0.001로 떨어졌습니다.
트레이드오프: Groq 모델은 복잡한 프롬프트에서 도구 호출의 안정성이 떨어질 수 있습니다. 이것이 바로 이중 레이어 폴백이 존재하는 이유입니다. llama-3.3-70b가 도구 호출에 실패하면 qwen3-32b가 이어받습니다. 그마저 실패하면 llama-4-scout가 시도합니다. 세 개의 Groq 모델이 모두 실패하면, 100%에 가까운 도구 호출 안정성을 가진 gpt-4.1-mini가 처리합니다.
프로덕션 환경에서 기본 모델은 약 85%의 확률로 성공합니다. 체인이 gpt-4.1-mini까지 도달하는 경우는 전체 통합의 2% 미만입니다. 전체 실패율은 사실상 0입니다.
프로덕션 결과
우리는 이 시스템을 두 개의 LemonClaw 인스턴스에 배포하고 실제 Telegram 대화로 테스트했습니다.
첫 번째 배포 (단일 레이어 폴백만 적용):
메모리 통합 (archive_all): 56개 메시지
llama-3.3-70b-versatile → "함수 호출 실패"
폴백 진행 → qwen3-32b
qwen3-32b: LLM이 save_memory를 호출하지 않음, 스킵
→ "메모리 보관 실패, 세션이 지워지지 않음."
전송 레이어가 첫 번째 실패를 잡아내고 폴백했습니다. 하지만 qwen3-32b가 도구를 호출하지 않고 텍스트를 반환했습니다. 단일 레이어 폴백으로는 이를 처리할 수 없었습니다. 이것이 바로 다른 모든 프레임워크가 소리 없이 기억을 잃어버리게 되는 시나리오입니다.
두 번째 배포 (이중 레이어 폴백 적용):
메모리 통합 (archive_all): 60개 메시지
model=llama-3.3-70b-versatile → 성공
메모리 통합 완료: 60개 메시지 남음
동일한 모델, 동일한 메시지 양입니다. 이번에는 첫 번째 시도에 성공했습니다. 도구 호출 실패의 간헐적인 특성 때문에 단일 백업 모델이 아닌 폴백 체인이 필요한 것입니다.
기본 모델이 실패하더라도 체인이 이를 보완합니다.
llama-3.3-70b → 도구 호출 실패
→ consolidate() 폴백 → qwen3-32b
→ qwen3-32b가 도구를 호출하지 않음
→ consolidate() 폴백 → llama-4-scout
→ llama-4-scout가 도구를 호출하지 않음
→ consolidate() 폴백 → gpt-4.1-mini
→ gpt-4.1-mini가 save_memory 호출 성공 ✓
메모리 통합 완료
네 개의 모델을 시도한 끝에 기억이 저장되었습니다. 사용자는 "새 세션이 시작되었습니다."라는 메시지만 볼 뿐, 이 모든 과정이 일어났는지 전혀 모릅니다.
아키텍처의 격차
LemonClaw의 메모리 시스템과 대안들의 기능별 비교:
| 기능 | 일반적인 AI 에이전트 프레임워크 | LemonClaw |
|---|---|---|
| 통합 모델 | 대화 모델과 동일 (비싸고 느림) | 독립적인 모델 체인, Groq 가속 |
| 실패 처리 | 경고 로그 기록, 기억 소실 | 이중 레이어 폴백, 5단계 깊이 |
| 전송 폴백 | 동일 모델 3회 재시도 | 서로 다른 모델 간의 체인 폴백 |
| 비즈니스 로직 폴백 | 없음 | 도구 호출 검증 + 모델 전환 |
| 타임아웃 보호 | 없음 (Cloudflare 524가 세션 차단) | asyncio.wait_for(timeout=30) + 폴백 |
| 세션 정리 | 없음 (컨텍스트가 계속 커짐) | 통합 후 오래된 메시지 정리 |
| 히스토리 검색 | 없음 | HISTORY.md 롤링 윈도우, grep 검색 가능 |
| 내부용 모델 | 지원하지 않음 | 시스템 전용 모델을 위한 hidden=True |
| 순환 방지 | 불필요 (체인이 없음) | visited 세트로 A→B→A 루프 방지 |
| 게이트웨이 확인 | 단일 API 형식 가정 | 프로토콜 감지 기능을 갖춘 모델별 게이트웨이 |
이 표의 모든 행은 우리가 직접 겪었거나 다른 프레임워크의 이슈 트래커에서 관찰한 프로덕션 실패 사례를 나타냅니다. 이중 레이어 폴백, 숨겨진 모델 카탈로그, 모델별 게이트웨이 확인, 타임아웃 트리거 폴백 등은 우리가 조사한 OpenClaw, nanobot 또는 다른 어떤 오픈 소스 에이전트 프레임워크에도 존재하지 않았습니다.
우리가 배운 것
"요청 성공"이 "작업 성공"은 아닙니다. 일반적인 재시도 엔진은 HTTP 레벨에서 작동합니다. 유효한 JSON을 포함한 200 응답이 실제로는 모델이 도구를 사용하지 않아 발생한 실패라는 사실을 알 수 없습니다. 비즈니스 크리티컬한 작업에는 고유한 성공 기준과 폴백 로직이 필요합니다.
소형 모델은 대형 모델과 다르게 실패합니다. 대형 모델(GPT-4.1, Claude Sonnet)은 요청받았을 때 거의 항상 도구를 호출합니다. 빠른 추론 엔진 기반의 소형 모델들은 가끔 도구 스키마를 완전히 무시하는, 겉보기에만 유효한 응답을 생성합니다. 이는 프롬프트 엔지니어링으로 고칠 수 있는 버그가 아닙니다. 아키텍처적으로 완화해야 하는 성능의 격차입니다.
합성 데이터가 아닌 프로덕션 데이터로 테스트하세요. 6개의 가상 메시지로 진행한 초기 테스트는 모든 모델에서 통과했습니다. 하지만 도구 호출 내역, 타임스탬프, 혼용된 언어가 포함된 실제 60개 메시지 세션은 세 개의 Groq 모델 중 두 개에서 실패했습니다. 실제 데이터의 복잡성은 깨끗한 테스트 데이터가 결코 보여주지 못하는 실패 모드를 드러냅니다.
LemonClaw는 멀티 모델 라우팅, 영구 메모리 및 10개 이상의 채팅 플랫폼 연동 기능이 내장된 오픈 소스 AI 에이전트 프레임워크입니다. 여기에 설명된 이중 레이어 폴백 시스템은 오픈 소스 릴리스에 포함되어 있습니다. 직접 서버에서 실행해 보세요: github.com/hedging8563/lemonclaw
하나의 API 키로 300개 이상의 AI 모델이 필요하신가요? lemondata.cc는 OpenAI, Anthropic, Google, DeepSeek, Groq 등에 대한 통합 액세스를 제공합니다.
