AI 에이전트가 사용자와 30분 동안 대화를 나눴다고 가정해 봅시다. 프로젝트 요구 사항을 논의하고, 선호도를 공유하며, 결정을 내렸습니다. 그런 다음 사용자가 새로운 세션을 시작하기 위해 /new를 입력합니다.
에이전트는 해당 대화 내용을 장기 메모리로 통합(consolidate)하려고 시도합니다. 하지만 LLM 호출이 실패합니다. Rate limit에 걸리거나, Timeout이 발생합니다. 또는 모델이 필요한 tool을 호출하는 대신 일반 텍스트를 반환합니다.
메모리는 사라졌습니다. 30분간의 컨텍스트가 증발해 버린 것입니다.
이런 일은 생각보다 자주 발생합니다. Hermes Agent 런타임에서 전용 fallback 기능을 추가하기 전까지, 단일 모델에서의 메모리 통합 실패율은 약 15%에 달했습니다. 보이지 않는 인프라 역할을 해야 하는 기능치고는 용납할 수 없는 수치입니다.
단순히 메모리 하위 시스템뿐만 아니라 전체적인 제품 표면을 구축하고 있다면, 이 페이지를 one-key chatbot guide 및 AI API rate limiting guide와 함께 살펴보세요. 메모리 내구성은 에이전트가 실제로 사용 가능한 애플리케이션 내에 존재할 때만 의미가 있습니다.
다른 프레임워크들이 이를 처리하는 방식 (사실 처리하지 않음)
대부분의 AI 에이전트 프레임워크는 메모리 통합을 단순한 LLM 호출로 취급합니다. 성공하면 다행이고, 실패하면 메모리는 유실됩니다.
Hermes Agent의 초기 버전은 대화와 통합에 동일한 모델을 사용했습니다. 사용자가 절대 볼 수 없는 채팅 요약을 위해 0.003달러의 비용이 들고 8초 이상 걸리는 Claude Sonnet 호출을 수행했습니다. 이 호출이 실패하면(rate limit, timeout, 모델 오류 등) 프레임워크는 경고 로그를 남기고 그냥 넘어갑니다. 사용자의 컨텍스트는 사라집니다.
또 다른 인기 프레임워크인 nanobot도 동일한 아키텍처를 가지고 있습니다. 하나의 모델, 한 번의 시도, fallback 없음. 통합 함수에는 심지어 timeout 설정도 없습니다. 업스트림이 느려지면(Cloudflare 524 오류는 흔합니다) 연결이 끊길 때까지 전체 세션이 차단됩니다.
두 프레임워크 모두 통합 작업을 메인 모델과 분리하지 않습니다. 메모리 작업을 위한 fallback 로직도 없으며, "API 호출 실패"와 "API 호출은 성공했지만 모델이 요청한 대로 수행하지 않음"을 구분하지도 않습니다.
이것들은 예외적인 케이스가 아닙니다. 단일 모델에서 15%의 실패율이 발생한다면, 하루에 100번의 통합을 수행하는 프레임워크는 그중 15번의 메모리를 잃게 됩니다. 일주일이면 에이전트가 모든 것을 잊어버리는 대화가 105건에 달합니다.
문제는 Retry 로직보다 더 깊은 곳에 있습니다
가장 뻔한 해결책은 지수 백오프(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을 수행하지 못함. 일부 모델, 특히 빠른 추론 엔진에서 실행되는 작은 모델들은 복잡한 프롬프트에서 유효한 함수 호출을 생성하지 못하는 경우가 가끔 있습니다. API는 MidStreamFallbackError 내에 래핑된 ServiceUnavailableError와 함께 200 상태 코드를 반환합니다. 재시도 로직은 예외를 감지하고 동일한 모델로 재시도하지만, 똑같은 오류를 얻게 됩니다.
실패 모드 2: 모델이 "성공"했지만 tool을 호출하지 않음. LLM은 완벽하게 유효한 응답을 반환합니다. HTTP 200. 오류 없음. 하지만 구조화된 데이터와 함께 save_memory를 호출하는 대신, 일반 텍스트 요약을 작성합니다. 재시도 엔진은 이를 성공으로 간주합니다. 통합 함수는 tool 호출을 확인하지만 찾지 못하고 포기합니다.
두 번째 실패 모드가 정말 교활한 부분입니다. 전송 계층(transport layer)은 모든 것이 잘 작동했다고 생각하지만, 비즈니스 계층(business layer)은 그렇지 않다는 것을 알고 있습니다. HTTP 수준의 재시도는 사용자의 tool 스키마를 이해하지 못하는 모델을 고칠 수 없습니다.
이중 레이어 Fallback 아키텍처 (Dual-Layer Fallback Architecture)
저희는 서로 다른 수준에서 작동하는 두 개의 독립적인 fallback 루프를 통해 이 문제를 해결했습니다.
사용자가 /new 전송
│
▼
consolidate() ─── 비즈니스 계층 Fallback
│ "모델이 save_memory를 호출했는가?"
│ 아니오 → 체인의 다음 모델 시도
│
▼
_chat_with_retry() ─── 전송 계층 Fallback
│ HTTP 오류 → 지수 백오프
│ 모든 재시도 소진 → fallback 체인 순회
│
▼
MODEL_MAP fallback 체인:
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는 비즈니스 로직 실패를 처리합니다. Fallback 체인은 두 레이어 간에 공유되며 중앙 카탈로그에 한 번 정의됩니다.
이는 동일한 모델을 재시도하는 것과는 근본적으로 다른 접근 방식입니다. 모델이 tool 호출에 실패했을 때, 동일한 프롬프트로 재시도하는 것은 거의 도움이 되지 않습니다. 가중치가 다르고 tool calling 동작이 다른 다른 모델로 전환하는 것이 효과적입니다.
모델 카탈로그: 단일 진실 공급원 (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 명령에 나타나지 않게 하면서도 fallback 체인에 참여할 수 있게 합니다. 사용자는 전환 가능한 16개의 모델을 보지만, 시스템은 19개를 사용합니다. 3개의 숨겨진 모델은 대화 품질보다 속도와 비용이 더 중요한 메모리 통합과 같은 백그라운드 작업을 위해 존재합니다.
이 카탈로그는 모든 모델 라우팅의 단일 진실 공급원입니다. Fallback 체인에 새 모델을 추가하는 것은 한 줄을 추가하는 것을 의미합니다. 동기화할 설정 파일도, 업데이트할 환경 변수도, 수정할 배포 스크립트도 필요 없습니다.
전송 계층: 순환 감지 기능이 있는 체인형 Fallback
재시도 엔진은 무한 루프를 방지하기 위해 방문 세트(visited set)를 사용하여 fallback 체인을 순회합니다.
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단계: fallback 체인 순회
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)
# 이 모델에 맞는 올바른 gateway 확인
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와 같은 체인은 영원히 루프를 돌게 됩니다. 세트를 사용하면 엔진은 각 모델을 정확히 한 번씩만 시도합니다.
Gateway 확인도 중요합니다. 모델마다 API 형식이 다릅니다. Claude 모델은 Anthropic 형식의 gateway(/v1 접미사 없음)를 통해 라우팅됩니다. GPT 모델은 OpenAI 호환 gateway(/v1 포함)를 통해 라우팅됩니다. Groq 모델은 또 다른 엔드포인트를 사용합니다. Fallback 엔진은 체인의 각 모델에 대해 올바른 gateway를 확인하여 Anthropic 요청을 OpenAI 엔드포인트로 보내는 것과 같은 프로토콜 불일치를 방지합니다.
이는 대부분의 프레임워크가 완전히 무시하는 세부 사항입니다. 그들은 모든 모델이 동일한 프로토콜을 사용한다고 가정합니다. 하지만 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:
# 성공: 메모리 추출 및 저장
args = response.tool_calls[0].arguments
self.write_long_term(args["memory_update"])
self.append_history(args["history_entry"])
return True
# 모델이 tool을 호출하지 않음 — 체인의 다음 모델 시도
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 # 더 이상의 fallback 없음
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을 사용하고 있을 것입니다. 이는 사람이 읽지도 않을 출력을 만드는 작업에 입력 토큰 100만 개당 3달러의 비용과 통합당 8초 이상의 시간을 쓰는 셈입니다.
저희는 통합 작업을 대화 모델과 완전히 분리했습니다. 대화는 사용자가 선택한 모델을 사용합니다. 통합은 Groq에서 호스팅되는 전용 모델 체인을 사용합니다.
| 모델 | 속도 | 입력 비용 | 출력 비용 |
|---|---|---|---|
| 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 (이전 기본값) | ~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% 미만입니다. 전체 실패율은 사실상 제로에 가깝습니다.
프로덕션 결과
저희는 이를 두 개의 Hermes Agent 런타임 인스턴스에 배포하고 실제 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."라는 메시지를 보게 되며, 이 과정에서 어떤 일이 일어났는지 전혀 알지 못합니다.
아키텍처의 격차
기능별로 비교한 메모리 시스템과 대안들:
| 기능 | 일반적인 AI 에이전트 프레임워크 | 이중 레이어 fallback 설계 |
|---|---|---|
| 통합 모델 | 대화 모델과 동일 (비싸고 느림) | 독립적인 모델 체인, Groq 가속 |
| 실패 처리 | 경고 로그 기록, 메모리 유실 | 이중 레이어 fallback, 5단계 깊이 |
| 전송 fallback | 동일 모델 3회 재시도 | 서로 다른 모델 간의 체인형 fallback |
| 비즈니스 로직 fallback | 없음 | Tool call 검증 + 모델 전환 |
| Timeout 보호 | 없음 (Cloudflare 524가 세션 차단) | asyncio.wait_for(timeout=30) + fallback |
| 세션 단축 (Truncation) | 없음 (컨텍스트가 무한히 커짐) | 통합 후 오래된 메시지 삭제 |
| 히스토리 검색 | 없음 | HISTORY.md 롤링 윈도우, grep 검색 가능 |
| 내부 전용 모델 | 지원 안 함 | 시스템 전용 모델을 위한 hidden=True |
| 순환 방지 | 필요 없음 (체인 없음) | visited 세트로 A→B→A 루프 방지 |
| Gateway 확인 | 단일 API 형식 가정 | 프로토콜 감지 기능이 있는 모델별 gateway |
이 표의 각 행은 저희가 직접 경험했거나 다른 프레임워크의 이슈 트래커에서 관찰한 프로덕션 실패 사례를 나타냅니다. 이중 레이어 fallback, 숨겨진 모델 카탈로그, 모델별 gateway 확인, timeout 트리거 fallback 등은 nanobot이나 저희가 조사한 다른 어떤 오픈 소스 에이전트 프레임워크에도 존재하지 않습니다.
우리가 배운 것
"요청 성공"이 "작업 성공"은 아닙니다. 일반적인 재시도 엔진은 HTTP 수준에서 작동합니다. 그들은 유효한 JSON을 포함한 200 응답이 실제로는 모델이 요청한 tool을 사용하지 않았기 때문에 실패한 것이라는 사실을 알 수 없습니다. 비즈니스 크리티컬한 작업에는 자체적인 성공 기준과 fallback 로직이 필요합니다.
작은 모델은 큰 모델과 다르게 실패합니다. 큰 모델(GPT-4.1, Claude Sonnet)은 요청받았을 때 거의 항상 tool을 호출합니다. 빠른 추론 엔진의 작은 모델들은 때때로 tool 스키마를 완전히 무시하는, 그럴듯해 보이는 응답을 생성합니다. 이는 프롬프트 엔지니어링으로 해결할 수 있는 버그가 아닙니다. 아키텍처적인 완화가 필요한 역량의 격차입니다.
합성 데이터가 아닌 프로덕션 데이터로 테스트하세요. 6개의 합성 메시지를 사용한 초기 테스트는 모든 모델에서 통과했습니다. 하지만 tool 호출 기록, 타임스탬프, 혼합 언어가 포함된 실제 60개 메시지 세션은 세 개의 Groq 모델 중 두 개에서 실패했습니다. 실제 데이터의 복잡성은 깨끗한 테스트 데이터가 결코 보여주지 못하는 실패 모드를 드러냅니다.
이것이 바로 AI API rate limiting guide가 중요한 이유이기도 합니다. 메모리 시스템에는 단순히 "더 좋은 모델"만 필요한 것이 아닙니다. 전송 정책, 비즈니스 로직 성공 체크, 그리고 일반적인 공급자 장애 상황에서도 무너지지 않는 fallback 사다리가 필요합니다.
이 기사는 Hermes Agent의 안정성 패턴을 설명합니다. 여기에 제시된 아키텍처 패턴을 메모리 내구성, fallback 설계 및 gateway 인식 모델 라우팅의 참고 자료로 활용하세요.
하나의 API 키로 300개 이상의 AI 모델이 필요하신가요? tokenlab.sh는 OpenAI, Anthropic, Google, DeepSeek, Groq 등에 대한 통합 액세스를 제공합니다.
