설정

언어

당신의 AI Agent가 자꾸 기억을 잃어버리는 이유

L
LemonData
·2026년 3월 5일·751 조회수
당신의 AI Agent가 자꾸 기억을 잃어버리는 이유

사용자와 30분 동안 대화를 나눈 AI 에이전트가 있습니다. 프로젝트 요구 사항을 논의하고, 선호도를 공유하며, 결정을 내렸습니다. 그런 다음 사용자가 새로운 세션을 시작하기 위해 /new를 입력합니다.

에이전트는 해당 대화를 장기 메모리로 통합하려고 시도합니다. 하지만 LLM 호출이 실패합니다. Rate limit, timeout 발생, 또는 모델이 필요한 tool을 호출하는 대신 텍스트를 반환합니다.

메모리가 사라졌습니다. 30분간의 컨텍스트가 증발했습니다.

이런 일은 생각보다 자주 발생합니다. LemonClaw 인스턴스들을 추적한 결과, 단일 모델에서 메모리 통합 실패율이 약 15%에 달했습니다. 보이지 않는 인프라여야 할 기능치고는 용납할 수 없는 수치입니다.

메모리 하위 시스템뿐만 아니라 주변 제품 인터페이스를 구축하고 있다면, 이 페이지를 one-key chatbot guideself-hosted LemonClaw guide와 함께 살펴보세요. 메모리 내구성은 에이전트가 실제로 사용 가능한 애플리케이션 내에 존재할 때만 의미가 있습니다.

다른 프레임워크들이 이를 처리하는 방식 (사실 처리하지 않음)

대부분의 AI 에이전트 프레임워크는 메모리 통합을 단순한 LLM 호출로 취급합니다. 성공하면 다행이고, 실패하면 메모리는 유실됩니다.

LemonClaw의 이전 프레임워크는 대화와 통합에 동일한 모델을 사용합니다. 사용자가 절대 보지 못할 채팅을 요약하기 위해 0.003달러의 비용과 8초 이상의 시간이 소요되는 Claude Sonnet 호출을 수행합니다. 이 호출이 실패하면(rate limit, timeout, 모델 오류), 프레임워크는 경고를 기록하고 그냥 넘어갑니다. 사용자의 컨텍스트는 사라집니다.

또 다른 인기 프레임워크인 nanobot도 동일한 아키텍처를 가집니다. 하나의 모델, 한 번의 시도, fallback 없음. 통합 함수에는 timeout조차 없습니다. 업스트림이 느려지면(Cloudflare 524 오류가 흔함) 연결이 끊길 때까지 전체 세션이 차단됩니다.

어느 프레임워크도 통합을 메인 모델과 분리하지 않으며, 메모리 작업을 위한 fallback 로직도 없습니다. "API 호출 실패"와 "API 호출은 성공했지만 모델이 요청대로 수행하지 않음"을 구분하지도 않습니다.

이는 예외적인 케이스가 아닙니다. 단일 모델에서 15%의 실패율이 발생한다면, 하루에 100번의 통합을 수행하는 프레임워크는 그중 15번의 메모리를 잃게 됩니다. 일주일이면 에이전트가 모든 것을 잊어버리는 대화가 105건에 달합니다.

문제는 재시도 로직보다 더 깊은 곳에 있습니다

명백한 해결책은 지수 백오프(exponential backoff)를 포함한 재시도(retry)입니다. 우리도 이를 적용했었습니다. 이는 일시적인 HTTP 오류는 잘 처리합니다.

# Retry loop: 1s → 2s → 4s backoff
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을 수행하지 못함. 일부 모델, 특히 빠른 추론 엔진에서 실행되는 소형 모델들은 복잡한 프롬프트에서 유효한 function call을 생성하지 못하는 경우가 있습니다. API는 MidStreamFallbackError 내에 래핑된 ServiceUnavailableError와 함께 200 응답을 반환합니다. 재시도 로직은 이를 예외로 간주하고 동일한 모델로 재시도하지만, 동일한 오류가 발생합니다.

실패 모드 2: 모델이 "성공"했지만 tool을 호출하지 않음. LLM이 완벽하게 유효한 응답을 반환합니다. HTTP 200. 오류 없음. 하지만 구조화된 데이터와 함께 save_memory를 호출하는 대신 일반 텍스트 요약을 작성합니다. 재시도 엔진은 이를 성공으로 간주합니다. 통합 함수는 tool call을 확인하지만 찾지 못하고 포기합니다.

두 번째 실패 모드가 더 교활합니다. 전송 계층(transport layer)은 모든 것이 작동했다고 생각하지만, 비즈니스 계층(business layer)은 그렇지 않음을 압니다. 모델이 tool schema를 이해하지 못한다면 HTTP 수준의 재시도는 아무런 도움이 되지 않습니다.

이중 레이어 Fallback 아키텍처

우리는 서로 다른 수준에서 작동하는 두 개의 독립적인 fallback 루프로 이를 해결했습니다.

User sends /new
    │
    ▼
consolidate() ─── Business Layer Fallback
    │               "Did the model call save_memory?"
    │               No → try next model in chain
    │
    ▼
_chat_with_retry() ─── Transport Layer Fallback
    │                    HTTP errors → exponential backoff
    │                    All retries exhausted → walk fallback chain
    │
    ▼
MODEL_MAP fallback chain:
    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)                  (reliable)        (last resort)

레이어 1은 전송 실패를 처리합니다. 레이어 2는 비즈니스 로직 실패를 처리합니다. fallback 체인은 두 레이어 간에 공유되며 중앙 카탈로그에 한 번 정의됩니다.

이는 동일한 모델을 재시도하는 것과는 근본적으로 다른 접근 방식입니다. 모델이 tool 호출에 실패했을 때 동일한 프롬프트로 재시도하는 것은 거의 도움이 되지 않습니다. 가중치와 tool calling 동작이 다른 다른 모델로 전환하는 것이 효과적입니다.

모델 카탈로그: 단일 소스(Single Source of Truth)

카탈로그의 모든 모델에는 다음에 시도할 모델을 가리키는 선택적인 fallback 필드가 있습니다.

@dataclass(frozen=True)
class ModelEntry:
    id: str
    label: str
    tier: str
    description: str
    fallback: str | None = None
    hidden: bool = False  # Hidden from user-facing /model list

MODEL_CATALOG = [
    # User-visible models (16 models users can switch between)
    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"),

    # Hidden consolidation models (internal use only)
    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 명령 목록에서 숨기면서도 fallback 체인에는 참여하게 합니다. 사용자는 전환 가능한 16개의 모델을 보지만, 시스템은 19개를 사용합니다. 3개의 숨겨진 모델은 대화 품질보다 속도와 비용이 중요한 메모리 통합과 같은 백그라운드 작업을 위해서만 존재합니다.

이 카탈로그는 모든 모델 라우팅의 단일 소스입니다. fallback 체인에 새 모델을 추가하는 것은 단 한 줄을 추가하는 것을 의미합니다. 동기화할 설정 파일도, 업데이트할 환경 변수도, 수정할 배포 스크립트도 필요 없습니다.

전송 계층: 순환 감지를 포함한 체인형 Fallback

재시도 엔진은 무한 루프를 방지하기 위해 방문 세트(visited set)를 사용하여 fallback 체인을 순회합니다.

async def _chat_with_retry(self, kwargs, original_model):
    # Phase 1: Exponential backoff on the primary model
    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")

    # Phase 2: Walk the fallback chain
    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)

        # Resolve correct gateway for this model
        gw = self._resolve_gateway_for_model(current)
        resolved = self._resolve_model(current, gateway=gw)
        fb_kwargs = {**kwargs, "model": resolved}

        # Fix api_base for the target model's protocol
        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  # Try next in chain

    return LLMResponse(content="Service unavailable.", finish_reason="error")

visited 세트는 매우 중요합니다. 이것이 없으면 A→B→A와 같은 체인은 영원히 루프를 돌게 됩니다. 이를 통해 엔진은 각 모델을 정확히 한 번씩 시도합니다.

게이트웨이 확인(gateway resolution)도 중요합니다. 모델마다 서로 다른 API 형식이 필요합니다. Claude 모델은 Anthropic 형식 게이트웨이(/v1 접미사 없음)를 통해 라우팅됩니다. GPT 모델은 OpenAI 호환 게이트웨이(/v1 포함)를 통해 라우팅됩니다. Groq 모델은 또 다른 엔드포인트를 사용합니다. fallback 엔진은 체인의 각 모델에 맞는 올바른 게이트웨이를 확인하여, OpenAI 엔드포인트로 Anthropic 요청을 보내는 것과 같은 프로토콜 불일치를 방지합니다.

이는 대부분의 프레임워크가 완전히 무시하는 세부 사항입니다. 그들은 모든 모델이 동일한 프로토콜을 사용한다고 가정합니다. 프로덕션 환경에서 4가지 API 형식에 걸쳐 19개의 모델을 사용하는 경우, 이러한 가정은 즉시 깨집니다.

비즈니스 계층: Tool Call 검증

통합 함수는 그 위에 자체적인 fallback 루프를 추가합니다.

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:
            # Success: extract and save memory
            args = response.tool_calls[0].arguments
            self.write_long_term(args["memory_update"])
            self.append_history(args["history_entry"])
            return True

        # Model didn't call the tool — try next in chain
        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  # No more fallbacks

    return False

이는 _chat_with_retry가 성공적인 응답(HTTP 200, 유효한 내용)을 반환했지만 모델이 tool을 사용하지 않은 경우를 포착합니다. 통합 함수는 has_tool_calls를 확인하고, 없으면 체인의 다음 모델로 이동합니다.

timeout 래퍼(asyncio.wait_for)도 fallback을 트리거합니다. 모델이 30초 이상 걸리는 경우(느린 업스트림에서 Cloudflare 524 오류가 발생하는 경우), 함수는 TimeoutError를 포착하고 사용자의 세션을 무기한 차단하는 대신 다음 모델을 시도합니다.

통합 작업에 Groq을 사용하는 이유

메모리 통합은 백그라운드 작업입니다. 사용자는 출력을 보지 못합니다. 그저 작동하기만 하면 됩니다. 따라서 빠르고 저렴한 모델에 적합합니다.

대부분의 프레임워크는 모든 작업에 동일한 고비용 모델을 사용합니다. 대화에 Claude Sonnet을 사용한다면 메모리 통합에도 Claude Sonnet을 사용하게 됩니다. 이는 사람이 읽지도 않을 출력을 생성하는 작업에 입력 token 100만 개당 3달러의 비용과 8초 이상의 시간을 소요하는 것입니다.

우리는 통합을 대화 모델에서 완전히 분리했습니다. 대화는 사용자가 선택한 모델을 사용하고, 통합은 Groq에서 호스팅되는 전용 모델 체인을 사용합니다.

Model Speed Input Cost Output Cost
llama-3.3-70b-versatile 394 TPS $0.59/M $0.79/M
qwen3-32b 662 TPS $0.29/M $0.59/M
llama-4-scout-17b-16e 594 TPS $0.11/M $0.34/M
gpt-4.1-mini (previous) ~150 TPS $0.40/M $1.60/M

기본 모델(llama-3.3-70b)은 60개의 메시지 세션을 약 5초 만에 통합합니다. 이전 기본값(gpt-4.1-mini)은 8초 이상 걸렸습니다. 통합당 비용은 약 0.003달러에서 0.001달러로 떨어졌습니다.

트레이드오프: Groq 모델은 복잡한 프롬프트에서 tool calling의 신뢰성이 다소 떨어집니다. 이것이 바로 이중 레이어 fallback이 존재하는 이유입니다. llama-3.3-70b가 tool 호출에 실패하면 qwen3-32b가 이어받습니다. 그것도 실패하면 llama-4-scout이 시도합니다. 세 개의 Groq 모델이 모두 실패하면 gpt-4.1-mini가 거의 100%의 tool calling 신뢰성으로 이를 처리합니다.

프로덕션 환경에서 기본 모델은 약 85%의 확률로 성공합니다. 체인이 gpt-4.1-mini까지 도달하는 경우는 통합의 2% 미만입니다. 전체 실패율은 사실상 제로입니다.

프로덕션 결과

우리는 이를 두 개의 LemonClaw 인스턴스에 배포하고 실제 Telegram 대화로 테스트했습니다.

첫 번째 배포 (단일 레이어 fallback만 적용):

Memory consolidation (archive_all): 56 messages
llama-3.3-70b-versatile → "Failed to call a function"
Falling back → qwen3-32b
qwen3-32b: LLM did not call save_memory, skipping
→ "Memory archival failed, session not cleared."

전송 계층은 첫 번째 실패를 포착하고 fallback했습니다. 하지만 qwen3-32b가 tool을 호출하지 않고 텍스트를 반환했습니다. 단일 레이어 fallback은 이를 처리할 수 없었습니다. 이는 다른 모든 프레임워크에서 메모리가 소리 없이 유실되는 바로 그 시나리오입니다.

두 번째 배포 (이중 레이어 fallback):

Memory consolidation (archive_all): 60 messages
model=llama-3.3-70b-versatile → success
Memory consolidation done: 60 messages remaining

동일한 모델, 동일한 메시지 양. 이번에는 첫 번째 시도에서 성공했습니다. tool calling 실패의 간헐적인 특성이 바로 단일 백업 모델이 아닌 fallback 체인이 필요한 이유입니다.

기본 모델이 실패하더라도 체인이 이를 포착합니다.

llama-3.3-70b → tool call failed
→ consolidate() fallback → qwen3-32b
→ qwen3-32b didn't call tool
→ consolidate() fallback → llama-4-scout
→ llama-4-scout didn't call tool
→ consolidate() fallback → gpt-4.1-mini
→ gpt-4.1-mini called save_memory ✓
Memory consolidation done

네 개의 모델을 시도한 끝에 메모리가 저장되었습니다. 사용자는 "New session started."를 보게 되며, 이 과정에서 어떤 일이 일어났는지 전혀 알지 못합니다.

아키텍처의 격차

LemonClaw의 메모리 시스템과 대안들의 기능별 비교:

Capability Typical AI Agent Framework LemonClaw
Consolidation model Same as conversation (expensive, slow) Independent model chain, Groq-accelerated
Failure handling Log warning, lose memory Dual-layer fallback, 5 models deep
Transport fallback Retry same model 3x Chained fallback across different models
Business logic fallback None Tool call verification + model switching
Timeout protection None (Cloudflare 524 blocks session) asyncio.wait_for(timeout=30) + fallback
Session truncation None (context grows forever) Truncate old messages after consolidation
History search None HISTORY.md rolling window, grep-searchable
Internal models Not supported hidden=True for system-only models
Cycle prevention Not needed (no chains) visited set prevents A→B→A loops
Gateway resolution Single API format assumed Per-model gateway with protocol detection

이 표의 각 행은 우리가 직접 경험했거나 다른 프레임워크의 이슈 트래커에서 관찰한 프로덕션 실패 사례를 나타냅니다. 이중 레이어 fallback, 숨겨진 모델 카탈로그, 모델별 게이트웨이 확인, timeout 트리거 fallback 등은 nanobot이나 우리가 조사한 다른 오픈 소스 에이전트 프레임워크에는 존재하지 않습니다.

배운 점

"요청 성공"이 "작업 성공"은 아닙니다. 일반적인 재시도 엔진은 HTTP 수준에서 작동합니다. 모델이 요청한 tool을 사용하지 않았기 때문에 유효한 JSON이 포함된 200 응답이 실제로는 실패라는 것을 알 수 없습니다. 비즈니스 크리티컬한 작업에는 자체적인 성공 기준과 fallback 로직이 필요합니다.

소형 모델은 대형 모델과 다르게 실패합니다. 대형 모델(GPT-4.1, Claude Sonnet)은 요청 시 거의 항상 tool을 호출합니다. 빠른 추론 엔진의 소형 모델은 때때로 tool schema를 완전히 무시하는 유효해 보이는 응답을 생성합니다. 이는 프롬프트 엔지니어링으로 해결할 수 있는 버그가 아닙니다. 아키텍처적 완화가 필요한 역량의 차이입니다.

합성 데이터가 아닌 프로덕션 데이터로 테스트하세요. 6개의 합성 메시지를 사용한 초기 테스트는 모든 모델에서 통과했습니다. 하지만 tool 호출 기록, 타임스탬프, 혼합 언어가 포함된 실제 60개 메시지 세션은 3개의 Groq 모델 중 2개에서 실패했습니다. 실제 데이터의 복잡성은 깨끗한 테스트 데이터에서는 결코 나타나지 않는 실패 모드를 드러냅니다.

이것이 AI API rate limiting guide가 중요한 이유이기도 합니다. 메모리 시스템에는 단순히 "더 나은 모델"만 필요한 것이 아닙니다. 전송 정책, 비즈니스 로직 성공 확인, 그리고 일반적인 제공업체 장애에도 무너지지 않는 fallback 사다리가 필요합니다.


LemonClaw는 멀티 모델 라우팅, 영구 메모리, 10개 이상의 채팅 플랫폼 통합 기능이 내장된 오픈 소스 AI 에이전트 프레임워크입니다. 여기에 설명된 전체 이중 레이어 fallback 시스템은 오픈 소스 릴리스에 포함되어 있습니다. 본인의 서버에서 실행해 보세요: github.com/hedging8563/lemonclaw

하나의 API key로 300개 이상의 AI 모델이 필요하신가요? lemondata.cc는 OpenAI, Anthropic, Google, DeepSeek, Groq 등에 대한 통합 액세스를 제공합니다.

Share: