// APP FACTORY · EP. 09
만든 에이전트가 자기 결과를 판정하지 못하게 막고, 관대편향 방어 장치를 세 개 넣었습니다. 그런데 감사 기록을 전수로 세어 보니 21개 관점 중 3점 이하가 하나도 없었고 최저점 게이트는 한 번도 발동하지 않았습니다.
1. 자기검증 금지는 규약에 적어둔 원칙입니다
앱 공장의 규약 문서에는 여섯 번째 원칙으로 자기검증 금지가 들어 있습니다. 작업을 수행한 에이전트가 자기 산출물을 판정하지 못하게 하고, 반드시 작성자와 분리된 검증자가 판정하게 한다는 조항입니다.
검증은 세 층으로 나눠 뒀습니다. 첫째는 코드 게이트입니다. 정적 분석 이슈 수나 테스트 통과 수처럼 스크립트가 바로 참과 거짓을 판정할 수 있는 항목입니다. 둘째는 심사원 층입니다. 브랜드나 화면 디자인처럼 스크립트로 잴 수 없는 항목을 별도 에이전트가 채점합니다. 셋째 층은 뒤에서 다시 말씀드리겠습니다.
두 층을 가르는 기준은 하나입니다. 합격 조건이 코드로 판정 가능한가. 기획 단계에서 합격 조건마다 붙여둔 꼬리표가 여기서 라우팅 키로 쓰입니다. 코드 꼬리표가 붙은 조건은 자동으로 판정되고, 주관 꼬리표가 붙은 조건은 심사원에게 넘어갑니다.
앱 공장 8화에서 기획서를 판정 가능한 단위로 나눈 과정을 정리했습니다. 이번 글은 그 판정을 AI 심사원에게 맡긴 뒤 실제로 어떤 점수가 나왔는지 감사 기록으로 확인한 내용입니다. 원칙은 지켰는데도 결과가 이상하게 나왔습니다.
2. 관대편향을 막으려고 장치를 세 개 넣었습니다
주관 판정을 넘길 때 처음 설계한 통과 기준은 단순했습니다. 심사원을 3명 띄우고 5점 만점에 평균이 4.0을 넘으면 통과. 지금 보면 헐거운데 설계할 당시에는 그럴듯해 보였습니다. 임계값이 숫자로 적혀 있으면 그 자체로 엄격해 보이는 착각이 있습니다.
구현하기 전에 검증 에이전트 셋에게 설계를 먼저 뜯게 했더니, 그중 하나가 이 항목을 정확히 지적했습니다. 평균 4.0이라는 임계값에 근거가 없고 언어 모델의 관대편향에 그대로 뚫린다는 내용이었습니다. 그래서 방어 장치를 세 개 넣었습니다.
앵커는 점수마다 그것이 무슨 상태인지 실물 예시를 루브릭에 박아 넣는 장치입니다. 0점은 무엇이고 2점은 무엇이고 4점은 무엇인지를 문장으로 못 박고, 느낌만으로 4점을 주는 것을 금지했습니다. 척도도 0점, 2점, 4점 세 칸으로 줄였습니다. 칸이 많으면 그 사이에서 후한 쪽으로 기울기 때문입니다.
강제 약점 지적은 심사원이 점수와 별개로 가장 약한 점을 반드시 하나 적고 그것으로 감점까지 하게 만든 장치입니다. 좋다는 말만 쓰고 넘어가는 경로를 막았습니다.
최저점 게이트는 평균이 아무리 높아도 어느 한 관점이 2점 이하면 자동으로 재작성을 시키는 장치입니다. 평균 방식에서는 세 관점이 5점, 5점, 2점이면 평균 4.0으로 통과합니다. 계정 보호에서 2점을 받았는데 통과하는 것은 있을 수 없는 일이라고 봤습니다.
심사 결과도 자유서술로 받지 않았습니다. 가장 약한 점과 근거, 판정을 필수 필드로 둔 JSON만 허용했습니다.
{
"perspective": "A2",
"score": 2,
"weakest_point": "이름 접미 '추첨기'가 기존 앱과 겹침",
"evidence": "existing_apps: 접미 '추첨기' 앱 2개 존재",
"verdict": "REWRITE"
}
세 장치가 노리는 지점은 서로 다릅니다. 앵커는 점수의 의미를 고정하고, 강제 약점 지적은 심사원이 편한 길로 가는 것을 막고, 최저점 게이트는 잘한 항목으로 못한 항목을 덮는 것을 막습니다.
3. 감사 기록을 전수로 세어 봤습니다
여기까지가 설계입니다. 실제로 돌린 결과를 확인하려고 앱마다 남아 있는 감사 기록을 전부 열어 심사 점수를 모았습니다. 브랜드 심사와 화면 심사에서 관점 점수가 21개 나왔습니다.
그중 3점 이하로 끝난 관점은 0개였습니다. 20개가 4점이었고 1개가 5점이었습니다. 앵커를 박고 약점 지적을 강제하고 최저점 게이트까지 걸어둔 뒤의 결과가 이것입니다.
심사가 아무 일도 하지 않은 것은 아닙니다. 21개 중 2개는 처음에 3점이 나왔고, 지적받은 내용을 보완한 뒤 4점으로 올라갔습니다. 다만 척도를 0점, 2점, 4점 세 칸으로 줄여 놨는데 값이 4점 한 칸에 몰려 있습니다. 게다가 5점이 하나 있습니다. 이 루브릭은 4점이 만점이고 5점이라는 칸 자체가 없습니다. 앵커가 끝까지 집행되지는 않았다는 뜻입니다.
결과적으로 최저점 게이트는 한 번도 발동하지 않았습니다. 이것은 두 가지로 읽힙니다. 산출물이 정말로 다 좋았을 수도 있고, 게이트가 여전히 헐거울 수도 있습니다. 저는 이 둘을 구분할 방법을 아직 찾지 못했습니다. 다만 한 가지는 분명합니다. 발동한 적 없는 안전장치는 작동을 확인한 적도 없다는 것입니다. 브레이크를 달아 놓고 한 번도 밟아본 적이 없는 것과 같습니다.
4. 갈린 것은 모델이 아니라 지시였습니다
그런데 같은 저장소, 같은 시기, 같은 모델인데 점수가 완전히 다르게 나온 게이트가 하나 있습니다. 앱 공장 7화에서 다룬 주제 심사입니다.
거기서는 심사원 2명이 축 10개를 채점했는데 1점이 4개, 2점이 3개 나왔습니다. 평균을 내면 정확히 2.0이고 둘 다 기각 판정이었습니다. 채점하는 모델도 같고 채점 형식도 같은데, 한쪽은 전부 4점이고 한쪽은 절반 넘게 1점 아니면 2점입니다.
차이는 지시에 있었습니다. 주제 심사는 기각이 기본값이라고 못 박아 둔 게이트입니다. 통과시키려면 근거를 대라는 쪽입니다. 반대로 브랜드 심사는 채점해 달라는 형태였습니다. 앵커를 아무리 촘촘히 박아도 과제 자체가 채점이면 후하게 나옵니다.
관대편향은 모델의 성질이라기보다 지시의 성질에 가깝다는 것이 두 기록을 나란히 놓고 얻은 결론입니다.
5. 실제로 통과를 막은 것은 297줄짜리 스크립트였습니다
앞서 미뤄둔 세 번째 층이 여기입니다. 마일스톤 하나를 통과 처리하려면 그 전에 게이트 증거 파일을 먼저 써야 합니다. 통과했다고 선언하는 것만으로는 안 되고, 정적 분석 이슈가 몇 개였는지 테스트가 몇 개 통과했는지를 파일에 적어야 합니다. 그리고 그 파일을 검사하는 것은 언어 모델이 아니라 297줄짜리 파이썬 스크립트입니다.
{
"analyze": 0,
"tests": 32,
"check_arch": "pass",
"code_review": "APPROVE (0 critical/major …)",
"reviewer": "code-reviewer+verifier (작성자 executor와 분리)"
}
마지막 줄이 핵심입니다. reviewer 필드에 누가 검증했는지를 적게 되어 있습니다. 여기에 본인이라고 적히면 자기검증이기 때문입니다. 원칙을 문서에 써 놓는 대신 필드로 만들어 버린 것입니다.
검사하는 쪽 코드는 이렇게 동작합니다. 정적 분석 값은 정수여야 하고 0이어야 합니다. 아키텍처 가드는 문자열 pass와 정확히 같아야 하고, 코드 리뷰는 첫 낱말이 APPROVE여야 합니다. 그리고 reviewer 필드에 self, 작성자, 본인 같은 낱말이 들어오면 자기검증으로 판정하고 거부합니다. 낱말 6개를 모아둔 집합이 규약 원칙의 집행부인 셈입니다.
SELF_REVIEW_TOKENS = {
"self", "작성자", "author",
"same", "본인", "동일"
}
rv = ev.get("reviewer")
if rv.strip().lower() in SELF_REVIEW_TOKENS:
problems.append("작성자와 분리된 에이전트여야 함")
이 스크립트가 실제로 몇 번 막았는지 감사 기록 161줄을 세어 봤습니다. 통과가 98건, 통과 거부가 6건, 재시도가 8건, 사람을 부른 정지가 1건이었습니다. 심사원 층이 21개 관점에서 한 번도 막지 않는 동안, 이 층은 6번 막았습니다. 앞의 비유로 돌아가면 이쪽 브레이크는 밟혔던 기록이 남아 있습니다.
6. 야박한 검사가 이 층의 목적입니다
PASS_REJECTED — gate_evidence_invalid
check_arch != 'pass'
(값 'pass(뱅크+shard drift 게이트)')
PASS_REJECTED — gate_evidence_invalid
code_review가 'APPROVE'로 시작해야 함
(값 '1차 REQUEST_CHANGES(HIGH 2 …) → 수정 → 재심 APPROVE')
거부 6건 중 두 건은 지금 봐도 재미있습니다. 하나는 아키텍처 가드 필드에 pass라고 쓰고 괄호를 열어 무슨 게이트인지 덧붙였습니다. 사람이 보면 통과했다는 뜻인데 스크립트는 문자열이 다르다며 거절했습니다.
다른 하나는 코드 리뷰 필드에 첫 심사에서 수정 요청을 받았고 고쳐서 재심에서 승인됐다고 정직하게 썼습니다. 결론은 승인인데 첫 낱말이 APPROVE가 아니라서 거절당했습니다.
야박해 보이지만 이것이 이 층의 목적입니다. 서술을 조금이라도 허용하면 거기에 무엇을 써도 통과합니다. 같은 이유로 검사 코드에는 타입을 엄격하게 비교하는 줄이 있습니다. 파이썬에서 불리언은 정수의 하위 타입이라, True를 넣어도 정수 검사를 통과해 버립니다. 이슈가 0개라고 주장한 것이 아니라 True라고 쓴 것도 통과하는 셈입니다. 그 구멍을 한 줄로 막아 뒀습니다.
# bool은 int의 서브클래스라 type()으로 엄격 검사
if type(ev.get("analyze")) is not int:
problems.append("analyze는 정수 필드여야 함")
elif ev["analyze"] != 0:
problems.append("analyze는 0이어야 통과")
# isinstance(True, int) == True ← 이 검사를 쓰면 뚫린다
7. 주관 게이트가 유일하게 막은 자리
그렇다고 주관 게이트가 아무것도 막지 못한 것은 아닙니다. 딱 한 번 막은 자리가 있습니다.
드립공작소라는 앱인데, 문장 뱅크의 품질을 주관 게이트로 5라운드 돌렸습니다. 합격한 개수가 2개에서 4개, 13개, 13개, 12개로 움직였습니다. 후보를 아무리 손봐도 12개 언저리에서 진동했고, 재시도 상한을 넘기면서 게이트가 사람을 불렀습니다.
불려 온 사람이 한 일은 통과시켜 주기가 아니었습니다. 합격 조건이 현실과 맞는지를 다시 봤습니다. 이 앱에서 사용자는 후보를 12개쯤 훑어보고 마음에 드는 것을 직접 고릅니다. 그런데 조건은 그보다 넓은 범위가 다 좋기를 요구하고 있었습니다. 조건을 실제 사용 방식에 맞춰 다시 잡고 순위 층을 하나 얹은 뒤 재개하니 28개가 전부 통과했습니다.
이 게이트도 주관 판정이었습니다. 그런데 결과를 점수가 아니라 합격 개수 분의 후보 개수로 받았습니다. 세어야 하는 형태였습니다. 점수로 물으면 4점이 나오고, 개수로 물으면 12개가 나옵니다. 12개는 지난 라운드의 13개와 비교되지만 4점은 비교할 것이 없습니다.
8. 정리
자기검증 금지는 필요조건이지 충분조건이 아니었습니다. 검증자를 분리해도 채점을 시키면 통과합니다. 앱 공장 4화에서 기계가 막는 규칙만 지켜진다고 말씀드렸는데, 여기에 한 겹이 더 붙습니다. 기계가 막으려면 셀 수 있어야 합니다.
참고로 화면 디자인은 도구 원본이 암호화되어 있어 검증자가 열지도 못합니다. 그래서 거기서 뽑아낸 스펙 문서를 검증 입력으로 삼기로 계약을 정해 뒀습니다.
다음 편에서는 이것을 계속 돌리려고 미리 만들어 뒀던 자동화 엔진을 왜 버렸는지 이야기하겠습니다.


답글 남기기