장기 실행 백그라운드 에이전트(Long-Running Background Agents): 내구성 있는 실행(Durable Execution)
운영 환경의 장기 수행 에이전트(long-horizon agent)는 while True 안에서 돌아가지 않습니다. 모든 LLM 호출은 체크포인트(checkpoint), 재시도(retry), 리플레이(replay)를 갖춘 활동(activity)이 됩니다. Temporal의 OpenAI Agents SDK 통합은 2026년 3월 정식 출시(GA)되었습니다. Claude Code Routines(Anthropic)는 로컬에 상주하는 프로세스 없이 예약된 Claude Code 호출을 실행해 줍니다. 세션은 사람 입력이 필요한 지점에서 멈추고, 배포(deploy)를 견디며, thread_id를 키로 가지는 가장 최근 체크포인트에서 재개됩니다. 새로운 사용성 뒤에는 오래된 패턴인 워크플로 오케스트레이션(workflow orchestration)이 자리잡고 있으며, 새로 들어온 입력은 단 하나입니다. 바로 LLM 호출이 복구 시 결정적으로 리플레이되어야 하는 비결정적(non-deterministic) 활동이라는 점입니다.
유형: Learn
언어: Python (stdlib, 최소 동작하는 내구성 실행 상태 기계(state machine))
선수 지식: Phase 15 · 10 (권한 모드, Permission modes), Phase 15 · 01 (장기 수행 에이전트, Long-horizon agents)
예상 시간: 약 60분
문제
네 시간 동안 실행되는 에이전트를 생각해 봅시다. 이 에이전트는 도구를 세 번 호출하고, 사용자에게 두 번 묻고, LLM을 마흔 번 호출합니다. 그러다 중간에 실행 중이던 호스트(host)가 재부팅됩니다. 그러면 어떻게 될까요?
- 단순한
while True 루프에서는 모든 것이 사라집니다. 실행은 처음부터 다시 시작됩니다. 실제 부수 효과(side effect)를 가진 세 번의 도구 호출이 또다시 실행됩니다. 사용자는 이미 승인했던 사항들을 다시 승인하라는 프롬프트를 받습니다. 이미 끝났던 마흔 번의 LLM 호출 비용도 다시 청구됩니다.
- 내구성 있는 실행(durable execution)에서는 실행이 가장 최근 체크포인트에서 재개됩니다. 이미 완료된 활동은 다시 실행되지 않고, 내구성 로그(durable log)에 기록된 결과가 그대로 리플레이됩니다. 사용자는 이미 승인한 사항을 다시 승인하지 않아도 되고, 이미 끝난 LLM 호출은 다시 청구되지 않습니다.
이는 워크플로 엔진(workflow engine)이 지난 10년간 제공해 온 패턴과 동일합니다(Temporal, Cadence, Uber의 Cherami). 새로운 점은 이제 LLM 호출 자체가 일종의 활동이라는 사실입니다. LLM 호출은 비결정적이고, 비용이 비싸며, 부수 효과를 가질 수 있어서 이 패턴에 깔끔하게 맞아떨어집니다.
이번 lesson을 관통하는 주제는 장기 수행 작업의 신뢰성이 시간이 갈수록 떨어진다는 점입니다. METR은 이를 "35분 신뢰성 저하(35-minute degradation)"라고 부르는 현상으로 관찰했는데, 실행 시간(horizon)이 길어질수록 성공률이 대략 시간의 제곱에 비례해 떨어진다는 의미입니다. 내구성 있는 실행은 신뢰성 프로파일이 견딜 수 있는 한계보다 더 긴 실행을 가능하게 합니다. 설계가 옳다면 안전하게 실패하는 새로운 방법이 되지만, 설계가 잘못되면 더 위험하게 실패하는 새로운 방법이 됩니다.
개념
활동(Activity), 워크플로(Workflow), 리플레이(Replay)
- 워크플로(Workflow): 결정적(deterministic) 오케스트레이션 코드입니다. 활동의 순서, 분기(branch), 대기(wait) 지점을 정의합니다. 이벤트 로그에서 리플레이될 때 예상치 못한 발산(divergence)이 생기지 않도록 반드시 결정적이어야 합니다.
- 활동(Activity): 비결정적이며 실패할 가능성이 있는 작업 단위입니다. LLM 호출, 도구 호출, 파일 쓰기, HTTP 요청이 모두 여기에 해당합니다. 각 활동은 입력값과, 완료 후 출력값이 함께 기록됩니다.
- 이벤트 로그(Event log): 내구성 있는 저장소(durable backing store)입니다. 모든 활동의 시작·완료·실패·재시도, 그리고 모든 워크플로 결정(decision)이 기록됩니다.
- 리플레이(Replay): 복구 시 워크플로 코드는 처음부터 다시 실행됩니다. 이미 완료된 활동은 실제로 다시 실행되지 않고 기록된 결과를 반환하며, 완료되지 않았던 활동만 실제로 실행됩니다.
이는 React가 가상 돔(virtual DOM)에 대해 다시 렌더링(re-render)하거나, Git이 커밋(commit) 기록으로부터 워킹 트리(working tree)를 다시 만들어 내는 형태와 동일합니다. 오케스트레이터의 결정성이 내구성을 저렴하게 만들어 줍니다.
LLM 호출이 이 패턴에 맞는 이유
LLM 호출은 다음과 같은 성격을 가집니다.
- 비결정적입니다. 온도(temperature)가 0보다 크면 당연하고, 0이라 하더라도 모델 버전이 바뀌면 출력이 흔들립니다.
- 비용이 비쌉니다. 금전적 비용과 지연 시간(latency) 모두 부담입니다.
- 실패할 수 있습니다. 속도 제한(rate limit)과 시간 초과(timeout)가 발생합니다.
- 부수 효과(side effect)를 가질 수 있습니다. 도구를 호출한다면 더더욱 그렇습니다.
이는 활동의 프로파일과 정확히 일치합니다. 모든 LLM 호출을 활동으로 감싸면 지수 백오프(exponential backoff)를 적용한 재시도, 재시작을 가로질러 유지되는 체크포인팅(checkpointing), 그리고 디버깅을 위한 리플레이 가능한 추적(trace)을 얻을 수 있습니다.
thread_id를 키로 가지는 체크포인트
LangGraph, Microsoft Agent Framework, Cloudflare Durable Objects, 그리고 Claude Code Routines는 모두 같은 API 형태로 수렴했습니다. thread_id 혹은 그에 해당하는 값이 세션을 식별하고, 각 상태 전이(state transition)는 백엔드(backend)에 저장되며(기본은 PostgreSQL, 개발용은 SQLite, 캐시 용도로는 Redis), 재개(resume)는 가장 최근 체크포인트를 읽어 옵니다.
백엔드 선택은 중요합니다.
- PostgreSQL: 내구성 있고, 쿼리(query)가 가능하며, 배포를 견딥니다. LangGraph의 기본값입니다.
- SQLite: 로컬 개발 환경 전용입니다. 호스트가 바뀌면 데이터를 잃습니다.
- Redis: 빠르지만 AOF 또는 스냅샷(snapshot) 설정 없이는 휘발성(ephemeral)입니다.
- Cloudflare Durable Objects: 분산 처리가 투명하게 이루어지며, 고유 키(unique key)로 범위가 정해지고, 수 시간에서 수 주까지 살아남습니다.
사람의 입력을 일급 상태(first-class state)로 다루기
제안 후 커밋(propose-then-commit)(Lesson 15)은 내구성 있는 "사람 입력 대기(waiting on human)" 상태를 필요로 합니다. 워크플로는 일시 정지하고, 외부 큐(queue)는 대기 중인 요청을 보관하며, 승인(approval)이 들어오면 정확히 그 지점에서 재개됩니다. 내구성이 없으면 이런 동작은 최선의 노력(best-effort) 수준에 머무릅니다. 내구성이 있으면 밤사이 도착한 승인을 아침에 워크플로가 그대로 이어받을 수 있습니다.
35분 신뢰성 저하(35-minute degradation)
METR은 측정한 모든 에이전트 군에서 약 35분 이상 연속 운영하면 신뢰성이 떨어진다고 보고했습니다. 작업 지속 시간을 두 배로 늘리면 실패율(failure rate)은 대략 네 배가 됩니다. 내구성 있는 실행은 이 현상 자체를 고치지 못합니다. 다만 신뢰성 프로파일이 견딜 수 있는 한계 너머까지 실행을 끌고 갈 수 있게 해 줄 뿐입니다. 안전한 패턴은 내구성을, 재진입 시 새로운 사람 검토(fresh HITL)를 요구하는 체크포인트와 결합하고, 벽시계 시간(wall-clock time)과 무관하게 총 컴퓨트(compute)에 상한을 두는 예산 차단 스위치(budget kill switch)(Lesson 13)와도 함께 사용하는 것입니다.
내구성 있는 실행이 정답이 아닌 경우
- 사람 입력이 필요 없고 몇 분 안에 끝나는 실행. 오버헤드가 이득보다 큽니다.
- 엄격하게 읽기 전용인 정보 검색(strictly read-only information retrieval) 작업.
- 정확성이 하나의 컨텍스트 윈도(context window) 안에서 끝까지 유지되어야 하는 작업. 일부 추론(reasoning) 작업이나 일부 단발 생성(one-shot generation)이 여기에 해당합니다.
사용해보기
code/main.py는 stdlib Python만으로 최소 동작하는 내구성 실행 엔진을 구현합니다. 다음 기능을 지원합니다.
- 입력값과 출력값을 JSON 이벤트 로그에 기록하는
@activity 데코레이터(decorator).
- 활동을 순서대로 실행하는 workflow 함수.
- 완료된 활동을 다시 실행하지 않고 리플레이하는
run_or_replay(workflow, event_log) 함수.
드라이버(driver)는 활동 세 개로 이루어진 워크플로를 시뮬레이션합니다. 중간에 크래시(crash)가 발생하고, (a) 단순한 재시도가 모든 것을 다시 실행하는 경우와 (b) 리플레이가 빠진 활동만 실행하는 경우를 비교해 보여 줍니다.
산출물 만들기
outputs/skill-durable-execution-review.md는 제안된 장기 실행 에이전트 배포 설계가 올바른 내구성 실행 형태를 갖췄는지 검토합니다. 활동, 결정성(determinism), 체크포인트 백엔드, 사람 입력 상태, 재개 시 사람 검토(HITL-on-resume) 정책을 점검합니다.
연습문제
-
(쉬움) code/main.py를 실행해 보세요. 단순한 재시도와 리플레이 사이의 활동 실행 횟수 차이를 관찰하세요. 크래시가 발생하는 지점을 바꿔 가며 리플레이 횟수가 그에 따라 달라지는지 확인해 보세요.
-
(중간) 예제 엔진이 thread_id를 명시적으로 사용하도록 변경하세요. 두 개의 동시 세션이 같은 엔진을 공유하도록 시뮬레이션하고, 각 세션의 이벤트 로그가 서로 충돌하지 않는지 확인하세요.
-
(중간) 예제 엔진의 활동 하나를 선택하세요. 워크플로 결정 안에 벽시계 타임스탬프(wall-clock timestamp) 같은 비결정성을 도입해 보세요. 리플레이 시 발산이 발생하는 것을 직접 보여 주세요. 실제 엔진이 이를 어떻게 처리하는지도 설명하세요(부수 효과 등록(side-effect registration), Workflow.now() API 등).
-
(중간) LangChain의 "Runtime behind production deep agents" 글을 읽으세요. 런타임(runtime)이 영속화하는 모든 상태를 나열하고, 각 상태가 어떤 실패 모드(failure mode)를 막아 주는지 짝지어 보세요.
-
(어려움) 6시간짜리 자율 코딩 작업을 위한 체크포인트 정책을 설계하세요. 어디에서 체크포인트를 잡을 것인가요? 크래시 후 재개는 어떤 모습인가요? 어떤 단계에 새로운 사람 검토(fresh HITL)가 필요한가요?
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 워크플로(Workflow) | "에이전트의 스크립트" | 결정적 오케스트레이션 코드. 이벤트 로그로부터 리플레이 가능 |
| 활동(Activity) | "한 단계" | LLM 호출이나 도구 호출 같은 비결정적 단위. 실행 전·후로 기록됨 |
| 이벤트 로그(Event log) | "저장소" | 모든 상태 전이의 내구성 있는 기록 |
| 리플레이(Replay) | "재개" | 워크플로를 다시 실행하되, 완료된 활동은 재실행 없이 기록된 결과를 반환 |
| 체크포인트(Checkpoint) | "저장 지점" | thread_id를 키로 저장된 상태. 재개 시 가장 최근 값 사용 |
thread_id | "세션 키" | 내구성 있는 상태의 범위를 정하는 식별자 |
| 35분 신뢰성 저하(35-minute degradation) | "신뢰성 감쇠" | METR 관찰: 성공률이 실행 시간에 대해 대략 제곱 비율로 하락 |
| 비결정성(Non-determinism) | "리플레이 드리프트(drift)" | 벽시계, 난수(random), LLM 출력. 부수 효과로 등록되어야 함 |
더 읽을거리