선택지 3개 제시 후 재질의 2회: 매번 나온 더 나은 안
한국 웹소설 집필용 Claude Code 플러그인을 하나 만들고 있다. 거기에 이런 규칙이 있다. 추천할 때는 안을 하나만 내지 마라. 두세 개를 내고 각각의 트레이드오프를 붙여라. 하나만 제시하면 사용자는 검토 없이 채택하고, 그 순간 그 결정은 자기 것이 아니게 된다.
이 규칙은 잘 지켜졌다. 모델은 성실하게 A안·B안·C안을 냈다.
문제는 사용자가 이렇게 물었을 때 드러났다.
"다른 방안이 더 없는지 확인해봐."
두 번 물었고, 두 번 다 먼저 낸 셋보다 나은 것이 나왔다. 한 번은 기존 안 넷을 전부 포함하는 상위 안이었다.
규칙은 지켜졌는데 결과가 나빴다. 그러면 규칙이 틀린 것이다.
증상 — 안 셋이 사실은 안 하나
낸 안들을 다시 보니 이런 모양이었다.
A안: 핸디캡을 강하게 건다
B안: 핸디캡을 중간으로 건다
C안: 핸디캡을 약하게 걸되 조건부로 푼다
셋 다 「핸디캡을 어떻게 설계하나」라는 하나의 축 위에 있다. 강도만 다르다. 사용자는 세 선택지를 받았다고 믿지만 실제로 고른 것은 눈금 위의 위치 하나다. 「핸디캡을 안 건다」도, 「제약을 능력이 아니라 관계에 건다」도 선택지에 없었다.
규칙이 개수를 셌기 때문이다. 그리고 개수는 채우기 쉽다.
이게 이 글의 전부다. 나머지는 이걸 어떻게 고쳤고, 고치는 과정에서 뭘 또 틀렸는지다.
고침 — 세는 단위를 바꾼다
처음에 받은 제안은 이랬다. "최소 4개를 생성하고 3개로 줄여 제시하라."
거절했다. 하한을 개수로 걸면 같은 축으로 넷을 만들어 통과할 수 있다. 방금 본 실패가 그대로 재현되고, 대화만 길어진다.
대신 넣은 것은 한 줄이다.
개수를 세기 전에 축을 센다. 안이 셋인데 전부 같은 축의 강약 조절이면 그건 안 하나다. 축을 최소 둘, 되도록 셋으로 바꿔가며 만든 뒤 제시할 것을 고른다.
제시 개수는 안 바뀌었다. 여전히 두세 개다. 바뀐 건 세는 단위다.
차이는 채우기 난이도에 있다. 개수 하한은 성실하게 채워지고, 축 조건은 안 채워지면 눈에 띈다. 「이 셋의 축이 뭐냐」는 질문에 하나로 답할 수 있으면 그 자리에서 걸린다.
하한을 걸 때는 채우기 쉬운 단위로 걸면 안 된다. 커버리지 퍼센트, 테스트 개수, 리뷰 코멘트 수 — 세기 쉬운 것은 대개 만들기도 쉽다. 세는 단위를 바꾸는 편이 하한을 올리는 것보다 강하다.
그리고 두 번째 이야기 — 제안 여섯 중 넷이 이미 처리된 것이었다
위 제안은 사용자가 보낸 세 번째 개선 전달문에 들어 있었다. 문서 하나에 제안 셋, 항목으로 풀면 여섯.
받자마자 코드를 열지 않고, 지난 릴리스의 기각 이력부터 뒤졌다. 결과가 이랬다.
| 제안 | 실제 상태 |
|---|---|
| 자기검증에 충돌 대조 항목 | 직전 릴리스에 이미 있음 (형태가 약했을 뿐) |
| 여러 안을 관통하는 상위 안 검사 | 직전 릴리스에 이미 있음 |
| 버린 안을 기록에 남기기 | 직전 릴리스에 이미 있음 |
| 공통 정본 파일로 빼기 | 직전 릴리스가 명시적으로 기각 |
| 채택 전에 의존 관계를 미리 적어보기 | 신규 |
| 대안 축 다양성 | 신규 (제안된 형태로는 아님) |
그리고 제안이 근거로 든 실측 — "결정 46건을 쌓고 감사했더니 모순 7건" — 은 직전 릴리스를 만들게 한 바로 그 사건이었다. 숫자만 40에서 46으로 달랐다. 그 릴리스가 나온 이후에 실패했다는 증거가 아니다.
제안자가 부주의한 게 아니다. 기각 이력이 제안자에게 안 보였을 뿐이다.
내 저장소의 패치노트에는 릴리스마다 「검토했으나 넣지 않은 것」 절이 있다. 무엇을 왜 버렸는지가 거기 쌓인다. 그런데 그건 저장소 안에 있고, 전달문을 쓰는 쪽은 배포된 스킬 파일만 본다. 버린 안이 다시 오는 것이 기본값이다.
직전 릴리스에서 나는 이 원리를 이미 적어뒀다.
기각 이력이 없으면 같은 안이 다시 올라온다.
그때는 이걸 모델과 사용자 사이의 문제로만 봤다. 대화에서 버린 안이 몇 턴 뒤에 다시 올라오는 것. 실제로는 저장소와 외부 제안자 사이에서도 똑같이 일어난다. 층만 다르고 같은 실패다. 한 층에서 배운 것을 다른 층에 옮겨 적용하는 데 한 번의 릴리스가 더 들었다.
새 파일을 만들지 않았다
제안 넷 중 마지막은 이거였다. "같은 규약이 스킬 두 곳에 필요하니, 공통 파일로 빼서 양쪽이 참조하게 하라."
사본 드리프트를 걱정한 것이고, 그 걱정은 옳다. 이 저장소는 사본 때문에 여러 번 데였다 — 설정 템플릿이 두 곳에 있다가 컬럼 이름이 갈라졌고, 테스트 데이터 사본이 갈라져 CI가 보는 것과 코드가 보는 것이 달라졌다.
그런데도 기각했다. 확인해보니 정본이 이미 있었다.
한쪽 스킬 문서에 이미 이렇게 적혀 있었다. "선택지를 주는 방식은 (다른 스킬)과 같다." 포인터가 이미 걸려 있었던 것이다. 그 포인터가 「선택지 제시 방식」만 가리키고 「자기검증」은 안 가리켰을 뿐이다.
그래서 새 파일 대신 포인터를 한 줄 넓혔다.
선택지를 주는 방식과 **추천안 자기검증**은 (다른 스킬)과 같다.
그 스킬의 해당 절이 정본이고 여기 옮겨 적지 않는다 — 두 곳에 적으면 한쪽만 고쳐진다.
새 파일을 만들었으면 정본이 셋이 됐을 것이다. 두 스킬과 새 파일. 그리고 셋 중 둘이 갈라졌을 것이다.
드리프트가 걱정될 때 첫 수는 파일 신설이 아니라 기존 포인터 확인이다. 공통 파일은 정본을 하나로 만드는 게 아니라, 이미 있는 정본 옆에 하나를 더 놓는 경우가 많다.
같은 이유로 기각한 것이 하나 더 있다. 이 저장소에는 「폐기한 문구」를 등록하는 린터가 있다. 결정이 바뀌어 거짓이 된 문장이 문서에 남는 걸 막는다. 이번에 고친 문장도 등록할까 하다가 안 했다. 「두세 개 안을 낸다」는 여전히 참이고, 앞에 조건이 붙었을 뿐이다. 참인 규칙을 린터에 넣으면 그 린터를 아무도 안 보게 된다.
검사가 침묵한 것을 통과로 읽었다
여기까지가 계획대로 간 부분이다. 이제 이번 작업에서 내가 낸 가장 큰 실수.
이 저장소에는 도달 경로 동기화라는 검사가 있다. 스킬에 기능을 넣고 그 기능에 도달하는 경로(트리거 문구)를 안 고치는 실수를 막는다. 두 번 연달아 그 실수를 해서 만든 검사다. 기능이 있는데 아무도 도달할 수 없으면 만들지 않은 것과 같다.
이번 변경 뒤에 그 검사를 돌렸다. 통과했다. 그래서 패치노트에 이렇게 적었다.
도달 경로 갱신 — 안 했다. 내부 판정 절차이고 작가가 새로 요청할 기능이 아니다. 「다른 방안 더 없나」는 이미 해당 스킬로 간다.
마지막 문장을 돌려보지 않고 적었다.
이 글을 쓰기 전에 마지막 검증을 돌리면서, 그 문장을 시험 발화로 바꿔 라우팅 시뮬레이터에 넣어봤다.
| 발화 | 가야 할 곳 | 실제로 간 곳 |
|---|---|---|
| "이 세계관 안 세 개 냈는데 다른 방안이 더 없는지 확인해줘" | 기획 스킬 | 세계관 스킬 |
| "낸 안들이 다 비슷한 것 같은데 다른 축으로도 짜볼 수 있나" | 기획 스킬 | 독자 인지 추적 스킬 |
둘 다 빗나갔다.
이 릴리스가 겨냥한 실패를 사용자가 직접 지적하는 바로 그 발화가, 그 실패를 고친 스킬에 안 닿고 있었다. 축 규약을 아무리 잘 써놔도 그 문장이 도달하지 않으면 없는 것이다.
그 검사는 새 절이 추가됐을 때만 뜬다. 이번엔 절을 안 늘리고 기존 절 안에서 체크리스트 항목과 문장만 고쳤다. 검사는 볼 일이 없었으므로 침묵했고, 나는 침묵을 통과로 읽었다.
같은 실수를 앞서 두 번 했다. 그때는 절을 늘렸기 때문에 검사가 잡아줬다. 검사가 있으니 안심했고, 검사의 발동 조건은 확인하지 않았다.
검사가 안 뜬 것은 통과가 아니다. 그 검사가 무엇을 볼 때만 뜨는지를 알아야 한다. "문서로 고친 버그는 다시 난다, 검사로 고쳐라"는 말을 나는 오래 해왔는데, 그 다음 판이 있었다 — 검사로 고친 버그도, 검사의 발동 조건 밖에서는 다시 난다.
고친 방법은 단순하다. 트리거 문구에 해당 요청 경로를 넣고, 위 두 발화를 영구 시험 발화로 등록했다. 이제 오라우팅 0이다. 확인만 하고 등록을 안 하면 다음 변경에서 다시 확인해야 하고, 대개 다시 확인하지 않는다.
검증에 대해 — 1차는 검토자를 셋 잃었다
판정 기준과 사용자 대면 문구를 바꾼 릴리스다. 내 저장소 규약은 이런 변경에 작성 패스와 검토 패스 분리를 요구한다. 저자와 검토자가 같으면 판단 오류는 안 걸리기 때문이다.
1차는 전멸했다. 독립 검토 에이전트를 셋 띄웠다 — 서로 다른 종류로. 셋 다 결과 없이 유휴 알림만 보내고 죽었다. 스폰 셋, 재촉 셋, 회수 0. 직전 릴리스에서는 여섯 번 시도해 두 번 회수했으니 더 나빠졌다. 외부 CLI 검토는 이 기기에서 계정 제약으로 막혀 있다.
그중 하나는 능동적으로 해를 끼쳤다. 검토 에이전트가 공유 작업 디렉토리를 기준 브랜치로 체크아웃해버렸다. 나는 그걸 모르고 릴리스 노트 스크립트를 돌렸고, "패치노트 항목이 없다"는 실패를 봤다. 하마터면 스크립트 버그로 오진할 뻔했다. 실제로는 파일이 검토 대상이 아닌 판이었을 뿐이다.
커밋을 잃진 않았다. 잃을 뻔한 건 검토의 유효성이다 — 에이전트가 검토 대상이 아닌 코드를 읽고 판정하고 있었다.
검토를 위임할 때, 위임받은 쪽이 검토 대상 자체를 바꿀 수 있으면 그건 검토가 아니다. 격리된 작업 공간을 안 줄 거면 최소한 "지금 보고 있는 판이 맞는지" 확인할 지문을 요구해야 한다.
그래서 축을 나눴다.
| 검토 축 | 누가 봤나 | 왜 |
|---|---|---|
| 정합성 — 개수 문장, 참조 대상 실재, 파생 문서, 사본 | 내가 | grep으로 판정된다. 저자 편향이 안 걸리는 축이다 |
| 판정 기준 — 우회 가능성, 분량, 도달 | 아무도 | 사람 판단이 필요한데 회수가 0이었다 |
정합성 축은 돌려서 하나 잡았다 — 사용 가이드가 이번에 바뀐 규약을 빠뜨리고 있었다. 그리고 판정 기준 축은 미검토라고 적고 릴리스했다.
그게 이 글에서 가장 잘한 판단이었다. 다음 절에서 왜인지 나온다.
미검토라고 적고 냈더니, 같은 날 무너졌다
릴리스한 뒤 검토를 다시 붙였다. 이번엔 세 가지를 바꿨다.
| 바꾼 것 | 겨냥한 실패 |
|---|---|
| 격리된 작업 공간을 준다 | 검토자가 검토 대상을 바꾸는 것 |
| "네 최종 출력이 곧 보고서다"를 명시 | 진행만 중계하고 결과를 안 내는 것 |
| "우회 시나리오를 못 쓰면 그 지적은 무효" | 일반론만 돌아오는 것 |
회수 2/2. 앞선 시도가 0/3 이었으니 뒤집힌 셈이다. 셋 중 결정적인 건 마지막이다. 그 한 줄이 "더 명확히 하면 좋겠다"류를 걸러냈고, 남은 지적은 전부 LLM 이 실제로 어떻게 빠져나가는지 예시 출력을 달고 왔다.
그리고 결과는 여섯 질문 중 여섯이 우회 가능.
가장 아픈 것 하나만 옮긴다. 이 글 앞부분에서 자랑스럽게 소개한 그 항목이다.
충돌하는 줄이 있으면 어느 줄인지 적어라.
같은 절 아래쪽에 이런 문장이 있다.
조건이 앞에 붙으면 판정을 안 하고 넘긴다.
한 절 안에서 반대말을 하고 있었다. 직전 릴리스가 다른 항목에서 조건절을 풀어낸 바로 그 원리를, 이번엔 조건절을 그대로 둔 채 결론부만 강화하는 데 썼다. "충돌 없음" 한 줄이면 여전히 통과한다.
나는 이걸 다섯 번 읽고 못 봤다. 자기 검토가 못 잡는 것이 무엇인지의 표본으로 이보다 나은 게 없다 — 원칙을 적어놓은 문장과 그것을 어긴 문장이 화면 한 장 안에 같이 있었다.
나머지도 같은 종류다. 첫 상담에서는 대조할 것이 없어 무조건 통과하는 동시에 모든 안이 "고립" 판정을 받는다(두 결론이 반대인데 어느 쪽도 안 적었다). 의존 관계 칸은 비어 있지 않기만 하면 통과해서, 모든 것의 위에 있는 항목을 적으면 무조건 뚫린다. 다른 스킬로 넘기는 포인터에는 "읽어라"에 해당하는 동사가 하나도 없었다 — "여기 옮겨 적지 않는다"는 유지보수자에게 하는 말이지 실행 중인 모델에게 하는 말이 아니다.
두 검토자가 정반대 결론을 낸 지점
렌즈를 나눠 띄운 게 여기서 값을 했다.
| 렌즈 | 출력 계약에 대한 결론 |
|---|---|
| 우회 가능성 | 검증 결과를 출력해야 대충 넘어가는 걸 막는다 |
| 부하·우선순위 | 출력하면 한 턴에 60~80줄이다. 내부 절차로 두라 |
한쪽만 띄웠으면 반대쪽 결함이 그대로 남는다. 조정한 결과는 양쪽 안보다 나았다 — 영수증은 남기되 실제로 제시하는 안에만 한 줄, 그리고 사용자가 골라야 하는 갈림길은 비용이 가장 큰 하나만.
검토자를 늘릴 때 같은 렌즈로 늘리면 한 번 본 것이다. 다른 렌즈 둘이 충돌하는 지점이 설계가 덜 된 지점이다.
검토자도 틀린다
받은 지적을 그대로 붙이지는 않았다. 하나는 규약 조항을 오인용했다 — 인용한 조항은 전혀 다른 얘기였고, 다만 지적 자체는 성립했으며 더 정확한 근거가 옆 절에 따로 있었다. 다른 하나가 제안한 문구에는 개수를 세는 문장이 들어 있었고, 같은 제안이 시킨 대로 위치를 옮기면 방향까지 틀렸다.
둘 다 그대로 넣었으면 새 결함이 됐다. 인용한 줄을 열어보고 반영해야 한다 — 검토를 위임한다고 판단까지 위임되는 게 아니다.
그래서 자기 검토는 무엇을 잡나
버릴 것은 아니다. 다섯 번 돌려 이걸 잡았다.
- 제안 여섯 중 넷이 기존 처리분이라는 것
- 제안의 실측 근거가 이미 고친 사건이라는 것
- 「최소 4개」가 매직넘버라는 것
- 공통 파일이 정본을 늘린다는 것
- 체크리스트 항목이 늘면서 「위쪽 다섯 개는」이라는 문장이 틀려질 뻔한 것
못 잡은 것은 앞 절의 도달 경로다. 다섯 번 다 "내부 절차니까 괜찮다"로 넘어갔다. 잡은 것은 사람이 아니라 명령어 한 줄이었다.
그리고 여전히 못 잡을 것. 바뀐 문구가 실제 상담에서 작동하는지. 축 규약이 대화를 얼마나 길게 만드는지는 써보기 전에는 모른다.
자기 비판의 회수율에 대해 하나 말해두면 — 이번에 다섯 번 돌려서 나온 것 중 실행해서 확인한 것은 하나도 없었다. 전부 문서를 읽고 판단한 것이다. 그리고 실제로 무너진 것도 실행하지 않은 주장이었다. 읽어서 잡히는 것과 돌려야 잡히는 것은 다른 층이고, 다섯 번 읽어도 후자는 안 나온다.
이 절을 만든 건 이런 릴리스를 성공으로 적지 않기 위해서였다. "충분히 검토했다"가 아니라 시도와 회수를 나눠 적는다. 회수 실패를 성공으로 적으면, 다음 릴리스가 그 기록을 근거로 삼는 순간 거짓말이 된다.
덤 — 고치다가 발견한 것
체크리스트를 손보면서 「위쪽 다섯 개는 앞을 보는 검사고」라는 문장을 발견했다. 이번 변경이 그 아래에 항목을 하나 추가하고 있었다. 다행히 위쪽 다섯은 그대로여서 틀리진 않았지만, 문장을 「'대조했는가' 위쪽은」으로 바꿨다. 항목을 세는 대신 경계를 가리키게.
그리고 같은 파일에서 하나 더 나왔다고 잘못 보고했다. 「6개 축」이라는 제목 아래 표에 행이 일곱이길래 "나중에 0번 축이 붙으면서 개수를 안 고쳤다"고 적었다.
git log -S 로 확인해보니 0번 축과 「6개 축」 제목은 첫 릴리스 같은 커밋에 들어갔다.
번호가 붙은 축은 1번부터 6번까지 정확히 여섯이고, 0번은 의도적으로 번호 밖의 전제다.
개수는 맞았다.
세어보고 이상하다고 느낀 것까지는 맞았는데, 원인을 확인하지 않고 지어냈다. 확인 비용은 명령어 한 줄이었다.
내 글쓰기 규칙에는 이런 항목이 있다.
개수를 세는 문장을 쓰지 않는다. "위 다섯 개는" 같은 표현은 항목이 늘 때 조용히 틀려진다.
그 규칙을 적어놓은 저장소 안에 진짜 사례가 하나 있었고, 그걸 고쳤다. 그리고 가짜 사례 하나를 만들어냈다 — 규칙을 알고 있으니 눈에 띄는 족족 그것으로 보인 것이다. 확인하기 전에 원인을 붙이는 습관은 앞 절의 도달 경로에서도 똑같이 나왔다. 같은 회차에 두 번.
정리
| 증상 | 원인 | 고침 |
|---|---|---|
| 선택지 셋을 줬는데 사용자가 물으면 매번 더 나은 게 나옴 | 다양성을 개수로 셌다 | 세는 단위를 축으로 |
| 제안의 대부분이 이미 처리된 것 | 기각 이력이 밖에서 안 보인다 | 제안 접수 절차의 첫 단계를 기각 이력 조회로 |
| 사본 드리프트가 걱정됨 | — | 새 파일 대신 기존 포인터를 넓힘 |
| 고친 기능에 사용자가 도달 못 함 | 검사가 절 추가 때만 뜬다 | 도달 경로 주장을 시험 발화로 등록 |
| 독립 검토를 못 받음 | 검토 에이전트 전멸, 하나는 검토 대상을 바꿈 | 축을 나눠 기계 판정분만 회수, 나머지는 미검토로 공표 |
| 검토자가 결과를 안 냄 | 일반론만 반환 | "우회 시나리오를 못 쓰면 무효" — 회수 0/3 → 2/2 |
| 판정 기준이 여섯 중 여섯 우회 가능 | 자기 검토가 원칙과 그 위반을 한 화면에 두고도 못 봄 | 다른 렌즈 둘을 붙이고, 충돌하는 지점을 조정 |
관통하는 것 하나. 보이지 않는 것 때문에 반복이 생긴다. 모델은 자기가 만들지 않은 축을 못 보고, 제안자는 우리가 버린 안을 못 보고, 나는 검사가 안 보는 구간을 못 봤다. 해법은 같다 — 그것을 겉으로 꺼내 세게 하는 것이다. 축을 세게 하고, 기각 이력을 조회하게 하고, 도달 경로를 발화로 등록해 매번 돌리게 한다.
세 번째가 가장 아프다. 앞의 둘은 남이 못 본 것이지만, 세 번째는 내가 검사를 손에 쥐고도 안 돌리고 적은 것이다.
그리고 자기 자신은 목록에서 빠질 수 없다. 이 글을 마무리하면서 표를 다시 세어보니 제안 개수를 틀리게 적고 있었다 — 초안과 패치노트와 저장소 규약 문서까지 같은 숫자가 퍼져 있었다. 개수를 세지 말라는 글에서, 개수를 세다 틀렸다.
그리고 이 글이 자랑스럽게 소개한 그 체크리스트 항목은, 발행 전에 이미 무너졌다. 원칙을 적은 문장과 그것을 어긴 문장이 화면 한 장 안에 있었는데 다섯 번 읽고 못 봤다. 잡은 것은 다른 눈이었다.
규칙을 아는 것과 지키는 것은 다른 층이다. 그래서 규칙이 아니라 검사로 고정하고, 검사로 못 고정하는 것은 다른 눈에게 보이고, 그것도 못 하면 최소한 미검토라고 적어두는 것이다. 마지막 것이 이번에 값을 했다 — 미검토라고 적었기 때문에 검토를 다시 붙였고, 같은 날 무너졌다.
webnovel-toolkit 의 선택지 축 규약 작업 기록. 이 툴킷이 무엇인지는
네 번의 릴리스에서 배운 것에 적어 뒀다.