에이전트 CLI 플릿(fleet) 오케스트레이션을 위한 파일 기반 워크 버스(work-bus) — 메시지 브로커 없는 조정
요약
메시지 브로커나 무거운 프레임워크 없이 파일 시스템을 활용해 AI 에이전트 플릿을 조정하는 '파일 기반 워크 버스' 설계 방식을 소개합니다. 원자적 파일 쓰기를 통해 언어에 구애받지 않고 디버깅이 용이하며, 재시작 시에도 상태가 유지되는 가벼운 오케스트레이션 메커니즘을 제안합니다.
핵심 포인트
- 메시지 브로커 없이 파일 시스템을 활용한 가벼운 에이전트 조정
- 원자적 쓰기(Atomic writes)를 통한 데이터 무결성 및 안정성 확보
- 언어 독립적이며 ls 명령어로 디버깅 가능한 구조
- DAG 기반의 작업 분해 및 폴링 메커니즘 활용
- 상태 기반 설계를 통한 자가 치유 및 재시작 지원
AI 에이전트 플릿(fleet) 구축 및 실행에 관한 시리즈의 일부인 hexisteme notes에 처음 게시되었습니다.
메시지 브로커(message broker)나 무거운 프레임워크 없이 독립적인 AI 에이전트 CLI 플릿을 조정하려면, **파일 시스템 워크 버스(filesystem work-bus)**를 사용하십시오. 오케스트레이터(orchestrator)는 목표를 하위 작업(subtask) 그래프로 분해하고, 각 하위 작업마다 Task 파일을 작성하며, 각 워커(worker)가 다시 작성하는 Result 파일을 폴링(polling)합니다. 이때 모든 파일은 원자적(atomically)으로 작성됩니다. 내구성이 있는 조정 상태는 파일로서 디스크에 저장됩니다. 이는 언어에 구애받지 않고(language-agnostic), ls 명령어로 디버깅이 가능하며, 재시작 후에도 유지되고, 워커가 부재할 경우 실행이 실패하는 대신 건너뛰고 로그를 남기므로 자가 치유(self-healing)가 가능합니다.
예를 들어, 각각 독립적으로 설치된 CLI인 여러 AI 에이전트가 있다고 가정해 봅시다. 하나는 정보를 수집하고, 하나는 카피를 작성하며, 하나는 앱 스캐폴드(scaffold)를 구축합니다. 그리고 이들 중 여러 개를 순차적으로 실행해야 하는 목표를 수행하고 싶습니다. 무거운 해결책으로는 인프로세스 프레임워크(in-process framework, LangGraph 또는 AutoGPT 스타일의 루프)나 메시지 브로커(message broker, Redis, Kafka, RabbitMQ)가 있습니다. 두 가지 모두 단일 운영자 플릿이 필요로 하는 것보다 과합니다. 프레임워크는 워커들을 하나의 프로세스와 하나의 언어로 결합시키며, 브로커는 이제 직접 실행, 보안 설정 및 모니터링해야 하는 인프라가 됩니다.
이러한 형태에 적합한 더 가벼운 원시 요소(primitive)가 있습니다. 바로 파일로 구성된 워크 버스(work-bus)입니다.
메커니즘 (The mechanism)
컨덕터(conductor) 프로세스가 공유 디렉터리인 버스(bus)를 소유합니다. 목표를 실행하는 방법은 다음과 같습니다:
- 목표를 하위 작업의 유향 비순환 그래프(directed acyclic graph, DAG, 예: gather → narrate → build)로 **분해(Decompose)**합니다.
- 준비된 각 하위 작업에 대해, 필요한 기능(capability) 태그를 달아 버스에 **
Task파일을 작성(write)**합니다. - 짧은 백오프(backoff)와 함께 일치하는
Result파일을 **폴링(Poll)**합니다. - 각 결과를 **흡수(Absorb)**하고, 검증한 뒤, 그래프 내의 다음 하위 작업들을 해제합니다.
이 방식을 안전하게 만드는 단 하나의 규칙은 **원자적 쓰기 (Atomic writes)**입니다. 각 레코드를 임시 경로에 작성한 다음, 원래 위치로 rename하는 방식입니다. rename은 POSIX 파일 시스템에서 원자적(atomic)으로 수행되므로, 읽는 쪽에서는 파일 전체를 보거나 혹은 아무것도 보지 못하게 됩니다. 즉, 절반만 작성된 레코드를 보는 일은 절대 없습니다. Task와 Result는 타입이 지정된 레코드(작은 Pydantic 스키마)이며, 컨덕터(conductor)는 현재 진행 중인 작업들의 레지스트리를 유지합니다.
# 원자적 발행(atomic publish) — 읽는 쪽에서 부분적인 레코드를 보는 일이 없음
def publish(path, record):
tmp = path.with_suffix(".tmp")
...
이것은 이벤트가 아니라 상태(state)입니다 — 설계 의도에 따라
파일 기반 워크 버스(work-bus)가 단순히 변장한 이벤트 버스(event bus)가 아닌지 묻는 것은 타당합니다. 하지만 그렇지 않으며, 그 차이점이 바로 이 방식이 작동하는 이유입니다. 이벤트 버스는 푸시(push) 방식입니다. 프로듀서(producer)가 일시적인 이벤트를 방출하면, 그 순간에 듣고 있지 않은 모든 것은 이벤트를 놓치게 됩니다. 반면 파일 워크 버스는 상태(state) 방식입니다. Task와 Result 레코드는 소비될 때까지 유지되는 내구성이 있는(durable) 파일입니다. 늦게 시작하거나 실행 도중 재시작된 워커(worker)도 여전히 기다리고 있는 자신의 작업을 찾아낼 수 있습니다. (저는 state is truth, events are rumors에서 플릿(fleet)을 **모니터링(monitoring)**할 때도 동일한 원칙을 주장했습니다. 여기서는 플릿을 **조정(coordinating)**하는 데 있어 그 원칙이 다시 나타납니다.)
왜 여기서는 푸시가 괜찮지만 모니터링에는 그렇지 않은가. 당신은 이 워커들을 구축하고 제어하므로, 워커들이 버스를 읽고 쓰도록 만들 수 있습니다. 모니터링은 반대의 경우입니다. 당신은 제어할 수 없는 컴포넌트들을 관찰해야 하므로, 대신 그들의 상태를 **풀(pull)**해야 합니다. 내구성이 있는 파일을 통한 소유 워커의 조정(coordination), 상태 스캔을 통한 비소유 컴포넌트의 모니터링: 두 방식 모두 일시적인 이벤트보다 내구성이 있는 상태에 의존합니다.
이름이 아닌 역량(capability)에 의한 라우팅
컨덕터(conductor)는 "단계 2를 워커 X로 보내라"와 같이 하드와이어링(hard-wire)하지 않습니다. 각 워커는 역량(capability)을 광고하고, 각 서브태스크(subtask)는 필요한 역량을 선언합니다. 컨덕터는 배정 시점에 필요한 역량을 광고하는 상태가 양호한(healthy) 워커를 찾아 이들을 매칭합니다. 워커를 추가하거나 제거하면 라우팅(routing)이 적응하므로, 수정해야 할 배선도(wiring diagram)가 없습니다. 이것이 하나의 컨덕터가 단일한 통일된 계약(contract)을 통해 이질적이고 변화하는 플릿(fleet)을 조정할 수 있게 해주는 핵심입니다.
우아한 성능 저하(Graceful degradation): 부재 중인 워커 건너뛰기
아직 구축 중인 플릿에서 가장 중요한 동작은 다음과 같습니다: 워커가 누락되었다고 해서 실행이 실패해서는 안 됩니다. 만약 서브태스크가 현재 상태가 양호한 워커 중 어느 것도 광고하지 않는 역량을 필요로 한다면, 컨덕터는 해당 노드를 건너뜀(skipped)(로그에는 worker_absent로 기록됨)으로 표시하고, 그래프의 나머지 부분을 계속 진행하며, 완료된 결과물들을 바탕으로 합성(synthesize)합니다. 대부분의 워커가 아직 존재하지 않는 첫날에도 컨덕터는 여전히 엔드 투 엔드(end-to-end)로 실행되어 부분적인 출력을 생성하며, 건너뛰기 로그는 다음에 구축해야 할 역량이 무엇인지 알려주는 정확한 할 일 목록(to-do list)이 됩니다. 공백은 보고될 뿐, 충돌(crash)을 일으키지 않습니다.
네트워크 경계처럼 버스(bus)를 신뢰하기
워커의 출력은 경계를 넘나드는 신뢰할 수 없는 입력이며, 버스(bus)는 이를 그렇게 취급합니다. 모든 결과는 흡수(absorption)되기 전에 엄격한 스키마(schema)로 파싱(parse)됩니다. 불일치(예를 들어, 와이어 포맷(wire format)과 내부 열거형(enum) 간의 대소문자 차이)는 경계 지점에서 강제(coerce) 및 정규화(normalize)됩니다. 부하를 담당하는 주장(claims)은 출처(provenance) 라벨을 지녀야 하며 증거를 포함해야 합니다. 증거 ID 없이 FACT로 표시되어 도착한 주장은 신뢰되는 것이 아니라 파싱 시점에 거부됩니다. 이러한 타입화된 계약(typed contract) 덕분에, 당신이 서로 다른 시점에 서로 다른 언어로 작성한 독립적인 워커들이 컨덕터가 그들을 맹목적으로 신뢰할 필요 없이 상호 운용(interoperate)할 수 있습니다.
솔직한 한계
⚠️ 재라우팅(re-routing)에 대한 중단 조건이 없음. 기능 기반 라우팅(Capability-based routing)에는 날카로운 단점이 있습니다. 만약 특정 노드가 "기능 C를 광고하는 모든 워커"로 재라우팅될 수 있는데, 결과가 계속 검증(validation)에 실패한다면, 단순한 컨덕터(conductor)는 무한 루프 내에서 재라우팅을 반복할 수 있습니다. 파일 워크 버스(file work-bus)는 노드당 명시적인 시도 예산(attempt budget)과 데드 레터(dead-letter) 결과 처리가 필요하며, 그렇지 않으면 무한히 회전할 수 있습니다. 내구성(Durability)과 디커플링(decoupling)은 이 방식의 이점이지만, 이를 안전하게 확보하기 위해 지불해야 하는 비용은 바로 제한된 재시도 정책(bounded retry policy)입니다.
실제 브로커(broker)를 사용해야 할 때
이 패턴은 _한 명의 운영자에 의해 조정되며, 수 초에서 수 분 정도 소요되는 작업을 수행하는 소규모의 이기종 플릿(heterogeneous fleet)_에 적합합니다. 만약 많은 프로듀서(producer)와 컨슈머(consumer)를 대상으로 하는 고처리량(high-throughput), 저지연(low-latency) 팬아웃(fan-out)이 필요하다면, 실제 메시지 버스(message bus)를 실행하십시오. 파일 버스(file-bus)의 폴링(polling) 및 단일 컨덕터 모델은 이를 따라갈 수 없습니다. 도구를 선택할 때는 가장 뼈아픈 실패 지점에 맞추십시오. 단독 플릿의 경우, 고통은 운영 오버헤드(operational overhead)와 취약한 결합(brittle coupling)이며, 원자적 파일(atomic files) 디렉토리는 이 두 가지를 모두 제거합니다.
AI 에이전트 플릿 구축에 관한 추가 노트 — 모니터링을 위해 이벤트 버스(event bus)를 거부한 이유, 사실(facts) vs 추론(inferences) 레이블링, 재사용 가능한 결정 단위(reusable decision units) — hexisteme.github.io/notes에서 확인하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기