나의 Claude 코드 비서실장: VM 1대, 전문가 8명, 그리고 Telegram 채팅 하나
요약
이 글은 Claude Code 세션을 활용하여 복잡한 업무 흐름을 자동화하는 개인 에이전트 시스템 구축 과정을 설명합니다. 하나의 '비서실장' 대화가 들어온 작업을 여러 전문 '전문가' 프로세스에 라우팅하고, 타이머 루프를 통해 스케줄링된 작업을 처리합니다. 이 구조는 작업의 분산과 안정성을 높여 효율적인 업무 환경을 만듭니다.
핵심 포인트
- Claude Code 세션을 활용한 개인화된 에이전트 시스템 구축 방법 제시
- 비서실장(Chief of Staff) 역할을 통해 작업 라우팅 및 위임 구현
- 전문가들은 독립 프로세스로 운영되어 안정성과 확장성 확보
- 시스템의 실패 사례 분석을 통해 실질적인 개선점 도출
지난 한 주 동안 회의 외의 대부분의 작업은 하나의 대화를 통해 진행되었습니다. 저는 Telegram에서 Claude Code 세션에 텍스트 또는 음성 메모를 보내면, 이 세션이 각자 맡은 임무가 있는 다른 8개의 Claude 세션 중 하나로 작업을 전달합니다. 저녁이 되면 실험 측정 결과, 할 일 목록으로 바뀐 받은 편지함, 그리고 연구 노트까지 얻게 되는데, 터미널을 열어본 적이 없습니다.
그것은 이전보다 훨씬 깔끔해 보입니다. 이 시스템은 거의 매일 새로운 방식으로 고장 났고, 제가 그것에 대해 아는 대부분의 것은 바로 그 고장들로부터 나왔습니다. 본 포스트에서는 에이전트 구조와 비용, 그리고 이를 형성한 실패 사례들을 직접 구축할 수 있는 수준의 디테일로 설명합니다.
시스템의 형태
모든 것은 영구적으로 로그인된 데스크톱을 가진 하나의 Azure VM에서 실행되는데, 일부 작업은 실제 브라우저가 필요하기 때문입니다. 이 위에는 세 가지 종류의 프로세스가 실행됩니다:
- 비서실장(chief of staff). 제가 대화하는 장기 실행 Claude Code 세션 하나입니다. 저는 주로 음성 메모가 프로세스보다는 사람처럼 들리게 하려고 이름을 Elaine이라고 지었습니다. 이것은 스스로 작업을 수행하지 않습니다. 대신 각 작업을 전문가에게 라우팅하고 결과를 전달합니다.
- 전문가(Specialists). 백그라운드 Claude Code 세션(
claude --bg --name <역할>)들로, 각각 작성된 간략한 업무 지침이 있습니다: 콘텐츠, SEO, 비디오, 받은 편지함 분류(inbox triage), VM 자체를 위한 시스템 관리자(sysadmin), 그리고 제품 저장소 엔지니어들입니다. 이들은 서브 에이전트라기보다는 별도의 프로세스이기 때문에 비서실장이 재시작되어도 살아남습니다. - 타이머 루프(Timer loops). 대화가 전혀 없이 스케줄에 따라 헤드리스(
headless)claude -p작업을 실행하는 systemd 사용자 타이머입니다.
비서실장이 작업을 직접 수행하지 않는 이유
첫 번째 버전에서는 비서실장이 작은 작업들을 돕도록 했습니다. Claude Code는 한 번에 하나의 턴(turn)만 처리할 수 있기 때문에, 로그를 검색하는 데 소비된 매 분은 다음 요청이 대기열에 머무르는 시간이었습니다.
규칙은 엄격해졌습니다. 들어오는 모든 작업에 대해 최대 한 번의 빠른 읽기(quick read)를 수행하여 누가 이 작업을 해야 할지 결정하고, 그 전문가에게 요약본을 보내고 자신의 차례를 마칩니다. 만약 적합한 전문가가 없다면, 새로운 전문가를 투입합니다. 검증 또한 위임됩니다: '완료'라는 주장은 증거 제시 요청이나 다른 전문가의 확인을 받게 됩니다.
제가 예상하지 못했던 부가적인 이점은 다음과 같습니다: 모든 전문가의 컨텍스트가 자신의 도메인에 머무른다는 것입니다. 콘텐츠 세션(content session)은 콘텐츠 실험 원장(content experiment ledger)에 대한 자세한 내용을 알지만, 데이터베이스 마이그레이션(database migrations)에 대해서는 아무것도 모르기 때문에, 그 차례는 짧게 유지되고 실수를 추적하기 쉽습니다.
Telegram을 유일한 인터페이스로 사용하다
저는 VM의 터미널에 거의 접속하지 않기 때문에, 거기서만 표시되는 것은 저에게 도달하지 않습니다. Claude Code의 채널 기능은 세션을 Telegram 봇과 연결하며, 이 봇이 시스템이 저와 소통하는 유일한 방법이 되었습니다.
- 양방향 음성 메모. 들어오는 음성 메모는 Azure의 음성 인식(speech-to-text) 배포를 통해 전사(transcribed)되며, 비서실장은 원본 음성 버블처럼 답장할 수 있고, 이 모든 것이 한 달에 약 1달러가 들었습니다. 유일한 함정은 Telegram이
.oga파일을 보내고, 엔드포인트는 파일 이름을.ogg로 변경하지 않으면 해당 확장자를 거부한다는 것입니다. - 탭 가능한 결정. 결정 사항은 옵션들이 버튼으로 있는 답장 키보드로 도착합니다. 인라인 버튼(Inline buttons)이 더 보기 좋았지만, 플러그인이 콜백(callbacks)이 아닌 메시지를 전달하기 때문에 그 탭들은 세션에 도달하지 못했습니다.
- 하나의 채팅, 다른 곳에서의 긴 읽기. 모든 메시지는 프로젝트 이름을 명시하며, 초안이나 계획과 같이 긴 내용은 채팅에 링크가 포함된 Notion 페이지로 전송됩니다. 저는 한 프로젝트당 하나의 주제를 시도해 보았지만, 한 사람과 하나의 비서에게는 절약되는 것보다 더 많은 주의력을 소모했습니다.
감시견(watchdog)과 그것이 저에게 가르쳐준 Telegram에 대한 것들
Telegram은 봇 토큰당 정확히 하나의 폴러(poller)만 허용하며, Claude Code 플러그인은 새 폴러가 시작될 때 기존의 폴러를 종료함으로써 이를 강제합니다. 이는 한 세션에서는 합리적이지만 아홉 개를 실행하는 기계에는 위험합니다. 처음 이틀 동안 Telegram 브릿지는 네 가지 방식으로 다운되었습니다:
- 다른 세션이 플러그인을 시작하여 비서실장의 폴러를 종료하고 대신 가져갔고, 그 결과 제 메시지가 전문가의 세션으로 도착했습니다.
- 헬스 체크가 두 번째 복사본을 생성했습니다.
claude mcp list는 각 MCP 서버를 확인하기 위해 시작되는데, 이는 두 번째 폴러를 시작하여 활성 폴러를 종료시키고 나가는 과정을 거칩니다. 그 결과 폴러 자체가 없게 되었습니다. - 에이전트 뷰에서 오래된 세션을 삭제하는 것이 한 분 안에 활성 폴러를 종료시켰습니다.
- **유휴 퇴직(Idle retirement)**은 약 한 시간 후에 세션을 중지시켰고, 고정(pinning)하면 이를 면제받을 수 있습니다.
워치독(watchdog)은 2분마다 짧은 쉘 스크립트를 실행하는 systemd 타이머입니다. 건강한 틱(tick)당 CPU는 30~40밀리초, 메모리는 약 5MB를 사용합니다. 이 스크립트는 폴러가 살아 있는지 확인하고, 플러그인의 디렉토리에서 실행되며, 비서실장 세션을 조상으로 가집니다. 마지막 체크는 실패 사례 1에서 비롯되었습니다: 폴러가 살아 있고 건강한 상태로 세 번의 틱 동안 존재했지만, 잘못된 세션에 속해 있었습니다. '폴러가 존재함'과 '내 세션이 폴러를 소유함'은 다른 건강 신호이며, 후자만이 메시지가 자신에게 도착할지 여부를 알려줍니다.
두 번의 실패한 체크 후에 워치독은 잠금(lock) 및 쿨다운(cool-downs)과 함께 비서실장을 재시작합니다. 이 재시작 과정 자체에도 함정이 있습니다: claude --bg --resume <session-id>는 다른 플래그 없이 세션을 제자리에 깨우기만 합니다. 어떤 플래그라도 전달하면, 심지어 원래의 플래그라 할지라도, 이름과 Telegram 채널이 없는 새로운 ID를 가진 복사본을 얻게 됩니다.
타이머 루프: 결정론적 작업을 모델 외부로 이동시키기
각 루프는 두 단계로 실행됩니다. 첫 번째는 모델 없이 순수한 bash이며, 처리해야 할 일이 있는지 확인하고, 입력을 수집하며, 할 일이 없으면 종료합니다. 그 후에야 러너(runner)가 이미 프롬프트에 입력이 포함된 상태에서 더 저렴한 모델을 사용하여 claude -p를 시작합니다.
우리가 계속 배우는 교훈은 이것이었습니다. 결정론적인 모든 것은 모델의 지침(instructions)에 들어가는 것이 아니라 스크립트(script)에 속해야 한다는 것입니다.
- 헤드리스 실행(headless run)은
sleep을 호출할 수 없으므로, 대기하는 기능은 헬퍼(helper)에 존재합니다. - 브라우저 헬퍼는 재시도 전에 페이지를 다시 읽어들이므로, 절대 두 번 제출하지 않습니다.
- 스킬 파일(skill file)은 절대로 메모리(memory)를 가리키면 안 됩니다. 다른 디렉토리에서 실행되는 헤드리스 실행은 아무것도 로드하지 않으며, 참조는 조용히 아무 일도 하지 않습니다.
마크다운 파일을 이용한 메모리(Memory)
모든 세션은 공유된 마크다운 파일 디렉토리를 읽어들이며, 각 파일에는 이름, 한 줄 설명, 유형이 담긴 하나의 사실(fact)이 있고, 이들은 [[name]]으로 작은 그래프를 이루며 연결됩니다. '메모리당 한 줄' 인덱스는 모든 세션 시작 시 로드됩니다.
두 가지 규칙이 이것이 썩는 것을 방지합니다. 전문가들은 자신의 영역에서 사실을 수정할 수 있지만 반드시 그렇게 밝혀야 하며, 아무도 비서실장(chief of staff)의 동의 없이 메모리를 삭제해서는 안 됩니다. 왜냐하면 오래된 것처럼 보이는 메모리가 종종 어떤 규칙이 존재하는 이유에 대한 유일한 기록이기 때문입니다. 약점은 결정 로그(decision log)였는데, 이것이 5일 만에 188 KB까지 커졌고 매 세션 시작 시 다시 읽혔습니다. 이제는 짧은 라이브 큐와 아카이브로 나뉘어 있습니다.
비용 (What it costs)
어느 날 초기에 Anthropic 대시보드에는 약 $195가 표시되었는데, 저는 그 이유를 알지 못했습니다. 감사 결과 지출의 88%가 모델이 자신의 컨텍스트(context)를 재읽는 데 사용된 것임이 밝혀졌습니다. 여기에는 캐시 읽기(cache reads) 48%와 캐시 쓰기(cache writes) 40%가 포함되었고, 출력은 12% 미만이었습니다. 비서실장은 자동 압축(auto-compaction)이 1M 토큰 창에서 절대 트리거되지 않았기 때문에 매 턴마다 약 440,000 토큰을 재읽고 있었습니다.
도움이 된 것은 기발한 프롬프트가 아니라 컨텍스트와 턴(turns)이었습니다. 일반적인 창(window)을 사용했을 때, 비서실장의 턴당 비용은 약 28센트였지만, 13센트로 줄였습니다. 이는 Claude Opus 5.5를 기반으로 작동하며, 저는 수동으로 컨텍스트 창을 256,000 토큰으로 제한하여 자동 압축(auto-compaction)이 일찍 작동하게 함으로써 턴 비용을 저렴하게 유지하고 스레드를 반응성 있게 만들었습니다. 전문가들 역시 Claude Opus 5.5를 사용하며, 예약된 작업은 Claude Sonnet 5.5를 사용하여 턴당 몇 센트만 들고, 아무것도 Fable을 사용하지 않습니다. 그리고 모든 작업마다 하나의 완전한 브리프(brief)가 필요합니다. 왜냐하면 우리가 측정했던 어떤 것보다도 비용이 턴에 더 가깝게 따르기 때문입니다. 여전히 저렴하지는 않습니다. 일일 지출은 조용한 날에는 약 $30에서, 바쁜 빌드 날에는 약 $200까지 범위가 있습니다.
복사할 만한 실패 사례들
이 시스템의 대부분 버그는 조용히 실패했습니다: 깨끗한 로그, 종료 코드 0(exit code 0), 그리고 아무 일도 일어나지 않았습니다.
- 오래된 체크아웃(Stale checkouts). 타이머들은 각 저장소의 메인 체크아웃에서 스크립트를 실행하는 반면, 전문가들은 별도의 git worktree에서 작업합니다. 어느 날 다섯 개의 수정 사항이 푸시되었지만, 아무도 메인 체크아웃을 fast-forward 하지 않았기 때문에 그중 어느 것도 5시간 동안 실행되지 않았습니다. 이제 모든 보고서는 메인 체크아웃이 원격(remote)과 일치한다고 명시합니다.
- 정상적으로 읽히는 규칙들. 이 루프가 실제로 작동하기 전에, 첫날의 모든 타이머 발동을 실제 데이터에 시뮬레이션한 결과 네 가지 버그를 발견했는데, 여기에는 루프가 절대 획득할 수 없는 잠금(lock)도 포함되었습니다. 각 항목은 서류상으로는 문제없이 읽혔고 종료 코드 0으로 끝났을 것입니다.
- 테스트 대신 확인만 하는 검사들. 로그에서 예상하는 라인을 grep 하는 것은 당신의 기대를 확인시켜 줄 뿐, 코드를 테스트하지는 않습니다.
- 테스트가 아니었던 것. 드라이 런(dry run)은 한 번 실제 알림을 보내게 했습니다. 왜냐하면 모델 호출만 스텁(stubbed)되었기 때문입니다. 이제 러너들(Runners)에는 모든 사이드 채널을 무음 처리하는 스위치가 있습니다.
이 모든 것의 배경에 깔린 습관은 이렇습니다: 우리는 규칙이 실제 상태를 거쳐 실행될 때까지 신뢰하지 않으며, 보고서가 디스크에 기록될 때까지 그 결과를 신뢰하지 않습니다.
제가 직접 구축한 이유
시중에는 좋은 기성품 옵션들이 있습니다: xAI의 Grok Bot, Meta의 Muse, OpenAI의 Codex, Anthropic의 Claude Cowork, 그리고 각각에 대한 오픈 소스 대안들입니다. 이 모든 것들은 어떤 식으로든 저를 제약했습니다.
저는 제가 일하는 방식에 완전히 맞춰진 것을 원했습니다. 맞춤형 빌드는 각 도구의 최고 장점들을 가져와서 필요할 때마다 공급자를 전환하지 않고도 설정을 변경할 수 있게 해줍니다.
이것은 최종 빌드가 아니며, 다음으로 개인 비서에게 구축할 것에 대해 기대가 큽니다.
만약 여러분이 직접 만들었다면, 가장 먼저 무엇에 연결하시겠습니까?
원래 nulltensor.com에 게시되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기