AiSubway v2
AI 지하철 노선 설계 도구
도시 지하철 건설 게임을 AI 팀이 직접 조작해 노선을 설계하고, 검수까지 거치는 환경을 만들었다.
무엇을 만들었나
도시 지하철 건설 게임을 사람 대신 AI 여러 개가 역할을 나눠 플레이하도록 만든 도구다. 서울 지도에서 AI 팀이 3개 노선·67역을 대출(채권) 없이 지어 대중교통 이용 비율을 2.06%까지 올렸고, 이 수치는 별도 과정으로 다시 계산해 일치함을 확인했다. 개발 중 드러난 채점·규칙 오류는 게임의 원래 동작과 대조해 고쳤다.
구현 방식과 보안 설계는 ‘기술 상세’에 있습니다.
구조와 설계
게임 세이브 파일을 읽어 노선·역·열차 배치를 설계·채점·검증하는 Node.js(ESM) CLI(v2)와 지도 대시보드이며, Subway Builder 게임을 대상으로 한다. 채점은 경로탐색 방식(RAPTOR) 기반으로 구현하고 게임이 보여 주는 결과와 일치하는지 대조했고, 운영비 과금·채점 기간 분할·종착역 회차 크로스오버·왕복시간 정의처럼 게임과 달랐던 규칙을 발견할 때마다 게임 동작 기준으로 고쳤다. 설계 산출물은 sha256 지문과 셸 실행 해시 체인 기록으로 증거 체인에 묶고, 적법성·예산·채점·증거 게이트를 통과한 설계만 정본 타임라인에 승격한다. 구현은 코딩 에이전트(Codex, 사용량 한도 때는 Claude opus)가, 검수는 별도 에이전트가 맡도록 분리했고, 헤드라인 수치는 오케스트레이터가 독립 재채점한 값만 기준으로 삼았다. 기존 동작 보존을 우선해 옛 채점 모드는 바이트 동일하게 유지했고, 노선도 라벨 여백은 기존 출력의 회귀 테스트를 깨지 않도록 상태 기반 출력에만 적용했다.
사용 기술
Node.js (ESM), 자체 테스트 러너 (fast/full 티어), Web Worker 기반 RAPTOR 채점, SVG 노선도 렌더링, Leaflet + OpenStreetMap 대시보드, sha256 산출물 지문, Python 훅, Claude Code 서브에이전트 (Opus/Sonnet), Codex CLI
보안·안정성 설계
- 읽기 전용 옵션(--no-emit)으로 채점할 때 설계 상태 파일을 저장하지 않도록 고치고 결과에 wroteState: false를 명시했다. 실행 전후 파일 sha256이 같음을 회귀 테스트로 확인했다.
- 예산 검사에 도시 불일치(CITY_MISMATCH) 가드를 넣어, 역 중심점이 선택된 수요 데이터 경계 밖이면 판정 없이 거부(exit 2)한다. 틀린 도시로 낸 PASS/FAIL이 게이트를 통과하지 못하게 한 조치다.
- 모든 셸 실행을 해시 체인으로 기록하는 훅과, 게이트가 인용된 실행을 그 기록과 대조하는 구조로 에이전트가 자기 성적표를 쓰는 것을 막았다. 검수 과정에서 우회 2건(위조 segment-break, 명령 텍스트 인용을 실행으로 오인)을 잡아 차단했다.
- 저장 점수에 채점 방법 4개 필드를 기록하고, plan 명령이 방법이 일치하지 않거나 필드가 없는 옛 점수를 기준선으로 재사용하지 못하게 가드했다(필드 누락은 불일치로 처리).
- 종착역 회차 규칙 검사를 strict 기본값으로 전환하되 옛 로그 재생용 override를 남겼다. strict 위반 시에는 상태를 바꾸지 않고 거부한다.
- save-metro가 지원하지 않는 크로스오버를 조용히 버리지 않고 거부한다.
- 구현자의 자기 보고를 믿지 않고 산출물 원본을 열어 검수하는 규칙을 두었고, 여러 에이전트가 같은 진입점 파일을 동시에 고쳐 생긴 장애 뒤에는 편집자를 1명으로 제한했다.
- 예산 게이트가 공식 채점 결과를 재사용할 때 채점 방법·데이터·코드 해시·증인이 붙은 실행을 모두 맞춰 보고 일부를 직접 다시 계산해 대조하게 했다. 검수에서 약 20종의 위조 시도를 모두 거부했고, 옛 실행을 증인으로 받던 구멍 1건을 찾아 막았다.
- 대시보드의 계획안 보기는 설계 폴더를 읽기만 하고, 경로 이탈 등 이상한 id를 거부하도록 만들었다. 검수에서 실행 전후 상태 파일 해시가 같고 손상된 파일에도 서버가 멈추지 않음을 확인했다.
작업 기록
12·13사이클, 채권 조기 상환과 새 기준 승격
11사이클에 처음 발행한 대형 채권 $1B를 전액 조기 상환하고 노선 연장과 증편을 넣은 설계를 새 채점 기준에서 다시 지어 승격했다. 분담률은 3.5598%에서 4.3810%(승객 +23.07%)로 올랐고 채권 잔액은 0이 됐으며, 오케스트레이터의 재채점이 끝자리까지 일치했다.
게임 세이브 내보내기 재작성과 사전 검사기
설계한 망을 게임 세이브로 내보내는 기능을 게임과 같은 레일 단위로 다시 만들고, 종착 크로스오버와 Y 분기를 지원하게 했다. 내보낸 세이브가 게임에서 읽힐 수 있는 형식인지 미리 검사하는 검사기를 붙여 오류 0을 확인했지만, 실제 게임에서 열어 보는 확인은 아직 하지 않았다.
대시보드에 미승격 계획안 보기·비교 모드 추가
승격 전 계획안을 지금 망 위에 겹쳐 신설 구간, 게이트 통과 현황, 점수 변화를 한 화면에서 보게 했다. 계획안 점수는 공식 채점 판정에서만 읽고 없거나 오래된 값이면 숫자 대신 상태로 표시하며, 읽기 전용 동작과 수치는 독립 검수가 재계산해 확인했다.
채점 정합성 수정과 기준값 재설정
겹친 구간의 배차 효과가 점수에 잡히도록 노선별 출발 위상을 분산하고, 열차 이동 시간을 게임 동작에 맞춰 노선 한 바퀴 오차를 −7.09%에서 −0.27%로 줄였다. 예산 게이트는 증인 규칙을 완화하지 않고 공식 채점 결과를 엄격한 조건으로 재사용해 804초에서 412초로 줄였고, 기준값은 한 번에 3.5598%로 다시 잡았다.
9~11사이클 운영과 설계 3건 승격
AI 함대가 서울 망에 설계 3건을 차례로 승격했다. 공식 지표를 4앵커 평균으로 바꿔 기준값을 3.1421%로 다시 잡은 뒤, 마지막 승격에서 분담률이 4.0076%(승객 418,889명, 이후 채점 기준 개정 전 값)까지 올랐다. 승격마다 오케스트레이터가 새 사본으로 다시 채점해 함대 값과 끝자리까지 일치함을 확인했고, 공유 기기의 메모리·CPU 여유를 확인한 뒤에만 채점을 시작하는 부하 게이트를 두었다.
공식 점수를 고정 시각 앵커 4개 평균으로 변경
시각표 위상 하나에 따라 상대 ±1% 정도 흔들리던 공식 분담률을 고정 시각 앵커 평균으로 바꾸고, 운임 수입도 같은 승객 집합에서 계산하게 했다. 앵커 12개 구현의 전수 채점이 순차 44분 걸려 4개로 줄였고, 병렬 4개로 3분 18초, 관련 테스트 414개 통과를 확인했으며 독립 검수 2회(1차에서 찾은 부수 결함 1건 수정 후 2차 통과)를 거쳤고, 새 기준값 재설정은 남아 있다.
역 이름 변경 명령 추가와 별칭 통합
영문 이름 저장을 지원하는 역 이름 변경 명령을 추가하고, 두 명령 이름이 같은 실행 함수를 쓰게 통합해 일회성 보너스 규칙이 똑같이 적용되게 했다. 통합 테스트 54개, fast tier 648개가 통과했다.
서울 판 노선 건설 1~6사이클 완료
서브에이전트 함대가 서울 맵에 3개 노선·67역을 채권 없이 건설해 분담률을 2.06%까지 올렸다. 헤드라인 수치는 오케스트레이터의 독립 재채점과 소수점까지 일치했다.
종착역 회차·크로스오버 규칙 반영
v2가 회차를 게임보다 관대하게 허용하던 것을 게임 동작 대조로 확인하고, 크로스오버 모델·건설 명령·합법성 규칙·회차 시간의 채점 반영을 구현했다. 단계별 검수가 모두 통과했고 fast tier 859개가 통과했다.
채점 방식 정합성 수정과 점수 이력 태그
채점기가 게임과 다르게 승객을 세던 문제(기준 설계 1.0699% → 1.0849%)와 한 바퀴 시간이 세 곳에서 따로 계산되던 문제(최대 60초 오차)를 고쳤다. 저장 점수에 채점 방법 태그를 붙여 옛 점수와 섞이지 않게 했고 fast tier 859/859가 통과했다.
채점·노선도·예산 검사 결함 3건 수정
읽기 전용 옵션이 상태 파일을 덮어쓰던 문제, 노선도 끝역 라벨이 잘리던 문제, 서울 설계를 다른 도시 수요로 채점해 거짓 FAIL을 내던 문제를 실패 후 통과 테스트로 고쳤다. 예산 검사 33/33, 채점 3/3, 노선도 25/25가 통과했다.
이 페이지는 작업 기록 문서 11건을 바탕으로 정리했습니다. 고객 정보, 접속 주소, 비밀값은 공개 전에 걸러 냅니다.