AI 코딩 에이전트에 루프를 씌우는 것은 단발 프롬프트가 한계에 부딪히면 누구나 떠올리는 다음 수다. 돌리고, 채점하고, 점수가 낮으면 다시 돌린다.
그런데 바로 두 개의 벽에 부딪힌다.
- 점수는 오르는데 결과물은 여전히 틀렸다. 에이전트가 과제가 아니라 채점기를 만족시키는 법을 배운 것이다.
- 실패가 막다른 길이다. 루프의 부품은 다 있는데, 실패한 실행이 다음 실행으로 이어지지 않는다.
둘 다 모델의 문제가 아니라 환경 설계의 문제고, 모델이 강해진다고 사라지지 않는다. 강한 모델은 지름길을 더 빨리 찾을 뿐이다.
두 벽을 구체적이고 읽을 수 있는 예제로 짚어보려 한다. 오픈소스 Agent OS인 Ouroboros에 최근 합류한 RFC다. 설계는 이슈 #1917에, 구현은 #1916에 있다. 전문을 직접 읽을 수 있다는 것, 그게 추상적인 얘기 대신 이 사례를 고른 이유다.
벽 1: 수험생에게 답안지를 쥐여줬다
대부분의 에이전트 하네스는 수용 기준을 워커의 프롬프트에 그대로 렌더링한다. 채점에 쓸 명령과 채점 기준이 될 단정문까지 포함해서. 동기는 그럴듯하다. 에이전트가 어떻게 검사받을지 알면 과녁을 제대로 겨눌 수 있다는 것이다.
Ouroboros도 정확히 그렇게 했었다. _build_success_contract_block이 verify_command와 Expected output: <단정문>을 워커의 지시문에 렌더링했다. 두 번째 누출은 더 찾기 어려웠다. 재시도 때 실패 사유에 단정문의 repr()이 실려 result.error를 타고 다음 프롬프트로 되돌아갔다.
에이전트가 단정문을 볼 수 있게 되는 순간, 단정문을 만족시키는 것이 요구사항을 만족시키는 것보다 싸진다. RFC는 이걸 직설적으로 명명한다. 궁지에 몰린 워커의 가장 싼 경로는 수용 기준을 구현하는 것이 아니라 단정 문자열을 갖고 노는 것이고, 포스트모템(seed_2be2907edc07)이 첨부돼 있다. 교과서적인 보상 해킹(reward hacking)이다. 능력을 재고 있다고 생각하지만, 실제로 재는 것은 답을 베끼는 능력이다.
해법: 프롬프트에 넣지 않는다, 되돌릴 스위치도 없이
두 누출 경로를 모두 막아야 한다. 정방향만 막으면 아무것도 달라지지 않는다.
- 정방향:
_build_success_contract_block은 이제 수용 기준 설명과expected_artifacts만 렌더링한다. 검증은 하네스가 독립적으로 수행하므로 채점 로직이 워커의 성공 계약에 들어가지 않는다. - 역방향: 검증 게이트의 실패 사유에 더 이상 단정문
repr()이 실리지 않는다. 재시도 힌트는 단정문을 모든 조각에서 걸러내는 전용 빌더(orchestrator/retry_hints.py)가 만든다. 명령 출력의 마지막 2,000자 꼬리까지 포함해서.
그 2,000자 꼬리가 베껴갈 가치가 있는 대목이다. 정방향을 막고 로그 꼬리를 잊으면 누출은 그대로 열려 있다.
한계도 처음부터 적어둔다. 이 탈민감화는 다섯 가지 인코딩에 대한 축자(verbatim) 매칭이라, 줄바꿈되거나 diff 접두사가 붙는 식으로 형태가 바뀐 단정문 사본은 여전히 통과한다. 이건 해결된 척하지 않고 공개 이슈(#2020)로 열려 있다.
RFC에는 "공개 수준" 설정 노브가 제안됐다가 기각된 기록도 남아 있다. 끌 수 있는 정보 차단벽은 누군가 일정에 쫓기는 어느 오후에 꺼질 것이고, 그 뒤로 점수가 더 좋아 보이기 때문에 아무도 알아차리지 못한다.
막힌 에이전트가 대신 받는 것
전부 숨기기만 하면 워커는 허우적댈 수밖에 없다. 그래서 RFC는 은닉과 힌트 루프를 짝으로 묶는다. 다음 라운드의 지시문은 세션이 실제로 한 일에서 재구성된다. 툴 호출 궤적, 증거 목록(deliver_gate.load_ac_evidence_manifest 재사용, 읽기 전용), 검증기의 판정 결과. 단정문에서가 아니라.
| 답을 공개하면 | 궤적 기반 힌트면 | |
|---|---|---|
| 에이전트가 보는 것 | "단정문이 요구하는 출력은 X" |
"A와 B를 호출했고, 산출물 C가 없고, 3단계 검증에서 실패했다" |
| 가장 싼 경로 |
X를 지어낸다 |
실제로 C를 만든다 |
| 점수 상승 = 능력 상승인가 | 아니오 | 예 |
정보 비대칭이다. 사람의 시험이 쓰는 것과 같은 배치다. 출제자는 답을 알고, 수험생은 자기가 어디서 틀렸는지만 배운다.
벽 2: 실패가 막다른 길이다
RFC에 따르면 부품은 전부 이미 있었다. 검증 게이트, 실행-평가 체인, evolve_step, Ralph 드라이버, focus.select_evolution_focus. 그런데 아무것도 연결돼 있지 않았다.
- 실패한 실행은 정식 평가에 들어가지 않았다. 실패는 종착지였고 BLOCKED로 표시될 뿐이었다.
- 기각된 평가는 진화에 들어가지 않았다. 역시 종착지였다.
루프는 끊어진 세 토막으로 존재했다. 실패는 보고될 뿐, 다음 실행에 쓰이지 않았다.
해법: 실행을 평가로, 평가를 진화로 잇는다
세 가지 제약이 핵심이다.
1. 실패한 실행도 평가로 이어진다. _run_succeeded 게이트를 완화해서, 세션을 만든 실행이라면 정식 평가로 체인된다. fail-open은 유지된다. 큐 등록이 실패해도 실행의 결과가 뒤집히지는 않는다.
2. 기각된 평가는 예산이 있는 진화 루프를 발동한다. 여기서 수렴 루프를 새로 구현한 사람은 없다. 평가 잡의 종결 경로가 final_approved is False일 때 기존 진화 기계를 큐에 넣을 뿐이다.
새로 만든 것은 1세대 다리다. 실행의 시드와 체인된 평가의 다중 수용 기준 체크리스트를 계보 이벤트로 투영해서, evolve_step이 평범한 실행을 1세대로 재생하고 2세대를 이미 초점이 맞춰진 상태로 시작한다.
3. 실패한 수용 기준만 다음 세대로 가고, 통과한 것은 동결된다. 이 제약의 효과가 가장 크다.
이걸 작동시키려면 체크리스트를 ACResult로 바꾸는 변환기가 엄격한 기준을 만족해야 했다. 인덱스 전체 커버리지, 축자 그대로의 ac_content, semantic_ac_key 동일성. 그 엄격함이 focus.select_evolution_focus가 통과한 수용 기준을 동결할 수 있게 하는 조건이다.
이미 통과한 것을 동결하지 않는 루프는 맞게 한 일을 다시 하면서 토큰을 태우고, 그 과정에서 맞던 구현을 부순다. 증상은 세대 사이를 오가는 점수다.
루프는 멈출 수 있어야 한다
루프는 스스로 멈추지 않으므로, Ouroboros는 Ralph의 기존 정지 조건을 재사용한다. QA 통과, 수렴, 진동 감지, 등급 퇴행, 실제 경과 시간. execution.auto_evolve_max_generations의 기본값은 3이고 1~10으로 제한된다. BLOCKED는 예산이 소진된 뒤에만 일어난다.
가짜 수렴을 잡는 것은 진동 감지와 등급 퇴행 둘이다. A에서 B로, 다시 A로 튀는 점수, 또는 부모보다 나쁜 세대. 둘 다 토큰을 더 태우는 대신 루프를 세운다.
하나 더 고쳤다. evolution/loop.py에 맨 except가 있어서 evaluation_summary=None으로 조용히 빠지는 경로가 세 개 있었다. 지금은 실패 사유를 실은 기각 요약을 기록한다. fail-closed 초점 의미론을 지키면서 실패를 지속 기록으로 남기는 것이다.
단일 실행에서 예외를 삼키면 한 번 틀린다. 루프에서 삼키면 오류가 N세대에 걸쳐 증폭되는 동안 로그에는 아무것도 없다.
싼 게이트를 비싼 게이트 앞에
같은 패턴이 설계의 나머지에도 이어진다.
평가는 계층형이다. 기계 검증(공짜, 결정론적 검사), 그다음 의미 검증, 그다음 다중 모델 합의. 1층에서 기각된 것은 LLM 심판에게 도달하지 않는다.
인터뷰 단계는 계층 대신 숫자를 쓴다. 모호도는 가중 명료도의 역으로 수치화된다.
Ambiguity = 1 - Sum(clarity_i * weight_i)
0.2를 넘는 점수는 스펙 생성을 막는다. 단, force를 명시하면 통과할 수 있다. CLI는 그 선택지를 계속·취소 옆에 화면에 띄운다. 루프는 마지막 두 세대 사이의 온톨로지 유사도가 0.95에 도달하면 수렴하고, 그 유사도가 진전 없이 세 세대째 평평하면 별도의 정체 감지기가 루프를 세운다. 둘 다 수학적 게이트다. README가 두 게이트의 발상을 이렇게 적는다. 명확해지기 전에는 쓰지 말고, 안정되기 전에는 멈추지 말 것.
당신의 루프에서 확인할 것
- 채점 기준을 수험생에게 보여주지 말 것. 하네스가 에러 메시지, 로그 꼬리, 재시도 프롬프트로 단정 문자열을 흘리는지 감사하라. 정방향과 역방향을 다 막고, 설정 옵션으로 만들지 마라.
- 실패에는 다음 단계가 있어야 한다. 실패한 실행은 평가에 닿아야 하고, 기각된 평가는 진화에 닿아야 하고, 통과한 작업은 동결돼야 한다.
- 루프는 멈춰야 하고 가짜 수렴을 감지해야 한다. 진동, 등급 퇴행, 경과 시간, 세대 예산.
RFC 전문: Q00/ouroboros#1917. 구현: #1916. 설계 문서는 트리 안 docs/hidden-checklist-convergence/에 있다(요구사항·아키텍처·구현). 프로젝트는 github.com/Q00/ouroboros. MIT이고, 로컬 우선이고, 13개 런타임 패밀리 앞단에서 동작한다.
다세대 에이전트 루프를 실제로 운영하고 있다면, 가짜 수렴은 어떻게 잡고 있는지 듣고 싶다. 좋은 답을 가장 적게 본 부분이 거기다.
이 글의 영문판은 게시 후 과장 4건을 스스로 정정했다(무조건 은닉이 아니라 5인코딩 축자 매칭이라는 것, 모호도 게이트는 force로 통과할 수 있다는 것 등). 이 한국어판은 그 정정이 반영된 최종 상태를 처음부터 본문으로 썼다. 정정 이력 원문은 영문판 말미에 있다.
Top comments (0)