ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • AI 가 "다 했다" 라고 보고할 때 의심하라 — 외부 검증 후크 두 줄의 의미 (Ralph Loop 시리즈 6편)
    IT 2026. 5. 17. 23:00
    AI 가 \"다 했다\" 라고 보고할 때 의심하라 — 외부 검증 후크 두 줄의 의미 (Ralph Loop 시리즈 6편)

    자율 에이전트가 가장 위험할 때는 실패할 때 가 아니라 실패했는데 성공했다고 보고할 때 다. ralph-loop 초반에 가장 자주 본 실패 패턴이 이거였다 — 모델이 "작업 완료, CHECKLIST 토글, 커밋 했음" 이라고 응답을 끝냈는데 정작 git log 에는 commit 이 없거나, CHECKLIST.md 의 라인이 그대로 - [ ] 인 경우. 응답 텍스트만 보면 진척이 잘 되는 것 같은데 실제로는 작업이 안 되어 있다.

    이 글은 그 거짓 보고를 잡기 위해 ralph-loop.sh 가 매 iter 끝에 수행하는 두 줄짜리 외부 검증 후크 — (1) CHECKLIST 라인이 진짜 토글됐는가 (2) 직전 3분 안에 git commit 이 진짜 생성됐는가 — 를 분해한다. 단순한 grep 두 번이지만 그 안에 AI 응답을 신뢰하지 않는 운영 철학 이 박혀있다. 시리즈 6편이고, 1~5편은 컨텍스트 3축 / 부팅·셧다운 / vLLM 헬스 보장 / prompt 다층 메시지 / opencode 호출 함정을 다뤘다.

    출발점 — 모델이 거짓말을 한다는 게 아니라

    오해를 먼저 풀자. 모델이 의도적으로 거짓말 하는 게 아니다. ralph-loop 38 iter 동안 관찰한 거짓 보고의 원인은 다음 중 하나였다.

    1. shell escape 사고 — 모델이 git commit -m "feat: 한국어 메시지 with \"중첩 따옴표\"" 같은 형식으로 commit 하다가 셸이 따옴표를 해석 실패해 commit 자체가 깨짐. 모델은 응답에 "커밋 완료" 적었지만 실제론 commit 안 됨
    2. 도구 호출 실패의 부분 인지 — Edit 도구 호출이 SchemaError 로 실패했지만 모델이 그걸 부분적으로 인지하고 나머지는 됐다고 가정 하고 응답 마무리
    3. 응답 토큰 limit — 모델이 작업 끝까지 갔지만 응답 텍스트가 limit 에 걸려 "커밋했음" 까지 출력하기 전에 잘림. CHECKLIST 토글까진 했는데 commit 만 누락
    4. vLLM streaming 끊김 — 시리즈 3편의 KV cache 자살. 응답 도중 끊겨서 모델이 "작업 끝낼 의도였는데" 못 끝냄

    모두 모델 책임 이 아니라 그 사이 어딘가의 layer 책임이다. 그래도 결과는 같다 — 모델 응답 텍스트와 실제 시스템 상태가 어긋난다. 자율 시스템이 모델 응답만 신뢰 하면 그 어긋남이 누적되어 잘못된 진척률 보고로 이어진다.

    해결의 출발점은 — 모델이 보고하는 채널과 실제 결과를 검증하는 채널을 분리 하는 것. 모델은 응답 텍스트로 보고하고, 우리는 모델이 건드린 외부 사실 에서 검증한다.

    두 외부 사실 — CHECKLIST 토글 + git commit 존재

    ralph-loop 의 작업 단위에서 모델이 건드려야 하는 외부 사실 은 정확히 두 가지다.

    1. CHECKLIST.md 의 해당 라인이 - [ ]- [x] 로 바뀌었는가
    2. 그 변경이 git commit 으로 박혔는가

    이 둘이 모두 만족되면 작업 진척이 영구적으로 시스템에 박힌 상태다. 한 가지만 만족되면 — CHECKLIST 만 토글되고 commit 이 없으면 다음 iter 에 git status 에 dirty 로 보일 거고, commit 만 있고 CHECKLIST 토글 안 됐으면 같은 항목을 다음 iter 에 또 시도하게 된다. 둘 다 안 되면 진척 0.

    그래서 검증 후크는 두 사실을 외부에서 확인한다.

    # 검증 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
    
    # 두 검증의 AND 가 진척의 정의
    if $checked && $committed; then
      echo "✅ iter $iter 완료 — 다음 항목으로"
    else
      echo "⚠️  iter $iter 미완 (checked=$checked, committed=$committed)"
    fi

    코드 설명 — 두 검증이 모두 true 일 때만 iter 완료 로 판정한다. 한 가지라도 false 면 미완. 검증 1 은 CHECKLIST.md 에 그 항목이 여전히 미체크 상태로 남아있는가 를 grep 으로 본다. 남아있으면 토글 안 된 것 → checked=false. 검증 2 는 직전 3분 안에 어떤 커밋이라도 있는가 를 git log 의 --since 옵션으로 본다. 없으면 committed=false. $checked && $committed 의 AND 가 진척의 조작적 정의 가 된다.

    그림 설명 — 모델 응답이 "작업 완료" 라고 보고해도 ralph-loop 는 그걸 직접 신뢰하지 않는다. 두 외부 채널 — CHECKLIST.md 의 grep 결과와 git log 의 since 결과 — 으로 실제 사실 을 확인한다. 두 채널이 모두 true 일 때만 진척으로 인정. 한 가지라도 false 면 시도 카운트가 늘어 다음 iter 에서 재시도 한다 (시리즈 7편의 dead-letter 패턴). 모델 응답 채널과 검증 채널이 물리적으로 분리 되어 있다는 게 핵심이다.

    왜 검증 1 의 grep 패턴이 까다로운가

    검증 1 의 한 줄을 자세히 본다.

    escaped=$(printf '%s' "$item" | sed 's/[][\.*^$/]/\\&/g')
    checked=true
    if grep -nq "^- \[ \] ${escaped}\$" CHECKLIST.md; then
      checked=false
    fi

    $item 을 그대로 grep 패턴에 넣지 않고 sed 로 escape 한 $escaped 를 쓰는가?

    CHECKLIST 의 항목 이름에는 정규식 특수문자가 자주 포함된다.

    • src/main.js 부트스트랩 (DOMContentLoaded → Game 인스턴스 생성) — 괄호 (), 화살표 →
    • tests/collision.test.js — 6 케이스 (겹침/접점/분리/한쪽 포함 등) — 슬래시 /, 괄호
    • src/levels.js — LEVELS 배열 1~10 (PLAN.md 매트릭스 그대로) — 점 ., 물결 ~, 괄호

    이런 항목 이름을 그대로 grep 에 넣으면 — 점은 임의 한 글자 로, 괄호는 그룹 으로, 슬래시는 delimiter 로 해석되어 매치가 잘못 된다. 예를 들어 src/main.js 의 점이 임의 문자로 해석되면 srcXmainXjs 도 매치된다. 가능성은 낮지만 — 비슷한 이름의 다른 항목이 우연히 매치되면 완료된 다른 항목을 미완으로 오판 할 수 있다.

    해결은 sed 로 정규식 특수문자 5종 ([]\.*^$/) 을 모두 escape 하는 것. 's/[][\.*^$/]/\\&/g' 가 그 변환을 한 줄로 한다.

    # item: src/main.js 부트스트랩 (DOMContentLoaded → Game 인스턴스 생성)
    # escaped: src\/main\.js 부트스트랩 \(DOMContentLoaded → Game 인스턴스 생성\)

    이렇게 escape 한 후 "^- \[ \] ${escaped}\$" 패턴으로 grep 한다. ^\$ 가 줄 시작·끝을 명시해 정확히 그 항목 줄만 매치한다. \[\] 도 바깥쪽에서 한 번 더 escape 한다 (대괄호 자체가 grep 에서 character class 라).

    이 한 줄 escape 가 없으면 — 한국어 항목 이름 + 특수문자 조합에서 가끔 완료된 항목을 미완으로 또는 미완 항목을 완료로 오판하는 사고가 발생한다. 38 iter 동안 한 번도 그런 사고가 없었던 건 escape 덕이다.

    왜 검증 2 의 시간 윈도우가 3분인가

    검증 2 의 --since="3 minute ago" 가 또 디테일이다.

    if ! git log --oneline -1 --since="3 minute ago" | grep -q .; then
      committed=false
    fi

    왜 3분인가? 이 숫자가 두 가지 트레이드오프 사이의 균형이다.

    너무 짧으면 (예: 30초) — opencode 가 작업 시작부터 commit 까지 30초 안에 끝내야 한다. 정상 iter 도 30초 ~ 2분 사이라 30초 윈도우는 자주 false negative (실제 commit 했는데 윈도우 밖이라 미완으로 오판) 를 일으킨다.

    너무 길면 (예: 30분)직전 iter 의 commit 을 이번 iter 의 commit 으로 오판할 수 있다. iter A 에서 commit 했고 iter B 가 시작됐을 때 30분 윈도우면 iter A 의 commit 이 검색에 잡혀 false positive (실제 commit 안 했는데 검증 통과로 오판) 를 일으킨다.

    3분은 — opencode 의 정상 작업 시간 (≤2분) + 안전 마진 (1분) 이고 — iter 간 간격은 보통 2초 sleep 만 있으니 직전 iter 의 commit 이 3분 안에 끊임없이 새로 들어오진 않는 시간이다. 38 iter 운영 중 false positive / negative 모두 0이었다.

    이 윈도우 시간은 작업 도메인에 따라 조정 해야 한다. 더 무거운 작업 (예: ML 모델 학습, 큰 파일 다운로드) 에 ralph-loop 패턴을 쓴다면 윈도우를 30분 또는 1시간으로 늘려야 할 수 있다. 단 그러면 false positive 위험이 생기니 — 직전 iter 의 commit hash 를 변수에 저장하고 그것과 다른 commit 만 카운트 하는 더 정밀한 방법으로 가야 한다.

    검증 결과의 두 가지 분기 — 다음 항목 진행 vs 시도 카운트 누적

    검증 후크 직후 두 분기다.

    if $checked && $committed; then
      echo "✅ iter $iter 완료 — 다음 항목으로"
      echo "0" > "$LOG_DIR/.attempts"
      : > "$LOG_DIR/.prev_item"
    else
      echo "⚠️  iter $iter 미완 (checked=$checked, committed=$committed, opencode_rc=$opencode_rc, 시도=$attempts/3)"
      if [ "$attempts" -ge 3 ]; then
        # 3-strike 자동 스킵 (시리즈 7편)
        sed -i "s|^- \[ \] ${escaped}\$|- [!] ${escaped}|" CHECKLIST.md
        git add CHECKLIST.md
        git commit -m "skip: $(printf '%s' "$item" | head -c 60) (3회 실패)" || true
        echo "🚫 항목 자동 스킵"
        echo "0" > "$LOG_DIR/.attempts"
        : > "$LOG_DIR/.prev_item"
      fi
    fi

    분기 1 (성공) — 두 검증 모두 true 면 시도 카운트를 0 으로 리셋하고 prev_item 도 비운다. 다음 iter 의 첫 미체크 항목 (다른 항목) 에 대해 시도 1로 시작한다.

    분기 2 (미완) — 한 가지라도 false 면 미완 메시지 출력. opencode_rc 도 함께 출력해 어떻게 미완인지 진단 정보 제공. 시도 카운트 자체는 다음 iter 시작 시점에 누적되는데 (시리즈 7편), 만약 이미 3회 누적이면 즉시 자동 스킵 처리한다.

    이 분기에서 핵심은 — 검증 실패 시 곧장 ralph-loop 를 죽이지 않는다. 초창기 버전에선 검증 실패 = 즉시 exit 2 였는데, 그러면 사람이 매번 끼어들어야 한다. 자율성 0. 시도 카운트 + dead-letter 로 자동 우회 하는 패턴이 자율성을 살린다 (시리즈 7편).

    왜 두 검증이 둘 다 필요한가 — 한 가지만으론 못 막는 시나리오

    두 검증을 하나만 두면 안 되는 이유는 다음과 같다.

    시나리오 검증 1 만 (CHECKLIST 만) 검증 2 만 (commit 만) 둘 다
    모델이 CHECKLIST 토글 + commit 둘 다 함
    CHECKLIST 토글했는데 commit 누락 (escape 사고) ✅ (false positive! 진짜 미완인데 통과) ❌ 잡음 ❌ 잡음
    commit 했는데 CHECKLIST 토글 안 함 (작업 도중 종료) ❌ 잡음 ✅ (false positive! commit 만 봐서 통과) ❌ 잡음
    둘 다 안 함 ❌ 잡음 ❌ 잡음 ❌ 잡음

    두 가지 false positive 시나리오가 한 검증만 있으면 잡지 못한다. 둘 다 있어야 — 완전한 진척이라는 의미를 정확히 표현 한다.

    특히 두 번째 행 (CHECKLIST 토글 + commit 누락) 이 자주 본 케이스다. 모델이 CHECKLIST 를 먼저 Edit 으로 토글하고 그 다음 git commit 시도했는데 — commit 의 shell escape 사고로 commit 자체가 깨짐. 그 결과 모델 응답에는 commit 했다고 적혀있고 CHECKLIST 도 토글됐는데 git log 에 없는 상태. 검증 1 만으로는 못 잡는다.

    이 패턴의 일반화 — 외부 사실의 AND

    이 검증 후크 패턴은 ralph-loop 의 작업 단위 정의에 의존한다. 우리 작업 단위는 CHECKLIST 한 항목 = 한 commit 이고, 그래서 검증할 외부 사실이 정확히 두 가지다. 다른 도메인이라면 다르다.

    • DB 마이그레이션 자동화: 검증 = (1) 스키마 버전 테이블이 새 버전으로 업데이트됐는가 + (2) 마이그레이션 스크립트가 실행 로그에 박혔는가 + (3) 새 컬럼/테이블이 실제로 존재하는가 (3개 사실)
    • 인시던트 자동 대응: 검증 = (1) 알람 상태가 resolved 로 바뀌었는가 + (2) post-mortem 문서가 생성됐는가 + (3) 관련 메트릭이 정상 범위로 돌아왔는가
    • 코드 리뷰 자동화: 검증 = (1) PR 의 review status 가 approved/changes_requested 인가 + (2) 인라인 comment 가 N개 이상 달렸는가

    핵심은 — "작업 완료" 의 의미를 모델 응답 텍스트가 아니라 외부 시스템의 실제 상태로 정의 하는 것. 그 외부 사실들의 AND 가 진척의 조작적 정의가 된다.

    모델 응답을 전혀 안 보는 건 아니다

    오해를 또 하나 풀어둔다. ralph-loop 가 모델 응답을 무시하는 건 아니다. 응답을 .ralph-logs/iter-N.log 파일에 모두 저장하고, 사용자가 사후에 읽을 수 있다. 디버깅, 패턴 분석, 모델 성능 평가에 다 쓴다.

    응답을 진척 판정의 기준으로 쓰지 않는다 가 정확한 표현이다. 응답은 모델이 무엇을 시도했는지의 기록 이고, 외부 검증은 그 시도가 시스템에 영구적으로 박혔는지의 사실 이다. 두 정보가 다르면 — 응답은 보존하지만 진척으로는 안 잡는다.

    이 분리가 운영 시 모델 행동 분석진척 판정 이라는 두 일을 동시에 가능하게 한다. 응답 로그를 grep 해서 "prompt 강화 후 모델이 도구 호출 재시도를 더 자주 하나" 같은 질문에 답할 수 있고, 진척 통계는 외부 사실에 박힌 진짜 결과만 카운트한다.

    결론 — "신뢰하지 마라, 검증하라"

    이 글의 한 문장 결론 — 자율 시스템에서 모델 응답은 시도의 기록 이지 결과의 보고 가 아니다. 결과는 외부 시스템의 사실에서 검증해야 한다.

    이건 모델을 의심하는 게 아니라 — 모델이 시도한 일이 어딘가에서 실패할 가능성을 인정 하는 거다. shell escape 사고, 도구 호출 schema 에러, 응답 토큰 limit, vLLM 자살 — 모두 모델 책임 밖의 layer 에서 일어나는 일인데 결과는 모델이 거짓 보고한 것처럼 보인다. 외부 검증이 그 모든 layer 의 실패를 결과 기준으로 일관되게 잡는다.

    또 — 외부 검증이 진척 통계의 진실 이 된다는 점도 중요하다. 시리즈 7편에서 다룰 진척률의 거짓말 함정 — 스킵을 진척으로 카운트하면 80% 가 보이지만 실제론 32% 인 사고 — 가 외부 검증 없이 모델 응답만 신뢰하면 더 자주 발생한다. 외부 검증은 진척의 정의 자체를 외부 사실에 묶어서 그 함정을 피한다.

    두 줄 grep 으로 가능한 신뢰 시스템이라는 게 — 처음엔 단순해 보이지만 — 그 단순함 안에 AI 응답을 신뢰하지 않는다는 운영 원칙 이 깔끔하게 박혀있다. 자율 시스템 디자인의 핵심이다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.