AI에 증인(Witness)을 세우다: '악마의 대변인'이 세 가지 디자인을 무너뜨린 과정
요약
sonmat v0.9에 추가된 외부 검증 에이전트 'sonmat-witness'의 설계 원칙을 다룹니다. 메인 세션의 추론 과정에 오염되지 않도록 구조적 격리를 통해 사용자의 실제 요청과 AI 출력을 독립적으로 비교 검증하는 방식을 설명합니다.
핵심 포인트
- 검증기는 실행기의 추론 과정을 볼 수 없어야 함
- 검증기의 입력값은 가공되지 않은 사용자의 원문이어야 함
- 자기 의심은 외부의 독립된 주체(Witness)를 통해 완성됨
- 구조적 격리가 없는 내부 가드(Guard)는 검증에 한계가 있음
요약하자면 이렇습니다: sonmat v0.9에 sonmat-witness라는 에이전트가 추가되었습니다. 이는 메인 세션의 추론 과정(reasoning)을 읽지 않고, 메인 세션의 출력이 사용자가 실제로 요청한 것과 일치하는지 확인하는 외부 검증기(external verifier)입니다. 이것이 깔끔한 디자인의 핵심입니다. 하지만 진짜 이야기는 제가 디자인을 하는 과정에서 일어났습니다. 단 한 번의 세션 동안 저는 똑같은 함정에 세 번 빠졌고, 그때마다 단 한 번의 /devil(악마의 대변인) 호출이 디자인의 밑바탕이 된 가정을 무너뜨렸습니다. 이 포스트는 이 두 가지 측면 모두에 대해 다룹니다.
왜 굳이 증인(witness)이 필요한가
sonmat은 단 하나의 규칙에서 시작되었습니다: 의심하라. AI든 사람이든 무엇을 썼든 — 액면 그대로 받아들이지 마세요. 다시 돌아가서 한 번 더 의심스럽게 살펴보세요. 그것이 프로젝트 전체의 뿌리가 되는 규율입니다. v0.9가 직면한 문제는 그 규칙이 미처 닿지 않는 곳이었습니다. 자기 의심(Self-doubt)은 자기 자신에게는 작동하지 않습니다. 사람들은 이 사실을 알고 있습니다. 자신의 글을 스스로 교정할 수는 없습니다. 뇌가 의도한 바를 알고 있기 때문에 눈은 오타를 그냥 지나쳐 버립니다. 수술실(OR)에서 외과의사는 자신의 타임아웃(Time Out)을 직접 읽지 않습니다. 다른 팀원이 읽습니다. 비행 갑판에서 기장이 "FLAPS — TWO"라고 호출하면, 부기장은 단순히 "two"라고 대답만 하지 않습니다. 레버를 확인하고 그 위치를 다시 읽어줍니다. 의심은 작성자 외부에서 와야 합니다. 따라서 자기 의심이 닿지 않는다면, 의심을 수행할 외부의 장소가 필요합니다. 그 역할을 witness가 채웁니다.
처음에 떠올린 이름은 사실 '훈수군(hunsugun)'이었습니다. 바둑판 옆에서 지켜보며 놓친 수를 지적해 주는 사람을 뜻하는 한국어입니다. 역할에 딱 맞았습니다. 하지만 이 단어는 영어로 깔끔하게 전달되지 않고, 시리즈가 두 언어로 모두 진행되기 때문에 witness라는 단어를 빌려왔습니다. 더 넓은 의미를 담고 있으며, 법정의 어조(courtroom register) 또한 그 역할에 잘 어울리는 것으로 드러났습니다. 증인석이 실제로 제 역할을 하기 위해서는 두 가지 조건이 충족되어야 합니다.
첫째, 검증기(verifier)는 실행기(executor)의 추론(reasoning)을 볼 수 없어야 합니다. 만약 볼 수 있다면, 실행기의 합리화(rationalization)를 그대로 물려받아 단순한 승인 도구(rubber stamp)로 전락하게 됩니다. 둘째, 검증기의 입력값은 사용자의 실제 발화(actual words)여야 합니다.
사용자가 "무엇을 원했는지"에 대한 메인 세션(main session)의 해석이 아니라, 사용자가 입력한 가공되지 않은 발화(raw turn)여야 합니다. Sonmat의 기존 가드(guard) 기술은 둘 다 해결하지 못했습니다. guard는 메인 세션 내부에서 실행되며 전체 컨텍스트(context)를 공유합니다. 이는 테스트 실행, 민감한 파일 차단, 규정 준수 강제와 같은 운영상의 점검(operational checks)에는 적합하지만, "이 출력이 요청된 내용과 일치하는가"를 확인하는 데는 무용지물입니다. 왜냐하면 검사기(checker)와 작성기(writer) 사이에 구조적 격리(structural isolation)가 없기 때문입니다. 그래서 저는 witness를 분리했습니다. 실제 코드가 어떤 모습인지는 잠시 후에 설명하겠지만, 우선 이 릴리스를 만든 것은 디자인 자체가 아닙니다. 디자인 과정에서 일어난 일이 이 릴리스를 만들었습니다.
한 세션 내에서의 세 번의 반전
witness를 구축하면서 저는 똑같은 종류의 실수를 세 번 연속으로 저질렀습니다. 매번 저는 그럴듯하게 들리는 이름이나 구조를 받아들이고 그 틀 안에서 설계를 시작했습니다. 그때마다 /devil(/devil's-advocate 기술, 포스트 03의 주제)이 그 아래에 있는 핵심 가정(load-bearing assumption)을 끌어내어 그것이 버티지 못한다는 것을 보여주었습니다. 한 세션 안에서 세 번의 뒤집힘이 있었습니다.
1라운드: "역전된 3계층 아키텍처 (the inverted three-tier architecture)"
한 달 전 sonmat의 메모리에 다음과 같은 문구가 있었습니다: "역전된 3계층 디자인 진행 중." 메인 세션이 인간 대화 상대자 역할을 하고, 그 아래에 오케스트레이터(orchestrator)가 있으며, 그 아래에 워커(workers)가 있는 구조입니다. 저는 한 달 동안 머릿속에 이 그림을 담아두고 있었습니다. witness가 논의되었을 때, 그것은 자연스럽게 이 3계층의 최상단에 끼워 맞춰졌습니다. 즉, 검증기(verifier)가 오케스트레이터 계층에 앉아 워커들이 생성하는 것을 지켜보는 형태였습니다. 깔끔했습니다. 돌이켜보면 지나치게 깔끔해서 의문을 제기할 생각조차 하지 못할 정도였습니다.
마지막 점검으로 /devil CCT를 실행했습니다. CCT는 /devil 내부의 발견 단계입니다: 먼저 주장의 핵심이 되는 단 하나의 지지선을 찾아내는 것입니다.
Claim-crux (주장의 핵심): 이 아키텍처는 Claude Code가 중첩된 하위 에이전트 위임(nested subagent delegation)을 지원한다는 가정에 기반하고 있습니다.
Counter-fit (반박): 플랫폼 문서를 읽기도 전에 전체 디자인이 그려졌습니다. 그 가정이 유효하다는 증거가 전혀 없습니다.
그 한 문장이 차갑게 내리꽂혔습니다.
한 달 된 그림이었고, 그 모든 것이 아무도 확인하지 않은 단 한 문장에 매달려 있었습니다. 그래서 저는 문서를 읽었습니다. Claude Code의 멀티 에이전트 (multi-agent) 페이지에는 다음과 같이 적혀 있었습니다: "단 한 단계의 위임 (delegation)만 지원됩니다: 코디네이터 (coordinator)는 다른 에이전트들을 호출할 수 있지만, 해당 에이전트들은 자신만의 에이전트를 호출할 수 없습니다." 명시적인 금지 사항이었습니다. 3계층 (Three-tier) 구조는 현재의 하네스 (harness) 상에서 구조적으로 불가능했습니다. 한 달 전 "역 3계층 (inverted three-tier)"이라는 개념이 메모리 파일에 기록된 순간부터, 이후의 모든 디자인 대화는 그 틀 안에서 이루어졌습니다. 그리고 아무도 플랫폼이 이를 지원하는지 묻지 않았습니다. 2계층 (Two-tier, witness-pair)이 실제 한계치였으며, 저는 이미 내내 그 범위 내에서 작업하고 있었습니다. 3계층은 환상에 불과했습니다.
2라운드: "PreToolUse 훅 (hook)이 witness를 생성하고 도구 호출 (tool call)을 거부한다"
3계층이 철회되자, 저는 다음 계획을 더 신중하게 그렸습니다. 1라운드가 불과 한 시간 전에 무너졌기에, 신중한 태도로 진행하며 검증하는 것이 옳았습니다. 제가 생각해낸 계획은 다음과 같습니다: "autoloop의 커밋 (commit) 단계에서, PreToolUse 훅이 에이전트 유형 (agent-type) 훅으로서 witness를 생성한다. 만약 witness가 BLOCK을 반환하면, 훅은 도구 호출을 거부한다." 유혹적인 제안이었습니다. 플랫폼 레벨의 강제성 (enforcement) — 메인 세션이 이를 우회할 방법이 없습니다. 훅이 게이팅 (gating)을 수행함으로써, witness는 규율 (discipline)에 의존하는 대신 신뢰할 수 있는 초크포인트 (chokepoint)가 됩니다. 하지만 — 1라운드 역시 한 시간 전에는 유혹적이었기에, 이번 라운드에서 제 머릿속에 가장 먼저 떠오른 생각은 "이 파이프라인 (pipeline)이 실제로 문서에 있는가?"였습니다.
다시 공식 가이드로 돌아갔습니다. 에이전트 유형 (agent-type) 훅은 존재합니다. 문서화되어 있습니다. 하지만 판결 (verdict) 기반의 거부를 수행하며 PreToolUse에서 실행되는 에이전트 훅의 사례는 단 하나도 없었습니다. 문서화된 모든 사례는 테스트 통과를 확인하기 위해 사용되는 Stop 단계에 있었습니다. "훅이 하위 에이전트 (subagent)를 동기식 (synchronously)으로 생성하고, 대기하며, 판결로부터 거부 (deny)를 채운다"는 의미론 (semantics)은 어디에도 설명되어 있지 않았습니다.
다시 CCT입니다.
주장 핵심 (Claim-crux): witness가 신뢰를 얻으려면 훅 계층의 강제성 (hook-layer enforcement)이 필요하다.
반론 적합성 (Counter-fit): autoloop의 다른 모든 단계 ([Plan], [Define], [Execute], [Evaluate])는 오직 규율 (discipline)만으로 실행된다. 왜 witness 게이트만 특별 대우를 받아야 하는가?
인과 체인 (Cause-chain): "hook enforcement (훅 강제) → trustworthy gating (신뢰할 수 있는 게이팅)"은 훅의 의미론 (semantics)이 실제로 존재할 때만 유효하다. 하지만 그것들은 존재하지 않는다. 대안은 autoloop 규율 (autoloop discipline)뿐이며, autoloop는 이미 다른 모든 것들에 대해 그런 방식으로 작동하고 있다. 뒤집힌 것이다. witness는 훅이 필요하지 않다. Task 도구를 통해 [Judge] 내부에서 생성하면 된다. Task + subagent_type은 잘 문서화된 프리미티브 (primitive)이다. Autoloop는 다른 모든 단계(phase)를 실행하는 것과 동일한 방식으로 witness를 실행할 것이며, 강제 (enforcement)는 autoloop 규율에 따른다. 이는 다른 모든 단계에서 이미 신뢰받고 있는 것과 동일한 것이다. 새로운 보증은 없으며, 보증이 존재하는 척하지도 않는다. 다만 1라운드와 형태는 같다. 매력적인 이름, 프레임 내부의 디자인, 플랫폼 체크 부재, 그리고 뒤늦은 붕괴. 한 시간 전의 나는 한 시간 후의 내가 저지른 실수를 거의 똑같이 반복하고 있었다.
3라운드: witness 자체에 의구심을 돌리다
이 시점에서 witness는 출시 가능한 수준으로 보였다. 실행은 격리되었고, 가공되지 않은 사용자 턴 (raw user turn)이 입력으로 들어오며, 인용 규칙 (citation rule)도 마련되어 있었다. 두 번의 실패를 겪었으니 — 설마 세 번째 실패까지 나타나지는 않겠지. 하지만 나란히 놓인 두 번의 실패가 나를 괴롭히기 시작했다. 두 실패는 모두 같은 형태를 띠고 있었다: 매력적인 이름 → 프레임 내부의 디자인 → 플랫폼 체크 부재 → 붕괴. "Inverted three-tier"가 실패했다. "PreToolUse hook deny"가 실패했다. 그렇다면 "witness" 자체는 어떠한가? 나는 그 이름에 대해 검증했는가? 마지막 라운드다. 이번에 나는 내가 종이에 적을 수 있는 가장 강력한 형태의 주장을 담아, witness 자체를 향해 /devil (악마의 대변인)을 겨누었다: "witness는 우리가 설계한 결정론적 비교기 (deterministic comparator)처럼 동작한다 — 즉, 비교할 뿐 추론하지 않는다." CCT가 포착한 것은 1, 2라운드와는 달랐다.
주장의 핵심 (Claim-crux): 에이전트 프롬프트 (agent prompt)에 "추론하지 말고 비교하라"라고 적는 것이 LLM을 실제로 그렇게 행동하게 만든다는 것.
반박 (Counter-fit): witness는 에이전트 파일 (agent file) 하나가 아니다. witness는 에이전트 파일 + 입력 + 모델의 기본 성향 (default tendencies)이 모두 쌓인 결과물이다. 동일한 클래스의 LLM이 메인 세션과 witness를 모두 실행한다. 셀프 체크 (self-check) 중에 메인 세션이 합리화 (rationalize)를 시도하게 만드는 실패 모드 (failure mode)가 witness에서도 나타날 수 있으며, "우리가 규칙을 작성했다"는 사실은 그것에 대한 구조적 보호책 (structural protection)이 되지 못한다.
인과 관계 체인 (Cause-chain): "에이전트 규칙 (agent rules) → 엄격한 비교자 동작 (strict comparator behavior)"은 지시 이행 (instruction-following) 과정을 관통하며, 이는 바로 witness가 검증해야 하는 대상인 메커니즘 그 자체이다. 순환 논리다. 여기서 잠시 멈추자. 1라운드와 2라운드는 외부적인 이유로 실패했다. 즉, 플랫폼이 내가 설계한 내용을 지원하지 않았다. 이는 검색 가능하고 사실적이며 명확한 해결책이 있는 문제였다. 3라운드는 달랐다. 문제는 witness의 설계 내부에 있었다. witness의 동작을 강제하는 메커니즘이 바로 witness가 검증해야 하는 메커니즘과 동일했다. 자기 참조적 (Self-referential)이다. 강한 형태 (Strong-form)의 주장들은 이를 견뎌내지 못했다. 하지만 약한 형태 (weak form)는 살아남았다. 강한 형태인 "witness는 결정론적 비교자 (deterministic comparator)이다"라는 주장은 약화되었다. 반면 약한 형태인 "witness는 여전히 메인 자체 점검 (main self-check)보다 낫다"라는 주장은 유지되었다. 그 이유는 계층 (layers)이 깔끔하게 분리되어 있기 때문이다.
계층 1 — 실행 격리 (execution isolation, 하네스에 의해 강제됨) ──── 실질적인 구조적 보장 (real structural guarantee)
계층 2 — 생성 프롬프트 규칙 (spawn-prompt rules) ──────────────────────── 열망적 계약 (aspirational contract)
계층 3 — 인용 규칙 (citation rule) ───────────────────────────── 열망적 계약 (aspirational contract)
계층 1은 플랫폼이 강제한다. 설령 부실한 witness라 할지라도 메인 세션의 합리화 문맥 (rationalization context)을 읽을 수 없다. 확인 도장을 찍어주는 식의 실패 모드 (confirmation-rubber-stamp failure mode)로 이어질 경로가 없는데, 왜냐하면 witness가 도장을 찍어줘야 할 입력값 자체에 도달할 수 없기 때문이다. 이것만으로도 witness는 자체 점검 (self-check)보다 엄격하게 우월하다. 계층 2와 3은 프롬프트 수준의 행동 계약 (behavioral contracts)이다. 런타임 (runtime)은 이를 강제하지 않는다. 만약 witness가 내부적으로 "이 결과는 §2에 해당하지만 근거가 약해 보이니 WARN으로 분류하겠다"라고 결정한다면, 이를 막을 구조적인 장치는 없다. 이것이 v0.9.1이 목표로 했던 지점이다. witness.md의 '§Isolation' 스택은 이제 이 세 계층을 '강제됨 (enforced)' 대 '열망적 (aspirational)'으로 명확히 구분하며, 운영자들에게 초기 사용 단계에서 계층 2와 3이 실제로 유지되는지 확인하기 위해 판결을 수동으로 샘플링할 것을 권고한다. 드리프트 (Drift) 현상이 관찰되면 이를 에이전트 파일에 피드백한다. 기록자 (scribe)가 이미 witness의 판결을 저널에 기록하고 있으므로 관찰 채널은 이미 존재한다. Witness는 완벽한 검증기 (verifier)는 아니다.
이는 self-check (자기 점검)보다 구조적으로 더 나은 방식이며, 솔직히 그 차이는 layer 1 (레이어 1)에 의해 제한됩니다. 나머지는 제가 사용하는 과정에서 현실에 맞춰 조정됩니다. 세 번의 파괴 과정을 거쳐 살아남은 witness (증인)는 다음과 같은 모습입니다. Task 도구를 통해 생성된 subagent (하위 에이전트)입니다. 1라운드의 3계층 구조는 존재하지 않습니다. 2계층이 harness (하네스)의 상한선이며, witness는 바로 그곳에 위치합니다. autoloop (자동 루프)의 [Judge] (판사) 단계 내부에서 실행됩니다. 2라운드의 hook-level (훅 레벨) 강제 집행 역시 실재하지 않으므로, witness는 다른 모든 단계가 작동하는 방식과 동일하게 autoloop의 규율에 따라 실행됩니다. 오직 가공되지 않은 user turn (사용자 턴)과 생성된 artifact (결과물)만을 제공받습니다. 메인 세션의 코멘터리, chain-of-thought (사고의 사슬), commit message (커밋 메시지) 등은 제공되지 않으며 — 만약 이 중 어떤 것이라도 유출된다면 witness는 이를 인용할 수 없습니다. 그것이 harness에 의해 강제되는 layer 1입니다. 인용은 필수 사항입니다. 모든 발견 사항은 user turn N에 근거해야 합니다:
목표가 있기 전에는 얼마나 깊게 파고들지 결정할 수 없습니다. 무언가가 표면 위로 떠오르면, 그 표면이 깊이를 결정합니다. 체스 플레이어들은 어떤 긴 변수를 계산하기 전에 CCT — 체크(Checks), 캡처(Captures), 위협(Threats) — 를 수행합니다. 수술팀은 절개 전 타임아웃(Time Out)을 실시합니다. 항공 분야의 확인 및 응답(challenge-and-response) 방식이 작동하는 이유는 기장(PM)이 부기장(PF)의 말만 믿지 않고, 기장이 직접 스위치를 확인하기 때문입니다. 다섯 가지의 서로 다른 검증 전통이 있지만, 구조적인 움직임은 동일합니다. 저는 /devil을 위해 CCT — 핵심 주장(Claim-crux), 반박 적합성(Counter-fit), 인과 사슬(Cause-chain) — 를 차용했습니다. 네 개의 축을 따라 평행하게 주장을 공격하는 대신, 먼저 하중을 견디고 있는 단 하나의 가정을 찾아내어 그것이 증거(Evidence), 논리(Logic), 또는 대안(Alternatives) 중 어디에 위치하는지 분류한 뒤, 그 하나의 축에 깊이를 쏟아붓습니다. 나머지 축들은 가볍게 훑고 지나갑니다. 오늘 진행된 세 번의 /devil 라운드는 t의 첫 번째 실전 테스트였습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기