DEV Community

Q00
Q00

Posted on Edited on

당신의 에이전트 루프는 모델에게 커닝을 가르치고 있다

AI 코딩 에이전트에 루프를 씌우는 건 단발 프롬프트가 막히면 누구나 떠올리는 다음 수입니다. 돌리고, 채점하고, 점수가 낮으면 다시 돌립니다.

그런데 바로 벽 두 개에 부딪힙니다.

  1. 점수는 오르는데 결과물은 여전히 틀립니다. 에이전트가 과제가 아니라 채점기를 만족시키는 법을 배운 겁니다.
  2. 실패가 막다른 길입니다. 루프의 부품은 다 있는데 실패한 실행이 다음 실행으로 안 이어집니다.

둘 다 모델 문제가 아니라 환경 설계 문제입니다. 모델이 강해진다고 사라지지 않아요. 강한 모델은 지름길을 더 빨리 찾을 뿐입니다.

오픈소스 Agent OS인 Ouroboros에 최근 합류한 RFC로 이 두 벽을 짚어보려고 합니다. 설계는 이슈 #1917에, 구현은 #1916에 있습니다. 전문을 직접 읽을 수 있다는 게 이 사례를 고른 이유입니다.

벽 1: 수험생에게 답안지를 쥐여줬다

대부분의 에이전트 하네스는 수용 기준을 워커 프롬프트에 그대로 렌더링합니다. 채점에 쓸 명령과 채점 기준이 될 단정문까지요.

동기는 그럴듯합니다. 에이전트가 어떻게 검사받을지 알면 과녁을 제대로 겨눌 수 있다는 거죠.

Ouroboros도 그렇게 했었습니다.
_build_success_contract_blockverify_commandExpected output: <단정문>을 워커 지시문에 렌더링했어요.

두 번째 누출은 더 찾기 어려웠습니다. 재시도 때 실패 사유에 단정문의 repr()이 실려서 result.error를 타고 다음 프롬프트로 되돌아왔습니다.

에이전트가 단정문을 보는 순간, 단정문을 맞추는 게 요구사항을 맞추는 것보다 싸집니다.

RFC는 이걸 직설적으로 부릅니다. 궁지에 몰린 워커의 가장 싼 경로는 수용 기준을 구현하는 게 아니라 단정 문자열을 갖고 노는 것이고, 포스트모템(seed_2be2907edc07)이 붙어 있습니다.

교과서적인 보상 해킹입니다. 능력을 본다고 생각했는데 실제로 본 건 답을 베끼는 능력이었어요.

해법: 프롬프트에 넣지 않습니다, 되돌릴 스위치도 없이

두 경로를 다 막아야 합니다. 앞문만 막으면 아무것도 안 달라집니다.

  • 앞문: _build_success_contract_block은 이제 수용 기준 설명과 expected_artifacts만 렌더링합니다. 검증은 하네스가 따로 하니까 채점 로직이 워커의 성공 계약에 안 들어갑니다.
  • 뒷문: 검증 게이트의 실패 사유에 단정문 repr()이 더는 안 실립니다. 재시도 힌트는 단정문을 걸러내는 전용 빌더(orchestrator/retry_hints.py)가 만듭니다. 명령 출력의 마지막 2,000자 꼬리까지요.

그 2,000자 꼬리가 베껴갈 만한 대목입니다. 앞문을 막고 로그 꼬리를 잊으면 누출은 그대로 열려 있습니다.

아직 구멍은 있어요. 지금은 문자열을 그대로 비교하는 방식이라 줄바꿈된 사본은 통과합니다. 공개 이슈(#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로 표시될 뿐이었어요.
  • 기각된 평가는 진화에 안 들어갔습니다. 역시 거기서 끝이었고요.

루프가 끊어진 세 토막으로 있었습니다. 실패는 보고만 되고 다음 실행에 안 쓰였습니다.

해법: 실행을 평가로, 평가를 진화로 잇습니다

제약 세 개가 핵심입니다.

첫째, 실패한 실행도 평가로 이어집니다. _run_succeeded 게이트를 완화해서 세션을 만든 실행이면 정식 평가로 체인됩니다.

fail-open은 유지돼요. 큐 등록이 실패해도 실행 결과가 뒤집히진 않습니다.

둘째, 기각된 평가가 예산 있는 진화 루프를 발동합니다. 여기서 수렴 루프를 새로 구현한 사람은 없습니다. 평가 잡의 종결 경로가 final_approved is False일 때 기존 진화 기계를 큐에 넣을 뿐이에요.

새로 만든 건 1세대 다리입니다. 실행의 시드와 체인된 평가의 다중 수용 기준 체크리스트를 계보 이벤트로 옮겨서, evolve_step이 평범한 실행을 1세대로 재생하고 2세대를 이미 초점 맞춰진 상태로 시작합니다.

셋째, 실패한 수용 기준만 다음 세대로 가고 통과한 건 동결됩니다. 이 제약의 효과가 제일 큽니다.

작동시키려면 체크리스트를 ACResult로 바꾸는 변환기가 엄격한 기준을 만족해야 했습니다. 인덱스 전체 커버리지, 글자 그대로의 ac_content, semantic_ac_key 동일성이요.

이미 통과한 걸 동결하지 않는 루프는 맞게 한 일을 또 하면서 토큰을 태웁니다. 그러다 맞던 구현을 부숩니다. 증상은 세대 사이를 오가는 점수예요.

루프는 멈출 수 있어야 합니다

루프는 스스로 안 멈추니까, Ouroboros는 Ralph의 기존 정지 조건을 재사용합니다. QA 통과, 수렴, 진동 감지, 등급 퇴행, 실제 경과 시간이요.

execution.auto_evolve_max_generations의 기본값은 3이고 1~10으로 제한됩니다. BLOCKED는 예산이 소진된 뒤에만 일어납니다.

가짜 수렴을 잡는 건 진동 감지와 등급 퇴행 둘입니다. A에서 B로 다시 A로 튀는 점수, 또는 부모보다 나쁜 세대요. 둘 다 토큰을 더 태우는 대신 루프를 세웁니다.

하나 더 고쳤습니다. evolution/loop.py에 맨 except가 있어서 evaluation_summary=None으로 조용히 빠지는 경로가 세 개 있었어요. 지금은 실패 사유를 실은 기각 요약을 기록합니다.

단일 실행에서 예외를 삼키면 한 번 틀립니다. 루프에서 삼키면 오류가 N세대에 걸쳐 증폭되는 동안 로그에는 아무것도 없습니다.

싼 게이트를 비싼 게이트 앞에

같은 패턴이 설계 나머지에도 이어집니다.

평가는 층으로 나뉩니다. 기계 검증(공짜, 결정론적 검사), 그다음 의미 검증, 그다음 다중 모델 합의요. 1층에서 기각된 건 LLM 심판에게 안 갑니다.

인터뷰 단계는 층 대신 숫자를 씁니다. 얼마나 분명한지에 가중치를 매기고, 그 값을 1에서 뺀 게 모호도입니다.

Ambiguity = 1 - Sum(clarity_i * weight_i)
Enter fullscreen mode Exit fullscreen mode

0.2를 넘는 점수는 스펙 생성을 막습니다. 단, force를 명시하면 통과할 수 있어요. CLI는 그 선택지를 계속·취소 옆에 띄웁니다.

루프는 마지막 두 세대 사이 온톨로지 유사도가 0.95에 도달하면 수렴합니다. 그 유사도가 진전 없이 세 세대째 평평하면 별도의 정체 감지기가 루프를 세웁니다.

README가 두 게이트의 발상을 이렇게 적습니다. 명확해지기 전에는 쓰지 말고, 안정되기 전에는 멈추지 말 것.

자기 루프에서 확인할 것

  1. 채점 기준을 수험생에게 보여주지 마세요. 하네스가 에러 메시지, 로그 꼬리, 재시도 프롬프트로 단정 문자열을 흘리는지 보세요. 앞뒤를 다 막고, 설정 옵션으로 만들지 마시고요.
  2. 실패에는 다음 단계가 있어야 합니다. 실패한 실행은 평가에 닿고, 기각된 평가는 진화에 닿고, 통과한 작업은 동결돼야 합니다.
  3. 루프는 멈춰야 하고 가짜 수렴을 감지해야 합니다. 진동, 등급 퇴행, 경과 시간, 세대 예산이요.

RFC 전문은 Q00/ouroboros#1917, 구현은 #1916입니다. 설계 문서는 트리 안 docs/hidden-checklist-convergence/에 있어요.

프로젝트는 github.com/Q00/ouroboros입니다. MIT이고, 로컬 우선이고, 13개 런타임 패밀리 앞단에서 돕니다.

직접 돌려보기

설치는 한 줄입니다.

curl -fsSL https://raw.githubusercontent.com/Q00/ouroboros/main/scripts/install.sh \
  | OUROBOROS_INSTALL_REF=devto-ko bash
Enter fullscreen mode Exit fullscreen mode

파이프로 받는 게 꺼려지면 pipx install 'ouroboros-ai[mcp]'로 CLI만 받아도 됩니다. 스크립트가 더 하는 일은 MCP 서버를 호스트에 등록하는 것뿐이고, 그건 ouroboros setup으로 나중에 해도 돼요.

uv가 없으면 설치가 끝나도 MCP 서버가 안 붙습니다. 플러그인 쪽 서버를 uvx로 띄우기 때문이에요. pipx install uvbrew install uv 아무거나 됩니다.

그다음:

ouroboros init start --orchestrator "아까 에이전트한테 시키려던 그 문장"
Enter fullscreen mode Exit fullscreen mode

인터뷰가 그 문장에 남은 모호함을 0~1로 매깁니다. 0.2를 넘으면 위에서 말한 게이트에 걸리고, force도 같은 화면에 뜹니다.

재밌는 건 점수가 아니라 대답 못 한 질문입니다. 원래 안 정해두고 시작하던 게 거기서 드러나요.

한국어 문서는 ouroboros.page/learn에 있습니다. MCP에서 막히면 11장부터요.

멀티 세대 루프 돌리고 계시다면, 가짜 수렴은 어떻게 잡고 계신가요?


이 글의 영문판은 게시 후 과장 4건을 정정했습니다. 무조건 숨기는 게 아니라 글자 그대로 비교하는 것뿐이라는 것, 모호도 게이트는 force로 통과할 수 있다는 것 등이요.

이 한국어판은 그 정정이 반영된 상태로 썼습니다. 정정 이력은 영문판 말미에 있습니다.

Top comments (0)