
오케스트레이터, 플래너, 6명의 전문가: 프로덕션 환경에서 살아남은 멀티 에이전트 파이프라인
요약
생물정보학 도구인 spatialGE를 GenePattern 플랫폼에 통합하는 과정에서 겪는 복잡한 멀티 에이전트 파이프라인 구축 경험을 다룹니다. 비선형 워크플로와 다양한 데이터 형식을 컨테이너화된 다단계 파이프라인으로 변환하는 엔지니어링적 도전 과제를 설명합니다.
핵심 포인트
- 비선형 워크플로를 가진 도구의 컨테이너화 및 통합 전략
- 도메인 지식(생물정보학)과 소프트웨어 엔지니어링 간의 간극 해소
- 독립적인 스크립트를 일관성 있는 다단계 파이프라인으로 구축하는 방법
- 문서와 AI 어시스턴트를 활용한 복잡한 의존성 파악 및 문제 해결
수요일 오후 5시, 두 개의 모니터에 12개의 탭이 열려 있습니다. 그중 4개는 Google 검색 결과입니다. 하나는 몇 달 전의 Biostars 스레드로, 제가 이해하려는 내용과 가장 유사하기 때문에 3일 동안 북마크해 두었습니다. 또 다른 하나는 AI 어시스턴트와의 대화창인데, 벌써 20번째 메시지가 오가고 있습니다. 이 AI는 이번 학기에 같은 질문을 다섯 번이나 받는 조교(TA)처럼 인내심을 가지고 공간 전사체 구배(spatial transcriptomic gradients)를 차근차근 설명해 주고 있습니다.
저는 생물학자가 아닙니다. 훈련받은 소프트웨어 엔지니어입니다. 그리고 저는 spatialGE 도구 모음을 GenePattern에 통합하는 업무를 맡았습니다. GenePattern은 주로 자신의 데이터가 과학적 질문에 대한 답으로 변환되기를 원하는 연구자들이 사용하는 생물정보학 (bioinformatics) 플랫폼입니다.
이 작업은 대충 넘길 수 있는 일이 아닙니다. spatialGE는 최소 9개의 뚜렷한 단계로 구성된 비선형 워크플로 (non-linear workflow)를 가지고 있으며, 두 가지 별개의 데이터 형식을 사용합니다. 제가 구축하는 모듈들은 단순히 폴더를 공유하는 9개의 독립된 스크립트가 아니라, 일관성 있고 컨테이너화된 (containerized) 다단계 파이프라인 (multi-step pipeline)으로서 함께 작동해야 합니다. 이를 제대로 수행하려면 연구자가 언제 농축 분석 (enrichment)을 사용할지, 차등 발현 (differential expression)을 사용할지, 아니면 수많은 정규화 (normalization) 방식 중 하나를 사용할지를 실제로 이해해야 합니다. 만약 제가 이를 잘못 이해한다면, 내부 코드가 아무리 깔끔하더라도 해당 모듈은 잘못된 것입니다.
spatialGE를 작성한 사람들은 이 자리에 없습니다. 그들은 Slack에도 없습니다. 제가 파악하기로는 그들은 두 개의 시간대 차이가 나고 아주 긴 이메일 체인 너머에 있으며, 저는 다른 모든 옵션을 다 소진하기 전까지는 그들을 번거롭게 하지 않기로 결정했습니다. 그래서 지금은 저와 문서, 그리고 저녁을 차리러 가기 전까지 흡수할 수 있는 만큼의 Biostars뿐입니다.
이것은 공간 전사체학 (spatial transcriptomics)에 관한 이야기는 아니지만, 거기서부터 시작됩니다. GenePattern은 2000년대 초반부터 존재해 왔으며 그 이면에 담긴 아이디어는 시간이 흘러도 여전히 유효합니다. 연구자들은 의존성 트리 (dependency trees)를 풀거나, 8년 전 어떤 대학원생이 작성한 도구의 커맨드 라인 플래그 (command-line flags)를 외우는 데 오후 시간을 허비하고 싶어 하지 않기 때문입니다. 이 플랫폼은 소프트웨어 엔지니어링 관행에 특별한 관심이 없는 학술 연구실에서 작성한 경우가 대부분인 수백 개의 독립적으로 개발된 생물정보학 (bioinformatics) 도구들을 가져와, 각각을 사용자 친화적인 웹 양식으로 감싸줍니다. 이는 "흥미로운 과학을 수행하는 도구가 여기 있습니다"와 "프로그래머가 아닌 사람도 실제로 실행할 수 있는 무언가가 여기 있습니다" 사이의 번역 계층 (translation layer) 역할을 합니다.
문제는 그 도구들 중 하나하나를 수동으로 통합해야 한다는 점입니다. 누군가는 문서와 함께, 혹은 운 좋게 답변을 받을 수 있다면 그것을 만든 과학자들과 대화하며 어떤 파라미터 (parameters)가 실제로 중요한지, 실제 의존성 (dependencies)은 무엇인지, 어떻게 컨테이너화된 환경 (containerized environment)에 감쌀 것인지, 그리고 이 모든 것이 제대로 작동하는지 어떻게 테스트할 것인지를 파악해야 합니다.
spatialGE의 경우, 그 '누군가'는 바로 저였습니다. 그리고 이 이야기가 끝날 때쯤, 그 '누군가'는 결국 우리의 모듈 출력을 약 500% 정도 끌어올린 AI 에이전트 (AI agents) 파이프라인이 될 예정이었습니다.
툴킷이 승인을 받다
우리는 처리해야 할 백로그 (backlog)가 산더미처럼 쌓인 소규모 팀이었습니다. 전통적으로 새로운 도구를 통합하는 작업은 며칠이 걸리는 일이었습니다. 문서를 읽고, 가능하다면 연구실과 대화하고, 래퍼 (wrapper)를 작성하고, 매니페스트 (manifest)를 작성하고, 테스트를 작성한 뒤, 과학자라면 당연하게 여길 예외 케이스 (edge case)를 놓치지 않았기를 기도해야 했습니다.
저는 GenePattern 생태계의 다른 한 축인 GenePattern Copilot이라는 프로젝트를 막 마친 상태였습니다. 이는 플랫폼 및 유전체학 (genomics) 질문에 답할 수 있을 뿐만 아니라, 사용자를 대신하여 작업을 수행할 수 있는 에이전트형 어시스턴트 (agentic assistant)였습니다. 즉, 에이전트형 AI (agentic AI)는 이미 제 머릿속에 자리 잡고 있었습니다. 그래서 Biostars를 통해 시행착오를 겪으며 공간 전사체학 (spatial transcriptomics)을 독학한 지 일주일 만에, 반복적이고 명확하게 규정된 작업을 수행하며 막혀 있는 모든 엔지니어가 결국 하게 되는 생각을 했습니다. 이것을 수행할 더 나은 방법이 분명히 있을 것이다.
저는 화이트보드에 아키텍처 (architecture)를 스케치했습니다. 단일 모델이 모든 것을 엔드 투 엔드 (end to end)로 수행하는 것이 아니라, 계층 구조를 만드는 것이었습니다. 최상단에는 오케스트레이터 (orchestrator) 에이전트가 있고, 그 아래에는 GenePattern 모듈이 필요로 하는 각각의 특정 산출물 (artifact)을 생성하는 역할을 맡은 에이전트 세트가 있는 구조였습니다. 저는 이 스케치를 상사에게 가져갔습니다. 승인이 떨어졌고, GenePattern 모듈 툴킷 (GenePattern Module Toolkit)이 탄생했습니다.
아이디어는 설명하기는 간단했지만 구축하기는 훨씬 어려웠습니다. 툴킷을 특정 도구의 문서와 Git 리포지토리 (git repository)를 가리키게 하고, 선택적으로 어떤 사용자 스토리 (user stories)나 기능을 우선시할지 알려주면, 툴킷이 작업을 수행하게 하는 것이었습니다. 그 결과물은 사람이 3일 동안 처음부터 만드는 대신, 단 한 오후 만에 QA (quality assurance)를 마칠 수 있는 모듈로 나옵니다.
[
하나의 오케스트레이터, 8명의 에이전트
툴킷이 안정적인 형태를 갖추었을 때의 대략적인 모습은 다음과 같습니다. 최상단에는 8개의 서브 에이전트 (subagents) 파이프라인을 관리하는 오케스트레이터 (orchestrator)가 위치합니다. 가장 먼저 호출되는 두 에이전트는 온라인 소스에서 문서와 컨텍스트를 가져오는 리서처 에이전트 (researcher agent)와, 다른 모든 에이전트가 동일한 가정 세트를 바탕으로 작업하고 있는지 확인하는 것이 유일한 임무인 플래너 에이전트 (planner agent)입니다. 그 후 실행 권한은 6개의 아티팩트 에이전트 (artifact agents) 파이프라인으로 전달되며, 각 에이전트는 매니페스트 (manifest), 래퍼 스크립트 (wrapper script), 파라미터 그룹 (parameter groups), 문서 (documentation), 테스트 (tests), Dockerfile이라는 단일 구성 요소를 담당합니다.
아티팩트 에이전트들은 검증 체인 (chain of verification)이라 불리는 패턴을 사용하지만, 당시 저는 그저 "다음 단계로 넘어가기 전에 해당 항목을 확인하라"고 불렀습니다. 각 에이전트가 자신의 아티팩트를 생성하면, 린터 (linter) 스크립트가 해당 아티팩트에 대해 실행되어 실제로 유효한지 확인합니다. 통과하면 파이프라인은 다음 에이전트로 넘어갑니다. 만약 실패하면 에러가 오케스트레이터로 에스컬레이션 (escalated)되며, 오케스트레이터는 판단을 내려야 합니다. 이것이 단순히 해당 아티팩트의 일시적인 오류인지, 아니면 이전 에이전트가 생성한 무언가로 인한 상류 (upstream) 실패의 증상인지 말입니다. 만약 상류 문제라면, 오케스트레이터는 파이프라인을 해당 이전 단계로 되돌려 그 지점부터 다시 실행합니다. 이 과정은 최대 반복 횟수에 의해 제한되며, 횟수를 초과하면 포기하고 해당 실행 건을 인간의 평가 (human evaluation)를 위해 플래그 (flag)를 지정합니다.
모델이 아닌 스캐폴딩 (scaffolding)부터 시작하기
GenePattern은 오래되었고 20년 동안 유기적으로 성장해 왔습니다. 매니페스트 파일가 무엇을 포함해야 하는지에 대한 공식적인 정의는커녕, 나머지 아티팩트들에 대한 정의도 전혀 없었습니다. 이는 에이전트를 생각하기 전에 먼저 해결해야 할 문제였습니다. 무엇인가를 대조하여 확인할 기준이 되는 그라운드 트루스 (ground truth)가 없었기 때문입니다.
따라서 제가 AI 기반 프로젝트를 위해 가장 먼저 구축한 것은 AI가 아니었습니다. 그것은 바로 린터 스크립트(linter scripts)였으며, 사실상 유효한 GenePattern 매니페스트(manifest)와 기타 아티팩트(artifacts)가 어떤 모습이어야 하는지에 대한 사실상의 표준(de facto standard)을 정의하는 작업이었습니다.
매니페스트는 Java 프로퍼티(properties) 파일입니다. 모든 필수 키(key)가 존재해야 하며, 모든 값(value)은 플랫폼이 예상하는 타입이어야 합니다. 또한 값 내부에 포함된 리터럴 = 문자는 이스케이프(escape) 처리되어야 하며, 그렇지 않으면 파서(parser)가 이를 새로운 키의 시작으로 읽어버립니다. 이 중 어느 것도 생소한 것은 아닙니다. 인간 엔지니어라면 서너 개의 매니페스트를 작성한 후에는 자연스럽게 내재화하여 다시는 신경 쓰지 않게 되는 종류의 일들입니다. 하지만 이는 또한 LLM이 아주 즐겁게 틀려버릴 법한 정확한 유형의 디테일이기도 합니다. 왜냐하면 "매니페스트를 작성하라"는 지시사항 중 그 어디에도 이스케이프 처리되지 않은 =가 치명적이라는 내용은 없기 때문입니다.
린터는 누락된 키, 잘못된 값 타입, 이스케이프 처리되지 않은 = 문자, Dockerfile에서 누락된 의존성, 그리고 서로 일치해야 하는 아티팩트들 사이에서 조용히 어긋난 프로퍼티 이름들을 잡아냈습니다. 이 스크립트들은 이후 검증 체인(chain-of-verification) 패턴의 중추가 되었지만, 당시에는 그저 조직 내 지식(institutional knowledge)으로만 존재하던 규칙들을 코드화하려는 저의 시도였을 뿐이었습니다.
모델과 전혀 상관없는 엄격하고 결정론적인(deterministic) 스크립트를 작성하는 것으로 에이전트형 AI(agentic AI) 프로젝트를 시작하는 것이 이상할까요? 저는 그렇지 않다고 생각합니다. 제가 구축한 모든 프로젝트는 동일한 교훈을 강화해 주었습니다. 모델을 둘러싼 하네스(harness, 안전장치)는 모델 자체만큼이나 중요하다는 것입니다. 하네스가 없다면 당신이 가진 것은 에이전트가 아니라, 매우 자신만만한 추측일 뿐입니다.
린터를 AI에 연결하기
린터가 독립적으로 작동하게 된 후, 다음 과제는 린터가 검사하고 있는 바로 그 아티팩트들을 실제로 작성할 수 있는 무언가에 린터를 연결하는 것이었습니다.
제가 선택한 에이전트 프레임워크는 Pydantic AI입니다. 빠르고 가벼우며 구조화된 출력 (structured output)에 능숙하기 때문입니다. 이는 매우 중요한 요소였는데, 모듈을 계획하는 것부터 매니페스트 (manifest)를 작성하는 것까지 이 시스템의 거의 모든 과정이, 정규 표현식 (regex)으로 분리해야 하는 문단 형태가 아니라 바로 활용할 수 있는 형태의 데이터를 모델로부터 받아내는 것에 달려 있었기 때문입니다.
그러한 배관 (plumbing) 작업이 완료된 후, 진짜 작업이 시작되었습니다. 바로 프롬프트 (prompt)를 반복 개선하는 것이었는데, 이는 출력을 뚫어지게 쳐다보며 에이전트가 왜 파라미터 (parameter)를 실수 (float)가 아닌 정수 (integer)로 결정했는지 파악하려고 애쓰는 과정을 '매혹적'이라고 표현하기에는 다소 기만적인 용어입니다. 한두 번이 아니라, 아티팩트 (artifact)가 자체 생성 과정은 무사히 통과했음에도 불구하고, 래퍼 스크립트 (wrapper script)가 예상하는 것과 매니페스트 키 (manifest key)의 철자가 약간 다르다는 식의 사소해 보이는 이유로 린터 (linter) 검사를 통과하지 못하는 일이 발생했습니다. 그러면 검증 체인 (chain-of-verification) 루프가 해당 실행을 실제로 문제를 일으킨 단계로 다시 되돌려 보냈습니다.
이것이 바로 패턴이 제 역할을 수행하는 방식입니다. 연구자가 실제로 모듈을 실행하려고 시도할 때까지는 드러나지 않고, 잘못된 기본값 (default)을 조용히 사용함으로써 문제를 일으킬 법한 작고 구체적인 실수를 잡아내는 것입니다.
무엇보다 우선적인 평가 (Evals)
저의 지난 프로젝트였던 GenePattern Copilot은 저에게 혹독한 교훈을 주었습니다. 처음부터 공식적인 평가 세트 (evaluation suite)를 구축하지 않으면, 결국 '느낌 (vibes)'에 의존해 디버깅하게 되며, 느낌은 확장성 (scale)이 없다는 사실입니다. 저는 이번 프로젝트를 시작하며 그 실수를 반복하지 않겠다고 다짐했습니다.
처음부터 제대로 된 평가 세트를 구축하는 것은 종종 그 자체로 하나의 프로젝트가 되기도 하지만, GenePattern에는 한 가지 장점이 있었습니다. 이미 프로덕션 (production) 환경에 자리 잡고 있는, 검증된 모듈들의 방대한 코퍼스 (corpus)를 보유한 성숙한 플랫폼이었다는 점입니다. 그 코퍼스는 저의 '검증된 출력값 (known-good outputs)'이 되었고, 저는 거기서부터 역으로 작업하여, 해당 검증된 모듈들과 유사한 결과물을 만들어내야 하는 합성 입력값 (synthetic inputs)을 생성했습니다. 툴킷 (toolkit)의 초기 실행 결과들도 그 세트에 추가되었는데, 주로 실패를 통해 기여했습니다. 각각의 잘못된 출력값은 향후 실행 결과와 비교하여 확인할 수 있는 문서화된 실패 상태 (failure state)가 되었습니다.
오류가 발생한 지점
단일 모델이 하나의 작업만 수행하는 시스템이라면 단 한 번의 입출력 평가(eval)만으로도 충분할 수 있습니다. 하지만 각각의 입력, 출력, 그리고 실패 모드(failure modes)를 가진 8개의 서브 에이전트(subagents)로 구성된 파이프라인에서는 그것만으로는 부족합니다. 저는 각 에이전트를 독립적으로 평가해야만 했습니다. 그렇지 않으면 다운스트림(downstream)에서 무언가 고장 났을 때 어떤 에이전트가 실제로 원인을 제공했는지 알 방법이 없기 때문입니다. 제 초기 실패의 거의 대부분이 바로 그 지점에서 발생했으며, 이 프로젝트는 멀티 에이전트 시스템(multi-agent system)에서 관측성(observability)은 선택 사항이 아니라는 점을 가르쳐 주었습니다.
한 에이전트의 출력이 다른 에이전트의 입력이 되는 순간, 세 단계 앞선 상류(upstream)에서 발생한 작은 모호함이 두 단계 뒤의 에이전트에서는 전혀 관련 없어 보이는 실패로 나타날 수 있습니다. 사후에 로그를 읽으며 이를 디버깅(debug)하려는 시도는 마치 타이어 자국(skid marks)만 보고 자동차 사고 현장을 재구성하는 것처럼 느껴졌습니다. 저는 그 과정이 일어나는 것을 더 명확하게 지켜볼 수 있어야 했습니다.
이를 위해 저는 Logfire를 선택했습니다. 주로 Pydantic AI를 만든 동일한 팀이 제작했다는 점과 통합에 10분밖에 걸리지 않았기 때문입니다. 이를 통해 초기 프롬프트(prompt)부터 오케스트레이터(orchestrator)를 거쳐 각 서브 에이전트(subagent)를 순서대로 통과하는 단일 요청(request)을 추적할 수 있었습니다. 더 중요한 것은, 한 에이전트에서 다음 에이전트로 전달되는 컨텍스트(context)가 다음 에이전트가 필요로 하는 것에서 정확히 어느 지점부터 벗어나기 시작하는지를 볼 수 있었다는 점입니다. 일단 이러한 가시성(visibility)을 확보하자, 안정성 개선 작업은 느리고 혼란스러웠던 단계에서 빠르고 구체적인 단계로 넘어갔습니다.
충돌에서 살아남는 법을 배우기
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기