모놀리스 프롬프트에서 이벤트 기반 에이전트로 — twio의 아키텍처 이야기
요약
twio가 모놀리스 프롬프트 구조에서 이벤트 기반 에이전트 아키텍처로 진화하며 겪은 기술적 여정을 다룹니다. LLM의 인지적 과부하를 줄이기 위해 프롬프트의 책임을 하네스(harness)로 점진적으로 이전하는 과정을 설명합니다.
핵심 포인트
- 거대 프롬프트(Monolith)는 예외 케이스 증가 시 모델의 주의력을 분산시킴
- 프롬프트의 책임을 시퀀싱, 상태, 라이프사이클 순으로 하네스로 이전
- 비선형적이고 파편화된 실제 업무를 처리하기 위한 구조적 가드레일 구축
- LLM이 유지하기 어려운 로직을 하드코딩된 하네스로 옮겨 자유도 확보
요약 (TL;DR) — 우리의 목표는 Cursor나 Claude Code처럼 사용자가 어디서든 시작하고, 무엇이든 물어볼 수 있으며, 고정된 파이프라인을 따라가지 않아도 되는 자유로운 형식의 에이전트(free-form agent)를 만드는 것이었습니다. 이를 달성하기 위해서는 프롬프트(prompt)의 책임을 점진적으로 하네스(harness)로 옮겨야 했습니다. 처음에는 시퀀싱(sequencing)과 상태(state)를, 그다음에는 라이프사이클(lifecycle)과 분류(triage)를 옮겼습니다. 상위 계층에서의 대화적 자유는 하위 계층에 구조적 가드레일(guardrails)을 지속적으로 추가했기에 가능했습니다.
우리는 twio가 모기지 브로커를 위한 Cursor처럼 느껴지기를 원했습니다. 즉, 단순히 대화만 하면 되는 자유로운 형식의 에이전트 말입니다. 금리 재설정(refix) 중간에 뛰어들어 "이 고객의 금리는 언제 만료되나요?"라고 묻거나, 고객의 이메일을 붙여넣기만 하면 에이전트가 작업을 이어가게 하는 것입니다. 고정된 파이프라인이나 "4단계 중 1단계" 같은 제약 없이, 실제 업무가 들어오는 방식 그대로 작동하게 하는 것이 목표였습니다.
우리의 첫 번째 버전은 이와 정반대였습니다. 엔드 투 엔드(end-to-end)로 금리 재설정을 완료할 수는 있었지만, 브로커가 스크립트의 정확한 순서를 따라야만 했습니다. 중간부터 시작하거나, 옆길로 새는 질문을 하거나, 3일 뒤에 답장을 하면 시스템은 깨져버렸습니다.
이것은 우리가 세 가지 아키텍처를 거치며 어떻게 그 대화적 자유를 추구했는지에 대한 이야기입니다. 겉으로 보기에 각 재구축은 구조적 변화처럼 보였습니다. 하지만 그 이면에는 책임의 지속적인 이전이 있었습니다. 즉, LLM이 유지하기 어려워하는 것들을 걷어내고 이를 하네스(harness)로 넘겨주는 과정이었습니다.
(진화 과정을 이해하기 위해, refix(모기지 금리 갱신)가 4단계의 선형적인 프로세스처럼 들리지만, 실제로는 파편화되어 있고 비선형적이며 며칠에 걸쳐 진행된다는 점을 기억해 두세요.)
아키텍처 1: 모놀리스 스크립트 (그리고 통제의 환상)
시작할 당시 우리는 LLM 하네스(harness)에 대한 사전 경험이 없었습니다. 자유가 목표였지만, 먼저 더 간단한 질문에 답해야 했습니다. LLM이 실제 모기지 워크플로우를 엔드 투 엔드(end-to-end)로 처리할 수 있는가?
우리의 첫 번째 아키텍처는 전체 금리 재설정을 구동하는 하나의 거대한 프롬프트였습니다. 대략 다음과 같은 모습이었습니다:
당신은 주택 담보 대출(mortgage) 어시스턴트입니다. 재설정(refix)을 수행하려면:
1. 고객의 이름을 조회합니다. 일치하는 고객이 여러 명일 경우, 누구인지 물어보세요...
2. 새로운 금리(rate)와 기간(term)을 요청합니다 — 고객이 이메일을 보내지 않은 경우에만...
...
그것은 단순하고, 예측 가능하며, 읽기 쉬웠습니다. 하지만 실제 운영 환경(production)의 현실과 마주하게 되었고, 두 가지 치명적인 결함으로 인해 무너졌습니다:
- 인지적 과부하 (Cognitive Overload): 워크플로우가 길어질수록 프롬프트는 예외 케이스(edge cases)로 인해 비대해졌고, 모델의 주의력(attention)을 분산시켰습니다. 프롬프트는 고정된 시퀀스를 가정했지만, 브로커들은 그렇게 일하지 않았습니다.
- 엉킨 결합 (Tangled Coupling): 데이터 조회(fetch), 검증(validate), 초안 작성(draft), 그리고 예외 케이스 로직을 하나의 프롬프트가 모두 담게 되면서, 텍스트가 복잡하게 얽힌 벽이 되었습니다. 전체 시스템을 실행하지 않고서는 "제안서 작성(build the proposal)" 단계만 따로 테스트할 수 없었습니다.
이러한 결함들을 드러낸 반복적인 장애 상황은 바로 이것이었습니다: "고객이 3일 후에 답장을 보냅니다." 모놀리스(monolith) 구조에서는 이를 처리할 공간이 전혀 없었습니다. 실행이 이미 종료되었거나, 오지 않을 차례를 기다리며 차단(blocked)된 상태였습니다.
진단: 모놀리스 프롬프트는 결코 섞여서는 안 될 세 가지 요소를 하나로 융합합니다.
graph LR
A[사용자 입력] --> B(거대한 프롬프트)
B --> C[제어 흐름 (Control Flow)]
...
이들이 하나로 융합되어 있어, 하나를 변경하는 것이 다른 것들을 망가뜨릴 위험을 초래합니다.
우리의 첫 번째 리팩터링(refactor) 과제는 명확했습니다: 이 세 가지를 분리하고, 시퀀스를 하드코딩(hardcoding)하는 것을 중단하라.
아키텍처 2: 플래너 + 단계 (흐름과 상태의 분리)
우리는 모놀리스를 **단계(Steps)**로 산산조각 냈습니다. 단계란 타입이 지정된 계약(typed contract)을 가진 작고 단일 목적을 가진 에이전트입니다. 즉, 집중된 프롬프트, 도구(tools)의 화이트리스트, 그리고 상태(state)를 위한 엄격하게 정의된 입출력(I/O) 스키마를 가집니다.
**플래너(Planner)**가 순서가 지정된 시퀀스를 생성하면, 오케스트레이터(orchestrator)가 이를 실행합니다. 이를 통해 모놀리스의 책임들을 깔끔하게 분리할 수 있었습니다:
graph LR
P[플래너] -->|명령| S1[단계 1: 당사자]
S1 -->|'parties' 슬라이스 작성| S2[단계 2: 제안서]
...
돌파구: 메모리로서의 컨텍스트 (Context as Memory)
이 시대의 가장 결정적인 통찰은 우리가 상태(state)를 처리하는 방식이었습니다. 단계(Steps)는 전체 대화 기록을 받는 대신, 범위가 제한된 뷰(scoped view)를 받았습니다.
refix_proposal는 parties를 읽고, proposal을 쓰며, 나머지 정보는 절대 보지 않습니다. 이는 단순한 접근 제어(access control)가 아니라, **메모리 관리 (memory management)**입니다. 모델은 거대한 컨텍스트보다 작고 관련 있는 컨텍스트(context) 위에서 훨씬 더 정확하게 추론합니다. 뷰(view)의 범위를 제한(scoping)한 것은 단순히 코드를 깔끔하게 만든 것이 아니라, 각 단계(steps)를 더욱 날카롭게 만들었습니다.
아키텍처 2(Architecture 2)는 거대한 도약이었습니다. 우리는 이를 통해 많은 기능을 출시했습니다. 하지만 여기에는 숨겨진 치명적인 가정이 있었습니다: 작업이 동기적(synchronous)이고 연속적(continuous)이라고 가정했다는 점입니다.
작업을 시작하려면 브로커(broker)가 홈 페이지에 접속하여 드롭다운 메뉴에서 워크플로 유형을 선택해야 했습니다 (예: "Refix"). "Refix"를 선택하면 수정 계획(fixed plan)이 실행되었습니다. 이는 무언가가 실행되기 전에 브로커가 작업의 형태를 알고 있어야 함을 의미합니다. (Cursor가 듣기 전에 메뉴에서 "리팩터(refactor)"를 선택하지는 않습니다. 그저 타이핑을 시작할 뿐입니다).
게다가, 해당 계획은 실행부터 완료까지(run-to-completion)를 전제로 했습니다. 그래서 우리의 반복적인 문제점이 다시 나타났습니다: 고객이 3일 후에 답장을 하는 상황입니다. 그 시점에 파이프라인(pipeline)은 이미 종료되었거나 중단된 상태였습니다. 우리는 들어오는 항목들을 분류(triage)하기 위해 별도의 "인박스(inbox)"를 덧붙이려 시도했지만, 이는 동기식 시스템에 비동기(async) 패치를 스테이플러로 찍어 붙이는 것처럼 느껴졌습니다. 결국 우리는 그것을 삭제했습니다.
진단: 리픽스(refix)는 실행하는 워크플로가 아닙니다. 그것은 며칠에 걸쳐 끊어진 이벤트(events)들에 의해 공급되는, 생명 주기가 긴 케이스(case)입니다.
다음 리팩터링을 위한 명령: 브로커가 작업을 사전에 선언하게 하지 마라. 작업이 완료될 때까지 계속 실행된다고 가정하지 마라.
아키텍처 3: 오픈 케이스 (이벤트 기반 패러다임, Event-Driven Paradigm)
우리는 워크플로 유형의 드롭다운 메뉴를 **하나의 보편적인 케이스(universal Case)**로 통합했습니다. 이메일, 채팅 메시지, 늦은 답장 등 모든 인바운드 이벤트(inbound event)가 동일한 진입점으로 흘러 들어옵니다. 가장 먼저 실행되는 것은 플래너(planner)이지만, 이제 플래너는 완전히 다른 직무를 수행합니다: 바로 **분류(Triage)**입니다.
알려진 워크플로를 순차적으로 실행하는 대신, 플래너는 단일 이벤트를 보고 경로를 선택합니다:
graph TD
Event((Inbound Event)) --> Triage{Triage Planner}
...
이 아키텍처를 작동하게 만드는 세 가지 메커니즘은 다음과 같습니다:
1. 암묵적 대기(Implicit Waiting)를 통한 장기 지속 케이스(Long-Lived Cases)
행동을 수행한 후, 케이스는 조용해집니다. 우리 시스템에서는 새로운 메시지의 부재 자체가 곧 대기 상태(waiting state)이며, 매달려 있는 파이프라인(dangling pipeline)은 존재하지 않습니다. 아키텍처 1을 망가뜨리고 아키텍처 2를 정체시켰던 문제들이 이제는 사소한 문제가 되었습니다.
2. 일급 시민으로서의 "아무것도 하지 않음(Do Nothing)" 경로
브로커가 "그들의 이율이 언제 만료되나요?"라고 물으면, 트리아지 플래너(triage planner)는 이에 직접 답변하고 멈춥니다. 4단계 계획이 생성되지 않습니다. 종종, 계획이 없는 것이 올바른 계획일 때가 있습니다.
3. 선언적 플레이북(Declarative Playbooks)
도메인 지식(Domain knowledge)이 코드에서 분리되어 마크다운(Markdown) 플레이북으로 이동했습니다. 이를 통해 모기지 전문가들이 엔진 코드를 건드리지 않고도 "방법(how)"을 편집할 수 있습니다.
자유를 수호하기: 데이터 흐름 검증(Data-Flow Validation)
언어 모델(Language model)에게 스스로 단계 시퀀스를 구성할 수 있는 자유를 주는 것은 위험합니다. 우리는 그 아래에 단단한 바닥을 마련했습니다. 바로 단 하나의 토큰(token)이라도 작업에 소비되기 전에 일관성 없는 계획을 거부하는 **데이터 흐름 검증기(data-flow validator)**입니다.
만약 플래너가 parties 이전에 refix_proposal을 내보내면, 검증기는 정확한 에러를 반환하고 플래너는 스스로를 수정합니다.
이는 우리가 **소프트 읽기(Soft Reads) 대 하드 읽기(Hard Reads)**라고 부르는 탁월한 메커니즘을 도입합니다:
- 하드 읽기 (Hard Read, 정확성): 순서 제약 조건(ordering constraint)입니다.
form_filler는 반드시parties를 기다려야 합니다. 만약 그것이 누락되었다면, 검증기가 이를 차단합니다. - 소프트 읽기 (Soft Read, 구성 가능성): 가시성(visibility) 전용입니다.
form_filler는refix_proposal이 존재한다면 이를 볼 수 있지만, 이를 계획에 강제로 포함시키지는 않습니다. 이를 통해 전체 재조정(refix) 시퀀스를 끌고 오지 않고도 일회성 요청에 해당 단계를 포함할 수 있습니다.
하나의 reads 선언이 단계의 메모리 범위를 지정함과 동시에 의존성 그래프(dependency graph)의 엣지(edge) 역할을 수행합니다. 하나의 선언으로 두 가지 작업을 수행하는 것입니다.
이 지점에서 twio는 마침내 우리가 구축하고자 했던 자유로운 형태의 에이전트(free-form agent)처럼 느껴지기 시작했습니다. 이것은 우리 여정의 종합입니다: 상단에는 자유를, 하단에는 구조를, 그리고 산문(prose) 속에 지식을 담는 것.
근본적인 패턴
한 걸음 물러나 보면, 세 가지 아키텍처는 하나의 축으로 정렬됩니다: 우리가 LLM의 작업 메모리(working memory)에 얼마나 많은 것을 담도록 요청했는가.
| 차원 (Dimension) | 아키텍처 1: 스크립트 (The Script) | 아키텍처 2: 플래너 + 단계 (Planner + Steps) | 아키텍처 3: 오픈 케이스 (The Open Case) |
|---|---|---|---|
| 작업 단위 (Unit of Work) | 하나의 거대한 프롬프트 (One giant prompt) | 단계 시퀀스 (A step sequence) | 장기 지속되는 케이스 (A long-lived case) |
| ... |
각 리팩토링(refactor)은 프롬프트로부터 하나의 책임을 덜어내어 하네스(harness)에 부여했습니다:
- 스크립트 → 플래너+단계: 시퀀싱(sequencing)과 상태(state)를 분리했습니다.
- 플래너+단계 → 오픈 케이스: 라이프사이클(lifecycle)과 분류(triage)를 분리했습니다.
이것이 우리에게 있어 LLM의 결(grain)에 맞춰 구축한다는 의미였습니다. 영리한 프롬프트가 아니라, 모델이 유지하기 어려워하는 것들—지속적인 상태(durable state), 장기 실행 제어 흐름(long-running control flow), 일 단위의 분류(cross-day triage)—이 무엇인지 꾸준히 파악하고, 이를 그것들을 유지하도록 설계된 하네스에 넘겨주는 것입니다.
여전히 어려운 점
최상위 수준의 자유도는 새로운 실패 모드(failure modes)를 도입합니다. 플래너가 잘못된 플레이북(playbook)을 매칭할 수 있습니다. 유효하지만 최적은 아닌 계획을 구성할 수도 있습니다. 산문 형태의 플레이북이 실제 단계(steps)가 실행하는 내용과 어긋날 수도 있습니다. 우리의 가드레일(guardrails)이 이러한 문제의 대부분을 잡아내지만, 이는 해결된 문제가 아니라 여전히 진행 중인 최전선입니다.
이 모든 여정의 관통하는 핵심은, 당신이 아키텍처에 도달하는 것이 아니라 현실에 의해 아키텍처로 밀려나게 된다는 점입니다. 궁극적으로, 3일 늦게 답장을 보내는 고객이 우리보다 더 많은 아키텍처를 작성했습니다.
아키텍처적 시사점 (Architectural Takeaways)
장기 지속되는 에이전트 시스템(agentic systems)을 구축하고 있다면:
- 하네스가 더 잘 처리할 수 있는 일을 모델에게 맡기지 마세요. 시퀀싱(sequencing), 지속적인 상태(durable state), 라이프사이클 관리(lifecycle management)는 전통적인 코드(traditional code)로 넘기세요.
- 컨텍스트(context)를 메모리(memory)로 취급하세요. 각 단계의 가시성(view)을 엄격하게 제한하세요. 모델은 작고 관련성 있는 컨텍스트 위에서 더 잘 추론합니다.
- 도착하는 단위를 작업(task)이 아닌 이벤트(event)로 모델링하세요. 작업은 파편화되어 있습니다. 당신의 아키텍처는 중단(interruption)을 예상해야 합니다.
- 일급 객체인 "그냥 응답하기(just respond)" 경로를 구축하세요. 직접적인 답변만으로 충분할 때 워크플로우(workflow)를 강제하는 것은 사용자 경험을 망칩니다.
- 가드레일로 자유를 획득하세요. 정적 검증(static validation, 예: 우리의 데이터 흐름 검사기)을 사용하여 스스로 구성되는 에이전트(self-composing agents)를 안전하게 만드세요.
저희는 모기지 브로커(mortgage brokers)를 위한 AI 어시스턴트인 twio를 구축하고 있습니다. 만약 여러분도 수명이 긴 이벤트 기반 에이전트 아키텍처(long-lived, event-driven agent architectures)에 대해 동일한 고민을 하고 계신다면, 서로의 경험을 공유하고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기