Prompt Combo의 출발점은 "프롬프트를 큐에 넣고 알아서 돌아가게 하자" 는 생각이었습니다. 말로 하면 간단하지만, 실제로 구현해보면 전혀 다른 문제였습니다.
처음에는 정말 단순하게 시작했습니다. 프롬프트 배열을 만들고 for...of 로 순차 실행한 뒤, 응답을 로그에 쌓으면 끝이라고 생각했습니다. 문제는 그 다음부터였습니다.
1차 구조: 순차 실행의 한계
첫 번째 구조는 아래와 같았습니다.
[Queue]
Prompt A -> Claude -> Response A
Prompt B -> Claude -> Response B
Prompt C -> Claude -> Response C
작동은 했습니다. 그런데 바로 첫 번째 피드백이 들어왔습니다.
"이 프롬프트는 GPT-4o로 돌리고, 저건 Claude로 돌리면 안 되나요?"
단일 모델만 가정한 큐 구조에서는 답이 나오지 않았습니다. 하지만 이 요구 하나가 아키텍처 전체를 흔들었습니다. 이제 아이템마다 어떤 모델을 어떤 API 키로, 어떤 파라미터로 실행할지 개별 설정이 가능해야 했습니다.
2차 구조: 모델별 라우팅
큐 아이템에 모델 정보를 붙였습니다.
interface QueueItem {
id: string
prompt: string
model: 'claude' | 'gpt' | 'gemini'
params: ModelParams
status: 'pending' | 'running' | 'done' | 'error'
}
각 모델별 API 클라이언트를 만들고, 러너가 아이템의 model 필드를 보고 적절한 클라이언트로 라우팅하는 구조였습니다. 여기서 동시 실행 이라는 두 번째 문제가 등장했습니다.
순차 실행이면 단순합니다. 하나 끝나면 다음 하나. 하지만 사용자는 곧 이렇게 묻습니다.
"Claude 응답 기다리는 동안 GPT도 같이 돌리면 안 되나요?"
가능합니다. 대신 그러려면 동시성 제어 가 필요합니다. 모델별 rate limit은 다르고, 에러 처리도 독립적이어야 하며, 터미널 뷰어는 여러 스트림을 동시에 보여줘야 했습니다.
3차 구조: 워커 풀
결국 워커 풀(Worker Pool) 구조로 돌아왔습니다.
------------------------------------------------------------
| Queue Manager |
| priority sorting + dependency resolve |
|-----------|-----------|-----------|-----------|
| | |
Worker 1 Worker 2 Worker 3
(Claude) (GPT) (Gemini)
| | |
|------ merged streams -----------|
|
Log Aggregator
이 구조의 핵심은 세 가지였습니다.
- 워커는 모델에 종속되지 않습니다. 워커는 실행 단위일 뿐이고, 어떤 모델이든 어떤 프롬프트든 받아서 처리합니다.
- 큐 매니저가 라우팅을 결정합니다. 우선순위, 의존 관계, 모델별 rate limit을 종합해서 다음 실행 대상을 고릅니다.
- 로그 애그리게이터가 스트림을 합칩니다. 각 워커에서 오는 SSE 스트림을 하나의 타임라인으로 병합합니다.
의존 관계가 있는 경우
이 구조에서 가장 가치 있었던 건 프롬프트 간 의존 관계 였습니다. 예를 들면:
- Prompt A: "이 코드를 분석해줘" -> Claude
- Prompt B: "A의 결과를 바탕으로 테스트 코드를 작성해줘" -> GPT
- Prompt C: "A와 B의 결과를 종합해서 문서화해줘" -> Claude
B는 A가 끝나야 실행할 수 있고, C는 A와 B가 모두 끝나야 합니다. 이런 순서를 처리하려면 DAG(Directed Acyclic Graph) 기반의 의존성 해석이 필요했습니다.
function getNextExecutable(queue, completed) {
return queue
.filter(item => item.status === 'pending')
.filter(item => item.dependsOn.every(depId => completed.has(depId)))
.sort((a, b) => a.priority - b.priority)
}
순환 의존성 감지, 실패한 의존성의 후속 처리, 재시도 규칙까지 들어가면 단순한 큐가 아니라 사실상 작업 스케줄러 에 가까워집니다.
결국 배운 점
세 번 구조를 바꾸면서 배운 건 "사용자의 멘탈 모델이 맞아야 한다" 는 점이었습니다. 사용자는 이를 그냥 "프롬프트 목록"처럼 생각합니다. 내부적으로는 워커 풀과 DAG가 돌아가더라도, UI에서는 드래그로 순서를 바꾸고 선으로 의존 관계를 연결하는 정도로 보여야 했습니다.
결국 중요한 건 내부 구조를 과시하는 게 아니라, 사용자가 복잡함을 느끼지 않게 만드는 일이었습니다. 그게 좋은 도구라고 생각합니다.
다음 글에서는 Cross Validation, 즉 여러 LLM에게 같은 질문을 보내고 응답을 비교하는 기능을 구현하면서 겪은 이야기를 정리합니다.