
AI 에이전트가 12단계 중 9단계에서 충돌했다면? 그게 문제가 되지 않게 만드는 방법
요약
AI 에이전트 실행 중 발생하는 프로세스 중단 및 오류 문제를 해결하기 위해 Temporal의 내구적 실행(Durable Execution) 개념을 제안합니다. 에이전트를 메모리 상의 객체가 아닌 내구적 워크플로로 설계하여, 장애 발생 시에도 중단된 지점부터 자동으로 재개하는 방법을 다룹니다.
핵심 포인트
- 에이전트 상태를 RAM이 아닌 이벤트 히스토리에 저장해야 함
- Temporal을 활용해 체크포인트 로직 없이 중단 지점부터 재개 가능
- 결정론적 워크플로와 부수 효과가 있는 액티비티의 분리가 핵심
- durable-agents 라이브러리를 통한 내구적 에이전트 구현 패턴 소개
Temporal의 내구적 실행 (Durable Execution)을 사용하여 충돌에 강하고 재개 가능한 AI 에이전트를 구축하는 방법: 프로세스를 종료해도 실행이 중단되지 않는 DeepAgents 스타일의 개발자 경험.
만약 당신이 실제 작업(도구 호출, 서브 에이전트에게 위임, 작업 완료 시까지 루프 실행 등)을 수행하는 AI 에이전트를 구축해 보았다면, 아마 다음과 같은 특유의 고통을 느껴본 적이 있을 것입니다.
에이전트가 12단계 작업 중 9단계에 도달했습니다. 웹을 검색하고, 세 개의 파일을 작성했으며, 서브 에이전트에게 작업을 위임했습니다. 그런데 갑자기 프로세스가 죽어버립니다. 배포(Deploy), OOM(Out of Memory) 킬, 네트워크 연결 끊김, 혹은 모델 제공업체의 일시적인 500 에러 등 원인은 무엇이든 결과는 같습니다: 전체 실행 과정이 사라집니다. 모든 상태(State)가 프로세스 메모리에 있었고, 프로세스 메모리가 증발해 버렸기 때문입니다.
에이전트를 프로토타이핑할 때 내구성 (Durability)은 보통 가장 먼저 고려하는 요소가 아니며, 그럴 만한 이유가 있습니다. 내구성은 재미있는 부분이 아니라 배관(Plumbing) 작업이기 때문입니다. 하지만 에이전트가 실제 작업을 수행하기 시작하면, 이를 진지하게 고려할 가치가 있습니다. 이 글은 내구성을 거의 공짜로 만들어주는 멘탈 모델, 즉 **"에이전트는 메모리 상의 객체가 아니라, 내구적 워크플로우 (Durable Workflow)이다"**라는 개념과, Temporal 위에서 실행함으로써 충돌, 재시작 및 인프라 장애로부터 살아남는 에이전트를 구축하는 방법에 대해 다룹니다.
또한 제가 구축해 온 작은 오픈 소스 라이브러리인 durable-agents를 소개해 드릴 예정입니다. 이 라이브러리는 여러분이 직접 배관 작업을 할 필요가 없도록 이 패턴을 패키징해 줍니다. 하지만 라이브러리보다 중요한 것은 아이디어입니다. 순수 Temporal을 사용하여 이 아이디어를 적용할 수 있으며, 제 저장소(Repo)를 전혀 사용하지 않더라도 무언가를 배우게 될 것입니다.
핵심 통찰: 에이전트 실행은 워크플로우다
전체 아이디어를 한 문장으로 요약하면 다음과 같습니다:
에이전트의 상태를 RAM에 저장하는 것을 멈추십시오. 대신 어떤 충돌에서도 살아남는 추가 전용(Append-only) 이벤트 히스토리에 저장하십시오.
그것이 바로 Temporal의 **내구성 있는 실행 (durable execution)**이 제공하는 것입니다. Temporal은 _워크플로 (workflows)_를 실행하기 위한 시스템으로, 워크플로는 모든 단계가 발생하는 즉시 이벤트 히스토리(event history)에 영구 저장되는 함수입니다. 만약 워커(worker) 프로세스가 종료되더라도, Temporal은 새로운 워커에서 해당 히스토리를 재생(replay)하며, 당신의 함수는 정확히 멈췄던 지점부터 다시 시작됩니다. 체크포인트(checkpoint) 코드를 작성할 필요도 없고, "N단계부터 재개"와 같은 로직도 필요 없습니다. 그저 계속 진행될 뿐입니다.
에이전트를 이 모델에 매핑하면 모든 것이 딱 맞아떨어집니다:
| 에이전트 개념 | Temporal 기본 요소 (primitive) |
|---|---|
| 에이전트 실행 | 워크플로 (workflow) |
| ... |
_워크플로 (workflow)_와 _액티비티 (activity)_의 분리가 핵심입니다. 워크플로 코드는 **결정론적 (deterministic)**입니다. 즉, 이는 오케스트레이션(orchestration) 로직이며 매번 동일하게 재생되어야 합니다. 결정론적이지 않거나 부수 효과 (side effects)가 있는 모든 것(OpenAI로의 HTTP 호출, 파일 읽기, 도구 실행 등)은 **액티비티 (activity)**에서 발생합니다. 액티비티는 실패 시 자동으로 재시도되며, 그 결과는 히스토리에 기록되므로 성공한 이후에는 다시 실행되지 않습니다.
이것이 충돌 상황에서도 생존할 수 있는 이유입니다. 워커가 돌아오면, Temporal은 마지막으로 기록된 이벤트까지 워크플로를 재생하고 재개합니다. 이미 생성한 계획, 이미 작성한 세 개의 파일, 이미 수집한 서브 에이전트의 결과물들이 모두 그대로 남아 있습니다. 오직 진행 중이던 (in-flight) 단계만 재시도될 뿐입니다.
실제 적용 사례
구체적인 예를 들어보겠습니다. 여기 완전한 리서치 에이전트가 있습니다. 주목할 점은 절차가 매우 간단하며, 내구성(durability)이 눈에 보이지 않게 작동한다는 것입니다. 당신은 일반적인 async Python 코드를 작성하기만 하면 됩니다.
먼저, 도구(tool)입니다. 도구는 단순히 데코레이터(decorator)가 붙은 async 함수일 뿐입니다. 모델에 필요한 JSON 스키마는 당신의 타입 힌트(type hints)와 독스트링(docstring)으로부터 생성됩니다:
from durable_agents import tool
@tool
...
이제 에이전트입니다. 이것은 실제 작업을 수행하는 프로세스인 **워커 (worker)**에서 실행됩니다:
from durable_agents import create_durable_agent
agent = create_durable_agent(
...
그리고 이를 트리거합니다. 여기서 잠시 주목할 만한 세부 사항이 있습니다. 바로 **클라이언트가 매우 가볍다(thin)**는 점입니다. 클라이언트는 어떠한 도구(tools)도 임포트하지 않으며, 스키마(schemas)도 알지 못합니다. 오직 태스크 큐(task-queue)의 이름만을 알고 있습니다:
from durable_agents import DurableAgentClient
client = DurableAgentClient(task_queue="research-agent")
...
에이전트 정의(모델, 도구, 프롬프트, 하위 에이전트)는 오직 워커(worker)에만 존재합니다. 워커가 곧 에이전트입니다. 웹 핸들러, 크론 잡(cron job), 또는 다른 서비스는 에이전트의 구현 방식에 의존하지 않고도 에이전트를 트리거할 수 있으며, 여러분의 도구 코드와 자격 증명(credentials)은 워커를 절대 벗어나지 않습니다. 도구 스키마(tool schemas) 또한 네트워크를 통해 전송되지 않습니다.
에이전트를 바라보는 관점을 바꿔주는 디테일: 두 가지 재시도 계층 (two retry layers)
이 부분은 모든 에이전트 빌더가 직면하는 문제를 재정의합니다. 에이전트는 완전히 다른 두 가지 방식으로 실패하며, 이에 따라 완전히 다른 두 가지 복구 전략이 필요합니다:
1. 인프라 결함 (Infrastructure faults). 네트워크가 일시적으로 끊깁니다. 모델 API가 503 에러를 반환합니다. 워커가 재배포됩니다. 이러한 문제는 일시적이며 에이전트의 잘못이 아닙. 올바른 대응은 지수 백오프(backoff)를 적용하여 작동할 때까지 정확히 동일한 작업을 재시도하는 것입니다. Temporal은 이를 액티비티(activity) 수준에서 자동으로 처리해 줍니다. 여러분은 재시도 코드를 전혀 작성할 필요가 없습니다.
2. 의미론적 결함 (Semantic faults). 모델이 잘못된 형식의 JSON을 반환합니다. 존재하지 않는 도구를 호출합니다. 도구에서 예외(exception)가 발생합니다. 동일한 호출을 재시도하는 것은 도움이 되지 않습니다. *입력값(input)*이 바뀌어야 합니다. 올바른 대응은 실패 내용을 **관찰(observation)**로서 모델에 다시 전달하여, 다음 단계에서 모델이 스스로를 수정하도록 하는 것입니다.
이 두 가지를 하나의 메커니즘으로 통합하여, 보통 잘못된 출력 시 루프를 중단시켜 버리는 try/except 문으로 처리하고 싶은 유혹이 생길 수 있습니다. 하지만 이 둘을 분리하여 유지하는 것이 바로 루프를 탄력적(resilient)으로 만드는 핵심입니다.
- 도구가 오류를 발생시키더라도 에이전트가 충돌(crash)하지는 않습니다. 예외(exception)는 포착되어 모델에게
ERROR calling tool 'x': ...와 같은 형태로 반환되며, 이는 모델이 추론할 수 있는 관찰값(observation)이 됩니다. - 잘못된 모델 출력(보통 단순히 잘린 응답)은 모델이 보게 되는 빈 결과(empty result)가 되어, 모델이 다른 방식으로 재시도하게 만듭니다.
- 한편, 이 모든 과정의 밑단에서는 Temporal이 인프라(infrastructure) 문제로 실패한 모든 액티비티(activity)를 투명하게 재시도합니다.
나쁜 모델 출력은 예외(exception)가 아니라 데이터(data)입니다. 이 단 한 번의 관점 전환이 에이전트를 극적으로 더 견고하게(robust) 만듭니다.
작동 방식 살펴보기: 멀티 에이전트 파이프라인 (multi-agent pipeline)
단일 에이전트보다 더 흥미로운 사례로 이를 보여드리기 위해, 제가 사용하는 예시를 소개합니다. 바로 디스크에 있는 레거시 Python 코드를 현대화하는 4개 에이전트 파이프라인인 **코드 고고학자 (Code Archaeologist)**입니다.
- **오케스트레이터 (orchestrator)**는 작업을 계획하고 각 단계를 위임합니다. 자체적인 도구는 없으며 오직 조정(coordinate) 역할만 수행합니다.
- **고고학자 (archaeologist)**는 레거시 코드를 읽고 무엇이 잘못되었는지(누락된 타입 힌트,
%포매팅, 전역 상태 등) 보고합니다. - **현대화 도구 (modernizer)**는 파일들을 다시 작성합니다: 어노테이션(annotations), f-strings,
pathlib, 컨텍스트 매니저(context managers) 등을 적용합니다. - **문서화 도구 (documenter)**는 독스트링(docstrings)을 추가하고
README를 작성합니다.
각 서브 에이전트는 자체적인 태스크 큐(task queue)에서 독립적이고 개별적으로 확인할 수 있는 히스토리를 가진 **자식 워크플로 (child workflow)**로 실행됩니다. 오케스트레이터는 이전 단계의 조사 결과를 담은 독립적인 태스크를 각 자식에게 전달함으로써 위임합니다. 자식은 부모의 전체 메시지 히스토리를 상속받지 않으므로, 컨텍스트(context)가 깨끗하고 범위가 제한된 상태(scoped)로 유지됩니다.
Temporal 웹 UI를 통해 이 모든 과정을 볼 수 있습니다. 오케스트레이터가 세 개의 자식 워크플로를 순차적으로 생성하는 과정, 각 워크플로의 계획 후 실행(plan-then-execute) 루프, 그리고 모든 LLM 호출과 도구 호출이 개별적이고 검사 가능한 이벤트로 나타납니다. "내 에이전트가 실제로 무엇을 했는가?"라는 질문에 대해 정확하고 시각적인 답변을 얻을 수 있습니다.

오케스트레이터(orchestrator)와 그 세 개의 하위 워크플로(child workflows): 고고학자(archaeologist), 현대화 도구(modernizer), 문서화 도구(documenter). 각각은 고유한 Temporal 워크플로(workflow)로서 실행됩니다.
결정적 장면: 워커(worker)를 강제 종료했지만, 에이전트는 개의치 않았다
이것은 전체 논지를 증명하는 테스트입니다.
저는 파이프라인(pipeline)을 시작하고 실행되도록 두었습니다. 세 개의 하위 에이전트(sub-agents)가 작업을 마쳤습니다. 분석(analysis), 재작성된 파일(rewritten files), 문서화(documentation)가 모두 완료되어 기록되었습니다. 그 후, 오케스트레이터가 마지막 단계(synthesize_result, 요약 작성)를 수행하던 중, 제가 워커(worker)를 죽였습니다. Ctrl-C. 프로세스가 종료되었습니다.
인메모리(in-memory) 프레임워크라면 이는 완전한 손실입니다. 세 에이전트 분량의 완료된 작업이 사라집니다. 처음부터 다시 시작해야 하죠.
실제로 일어난 일은 다음과 같습니다:

실행 도중 워커(worker) 종료: synthesize_result는 Attempt 2 / ∞ 상태로 머물러 있는 반면, 계획(plan)과 세 개의 하위 워크플로(child workflows)는 모두 완료(Completed) 상태를 유지합니다. 아무것도 재실행되지 않았습니다.
synthesize_result 액티비티(activity)는 재시도 루프(retry loop)에 진입하여, 빈 작업 큐(task queue)를 대상으로 인내심 있게 재시도를 시도했습니다. 결정적으로, 완료된 작업 중 어느 것도 재실행되지 않았습니다. 계획(plan)과 세 개의 하위 워크플로(child workflows)는 모두 히스토리(history)에 여전히 완료된 것으로 표시되어 있었습니다. 오직 진행 중이던 단 하나의 액티비티(activity)만이 대기 상태였습니다.
그 후 워커(worker)를 재시작했습니다. 재연결되는 즉시:

워커(worker)가 재연결되는 즉시, 대기 중이던 액티비티(activity)가 완료되었고 워크플로(workflow)가 종료되었습니다. 전체 경과 시간(wall-clock)에는 중단된 시간이 포함되지만, 작업의 손실이나 중복 실행은 없었습니다.
시스템은 대기 중이던 활동을 다시 가져와 완료했으며, 작업을 제출한 클라이언트는 아무 일도 일어나지 않은 것처럼 결과를 받았습니다. 작업 손실도, 중복 실행도 없었습니다.
이것이 바로 내구성 있는 실행 (Durable execution)입니다. 에이전트의 상태는 제가 종료시킨 프로세스가 아니라, Temporal의 이벤트 히스토리 (Event history)에 살아있었습니다.
어디서 시작해야 할지, 그리고 이에 대한 솔직한 상태
이 패턴을 시도해보고 싶다면 제 라이브러리가 꼭 필요한 것은 아닙니다. LLM 호출과 도구 (Tools)를 Temporal 활동 (Activities)으로 직접 연결할 수 있습니다. Temporal 문서는 매우 훌륭하며, 워크플로 (Workflow)와 활동 (Activity)의 분리라는 개념만 이해하면 충분합니다.
만약 이러한 내구성 위에 DeepAgents 스타일의 사용성 (@tool, @skill, 하위 에이전트 위임, 계획 후 실행 루프)을 더하고 싶다면, durable-agents가 이를 패키징해 줍니다.
현재 상태에 대해 솔직하게 말씀드리자면, 이 프로젝트는 알파 (alpha) 단계이며 현재는 OpenAI 전용입니다. 핵심 루프, 하위 에이전트 (Sub-agents), 기술 (Skills), 그리고 파일 시스템 도구는 현재 작동합니다. 지속성 메모리 (Persistent memory), 인간 참여형 (Human-in-the-loop), 그리고 더 많은 모델 제공자(Model providers) 등은 로드맵에 포함되어 있습니다. 제가 이것을 공유하는 주된 이유는 그 _아이디어_가 공유할 가치가 있기 때문입니다. 코드는 이를 연결하는 방법을 보여주는 작고 실행 가능한 하나의 예시일 뿐이며, 여러분은 다르게 연결할 수도 있습니다.
다음 단계
이 글은 시리즈의 첫 번째 글입니다. 내구성 있는 실행은 기초가 되지만, 동시에 메모리 제한이 있는 에이전트 (Memory-bound agents)에서 제대로 구현하기 정말 어려운 기능들을 가능하게 만드는 핵심이기도 합니다:
- 인간 참여형 (Human-in-the-loop): 워크플로를 Temporal 시그널 (Signal)에 주차함으로써, 프로세스를 열어두지 않고도 인간의 승인을 기다리며 며칠 동안 에이전트를 일시 중지할 수 있습니다.
- 지속성 메모리 (Persistent memory): 단일 실행보다 더 오래 지속되는 사실과 선호도.
- 더 깊은 관찰 가능성 (Observability): 이벤트 히스토리 기반의 트레이싱 (Tracing) 및 스트리밍 (Streaming).
이 기능들을 구현하면서 각각에 대해 글을 써 내려갈 예정입니다.
"에이전트는 워크플로다"라는 프레임워크가 유용했다면, 여러분은 자신의 에이전트에서 내구성을 어떻게 생각하고 있는지 듣고 싶습니다. 코드를 살펴보고 싶다면 아래에 리포지토리 (Repo)와 실행 가능한 크래시 테스트 (Crash-test) 링크를 남겨두었습니다.
Repo: https://github.com/piotrwachowski/durable-agents
크래시 테스트 재현 (Reproduce the crash test): docs/09-examples.md 파일의 내구성 테스트 (Durability test) (워커 (worker)를 충돌시키고 재개되는 과정을 관찰하세요) 섹션을 참조하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기