상시 가동(always-on) 에이전트 만들기

TMT

https://claude.com/ko/resources/articles/building-always-on-agents

지난 몇 달 사이 상시 가동 에이전트(always-on agent)가 다시 주목받고 있습니다.

상시 가동 에이전트는 상태를 유지하고 먼저 나서서 움직입니다. 세션을 넘나들며 컨텍스트를 관리하고, 사람이 직접 지켜보지 않아도 행동한다는 뜻입니다. 밑바닥에서는 다른 에이전트와 똑같은 핵심 루프로 돌아갑니다. 차이는 그 주변 아키텍처에 있습니다. 이 아키텍처는 에이전트가 언제 깨어날지, 실행과 실행 사이에 무엇을 넘겨줄지, 에이전트가 한 일이 사용자에게 어떻게 전달될지를 정하는 기본 요소(primitive)들로 이뤄집니다.

이렇게 다시 주목받게 된 데는 최근 모델이 발전하면서 상시 가동 에이전트를 실제로 돌리기가 쉬워진 영향이 큽니다. 첫째, 최신 모델은 판단력이 더 좋습니다. 사용자의 의도를 더 정확히 이해하고, 언제 사용자에게 확인해야 하는지 더 잘 압니다. 둘째, 지능의 값이 싸지고 있고, 그 혜택을 가장 크게 보는 것이 장기 과제입니다. 예를 들어 한 장기 코딩 벤치마크에서 Opus 5.5는 해결한 과제 하나당 비용이 Opus 5보다 78% 적었습니다. 일반적인 작업에서는 40% 적었습니다.

고객들도 이런 경험을 자기 제품에 어떻게 녹여 넣을 수 있는지 묻기 시작했습니다. 기존 에이전트를 더 능동적으로 만들고 싶어 하는 회사도 있고, 처음부터 새로 만들려는 회사도 있습니다.

이 글에서는 상시 가동 에이전트를 이루는 기본 요소를 하나씩 뜯어보고, 이들이 어떻게 맞물리는지 살펴봅니다.

상시 가동 에이전트를 움직이는 순환

상시 가동 에이전트는 사실 "상시" 켜져 있지 않습니다. 사용자가 그렇게 느낀다는 뜻일 뿐입니다.

실제로 에이전트는 시간 대부분을 잠든 채 보내고, 주기적으로 깨어나 일을 합니다. 한 번 깨어나서 하는 일 한 묶음을 실행(run)이라고 부릅니다. 에이전트는 이벤트를 받고, 맡은 일을 끝낸 다음, 무슨 일이 있었는지를 저장합니다. 실행은 세션(session) 안에서 이뤄지는데, 세션은 메시지와 도구 호출을 담은 기록입니다. 실행이 끝나면 에이전트는 다시 잠들고, 이 순환이 끝없이 되풀이됩니다.

5dc803bf06d6e92e4aecae762da096626e2fa9ec

핵심 기본 요소

상시 가동 에이전트를 둘러싼 아키텍처에는 다섯 가지 핵심 기본 요소가 있습니다. 이벤트 인박스, 타이머, 메모리, 권한, 아웃박스입니다. 하나씩 살펴보겠습니다.

이벤트 인박스

대부분의 AI 에이전트는 사용자가 메시지를 보낼 때만 실행됩니다. 반면 상시 가동 에이전트는 웹훅, 예약 작업, 백그라운드 잡 등 여러 종류의 이벤트로 실행될 수 있습니다.

이벤트 소스마다 에이전트를 직접 호출하게 하고 싶어지기 쉽지만, 그러면 몇 가지 문제가 생깁니다. 예를 들어 이벤트 두 개가 동시에 도착하면 서로 경쟁하는 실행이 두 개 시작될 수 있고, 중간에 프로세스가 죽으면 이벤트가 통째로 사라질 수 있습니다.

이럴 때 필요한 것이 이벤트 인박스(event inbox)입니다. 모든 소스는 에이전트를 호출하는 대신 이벤트를 인박스로 보내고, 워커가 그 이벤트를 가져갑니다.

가장 단순한 형태는 세 가지 구성 요소로 이뤄집니다.

  • 이벤트를 영속적으로 담아 두는 목록. 보통 데이터베이스 테이블입니다.
  • 이벤트를 목록에 쓰는 핸들러. 보통 HTTP 엔드포인트입니다.
  • 목록에서 이벤트를 읽어 가는 워커. 보통 백그라운드 서비스입니다.

인박스는 이벤트마다 상태를 추적해야 하고, 소스가 붙여 주는 고유 식별자로 중복 이벤트를 걸러 낼 수 있습니다. 인박스에는 관계형 데이터베이스가 잘 맞습니다. 이벤트가 들어올 때마다 새 행으로 추가하면 됩니다. 처음에는 사용자마다 인박스 하나로 시작하길 권하고, 병렬 처리가 더 필요하면 그때 더 잘게 나누면 됩니다.

image

새 이벤트가 들어오면 핸들러는 이벤트를 인박스에 쓰고 워커를 깨웁니다. 워커는 대기 중인 이벤트를 모두 읽어 실행을 시작합니다. 사용자의 답장처럼 앞서 하던 일을 이어 가는 이벤트는 기존 세션을 재개하고, 나머지는 새 세션을 시작합니다.

그다음 워커는 이벤트를 에이전트 루프에 넘기고, 실행이 끝나길 기다렸다가 해당 이벤트를 완료로 표시합니다. 구현 세부 사항은 까다로워질 수 있으니, 몇 가지 모범 사례를 소개합니다.

  • 핸들러는 이벤트 수신을 빠르게 확인해 줘야 합니다. 웹훅 제공자처럼 응답이 느리면 실패로 처리하는 소스도 있습니다. 핸들러에서 가벼운 검사는 해도 되지만, 프롬프트 인젝션 확인처럼 깊이 있는 검증은 실행 중에 해야 합니다.
  • 인박스를 기준 정보로 삼아야 합니다. 워커는 깨어나면 가장 먼저 인박스에서 최신 이벤트를 가져와야 합니다. 그래야 워커가 실행 도중에 죽더라도 새 워커가 같은 이벤트를 가져가 다시 시도할 수 있습니다.
  • 인박스 하나에는 한 번에 워커 하나만 활성화돼야 합니다. 그래야 두 실행이 같은 일을 중복하거나 서로의 결과를 덮어쓰지 않습니다. 실행 도중에 새 이벤트가 들어오면, 다음 실행으로 미루거나, 현재 실행에 합치거나, 현재 실행을 중단하고 다시 시작할 수 있습니다.
  • 워커는 대기 중인 이벤트를 한데 묶어 처리해야 합니다. 예를 들어 사용자가 메시지를 연달아 여러 개 보내면, 워커는 이를 실행 하나로 합쳐서 에이전트가 한 번만 답하게 할 수 있습니다.

타이머

이벤트 인박스와 함께, 타이머는 상시 가동 에이전트가 먼저 나서서 움직일 수 있게 해 줍니다. 타이머는 대체로 후속 확인(follow-up)과 일정(schedule) 두 가지로 나뉩니다. 후속 확인은 에이전트가 스스로 정하는 기한입니다. 답장이 왔는지 확인하는 일 같은 것입니다. 반면 일정은 데일리 뉴스레터처럼 정해진 주기로 되풀이되는 작업입니다.

타이머는 인박스와 분리된 자체 영속 저장소에 둡니다. 보통 또 다른 데이터베이스 테이블입니다. 인박스에는 바로 처리할 수 있는 이벤트만 있어야 하기 때문입니다. 후속 확인과 일정은 비슷한 스키마를 함께 쓸 수 있지만, 일정은 반복 주기도 추적해야 합니다.

image

타이머가 스스로 실행을 시작하지는 않습니다. 대신 보통 백그라운드 서비스인 스케줄러가 일정한 간격으로 저장소를 확인하고, 기한이 된 타이머를 발동시킵니다. 스케줄러는 매번 다음 일을 합니다.

  1. 저장소를 훑어 기한이 된 타이머를 찾습니다.
  2. 타이머마다 종류에 맞게 갱신합니다.
    1. 후속 확인은 완료로 표시합니다.
    2. 일정은 기한을 다음 회차로 갱신합니다.
  3. 인박스에 이벤트를 추가하고 워커를 깨웁니다.

스케줄러는 직접 서비스를 만들어도 되고, 내구성 실행 엔진을 써도 됩니다.

메모리

상시 가동 에이전트는 서로 끊어진 여러 실행에 걸쳐 일하므로, 연속성을 유지하려면 메모리에 기대야 합니다.

이벤트 인박스나 타이머처럼 메모리도 자체 저장소를 가져야 합니다. Claude Code의 자동 메모리나 Messages API의 메모리 도구 같은 저희 제품에서는 메모리를 파일 디렉터리로 구현합니다. 상시 가동 에이전트라면 이 디렉터리를 보통 데이터베이스나 오브젝트 스토어 같은 영속 저장소에 연결해야 합니다. 그래야 워커가 사라져도 메모리는 남습니다.

경험상 메모리에는 에이전트가 다시 찾아볼 수 없는 것만 담아야 합니다. 이슈 트래커처럼 이미 다른 시스템에 있는 정보라면, 그 위치를 가리키는 포인터만 저장하세요. 상시 가동 에이전트가 저장해야 할 메모리 종류는 다음과 같습니다.

  • 사용자 지시: 에이전트의 행동에 관한 사용자의 지시는 따로 파일에 저장하고, 실행이 시작될 때마다 불러와야 합니다. 사용자가 늘 곁에서 에이전트의 방향을 잡아 줄 수는 없으니, 지시가 실행에서 실행으로 이어져야 합니다.
  • 사용자 노트: 에이전트는 사용자에 관해 알게 된 것, 예를 들어 사용자의 역할, 선호, 사용자가 바로잡아 준 내용을 저장해야 합니다. 처음에는 파일 하나로 시작하고, 노트가 늘어나면 작은 파일 여러 개로 나누면 됩니다. 나눈 경우에는 실행이 시작될 때 색인만 불러오고, 에이전트가 더 자세한 내용이 필요할 때 개별 파일을 컨텍스트로 가져옵니다.
  • 지난 메시지: 에이전트는 같은 말을 되풀이하지 않도록, 이미 사용자에게 무엇을 말했는지 기록해 둬야 합니다. 별도 파일에 저장해 두고 실행이 시작될 때마다 불러와도 되고, 워커가 에이전트와 사용자 사이의 최근 메시지 몇 개를 불러오게 해도 됩니다.
  • 작업 진행 상황: 진행 중인 작업마다 에이전트는 무엇을 했고 무엇이 남았는지 담은 진행 상황 파일을 둬야 합니다. 그러면 새 실행이 그 파일을 읽고 이전 실행이 멈춘 곳부터 이어 갈 수 있습니다. 사용자 노트와 마찬가지로, 실행이 시작될 때는 색인만 불러오고 에이전트가 더 자세한 내용이 필요할 때 개별 파일을 컨텍스트로 가져옵니다.

아래 다이어그램은 한 가지 구성 예시입니다.

image

지금까지 설명한 것은 모두 실행 중에 저장하는 메모리입니다. 여기에 드리밍(dreaming)을 더할 수도 있습니다. 드리밍은 지난 세션을 되짚어 보며 메모리를 정리하는 작업입니다. 낡은 항목을 바꾸고, 여러 실행에 걸쳐서만 드러나는 패턴을 찾아낼 수 있습니다. 더 알고 싶다면 에이전트 메모리에 관한 글에서 자세히 다룹니다.

컨텍스트 윈도 안에서는

에이전트는 일을 시작하기 전에 시스템 프롬프트, 도구 정의, 메모리 파일을 불러옵니다. 이 초기 컨텍스트 "오버헤드"는 실행마다 들어가므로, 가볍게 유지하고 프롬프트 캐싱으로 비용을 낮추길 권합니다.

새 세션은 이 컨텍스트 오버헤드와 실행을 일으킨 이벤트만 가지고 시작합니다. 반면 기존 세션은 이전 실행들의 메시지와 도구 호출을 다시 불러옵니다.

세션이 너무 길어지면 오래된 메시지를 압축하거나 새 세션을 시작하면 됩니다. 둘 다 세부 내용을 잃으므로, 계속 남아야 하는 정보는 그때그때 메모리에 저장해 둬야 합니다.

48757f0a147acfe841c943674c6214d34ed4405a

권한

상시 가동 에이전트에서 위험이 가장 큰 곳이 권한입니다. 실행은 사용자의 접근 권한을 쥐고 있고, 애초에 사람이 지켜보지 않는 상태로 돌아가도록 설계돼 있습니다. 에이전트는 도구를 호출할 때마다 스스로 진행할지, 막을지, 승인을 기다릴지 정해야 합니다.

권한에 필요한 구성 요소는 다른 에이전트가 쓰는 것과 같습니다.

  • 자격 증명 저장소: 상시 가동 에이전트가 제 역할을 하려면 비밀번호나 액세스 토큰 같은 민감한 사용자 자격 증명이 필요한 경우가 많습니다. 권장하는 패턴은 에이전트 환경 바깥에 프록시를 두고, 이 프록시가 나가는 요청에 자격 증명을 붙이게 하는 것입니다. 에이전트는 자격 증명 없이 요청을 보내고, 프록시가 실제 자격 증명을 넣어서 전달합니다.
  • 자동 모드: 상시 가동 에이전트는 사용자가 계속 입력하지 않아도 일할 수 있어야 합니다. 저희는 Claude Code의 자동 모드로 이 문제의 한 형태를 이미 다뤘습니다. 자동 모드는 분류기로 위험도가 높은 행동을 검토하고, 위험하다고 판단한 행동을 막아서 Claude가 더 안전한 방법을 찾게 합니다. 다만 가장 중요한 결정에는 여전히 사용자의 검토가 필요합니다.
  • 지연된 도구 호출: Claude Code에서는 훅이 도구 호출을 미뤄 두고 사용자의 추가 입력을 기다릴 수 있습니다. 같은 패턴을 상시 가동 에이전트에도 쓸 수 있습니다. 어떤 행동은 하기 전에 사용자에게 확인을 받거나 승인을 받아야 할 수 있기 때문입니다. 호출이 미뤄지면 실행이 끝나고, 세션은 호출 도중 상태 그대로 저장됩니다. 요청은 사용자에게 보여지고(예를 들어 MCP Apps로 만든 인터랙티브 카드로), 사용자의 답은 인박스에 이벤트로 돌아옵니다. 이 이벤트가 새 실행을 시작하고, 새 실행은 저장된 세션을 사용자의 입력과 함께 재개합니다. 미뤄진 호출이 답을 기다리는 동안에도 워커는 다른 이벤트를 처리할 수 있으므로 에이전트가 멈춰 서지 않습니다.

아웃박스

대부분의 AI 에이전트는 응답이 만들어지는 대로 사용자에게 스트리밍합니다. 그래서 사용자는 Claude가 문제를 추론하고, 도구를 호출하고, 답을 쓰는 과정을 따라갈 수 있습니다.

반면 상시 가동 에이전트는 일 대부분을 백그라운드에서 합니다. 사용자는 단계별 과정 전체가 아니라 결과와 몇 가지 세부 사항을 받습니다.

image

아웃박스 다이어그램. 실행은 전할 말 없이 끝나거나, 전송 도구 호출로 끝나거나, 사용자에게 묻는 지연된 도구 호출로 끝납니다. 메시지는 데이터베이스 테이블인 아웃박스로 가고, 디스패처가 이를 사용자에게 전달합니다. 지연된 요청은 아웃박스를 거칠 수도 있고, 직접 만든 인터페이스에 보여 줄 수도 있습니다.

실행은 세 가지 방식 중 하나로 끝납니다.

  • 에이전트가 아웃박스에 메시지를 보냅니다.
  • 에이전트가 보낼 것이 없다고 판단합니다.
  • 에이전트가 지연된 도구 호출로 권한이나 추가 입력을 요청합니다.

상시 가동 에이전트 대부분은 메시지를 사용자에게 바로 전달하는 "전송" 도구 하나로 충분합니다. 정식 아웃박스(outbox)는 에이전트가 보내는 메시지의 속도를 조절하고 싶거나, 에이전트가 오류에서 회복할 수 있게 하고 싶을 때만 필요합니다.

아웃박스는 나가는 메시지를 담는 별도 테이블로, 이벤트 인박스와 짝을 이룹니다. 워커와 짝을 이루는 디스패처도 있는데, 보통 백그라운드 서비스입니다. 디스패처는 대기 중인 메시지를 읽고, 무엇을 전달할지 정해서 사용자에게 보냅니다. 지연된 도구 호출에서 나온 요청은 아웃박스를 거칠 수도 있고, 인터페이스에 바로 보여 줄 수도 있습니다.

전달 말고도, 에이전트가 메시지를 얼마나 보낼지 조율하는 일이 중요합니다. 너무 많이 보내는 에이전트는 시끄러워지고, 너무 적게 보내는 에이전트는 사용자가 알아야 할 것을 놓칩니다. 적절한 균형을 찾으려면 평가(eval)를 만들길 권합니다. 실제로는 에이전트가 처음에는 조용히 시작하고, 시간이 지나면서 사용자의 주의를 더 얻어 가는 편이 낫습니다.

모든 것이 맞물리는 방식

다섯 가지 기본 요소는 이렇게 맞물립니다.

9f8df7b2dcc9d4637d27f37dae6e40a2a5851c8a

모든 실행은 인박스의 이벤트에서 시작합니다. 사용자 메시지일 수도, 웹훅일 수도, 타이머일 수도 있습니다. 워커가 이벤트를 가져가면 실행을 시작하고, 새 세션이나 기존 세션에서 에이전트를 깨웁니다. 에이전트는 메모리를 읽어 기본 상황을 파악하고, 권한 범위 안에서 행동한 다음, 새로 알게 된 것을 메모리에 반영합니다. 실행이 끝나면 에이전트는 아웃박스에 행을 하나 추가하고, 디스패처가 그 메시지를 사용자에게 전달합니다.

이 요소들이 함께 하나의 순환을 이룹니다. 실행은 나중에 에이전트를 다시 깨울 타이머를 설정할 수 있고, 사용자가 메시지에 답하면 그 답은 인박스에 이벤트로 돌아옵니다. 실행 하나하나는 짧지만 순환은 끝없이 이어지고, 바로 그 덕분에 에이전트가 늘 켜져 있는 것처럼 느껴집니다.

Edit this page

Search the archive

Find a page by title, or search the text inside one.