에이전트를 웹앱 런타임으로 쓰려다 접었습니다 — 개발 인부로 쓰니 괜찮았습니다
연구실 논문 관리 웹앱을 하나 만들기로 했습니다. 평소 헤르메스를 붙들고 일하는 사람이다 보니 첫 생각은 당연히 "이걸 헤르메스로 돌리면 안 되나?"였습니다. 결론부터 말씀드리면, 런타임으로는 안 됐습니다.
만드는 일은 헤르메스 서브에이전트로 됐습니다. "앱을 굴리는 놈"과 "코드를 짜는 놈"을 분리하는 데서 이 프로젝트의 설계가 시작됐습니다.
헤르메스는 런타임이 아닙니다
헤르메스는 단일 사용자 에이전트입니다. 그래서 사람 여럿이 동시에 클릭하는 웹앱의 한복판에 둘 자리가 아니었고요.
에이전트를 통째로 런타임으로 삼으려는 시도는 식당 카운터에 집사 한 분을 세우는 격이었습니다. 집사는 유능한데, 시스템이 안 맞습니다.
그래서 앱 자체는 FastAPI 백엔드와 React 프런트로 따로 지었습니다. 앱 안의 '중간 판단', 예컨대 저널 추천 같은 것은 백엔드가 로컬 LLM을 직접 호출해서 처리하고요.
데이터가 미발표 논문이라 외부 LLM 공급자는 원천적으로 들일 수 없습니다. DECISIONS.md에 쌓인 열아홉 개의 의사결정 중 D-07로 못 박아둔 규칙입니다.
백엔드가 로컬 LLM을 부르는 방식은 오히려 소박합니다. queued, running, done, error 네 가지 상태로 백그라운드 큐에 올리고, 돌아온 JSON은 Pydantic 스키마로 검증합니다. 틀리면 재시도 한 번, 그래도 틀리면 명시적 에러를 던지고, 같은 입력은 해시를 재사용합니다.
이걸 "에이전트를 붙였다"고 부르기는 민망하고, LLM을 함수 한 개처럼 다뤘다고 하는 편이 정확했습니다. 헤르메스에게는 개발 에이전트와 운영 보조, 아침 다이제스트와 알림 정도만 맡혔습니다.
카드로 굴린 서브에이전트 개발
개발 시스템은 단순했습니다. PLAN.md를 P1부터 P4까지 단계별 작업 카드로 쪼개고, 메인 세션은 검수만 합니다. 실제 코딩은 xhigh reasoning을 켠 로컬 27B 서브에이전트가 카드 단위로 수행했습니다.
카드 완료 → 게이트 스크립트 10단계 통과 → 리포트 작성 → 사람이 승인 파일 생성
게이트는 타입체크에 백엔드 pytest, 프런트 vitest를 비롯한 열 단계의 관문입니다. 카드가 통과하면 리포트를 올리고, 사람이 읽고 나서야 승인 파일이 만들어집니다.
핵심은 이 p2.approved 파일을 에이전트가 만들면 안 된다는 규칙이었어요. 숙제는 아이 손으로 풀되, 체크 도장은 선생님 것이어야 한다는 원칙입니다.
의사결정 이력은 D-01부터 D-19까지 DECISIONS.md에 누적됐습니다. 한 번 정해놓고 일주일 뒤에 다시 논쟁에 빠지는 일이 없어서 이 파일이 생각보다 큰 역할을 했습니다.

사흘 동안 쌓인 숫자
저장소 첫 커밋은 10월 23일이고, 10월 25일까지 사흘간 커밋 36개가 쌓였습니다. 하루에 15개, 열아홉 개, 두 개 순서였습니다. 완료 작업은 24개, P1부터 P3까지이고 PLAN.md 체크박스 실측값입니다.
게이트 리포트에 기록된 테스트 수가 차트의 주인공입니다. 백엔드 pytest는 124개에서 출발해 273개를 지나 331개까지 올랐습니다.
프런트 vitest는 81개에서 153개, 169개로 늘었습니다. 게이트별 두 테스트의 합계는 205개, 426개, 500개가 됐습니다.
P2 게이트 리포트에는 '알려진 제약 7건'도 함께 명시돼 있습니다. 통과한 것만 세어주는 문화가 아니라는 뜻이라, 저는 이 줄이 마음에 듭니다.
함정 세 개를 밟았습니다
첫 번째 함정은 격리 검증의 오탐이었습니다. 서브에이전트가 격리 환경에서 "테스트 통과"라고 보고했는데, 정작 본 워크트리에는 라우터 마운트와 모델 등록이 빠져 있었던 사건입니다.
돌아간 곳에서 초록불이고, 살아야 할 곳에서 파일이 없었던 셈이지요. 이후 커밋 직전에는 반드시 본 트리에서 게이트 전체를 재실행하는 규율을 세웠습니다. "통과했다"는 말에는 환경 이름을 붙여줄 때만 믿기로 했습니다.
두 번째는 MSW 핸들러 누락에서 온 hang이었습니다. 페이지가 마운트되면 기본 탭이 자동 요청을 보내는데, 테스트 목 핸들러에 그 경로가 없으면 요청이 영원히 pending으로 남습니다.
그러다 워커가 CPU 100%로 도는 동기 무한 루프에 걸리고 증상도 신기해집니다. vitest list로 테스트 목록을 뽑으면 통과하는데, 실행만 올리면 그대로 hang입니다.
정리할 때도 함정이 겹쳤습니다. pkill을 막 쓰면 자기 셸까지 죽일 수 있어서, 죽은 워커는 명시한 PID로 하나하나 걷어냈습니다. 죽은 워커가 돌아간 뒤에야 다음 실행을 건드리면 오염되니, 디버깅보다 고아 프로세스 정리가 먼저입니다.
세 번째는 선언되지 않은 의존성이었습니다. httpx2를 requirements에 적지 않았는데 로컬 venv에는 들어서, 게이트는 내내 초록불이었습니다. 그러다 실제 서비스 컨테이너 기동에서 죽었습니다.
커밋 메시지에 그대로 남긴 고백이 있습니다. "venv엔 있어 게이트 미검출, 실기동에서 발각". 테스트가 초록이라고 실제로 뜨는 것은, 결국 별개의 문제였습니다.
P4에서는 LLM에게 어디까지 맡기나
다음 단계인 논문 진행 추적에서는 선을 좁혔습니다. 누구의 차례인가, 경과일, 응답 지연 같은 숫자 지표는 LLM 없이 결정적 계산만 수행합니다.
판단은 집필 단계 분류 같은 영역만 LLM이 맡고, 그 결과도 confirmed=false를 단 '제안'으로만 기록됩니다. 확정 권한은 사람에게 남는 구조입니다.
LLM을 "다 하는 놈"이 아니라 "판단 한 칸만 담당하는 부품"으로 배치하는 연습이고요. 솔직히 말씀드리면, 이건 완성된 시스템이 아닙니다.
P2와 P3는 사람 승인 대기 중입니다. 실제 논문을 넣고 수기 계산과 대조해야 통과되는 게이트라, 에이전트가 대신 서명할 수가 없거든요. P4 카드들(O-00~O-08)은 지금 실행 중인 진행형이고, 더미 데이터 잔재 같은 살풍경도 눈에 띕니다.
테스트 수가 곧 품질은 아니라고 믿습니다. 이 수치들은 '회귀를 잡아낸 횟수'일 뿐이고, 승인 판정은 사람 몫으로 남겼습니다.
O 카드를 소진하면 아침 다이제스트 v2와 배포처 정리가 다음 순서입니다. 서브에이전트에게 코드를 맡기는 실험은, 게이트를 누가 닫는지가 정해지자 비로소 편해졌습니다.