
기다리고, 깨어나고, 행동할 수 있는 AI 에이전트 구축하기
요약
Temporal, FastAPI, Ollama 등을 활용하여 주문 프로세스를 지속적으로 추적하고 관리하는 장기 실행 AI 에이전트 'Order Supervisor' 구축 방법을 소개합니다. 각 주문에 독립적인 워크플로를 할당하여 시스템 재시작 후에도 상태를 유지하며 이벤트에 반응하는 아키텍처를 다룹니다.
핵심 포인트
- Temporal을 활용한 장기 실행 워크플로 및 상태 유지 관리
- 주문당 하나의 독립적인 AI 감독관(Supervisor) 할당 구조
- FastAPI, PostgreSQL, Ollama를 결합한 풀스택 에이전트 아키텍처
- 이벤트 기반의 능동적 반응 및 대기 메커니즘 구현
대부분의 AI 에이전트는 작업에 응답하고 멈춥니다. 하지만 현실 세계의 프로세스는 항상 그렇게 작동하지 않습니다. 프로세스는 몇 시간 또는 며칠 동안 지속될 수도 있습니다.
만약 에이전트가 주문의 생성부터 완료까지 책임을 유지할 수 있다면 어떨까요?
그것이 바로 제가 Temporal, FastAPI, PostgreSQL, Ollama, 그리고 Next.js를 사용하여 구축한 장기 실행 AI 에이전트인 Order Supervisor가 되었습니다. 이 블로그에서는 이것이 어떻게 작동하는지, 아키텍처 뒤에 숨겨진 결정들, 그리고 이를 구축하면서 배운 점들을 살펴보겠습니다.
목표는 각 주문에 고유한 감독관(supervisor)을 부여하는 것이었습니다. 감독관은 발생한 일을 추적하고, 새로운 이벤트가 들어오면 반응하며, 필요할 때만 조치를 취해야 합니다. 주의를 기울여야 할 사항이 없다면, 다음 이벤트나 예정된 점검이 있을 때까지 단순히 기다립니다.
또한 저는 시스템이 재시작된 후에도 활성 주문의 상태를 잃어버리는 대신, 중단된 지점부터 계속 진행하기를 원했습니다. 이 지점에서 Temporal이 프로젝트의 중요한 부분이 되었습니다.

Next.js 프론트엔드는 주문 생성, 이벤트 전송, 그리고 감독관이 무엇을 하고 있는지 보여주는 역할을 담당합니다. FastAPI는 백엔드 API를 처리하며, Temporal은 장기 실행 워크플로 (workflows)를 관리합니다. PostgreSQL은 애플리케이션 데이터를 저장하고, Ollama는 감독관이 사용하는 로컬 모델을 실행합니다.
시스템의 핵심은 간단한 설정입니다: 하나의 주문, 하나의 Temporal 워크플로 (workflow). 워크플로는 주문과 함께 시작되어 주문이 완료되거나 종료될 때까지 활성 상태를 유지합니다.
실행(run)이 생성되면, 백엔드는 데이터베이스 레코드를 생성하고, 해당 실행 ID로 Temporal 워크플로를 시작하며, 첫 번째 신호(signal)를 보냅니다:
@router.post("", response_model=RunResponse, status_code=201)
async def create_run(body, request, db):
run = Run(supervisor_id=body.supervisor_id, status="pending")
...
각 워크플로 (workflow)는 워크플로 ID (order-supervisor-{run.id})를 통해 해당 실행 (run)에 연결되며, 이를 통해 모든 주문은 각자 독립적인 슈퍼바이저 (supervisor)를 갖게 됩니다.
대부분의 시간 동안 워크플로는 능동적으로 아무것도 하지 않습니다. 다른 결정이 필요한 상황이 발생할 때까지 기다립니다.

주문은 수명 주기 동안 결제 업데이트, 고객 요청, 이행 (fulfillment) 변경 또는 배송 문제와 같은 다양한 이벤트 (event)를 받을 수 있습니다. UI는 payment_failed, shipment_delayed, refund_requested, customer_message_received를 포함하여 10가지 이벤트 유형을 지원합니다.

이벤트가 들어오면 Temporal 시그널 (signal)로서 기존 워크플로로 전송됩니다:
async def _signal(request: Request, run_id: int, event: dict) -> None:
handle = request.app.state.temporal_client.get_workflow_handle(
f"order-supervisor-{run_id}"
...
이를 통해 프로세스를 처음부터 다시 시작하지 않고도 새로운 정보가 동일한 슈퍼바이저에 도달할 수 있습니다. 워크플로는 이미 이전에 발생한 일에 대한 컨텍스트 (context)를 가지고 있으므로 그 지점부터 계속 진행할 수 있습니다.
슈퍼바이저가 실행 중인 동안 지침 (instructions)을 추가할 수도 있습니다. 이 지침은 다음 프롬프트 (prompt)의 "추가 지침 (Additional Instructions)" 항목에 포함되므로 워크플로를 재시작할 필요가 없습니다. 예를 들어, 고객이 특별 취급을 요청하는 경우, 해당 지침을 추가하면 다음 사이클 (cycle)에서 반영됩니다.
슈퍼바이저가 실행될 때마다 워크플로는 현재 주문 상태, 최신 이벤트, 타임라인 (timeline), 메모리 (memory), 추가 지침 및 사용 가능한 액션 (actions)을 사용하여 프롬프트를 구축합니다. 그런 다음 모델은 이 컨텍스트를 사용하여 다음에 무엇을 할지 결정합니다.
def _build_content(context: LLMContext) -> str:
return f"""## Order
..."""
사용 가능한 액션 (actions)은 입력의 일부로서 모델에 전달됩니다. 모델은 새로운 액션을 생성할 수 없으며, 주어진 액션 중에서 선택하거나 아직 아무것도 할 필요가 없다고 결정할 수만 있습니다.
이 프로젝트를 위해, 저는 고객 및 결제, 이행(fulfillment), 물류 팀과 소통하기 위한 다섯 가지 액션과 내부 메모(internal note)를 생성하는 옵션을 정의했습니다.

모델이 결정을 반환하면, 애플리케이션은 선택된 액션을 확인하고 그에 상응하는 로직을 실행합니다. 모델은 자신이 가진 옵션 중에서 무엇을 할지 결정하는 반면, 애플리케이션은 실제로 무엇을 할 수 있는지 제어합니다.
때로는 아무것도 하지 않는 것이 올바른 결정일 때가 있습니다.
주문이 정상적으로 진행되고 있다면, 모델을 계속 호출할 이유가 없습니다. 워크플로 (workflow)는 단순히 새로운 이벤트가 도착하거나 예정된 깨어남(wake-up) 시간에 도달할 때까지 기다립니다.
sleep_until = decision.get("sleep_until")
duration = None
...
이를 통해 동일한 감독자 (supervisor)가 모델을 내내 활성화 상태로 유지하지 않고도 주문의 전체 생명 주기 동안 해당 주문을 담당할 수 있습니다.
예를 들어, 주문이 생성되어 워크플로가 시작되었다고 가정해 봅시다. 감독자는 현재 상태를 확인하고 주의를 기울일 사항이 없다고 판단하여 대기합니다.
나중에 shipment_delayed (배송 지연) 이벤트가 도착합니다.
동일한 워크플로가 깨어나 업데이트된 컨텍스트 (context)와 함께 감독자를 다시 실행합니다. 이제 모델은 자신에게 사용 가능한 액션 중에서 선택할 수 있습니다. 예를 들어, message_customer (고객에게 메시지 전송) 또는 message_logistics_team (물류 팀에 메시지 전송) 등이 있습니다.
애플리케이션은 선택된 액션을 실행하고 결과를 기록하며, 워크플로는 다시 대기 상태로 돌아갑니다.

실행된 액션(action)들은 실행(run)의 액션 히스토리(action history)에도 별도로 기록됩니다.
데모를 위해, 저는 모든 것을 로컬에서 실행할 수 있도록 Ollama를 통한 Phi-3 Mini를 사용했습니다. 전체 워크플로(workflow)를 보여주기에는 충분했지만, 더 작은 모델이다 보니 때때로 예상된 응답 형식(response format)을 따르거나 액션을 일관되게 선택하는 데 어려움을 겪기도 했습니다.
이것이 제가 모델을 오케스트레이션(orchestration)과 분리해 둔 이유이기도 합니다. 워크플로는 Phi-3 Mini나 Ollama에 종속되지 않으므로, 장기 실행 프로세스(long-running process)의 작동 방식을 변경하지 않고도 모델을 교체할 수 있습니다.
LLM 통합은 OpenAI 호환 API를 사용하므로, Temporal 워크플로 자체를 변경하지 않고도 모델 레이어(model layer)를 다른 호환 가능한 제공업체로 지정할 수 있습니다.
이것을 구축하며 배운 한 가지는, LLM은 이러한 에이전트 시스템의 일부일 뿐이라는 점입니다. 상태(state) 관리, 이벤트(event) 처리, 모델을 호출할 시점 결정, 그리고 프로세스를 오랜 시간 동안 계속 실행 상태로 유지하는 것 역시 그만큼 중요합니다.
만약 이 작업을 계속한다면, 더 강력한 모델을 시도해 보고, 동시에 실행되는 더 많은 주문을 테스트하며, 각 결정이 내려진 이유에 대해 더 나은 가시성(visibility)을 추가해 보고 싶습니다.
이 프로젝트를 통해 얻은 가장 큰 깨달음은, 에이전트가 프로세스에 대한 책임을 유지하기 위해 끊임없이 실행되고 있을 필요는 없다는 것입니다. 때로는 그저 기다리다가 언제 다시 깨어나야 할지를 아는 것만으로도 충분합니다.
저는 여전히 이 분야를 탐구하고 있으며, 다른 분들은 동일한 문제에 어떻게 접근할지 의견을 듣고 싶습니다.
여러분이라면 다르게 처리하셨을까요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기