-
AI 한테 코드를 자동으로 시킬 때 — 컨텍스트를 3축으로 쪼개라 (Ralph Loop 시리즈 1편)IT 2026. 5. 16. 21:00
로컬 LLM 으로 자율 코딩 루프를 돌려본 적이 있다면 한 번쯤 마주치는 장면이 있다. 매 iteration 마다 모델이 "방금 내가 뭘 만들었더라" 부터 다시 헤매는 장면이다. 5분 전에 분명히
spawner.js를 만들고 commit 까지 끝냈는데, 다음 iter 에 들어가면 모델은 그 사실을 모르고 처음부터 디렉토리를 훑는다. 더 나쁜 건 이미 만든 파일을 또 만들겠다고 시도 하다가 충돌을 일으키거나, "다 했다" 고 보고하고 끝내는데 정작 git log 에는 commit 이 없는 상황이다.이 글은 그 문제를 해결하기 위해 직접 짜본 Ralph Loop 패턴의 첫 디자인 결정을 다룬다. 결론부터 적으면 — LLM 한테 매 iter 던지는 컨텍스트를 한 덩어리로 던지지 말고, 불변·가변·실측 세 축으로 쪼개야 한다. 이 글은 9편짜리 시리즈의 첫 번째 글이고, 285줄짜리
ralph-loop.sh스크립트의 핵심 디자인을 한 편씩 풀어볼 예정이다.출발점 — 자동차 회피 게임을 자동으로 만들고 싶었다
주말 취미로 브라우저용 캐주얼 자동차 회피 게임 (탑다운, 좌우 이동, 10 레벨, 후반 동적 장애물) 을 만들기로 했는데, 직접 코딩하기보다 로컬에서 돌리는 Qwen3.6-35B-A3B-FP8 에 자율적으로 시키면 어디까지 가는지 보고 싶었다. DGX Spark 의 통합 메모리 128GB 에 vLLM 으로 모델을 띄우고, 자율 에이전트는 opencode CLI 를 쓰기로 했다. opencode 가 한 번 호출되면 모델 + 도구(파일 편집·bash·git)를 합쳐서 작업을 끝내고 돌아오는 구조라 ralph-loop 의 한 단위로 맞다.
처음엔 단순했다.
while true; do opencode run "다음 작업 1개를 하고 와" ; done. 그런데 매 iter 마다 LLM 이 0부터 디렉토리를 다시 읽고, 같은 파일을 또 만들고, "방금 만든 게 뭐냐" 를 매번 새로 파악하는 데 절반 이상의 시간을 썼다. 이게 곧 컨텍스트 설계 부재 의 직접적 결과다.발견 — 컨텍스트는 3종류로 분리해야 한다
몇 번의 시행착오 끝에 정착한 구조가 다음 세 축이다.
- PLAN.md (불변) — 게임의 비전·기술 스택·디렉토리 구조·레벨 매트릭스·코드 컨벤션·Ralph Loop 규약을 모두 담은 단일 문서. 첫 commit 에 들어간 뒤 어떤 iter 에서도 절대 수정되지 않는다. 모델한테 "이게 우리가 만들고 싶은 것" 의 권위 있는 정의로 매번 같은 모습으로 던진다.
- CHECKLIST.md (가변) — 8 phase × ~45 atomic 항목으로 분해된 작업 목록. 모델이 한 항목 끝낼 때마다
- [ ]를- [x]로 직접 토글한다. 작업하다 새 요구사항 발견하면 끝에- [ ] <새 항목>로 추가한다. 진척의 기록이자 다음 작업의 큐. - Git 상태 (실측 가변) — 매 iter 시작 시
git log --oneline -5,git diff HEAD~1,git status를 인라인으로 캡처해서 모델에게 던진다. 지금 진짜로 무엇이 있는가 의 ground truth 다. PLAN/CHECKLIST 가 말이라면 git 은 현실이다.
왜 이 셋이어야 하는지가 핵심이다. 만약 셋을 한 덩어리로 뭉뚱그리면 — 예를 들어 PLAN.md 안에 진척 추적까지 넣어두면 — 모델이 PLAN 을 수정하기 시작하고, 그러면 다음 iter 의 컨텍스트가 흔들린다. CHECKLIST 만 있고 git 없으면 모델이 "다 했다" 보고해도 실제로 코드가 거기 있는지 검증할 방법이 없다.
그림 설명 — 매 iter 마다 PLAN.md 와 CHECKLIST.md 는 opencode 의
-f옵션으로 파일째 첨부되고, git 상태는 prompt 안에 인라인 텍스트로 박힌다. opencode 가 한 항목을 구현하고 끝나면 결과가 두 갈래로 흩어진다 — CHECKLIST 라인은[x]로 토글되고, 변경 사항은 새 git commit 으로 박힌다. 다음 iter 가 시작되면 PLAN 은 똑같은 모습이지만 CHECKLIST 와 git 은 진척이 반영된 새 상태가 되어 있다. 즉 모델은 매번 새 세션이지만 작업은 누적된다.왜 PLAN 은 절대 수정되면 안 되는가
이 부분이 처음엔 직관에 반한다. 작업하다 보면 PLAN 의 일부 결정이 잘못된 게 드러나기도 한다 — 예를 들어 "차선 3개" 가 너무 좁아 보인다거나, 레벨 매트릭스의 spawn 간격이 비현실적이라거나. 그러면 PLAN 을 고치고 싶어진다.
그런데 PLAN 이 가변이 되는 순간 모델이 자기 작업의 권위 있는 정의를 스스로 수정 할 수 있게 된다. 한 iter 에서 어려운 항목을 만나면 PLAN 의 그 부분을 살짝 약하게 다시 쓰고, 그 약해진 PLAN 에 맞춰 작업을 끝내고 commit 한다. 외부에서 보면 진척이 잘 되는 것 같지만, 목표 자체가 작업 도중에 슬며시 깎인 것 이다. 이건 자율 에이전트가 빠지기 쉬운 함정이다.
그래서 PLAN.md 의 첫 줄에 명시했다.
# car-game — 영구 설계 문서 (PLAN.md) > ⚠️ **이 문서는 불변(immutable)이다.** Ralph Loop 어떤 단계에서도 수정 금지. > 변경이 필요하다면 사용자가 직접 편집한다.그리고 매 iter 의 prompt 안에서도 반복해서 강조한다.
## 컨텍스트 - **PLAN.md (불변)**: 첨부됨. 절대 수정 금지. - **CHECKLIST.md (가변)**: 첨부됨. 이번 iter 끝에 직접 수정. - **Git 상태 (가변)**: 아래 인라인 + 필요 시 git log/diff/status 직접 실행. ## 제약 - PLAN.md 는 절대 수정 금지 (불변 컨텍스트). - 한 iter 에 한 항목만. 욕심 내지 마라.이렇게 두 번 강조하니 38 iter 동안 PLAN.md 를 건드린 시도가 0 회였다. 모델이 가끔 "PLAN 의 이 부분이 모호한데..." 라고 응답에 적기는 했지만 실제 수정은 시도하지 않았다. 불변이라는 약속은 prompt 의 한 줄로도 충분히 작동한다.
왜 git 이 별도 축이어야 하는가
가장 처음엔 git 을 별도 축으로 안 두고 그냥 모델이 필요할 때 알아서 호출하라고 했다. 결과는 좋지 않았다 — 어떤 iter 는 git status 를 호출했지만 어떤 iter 는 그냥 PLAN/CHECKLIST 만 보고 작업을 시작했다. 후자의 경우 모델은 "src/spawner.js 가 있어야 한다" 는 PLAN 의 기술과 "src/spawner.js 가 없다" 는 직전 iter 의 실패한 commit 사이에서 상태를 잘못 추정했다.
해결은 단순했다. 매 iter 시작 시 ralph-loop.sh 가 직접 git 정보를 캡처해서 prompt 에 박는 것. 모델한테 "필요하면 호출해" 가 아니라 "이미 박아뒀다" 다.
# 매 iter 시작 시 git 컨텍스트 캡처 git_log=$(git log --oneline -5 2>/dev/null || echo "(no commits yet)") git_status=$(git status --short 2>/dev/null || echo "(repo error)") # prompt 안에 인라인으로 박기 prompt=$(cat <<EOF ... ### 직전 5 커밋 \`\`\` $git_log \`\`\` ### 현재 git status \`\`\` $git_status \`\`\` ... EOF )이렇게 박아둔 git 정보가 지금까지 진짜로 만들어진 것의 ground truth가 된다. CHECKLIST 가 "spawner.js 만들어야 함 [x]" 로 되어 있는데 git diff 에는 spawner.js 변경이 안 보이면, 모델은 즉시 그 불일치를 인지하고 "directory ls 부터 다시 확인" 으로 들어간다. 실제로 38 iter 중 몇 번은 이 불일치 감지 덕분에 잘못된 가정을 미리 차단했다.
핵심 코드 — ralph-loop.sh 의 한 iter 발췌
위에서 나눠 쓴 것을 한 덩어리로 보면 다음과 같다. 이게 한 iter 의 핵심이다.
while [ "$MAX_ITER" -eq 0 ] || [ "$iter" -lt "$MAX_ITER" ]; do iter=$((iter + 1)) # 1. 다음 미체크 항목 1개 추출 (가변 축에서 다음 작업 결정) next=$(grep -n '^- \[ \]' CHECKLIST.md | head -1 || true) if [ -z "$next" ]; then echo "✅ 모든 항목 완료 (iter=$iter)" break fi line_num=$(echo "$next" | cut -d: -f1) item=$(echo "$next" | sed 's/^[0-9]*:- \[ \] //') # 2. 매 iter 시작 시 git 컨텍스트 캡처 (실측 축) git_log=$(git log --oneline -5 2>/dev/null || echo "(no commits yet)") git_status=$(git status --short 2>/dev/null || echo "(repo error)") # 3. prompt 조립 — git 은 인라인, PLAN/CHECKLIST 는 -f 첨부 prompt=$(cat <<EOF 당신은 PLAN.md 의 설계를 따라 car-game 을 구현하는 자율 에이전트다. ## 컨텍스트 - **PLAN.md (불변)**: 첨부됨. 절대 수정 금지. - **CHECKLIST.md (가변)**: 첨부됨. 이번 iter 끝에 직접 수정. - **Git 상태 (가변)**: 아래 인라인. ### 직전 5 커밋 \`\`\` $git_log \`\`\` ### 현재 git status \`\`\` $git_status \`\`\` ## 이번 iteration 의 작업 **$item** (CHECKLIST.md line $line_num) EOF ) # 4. opencode 호출 — 새 세션, PLAN/CHECKLIST 는 -f 로 파일 첨부 opencode run \ --model "$MODEL" \ -f PLAN.md \ -f CHECKLIST.md \ -- "$prompt" # 5. 검증 후크 — 다음 글에서 다룰 주제 # ... done코드 설명 —
-f PLAN.md -f CHECKLIST.md두 개는 opencode 가 모델에게 파일로 첨부한다. 모델은 파일의 전체 내용을 한 번에 읽을 수 있고, 작업 중 필요할 때 다시 참조 가능하다. git 정보는 prompt 텍스트 안에 박았다. 매 iter 같은 PLAN 이 던져지지만 git 정보와 CHECKLIST 의 토글 상태는 매번 다르다. 이게 불변/가변/실측 3축 분리의 실제 구현이다.--구분자가 보이는데, 이건 yargs 의-farray 옵션이 다음 positional 까지 흡수하는 것을 막는 디테일이다 — 5편에서 더 자세히 다룬다.검증 후크 — 모델이 "다 했다" 보고할 때 의심하라
3축 분리만으로는 부족했다. 모델이 한 iter 끝에 "작업 완료, CHECKLIST 토글, commit 했음" 이라고 보고했지만 실제로는 commit 이 안 된 경우가 초반에 여러 번 있었다. shell escape 사고로 commit message 가 끊겼거나, 모델이 commit 이전 단계에서 응답을 끝냈거나 등 이유가 다양했다.
그래서 ralph-loop.sh 가 매 iter 끝에 두 가지 외부 검증을 한다.
# 검증 1 — CHECKLIST 라인이 진짜로 [x] 로 토글됐는가 escaped=$(printf '%s' "$item" | sed 's/[][\.*^$/]/\\&/g') checked=true if grep -nq "^- \[ \] ${escaped}\$" CHECKLIST.md; then checked=false fi # 검증 2 — 직전 3분 안에 git commit 이 진짜로 생성됐는가 committed=true if ! git log --oneline -1 --since="3 minute ago" | grep -q .; then committed=false fi if $checked && $committed; then echo "✅ iter $iter 완료 — 다음 항목으로" else echo "⚠️ iter $iter 미완" # 시도 카운트 누적, 3회 실패 시 자동 스킵 (다른 글에서) fi이 검증의 핵심은 "모델이 자기가 한 일을 보고하는 채널" 과 "그 일이 진짜로 일어났는지 검증하는 채널" 이 분리되어 있다는 것 이다. 모델은 응답 텍스트로 보고하고, 우리는 git 과 파일 시스템에서 사실을 확인한다. 두 채널이 같으면 진척, 다르면 실패. 이 불일치 감지가 38 iter 중 6~7회 정도 거짓 보고를 잡아냈다.
외부 검증 후크 자체는 시리즈 6편에서 깊이 다룬다. 여기서 강조하고 싶은 건 — 3축 컨텍스트 설계와 외부 검증은 같은 철학에서 나온다 는 점이다. 모델이 말하는 것 (응답 텍스트, CHECKLIST 토글) 과 모델이 실제로 한 것 (git commit, 파일 변경) 이 일치하는지 끊임없이 확인한다.
결론 — "프롬프트 엔지니어링" 너머의 "컨텍스트 엔지니어링"
요즘 LLM 응용에서 자주 듣는 말이 "프롬프트 엔지니어링" 인데, 자율 코딩 루프를 짜다 보면 그 표현이 부족하다는 걸 느낀다. 한 번 부르고 끝나는 호출이라면 prompt 한 덩어리를 잘 짜는 게 핵심이지만, 같은 작업을 38 iter 반복하면서 매번 새 세션으로 호출하는 구조에서는 컨텍스트 자체를 어떻게 분할·갱신·주입할지가 결정적 이다. 같은 prompt 라도 컨텍스트가 한 덩어리냐 3축이냐에 따라 진척률이 천양지차다.
이 글에서 다룬 3축 분리는 작은 취미 프로젝트의 ad-hoc 한 결정이지만, 근본 원리는 다른 자율 에이전트에도 적용된다. 영구 설계(불변) / 진행 추적(가변) / 실측 상태(live) 의 3분할은 어떤 도메인에서도 변하지 않는다. CRM 자동화 에이전트라면 회사 정책(불변) / 작업 큐(가변) / CRM 의 실시간 상태(live), 운영 자동화 에이전트라면 SOP(불변) / 인시던트 큐(가변) / 모니터링 메트릭(live) 식으로 매핑된다.
이 시리즈는 한 취미 프로젝트의 285줄짜리 bash 스크립트를 해부하면서 위 같은 일반 패턴들을 한 편씩 풀어나간다. 다음 글(2편)에서는 자율 스크립트의 부팅과 셧다운 lifecycle — GPU 자원 acquire, vLLM 컨테이너 기동, trap 으로 안전한 종료 — 을 다룬다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
AI 가 "다 했다" 라고 보고할 때 의심하라 — 외부 검증 후크 두 줄의 의미 (Ralph Loop 시리즈 6편) (0) 2026.05.17 opencode 한 줄 호출에 숨은 3가지 함정 — `--` 구분자, 5분 timeout, 직후 헬스체크 (Ralph Loop 시리즈 5편) (0) 2026.05.17 프롬프트만으로는 못 고친다 — 도구 가이드를 5단으로 줘도 schema 에러는 그대로였다 (Ralph Loop 시리즈 4편) (0) 2026.05.17 vLLM 이 매 iter 도중 자살할 수 있다 — 헬스 보장과 진단 자산을 한 함수에 (Ralph Loop 시리즈 3편) (0) 2026.05.16 자율 스크립트의 부팅과 셧다운 — 외부 자원을 잡았다면 정확히 한 번만 놓아라 (Ralph Loop 시리즈 2편) (0) 2026.05.16 시각 피드백의 시간차 — ripple·fly-up·confetti의 650/900/1100ms (0) 2026.05.15 SQLite로 streak를 영리하게 — substr DATE와 cursor 역순 (0) 2026.05.14 5축 25배지로 학습 동기를 설계하기 — 단기 도파민과 장기 약속 (1) 2026.05.14 끝까지 들으면 점수를 더 주는 챗봇 — 청취 완료 타이머의 디자인 (0) 2026.05.14 '잘 못 들었어요' 한 줄의 UX — STT 거부 후의 회복 흐름 (0) 2026.05.13