neoworks 운영 에이전트
프리랜서 문의함을 맡아 처리하는 에이전트: 모든 답장은 제 승인을 받아 나가고, 개발은 엔지니어 에이전트에게 넘겨요
- 기간
- 2026년 7월 – 지금
- 대상
- 제 프리랜서 사업
- 역할
- 1인: 설계, 개발, 배포, 운영
- 기술
- TypeScript, Claude Agent SDK, Postgres, Prisma, Hono, React PWA, Cloudflare Workers, Telegram Bot API
- 규모
- 커밋 153개, 4.7만 줄, 앱 3개 + 패키지 1개, 마이그레이션 24개
- 외주 작업, neoworks
다른 언어: english
어떤 프로젝트인가요
neoworks는 제 프리랜서 사업이에요. 일은 거의 다 한국 프리랜서 마켓을 통해 들어오고, 모든 작업이 거기 채팅방에서 시작돼요. 이건 그 문의함 위에서 돌아가는 시스템이에요. 모든 채팅방을 동기화하고, 고객마다 에이전트를 하나씩 붙여서 대화를 읽고 한국어로 답장 초안을 쓰게 하고, 실제 개발은 백그라운드 엔지니어 에이전트에게 넘겨요. 고객 눈에 보이는 건 전부 승인 단계를 거쳐요. 텔레그램 카드나 대시보드 푸시 알림으로요.
구성은 node 프로세스 하나(게이트웨이), 게이트웨이가 서빙하는 react PWA, 텔레그램 봇용 사이드 프로세스, 외부 요청용 cloudflare worker예요. 7월 중순까지 에이전트 턴 2,024번을 돌렸고 승인 요청 1,295건을 만들었어요. 그중 219건은 제가 거절했어요.
어려웠던 부분
접속부터가 문제였어요. 마켓이 cloudflare WAF 뒤에 있어서 제 서버 IP로는 403이 떠요. 처음엔 ssh SOCKS 터널을 썼는데 계속 손이 갔어요. 7월에 77줄짜리 cloudflare worker로 바꿨어요. 그 도메인에 한해서 cloudflare 엣지에서 사이트를 다시 불러오고, 공유 시크릿으로 막아둔 구조예요. 일주일 뒤에 429가 나기 시작했어요. 무료 플랜의 하루 10만 요청이 한 계정의 worker 약 20개 전체에 공유되는 거였어요. 지금은 계정을 따로 하나 쓰고 있어요.
제가 저를 로그아웃시키지 않기. API 클라이언트는 파일 락을 걸고 쿠키 파일 하나를 여러 프로세스가 같이 써요. 까다로운 부분은 비밀번호로 새로 로그인할 때마다 마켓 쪽 세션 묶음이 교체되면서 같이 쓰던 웹 세션이 끊긴다는 거예요. 그래서 토큰 갱신이 cloudflare 403, 429, 타임아웃으로 실패하면 일시적인 실패로 보고 나중에 다시 시도해요. 갱신 엔드포인트에서 진짜 401이 왔을 때만 재로그인해요.
조용히 죽는 턴. 턴 하나가 claude code 서브프로세스 하나예요. 초반엔 끝난 작업이 사라지곤 했어요. 프롬프트를 그냥 문자열로 넘기면 SDK가 첫 결과에서 stdin을 닫아버려서, 백그라운드 엔지니어가 일을 끝내고 나면 게이트웨이 도구 호출이 전부 "Stream closed"로 실패했어요. 지금은 백그라운드 작업이 결과를 보고할 때까지 열려 있는 입력 채널 위에서 턴을 돌려요. 그래도 8월에 고객 한 분이 5시간 40분 동안 답을 못 받은 일이 있었어요. 서브에이전트를 재개하면 완료 알림은 한 번 더 오는데 시작 이벤트는 다시 오지 않아서, 게이트웨이가 남은 작업이 없다고 판단하고 채널을 닫아버렸어요. 죽은 호출은 평범한 결과로 돌아왔으니 그 실행은 성공으로 저장됐고요. 이제는 작업을 다시 등록하고, 죽은 호출이 하나라도 있는 턴은 실패 처리한 뒤 재개해요.
서버가 멈추는 문제. PM2는 "online"이라고 하는데 게이트웨이가 한 번에 8분에서 65분씩 아무것도 안 하고 있었어요. 턴마다 서브프로세스에 서브에이전트까지 붙는데, 4~6개가 동시에 돌면 고객 컨테이너까지 같이 돌리는 11GB 서버가 스왑으로 밀려났어요. 이와 별개로 프로세스가 6GB를 넘겨서 OOM으로 죽었고, 그러면 systemd 기본 OOM 정책 때문에 서버의 PM2 앱 전체가 약 21분마다 재시작됐어요. 진짜 원인은 헬스 체크 쿼리 하나였어요. 고객 65명의 프롬프트 전문을 통째로 가져오고 있었고, 그게 문자열로만 2.9GB였어요. 지금 그 쿼리는 약 185MB를 써요.
분 단위로 잘린 타임스탬프. 한 고객이 견적 요청을 18초 간격으로 메시지 세 개에 나눠 보냈어요. 초안이 "더 새로운 메시지가 있나" 검사를 통과해버렸는데, 2번과 3번 메시지가 1번보다 새로워 보이지 않았기 때문이에요. 지금은 도착 시각과 메시지 id를 같이 비교해요.
제가 한 일
7월에 10단계 계획을 세우고 한 번에 다 반영했어요. 단계마다 플래그를 따로 둬서 롤백은 되돌리기 커밋이 아니라 설정 변경 하나로 끝나요.
- 따로 돌던 heartbeat 프로세스를 게이트웨이 안의 작업 프레임워크로 합쳐서, 운영 점검도 그냥 에이전트 실행이 되게 했어요
- 고객 프로젝트 폴더 49개(계획에선 8개로 잡았어요)를 하나의 트리로 옮기고 각각
metadata.json을 붙였어요. 옮기기 전에 돌던 컨테이너 47개, 옮긴 뒤에도 47개예요 - 메모리는 에이전트가 직접 쓰는 작은 파일들로 두고, 턴마다 인덱스를 넣어주고, 매주 압축 작업을 돌려요
- 세션은 컨텍스트가 넘쳐서 죽는 대신, 유휴 상태가 되면 읽기 전용 "메모리 플러시" 턴을 거쳐 교체돼요
- 메모리 큐 아래에 Postgres 원장을 깔아서 재시작해도 메시지를 잃지 않아요
핵심은 여전히 승인이에요. 자동 승인은 슬라이더로 조절하는데, 단순 확인 답장만 모아둔 고정 목록부터 전부 허용까지 고를 수 있고, 단순 확인보다 위 단계는 먼저 거부 가능 시간을 기다려요. 한번은 구매자 턴이 결제 오류로 4초 만에 죽었는데 두 시간 넘게 아무도 몰랐고(그 구매자는 다른 곳으로 갔어요), 그 뒤로 프런트 데스크를 추가했어요. 10분 동안 답이 없는 메시지에는 메인 턴과 별도로 답장만 하는 짧은 턴이 돌아요.
나중에는 모델 계정을 별도 라우터로 빼냈고, claude가 정책 사유로 거절한 턴은 DeepSeek으로 한 번 더 돌려요. 매일 도는 자기 점검이 전략 문구를 바꿀 수도 있는데, 과거 대화를 다시 돌려봤을 때 지금 문구보다 나은 경우에만 바뀌고, 모든 변경은 한 번 탭으로 되돌릴 수 있어요.
지금은
실제로 매일 쓰고 있어요. 저는 텔레그램에서 결정하고, 에이전트들은 읽고, 초안을 쓰고, 개발하고, 후속 연락을 해요. 게이트웨이는 한국 시간 새벽 2시에 스스로 재시작하는데, 진행 중인 턴이 없을 때만 해요. 그 뒤 커밋은 대부분 위에 쓴 것 같은 수정이고, 하나하나 장애 기록을 남겨뒀어요.