AI 코딩 에이전트를 사람 없이 밤새 돌리는 방법이 있다. 커뮤니티에서 Ralph 라고 부른다.
원리는 단순하다 — 에이전트가 한 번 일을 끝내면 셸이 곧바로 다시 부른다. 그걸 무한 반복한다.
while :; do cat PROMPT.md | claude-code ; done
한 줄씩 읽으면 이렇다. while : 는 “조건 없이 계속 반복하라”는 뜻이고,
cat PROMPT.md 는 지시문이 적힌 파일을 꺼내서, | claude-code 는 그걸 에이전트에게 밀어넣는다.
지시문 하나를 끝없이 다시 주는 것, 그게 전부다.
만들기는 이렇게 쉽다. 그런데 실제로 밤새 돌려보면 문제가 다른 데로 옮겨간다.
“어떻게 계속 돌게 하느냐”가 아니라 “언제 스스로 멈추느냐” 가 된다.
사람이 보고 있으면 이상할 때 Ctrl+C 를 누르면 되는데, 밤에는 그 사람이 없다.
이 글은 Ralph 패턴이 무엇인지, 그리고 무인으로 돌리려 할 때 한 줄짜리 루프에 무엇을
더 붙여야 하는지를 정리한다. 실제 구현 사례 하나를 곁들이는데, 그 시스템은 내가 설계한 것이
아니다. 동료가 만들어 돌린 것을 자료로 받아 정리했다.
결론부터
루프를 도는 코드보다 루프를 멈추는 코드가 많아야 한다.
| 한 줄짜리 원형 | 무인으로 돌리려면 | |
|---|---|---|
| 반복 | 무조건 계속 | 몇 번까지, 한 번에 몇 분까지 |
| 진행 판정 | 없음 | 진전 없는 반복이 N번이면 종료 |
| 실패 처리 | 없음 | 한 작업이 N번 실패하면 빼놓고 간다 |
| 전체 중단 | 없음 | 막힌 게 절반쯤 되면 전부 멈춘다 |
| 종료 이유 | 없음 | 왜 멈췄는지를 한 단어로 남긴다 |
전체 모양은 이렇다. 셸은 계속 살아 있고 세션은 매번 죽는다.
아래에서 하나씩 본다. 그 전에 왜 매 사이클 세션을 새로 띄워야 하는지부터 짚는다.
이게 이 패턴의 핵심이고, 근거가 논문에 있다.
Ralph 가 어디서 온 이름인가
Geoffrey Huntley 가 2025-07-14 에 쓴 글이 출처다.
정의는 이 한 문장이다.
In its purest form, Ralph is a Bash loop.
이름은 심슨의 Ralph Wiggum 에서 왔다. 계속 실패해도 굴하지 않고 다시 시도하는 캐릭터다.
패턴의 성격을 정확히 담은 작명이다 — 똑똑해서 되는 게 아니라 지치지 않아서 된다.
이 패턴이 얼마나 퍼졌는지 보여주는 방증이 하나 있다. Anthropic 이슈 트래커에
[BUG] Ralph Wiggum loops are broken now (#31568) 라는 항목이 있다.
공식에서도 이 사용 패턴을 인지하고 있다는 뜻이다.
왜 매 사이클 세션을 죽이는가
여기서 말하는 세션은 에이전트를 한 번 띄워서 끝날 때까지의 한 판을 말한다.
대화창 하나를 열어 계속 이어가는 것이 한 세션이고, 창을 닫고 새로 여는 것이 새 세션이다.
그러면 한 세션으로 6시간을 쭉 버티게 하면 안 되나. 안 된다.
그리고 이건 취향 문제가 아니라 측정된 성질이다.
2026-08-31 arXiv 에 올라온 How Fast Do Agents Rot?
(arXiv:2609.01660) 가 이걸 정면으로 다룬다. 제목을 옮기면 “에이전트는 얼마나 빨리 썩는가”다.
Task success follows a geometric law governed by a single per-step reliability parameter,
which rises with model scale but saturates well below 1 even for the strongest models,
guaranteeing eventual collapse at sufficiently long horizons.
요지는 이렇다. 한 걸음의 성공률이 아무리 높아도 1(100%)에는 못 미친다.
그런데 여러 걸음을 이어서 하려면 그 확률들이 곱해진다. 전부 성공해야 하니까.
한 걸음이 99% 여도, 100걸음을 다 성공할 확률은 0.99 × 0.99 × ... 를 100번 해서 37% 다.
덧셈이었다면 완만하게 줄겠지만 곱셈이라 급격히 무너진다. 실측 결과는 더 냉정하다.
The effect is sharpest on the agentic task, where every model tested,
including widely deployed systems, falls from
near-perfect success to near zero within sixteen steps
16스텝. 에이전트형 과제에서, 테스트한 모든 모델이 그랬다.
조건을 정확히 옮기면 이렇다 — 이 16스텝은 에이전트형 과제에서 가장 가파르게 나타난 값이다.
모든 종류의 작업이 다 그렇다는 뜻이 아니다. 그런데 하필 Ralph 가 하는 일이 바로 그 에이전트형 과제다.
도구를 부르고 결과를 보고 다음 걸음을 정하는 루프. 가장 빨리 무너지는 조건을 골라서
가장 오래 돌리려는 셈이다.
여기서 한 가지를 더 짚어야 한다. 흔히 이 현상을 “컨텍스트가 길어져서”로 설명하는데,
논문은 그 설명을 반박한다.
Degradation is driven by step count rather than context length:
bounding the context window steepens decay rather than easing it (…),
contradicting a lost-in-the-middle explanation and
warning against a common production shortcut
컨텍스트를 줄이면 오히려 더 가파르게 무너진다. 문제는 길이가 아니라 걸음 수다.
그래서 대책도 “컨텍스트를 아끼자”가 아니라 “걸음 수를 끊자” 가 된다.
매 사이클 프로세스를 새로 띄우는 설계가 여기서 나온다.
곡선의 요지는 높은 구간만 반복해서 쓴다는 것이다. 오래 끌수록 좋아지는 게 아니라
오래 끌수록 나빠지므로, 끊어서 앞부분만 계속 쓴다.
마지막 구절이 날카롭다. 논문은 컨텍스트 윈도우를 줄이는 것을 “흔한 프로덕션 지름길”
이라고 부르며 경고한다. 길어지면 무너지니까 줄이자는 대응은 직관적이고 그래서 흔한데,
측정해보니 그게 상황을 악화시켰다는 것이다.
근거로 붙은 수치는 감쇠 기울기 -0.69 대 -0.44, p=3x10⁻⁶ 이다.
초록은 두 값에 각각 라벨을 달지 않았지만, 제한했을 때가 더 가파르다는 것이 문장의 주장이다.
정확한 대응은 본문을 봐야 한다.
세션을 죽이기로 했으면 프롬프트가 그 전제를 명시해야 한다. 사례로 받은 시스템은
사이클 프롬프트 첫 두 줄이 이랬다.
이 사이클이 끝나면 세션은 죽는다. 기억하지 말고 파일에 써라.
다음 사이클은 파일만 본다.
공식 문서는 이걸 권고한 적이 없다
여기서 흔한 오해를 하나 짚는다. “Anthropic 공식 문서가 세션 이어붙이기를 쓰지 말라고 한다”
는 말이 도는데, 사실이 아니다.
공식 headless 문서는 오히려 --continue 와 --resume 사용법을 상세히 싣고,
세션 ID 를 받아 이어 붙이는 예제까지 보여준다.
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
즉 매 사이클 리셋은 공식 권고를 따른 것이 아니라, 위 논문 근거로 내리는 독립적인 설계 선택이다.
공식 문서를 근거로 대면 틀린 인용이 된다. 이 구분을 뭉개면 글 전체의 신뢰가 깎인다.
무인 루프는 멈춤 조건이 다섯 개 필요하다
사례로 받은 시스템은 셸 드라이버가 442줄인데, 그 대부분이 “언제 멈추나”다.
루프 자체는 원형과 똑같이 몇 줄이면 된다. 나머지가 전부 안전장치다.
| 조건 | 기본값 | 없으면 생기는 일 |
|---|---|---|
| 사이클당 시간 제한 | 1시간 | 한 사이클이 무한정 붙들고 있어도 아무도 모른다 |
| 사이클 수 상한 | 1000 | 최후의 안전판. 논리가 다 실패해도 여기서 끝난다 |
| 무진전 허용 횟수 | 3 | 같은 자리를 도는 걸 진행으로 착각한다 |
| 태스크당 재시도 상한 | 3 | 하나가 안 되는데 밤새 그것만 붙들고 있는다 |
| 전체 중단 임계 비율 | 0.4 | 40% 가 막혔으면 환경이 깨진 것이다. 더 도는 건 낭비다 |
마지막 줄이 특히 중요하다. 태스크 하나가 막히는 것과 절반 가까이가 막히는 것은 원인이 다르다.
전자는 그 태스크 문제고, 후자는 빌드가 깨졌거나 전제가 틀린 것이다.
후자에서 계속 도는 루프는 토큰만 태운다.
왜 멈췄는지를 기계가 읽을 수 있게
멈추는 것만으로는 부족하다. 아침에 일어나서 왜 멈췄는지 알 수 있어야 한다.
사례 시스템은 멈출 때 이유를 화면 출력의 마지막 줄에 한 단어로 남기고,
종료 코드(프로그램이 끝나면서 남기는 번호. 0이면 정상, 그 외는 문제가 있었다는 뜻)와
짝을 맞춘다. 사람은 글자를 읽고, 다른 프로그램은 번호를 읽는다.
STOP_REASON=finished 정상 완주
STOP_REASON=idle 진전이 없어서
STOP_REASON=halt 너무 많이 막혀서
STOP_REASON=cycle_timeout 한 사이클이 제한을 넘겨서
STOP_REASON=max_cycles 상한에 걸려서
finished 와 idle 은 둘 다 “더 할 게 없다”지만 의미가 정반대다.
전자는 다 했다는 뜻이고 후자는 못 하고 있다는 뜻이다. 한 단어로 갈라두지 않으면
아침에 로그를 뒤져야 한다.
사이클의 끝을 어떻게 알리나
세션이 매번 죽으므로, 셸은 “이번 사이클이 어떻게 끝났는지”를 출력으로만 알 수 있다.
사례 시스템의 규약은 이렇다 — 세션의 마지막 줄이 반드시 아래 중 하나다.
CYCLE_RESULT=progress 무언가 진행됐다
CYCLE_RESULT=idle 할 일을 못 집었다
CYCLE_RESULT=finished 전부 끝났다
CYCLE_RESULT=halt 중단해야 한다
정확히 마지막 줄만 비교한다. 문서 중간에 같은 문자열을 언급해도 인정하지 않는다.
이 규칙은 처음부터 있던 게 아니라 사고가 나고 붙었다. 드라이버 주석에 그 경위가 남아 있다.
# CYCLE_RESULT 는 claude -p 가 낸 자유 텍스트 전체가 아니라 그 마지막
# (빈 줄이 아닌) 줄만 본다. 예전에는 `*CYCLE_RESULT=progress*` 로 스트림
# 전체를 grep 했는데, 세션이 설명 문장 안에서 그 문자열을 그저 인용만 해도
# idle 카운터가 영원히 0 으로 리셋돼 유일한 정지장치가 무력화됐다.
last_line="$(printf '%s\n' "$out" | sed -e '/^[[:space:]]*$/d' | tail -n 1)"
읽어볼 만하다. 에이전트가 “CYCLE_RESULT=progress 를 출력하면 됩니다” 라고 설명만 해도
진행으로 집계됐다. 자기 할 일을 설명하는 문장이 곧 완료 신호가 된 것이다.
그 결과 무진전 종료 조건이 영원히 안 걸린다 — 주석의 표현대로 유일한 정지장치가 무력화됐다.
무인 루프에서 출력을 어떻게 파싱하느냐는 편의가 아니라 안전장치 그 자체다.
grep 한 글자 차이로 멈출 수 없는 시스템이 된다.
같은 증상, 다른 원인을 갈라놓기
비슷한 교훈이 하나 더 있다. 세션 한도에 걸려 claude 가 즉시 죽으면 CYCLE_RESULT 가
아예 안 나온다. 그러면 무진전으로 집계되고, 결국 “진전이 없어서 멈췄다”로 보고된다.
# ⛔ 세션 한도를 `idle` 이라 부르지 않는다. (...) 그 보고는 "루프가 할 일이 없다"로
# 읽히는데 사실은 "돌 수가 없다"이고 처방이 정반대다
# (계획을 보라 ↔ 한도가 풀릴 때까지 기다려라).
case "$out" in
*"hit your session limit"*|*"Session limit reached"*)
echo "STOP_REASON=session_limit"; exit 14 ;;
esac
두 상황의 화면 출력은 같은데 아침에 할 일이 정반대다. 하나는 계획을 고쳐야 하고
하나는 그냥 기다리면 된다. 무인 시스템의 보고는 사람이 다음 행동을 고르는 유일한 입력이라,
여기서 뭉개면 엉뚱한 곳을 파게 된다.
에이전트가 자기 성적을 매기지 못하게
무인 시스템에서 제일 위험한 건 에이전트가 스스로 “됐다”고 말하는 것이다.
사례 시스템의 프롬프트에 이 문장이 있다.
판정 로직은 전부 이 커맨드가 낸다. 직접 판단하지 않는다.
상태 변경을 CLI 커맨드 8개로만 하게 막아두고, 통과/실패 판정은 그 커맨드가 내린다.
에이전트는 명령을 부를 뿐 결과를 정하지 못한다.
이 방향은 Anthropic 이 쓴 에이전트 평가 글과도 맞는다.
We recommend choosing deterministic graders where possible,
LLM graders where necessary or for additional flexibility, (…)
그리고 자기보고를 믿지 말라는 예시도 든다.
A flight-booking agent might say “Your flight has been booked”… but the outcome is
whether a reservation exists in the environment’s SQL database
말이 아니라 환경의 상태를 봐야 한다. 다만 여기서 선을 하나 그어야 한다 —
공식 문서가 권하는 것은 “결정론적 채점을 선호하라”는 원칙까지이고,
구체적인 게이트 구성은 그 원칙을 각자 구현한 결과물이다. 공식이 처방한 형태가 아니다.
락을 못 잡은 것은 실패가 아니다
용어 두 개를 먼저 풀어야 한다.
- 게이트(gate) — 통과 검사대. 테스트가 통과했는지, 리뷰를 받았는지를 확인하는 관문이다. 여기를 못 넘으면 그 태스크는 완료가 아니다
- 락(lock) — 순서표. 여러 작업이 같은 것(예: 빌드 디렉터리)을 동시에 건드리면 깨지므로, 하나가 쓰는 동안 다른 하나는 기다리게 하는 장치다
이제 사례의 규칙을 볼 수 있다.
락과 관련된 것은 어느 것도
fail --gate로 가지 않는다.
락을 못 잡았다는 것은 그 게이트가 돌지 않았다는 뜻이고,
락이 회수됐다는 것은 그 게이트의 판정을 모른다는 뜻이다.
풀어 쓰면 이렇다. 순서표를 못 받아서 검사를 아예 못 받은 것과,
검사를 받았는데 떨어진 것은 완전히 다른 일이다. 그런데 둘 다 “실패”로 적으면
기록만 봐서는 구분이 안 된다.
“모른다”와 “실패다”를 구분한다. 이게 무인 시스템에서 왜 중요하냐면,
앞서 본 재시도 상한이 걸려 있기 때문이다.
순서를 못 받은 걸 실패로 세면 남은 시도 횟수가 깎인다. 세 번 그러면
코드에는 아무 문제가 없는 태스크가 격리된다. 줄을 세 번 못 섰다는 이유로
탈락하는 셈이다.
아침에 보면 더 고약하다. 기록에는 “실패”라고 적혀 있으니 코드부터 들여다보게 된다.
정작 원인은 코드 밖에 있는데, 그걸 알려면 로그를 한참 뒤져야 한다.
사람이 지켜보는 시스템이라면 “어 이상한데” 하고 넘어갈 수 있는 구분이다.
무인에서는 이 한 줄이 밤새 쌓인다.
면제를 만들면 상한도 같이 둬야 한다
여기서 한 발 더 간 설계가 있다. 같은 프롬프트에 이런 문장이 있다.
다시 부르는 것은 태스크당 최대 2회까지다. 횟수를 세지 않으면 이 분기는
attempts 를 소모하지 않는 무한 루프가 되어 사이클 타임아웃까지 같은 커맨드를 반복한다.
재시도 상한을 면제해줬더니 그 면제가 새 구멍이 됐다. 카운터를 안 태우니
영원히 반복할 수 있게 된 것이다. 그래서 면제에도 별도 상한을 붙였다.
무인 시스템에서 예외를 만들 때 따라붙는 규칙이다 — 면제의 범위를 정했으면
그 면제를 몇 번까지 쓸 수 있는지도 같이 정한다.
방어 문구는 터진 만큼 늘어난다
사례 시스템의 사이클 프롬프트는 874줄이고, 그중 않는다 가 61회, ⛔ 가 33회 나온다.
금지가 지시보다 많다. 처음부터 이랬을 리 없다. 개정 이력을 보면 그 과정이 보인다.
| 시점 | 누적 | 무슨 일이 있었나 |
|---|---|---|
| 최초 | 138줄 | 드라이버와 사이클 프롬프트 |
| 첫날 | 502줄 | 수정 라운드 4번 — exit code, 락, 상호배제 |
| 이튿날 | 709줄 | 수정 라운드 6번 — 우회 차단, 소유 범위 강제 |
| 이후 3주 | 874줄 | 7번, 대부분 소폭 |
이틀 만에 709줄이다. 최종 분량의 81%가 처음 이틀에 쓰였다.
그 뒤 3주간 늘어난 건 165줄뿐이다. 초기에 우회 경로가 쏟아졌다는 뜻이다.
붙은 문구들이 무엇을 막는지 보면 성격이 한결같다.
-
아무것도 안 쓰고 통과 — 구현이 파일을 한 줄도 안 바꿔도 게이트를 지날 수 있었다.
변경된 파일 없음을 실패 사유로 추가 -
테스트를 고쳐서 통과 — 빨간 테스트를 만나면 테스트 쪽을 고치려 든다.
테스트 파일 변경을 실패 사유로 추가해 테스트를 읽기 전용으로 -
빨갛지 않은 빨간 테스트 — 테스트 코드를 안 쓰고 명령어만 적어두고 “작성 완료”로 보고. 그래서 완료 처리 커맨드가 테스트를 실제로 돌려보고
exit 0이면 거부하도록 바뀌었다
마지막 것에는 후일담이 있다. 3주 뒤 개정 제목이 spec-done 이 못 보는 것 — 틀린 이유로 빨간 테스트 다.
테스트가 빨갛긴 한데 의도한 이유로 빨갛지 않은 경우가 또 나왔다는 뜻이다.
오타로 실행이 안 돼도 화면은 빨갛다.
무인 시스템의 프롬프트가 길어지는 건 설명이 늘어서가 아니라 막을 것이 늘어서다.
그리고 무엇을 막아야 하는지는 대체로 터지고 나서 안다.
막혔을 때 무엇을 남기고 멈추나
재시도 상한을 넘기면 태스크가 격리된다. 여기서 중요한 건 격리가 포기가 아니라는 점이다.
사례 시스템의 격리 기록에는 이런 것들이 들어간다.
- 무엇을 시도했고 각 라운드에서 무엇이 막혔는지
- 구체적인 실패 시나리오
- 처방 — 어느 함수에 무엇을 붙여야 하는지
- 이전 구현의 보존 위치 (버리지 않고 별도 브랜치에 남긴다)
- 이 태스크가 막고 있는 후속 태스크 수 (우선순위 판단용)
- 범위 밖이라 손대지 않았지만 같은 문제가 있을 법한 곳
실제 한 건에서는 리뷰 게이트가 세 라운드에 걸쳐 우회를 잡아냈다. 구현이 1차 우회를 막자
2차를, 그것도 막자 더 깊은 3차 우회를 찾아냈고, 결국 처방까지 적고 사람에게 넘겼다.
“3회 실패하면 격리”의 실제 의미는 이것이다 — 그냥 포기하는 게 아니라
사람이 짧게 보고 판단할 수 있는 문서를 남기고 멈춘다. 격리 파일 이름에 종류가 들어가서
목록만 봐도 성격을 안다.
격리는 실패가 아니라 스케줄링 상태다
수치를 보면 이 구분이 분명해진다. 세 모듈의 격리·해제 이벤트 집계다.
| 격리 | 해제 | 완료 | |
|---|---|---|---|
| 모듈 A | 3 | 3 | 16 |
| 모듈 B | 21 | 2 | 13 |
| 모듈 C | 76 | 70 | 15 |
모듈 C 는 76번 막혔는데 완주했다. 대부분이 “선행 태스크가 막혀서 딸려 막힌 것”이고,
원인이 풀리자 자동으로 해제됐다(70건). 격리 수만 보고 실패로 읽으면 정반대로 해석한다.
남는 구멍 — 그런데 구멍의 크기를 정확히 재야 한다
정직하게 쓰면 사례 시스템에도 구멍이 있다. 다만 그 크기를 과장하면 안 된다.
여기서 흔히 잘못 짚는다.
판정은 전부 CLI 가 내리는데 CYCLE_RESULT=progress 하나만은 에이전트가 자기 입으로 말한다.
“이번 사이클에 진전이 있었다”를 본인이 보고하는 구조다.
그런데 거짓말을 하면 무슨 일이 일어나는지를 따져보면 범위가 좁다.
에이전트가 거짓 progress 를 내면 |
|
|---|---|
| 태스크 상태 | 안 바뀐다. 완료 처리는 CLI 가 커밋·머지·판정 파일을 직접 확인한다 |
| 코드 | 안 들어간다. 게이트를 통과한 적이 없으므로 |
| 무진전 카운터 | 속는다. 0 으로 리셋된다 |
그래서 실제 위험은 “가짜 완료”가 아니라 “가짜 진행”이다.
잘못된 코드가 들어가는 게 아니라, 아무것도 못 하면서 무진전 종료 조건을 계속 피해
사이클 상한이나 사람이 볼 때까지 토큰을 태우는 것이다.
피해의 종류가 다르다. 전자는 코드를 되돌려야 하고 후자는 돈만 나간다.
구멍을 말할 때 이 둘을 뭉개면 “이 시스템은 가짜 완료가 가능하다”는 틀린 인상을 준다.
레퍼런스 해법은 이미 있다. OpenHands 의 Stuck Detector 는 이벤트 히스토리를
룰로 검사해 판정하고 에이전트에게 묻지 않는다. 다섯 가지를 본다.
- 같은 action → observation 이 반복되는가
- 같은 action → error 가 반복되는가
- 독백만 하고 있는가
- 핑퐁(두 상태를 오가는 것)에 빠졌는가
- 컨텍스트 윈도우 에러가 나는가
전부 밖에서 관찰 가능한 신호다. 에이전트의 협조가 필요 없다.
자기보고를 대체하려면 이 방향으로 가야 한다.
그럼 Ralph 를 아무 데나 쓰면 되나
안 된다. 그리고 이건 패턴을 만든 사람이 직접 말한 것이다.
Huntley 는 같은 글에서 이렇게 썼다.
There’s no way in heck would I use Ralph in an existing code base.
그 외에 본인이 꼽은 제약도 만만치 않다.
- 컨텍스트 창 낭비 — 쓰면 쓸수록 결과가 나빠진다
- 비결정적 실패 — 코드를 검색해서 이미 구현됐는지 판단하는데, 그 판단이 틀릴 수 있다
- 플레이스홀더 구현 — 모델이 최소한만 구현하고 넘어가려는 경향
즉 Ralph 는 새로 만드는 코드, 검증 수단이 갖춰진 영역에 쓰는 도구다.
기존 코드베이스에 풀어놓으면 무엇이 이미 있는지 잘못 판단해서 중복을 만들거나 덮어쓴다.
공식 플러그인과 셸 루프는 용도가 다르다
Claude Code 에는 Ralph Wiggum 플러그인(명령은 /ralph-loop)이 있는데, 셸 루프와 형태가 다르다.
README 가 직접 밝힌다.
The loop happens inside your current session - you don’t need external bash loops
현재 세션 안에서 Stop 훅으로 반복한다. 훅(hook)은 특정 시점에 끼어드는 장치를 말하는데,
여기서는 에이전트가 “다 끝났다”며 나가려는 순간을 가로채 같은 지시문을 다시 밀어넣는다.
밖에서 다시 부르는 게 아니라 안에서 못 나가게 잡는 방식이다.
그래서 사람 터미널이 열려 있어야 하고,
세션 하나의 수명에 묶인다. 앞서 본 걸음 수 문제도 그대로 안고 간다.
어느 쪽이 낫다는 이야기가 아니다. 대화 중에 한 태스크를 끝까지 밀어붙이는 것과
자는 동안 모듈 하나를 통째로 만드는 것은 다른 일이다. 전자에 외부 셸 루프는 과하고,
후자에 세션 수명 제약은 치명적이다. 자기 용도를 먼저 정하고 고르는 게 맞다.
공식 플래그 몇 개는 안 쓰면 손해다
마지막으로, 무인 루프를 짤 때 알아두면 좋은 것들이다. 사례 시스템도 이 중 몇 개는
안 쓰고 있었다.
| 플래그 | 쓰임 |
|---|---|
--max-budget-usd |
비용 상한. 서브에이전트 비용까지 포함한다 |
--max-turns |
턴 수 상한 |
--output-format json |
응답에 total_cost_usd 와 모델별 비용 내역이 들어온다 |
--json-schema |
출력을 스키마에 맞춘다. 마지막 줄 규약보다 견고하다 |
--permission-prompts none |
아무도 승인할 수 없는 환경용 (v2.1.259+) |
--bare |
훅·플러그인·MCP·CLAUDE.md 를 안 읽고 시작한다 |
--bare 에 대해 문서는 이렇게 쓴다.
--bareis the recommended mode for scripted and SDK calls,
and will become the default for-pin a future release
밤새 도는 시스템에 --max-budget-usd 가 없는 게 제일 아픈 구멍이다.
앞서 본 “자기보고 때문에 무한 반복”과 겹치면 비용이 상한 없이 올라간다.
멈춤 조건을 아무리 잘 짜도 그건 논리적 멈춤이고, 이건 금액으로 거는 마지막 방어선이다.
직접 확인하지 않은 것
- 사례 시스템은 내가 설계하지도 구현하지도 않았다. 동료가 만들어 돌린 것을 자료로 받았고, 줄 수·커밋 시점·프롬프트 축자 같은 것만 레포에서 직접 확인했다
- 실제로 몇 시간 연속 돌았고 태스크마다
test → feat → merge순서가 반복됐다는 것은 커밋 기록으로 확인했지만, 그 코드의 품질을 내가 리뷰하지는 않았다 - OpenHands Stuck Detector 는 문서로 확인한 것이고 직접 돌려보지 않았다
- 이 글의 영어 인용은 전부 원문을 받아 문자 단위로 대조했다. 남이 정리해준 것을 그대로 옮기지 않았다
- 위 공식 플래그들은 문서에 실재하는 것을 확인했고, 무인 루프에 붙여 장시간 돌려본 것은 아니다
정리
- Ralph 는
while :; do cat PROMPT.md | claude-code ; done한 줄이 원형이다. 만들기는 쉽고 멈추게 하기가 어렵다 - 한 세션으로 길게 끄는 건 구조적으로 안 된다. 논문 실측으로 모든 모델이 16스텝 안에 무너졌고, 원인은 컨텍스트 길이가 아니라 걸음 수였다. 그래서 대책이 리셋이다
-
매 사이클 리셋은 공식 권고가 아니다. 공식 문서는 오히려
--resume을 안내한다. 근거로 댈 것은 논문이지 문서가 아니다 - 무인으로 돌리려면 멈춤 조건이 최소 다섯 가지 필요하다 — 사이클 시간·사이클 수·무진전·재시도·전체 중단 비율. 그리고 왜 멈췄는지를 종료 코드로 남겨야 아침에 알 수 있다
- 판정을 에이전트에게 맡기지 않는다. “모른다”와 “실패다”를 구분하지 않으면 멀쩡한 작업이 격리돼 쌓인다
-
출력 파싱이 안전장치다. 스트림 전체를
grep하면 에이전트가 설명 삼아 쓴 문장이 완료 신호가 되고, 유일한 정지장치가 무력화된다 - 남는 구멍은 진행 보고가 자기보고라는 점이다. 다만 “가짜 완료”가 아니라 “가짜 진행” 이다 — 코드가 들어가는 게 아니라 토큰이 탄다. 구멍은 크기를 정확히 재야 한다
- 패턴을 만든 사람이 기존 코드베이스에는 안 쓰겠다고 했다. 새로 만드는 영역, 검증 수단이 있는 영역에 쓰는 도구다
마지막으로, 사례를 다 보고 나서 남는 문장은 이거다.
무인 루프의 설계는 “무엇을 하게 할까”가 아니라 “무엇을 못 하게 할까”다.
프롬프트 874줄에서 금지가 지시보다 많고, 판정은 전부 코드가 가져갔고, 테스트 파일은
읽기 전용이 됐다. 그리고 그 금지들은 하나같이 터진 뒤에 붙었다.
사람이 보고 있으면 “어 이상한데” 한마디로 끝날 일들이다. 그 한마디를 할 사람이 없을 때,
그 자리를 메우는 게 이 문장들이다.