테스트를 통과할 때까지 산문을 다시 쓰기: 모든 테스트는 통과했지만, 정작 중요한 체크는 단 한 번도 실행되지 않았다
요약
코드 개발 루프를 긴 자연어 산문 작성에 도입한 실험적 사례를 다룹니다. LLM을 활용한 '의미 체크(meaning check)'와 결정론적 체크를 구분하여, 테스트 통과 여부와 리뷰 실행 여부를 분리하는 설계의 중요성을 강조합니다.
핵심 포인트
- 문맥 없이 강제될 경우 잘못된 판단을 내리는 체크 항목은 제거해야 함
- Pass, Fail, Error의 3가지 상태를 설계하여 리뷰 미실행 상태를 구분할 것
- 모든 어설션을 실패로 처리할 경우 텍스트가 지나치게 완곡해지는 부작용 발생
- LLM 기반 리뷰어의 인증 만료가 테스트 신뢰성에 미치는 영향 주의
요약 (Summary)
모든 테스트는 통과했지만, 품질을 판단해야 할 의미 체크 (meaning check)가 단 한 번도 실행되지 않은 사건을 겪었습니다. 이 일은 테스트를 통과할 때까지 텍스트를 다시 쓰는 개발 루프 (development loop) 내에서, 긴 자연어 산문 (natural-language prose)인 연구 초록 (research abstract)을 작성하던 중에 발생했습니다. 리뷰어의 인증 (authentication)이 만료되자 의미 체크가 완전히 건너뛰어졌고, 통과한 것처럼 보였던 테스트들은 더 이상 안전성을 입증하는 근거가 되지 못했습니다.
테스트는 두 가지 계층으로 구성됩니다. 문맥을 보지 않고도 판단할 수 있는 결정론적 체크 (deterministic check)와, 리뷰어로서 CLI를 통해 LLM을 호출하는 의미 체크 (meaning check)입니다. 판단은 통과 (pass), 실패 (fail), 그리고 리뷰가 전혀 실행되지 않았음을 나타내는 에러 (error) 중 하나의 값을 가집니다.
연구 초록 및 연구 운영을 위해 리포지토리 (repository)를 실행하기 시작한 지 약 일주일 후에 이 메커니즘을 도입했으며, 그 후 2주 이상 동안 체크를 계속 추가하고, 동일한 루프를 두 번째 초록으로 확장하고, 리뷰어의 인증을 복구하고, 모델을 업데이트했습니다.
대상 및 핵심 요약 (Audience and takeaways)
이 글은 코드뿐만 아니라 자신의 글쓰기에도 개발 루프 (development loop)를 도입하고자 하는 개발자와 작가들을 위한 것입니다. 이는 테스트 통과 여부를 지켜보면서 산문을 다시 쓰는 루프입니다. 세 가지 핵심 요약이 있습니다. 첫 번째는 분류 기준입니다: 문맥을 보지 않고 강제될 때 잘못된 판단을 내리는 체크를 제거하십시오. 두 번째는 리뷰가 전혀 실행될 수 없는 상태를 품질의 통과 또는 실패 판단과 분리하여 유지하는 3가지 값 설계 (three-value design)입니다. 세 번째는 모든 어설션 (assertion)을 일률적으로 실패하게 만들었더니 텍스트가 온통 완곡한 표현 (hedging)으로 가득 차서 어떤 주장도 제대로 서지 못하게 되었던 실패 사례와 이를 어떻게 해결했는지에 대한 것입니다. 이 자료는 연구 초록 및 연구 운영을 위한 리포지토리에서 이 메커니즘을 2주 이상 운영한 기록입니다.
코드 개발 루프를 긴 글쓰기에 도입하기 (Bringing the code development loop into long-form writing)
코드를 작성할 때, 테스트는 작성하는 즉시 통과(pass) 또는 실패(fail)를 반환합니다. 이번 주제는 이러한 개발 루프(development loop)를 논문, 사양서(specifications), 제안서(proposals)와 같은 긴 자연어 산문(natural-language prose)에도 도입할 수 있는지에 대한 것입니다. 제가 목표로 삼은 것은 연구 초록(research abstract)이었습니다. 테스트 정의는 YAML에 저장되며, pytest를 기반으로 구축된 테스트 러너(test runner)가 매개변수화(parameterization)를 통해 하나의 항목을 하나의 테스트로 확장합니다. 그리고 테스트를 통과할 때까지 다시 쓰는 작업 방식이 자리를 잡기 시작했습니다.
미리 말씀드리자면, 이 테스트들은 저를 대신해 글을 써주는 도구가 아닙니다. 저는 제가 초록에 실제로 남겼던 코멘트들을 하나씩 규칙(rules)으로, 그리고 다시 체크(checks)로 변환하고 있을 뿐입니다. 따라서 기계가 재현할 수 있는 것은 과거의 코멘트들뿐이며, 무엇을 쓸지 결정하는 것은 여전히 작성자의 몫입니다.
문맥 없이 강제될 때 잘못 판단하는 체크 항목 제거하기
제가 제거한 체크 항목과 유지한 체크 항목을 가르는 단 하나의 기준이 있습니다. 저는 문맥(context)을 고려하지 않고 고립된 상태에서 강제될 때, 정당한 사용조차 잘못 판단하는 체크 항목들은 제거했습니다. 반면 글자 수나 출현 횟수와 같이 체크되는 값에 허용 범위(range)가 있어 잘못 판단하지 않는 체크 항목들은 유지했습니다. 처음에는 엄격한 강제형 체크 항목들만 넣었습니다. 단일 문장의 길이를 강제하거나, 문단 수를 엄격하게 제한하고, 문맥 없이 금지어를 단독으로 거부하는 방식이었습니다. 하지만 이러한 방식은 문맥이 있어야만 의미가 통하는 글쓰기조차 잘못 판단했고, 글쓰기의 자유를 앗아갔습니다. 그래서 한 번 구축한 뒤, 이들을 러너(runner)에서 제외하고 오래된 구현체로서 삭제했습니다.
이러한 분류의 기초는 논문의 평가가 결정론적 체크 (deterministic checks)만으로 수행될 수 있다고 가정하지 않고, 의미와 논리에 대한 판단에 무게를 두는 정책이었습니다. 공백과 조판 명령을 제외한 글자 수 범위, 특정 단어의 존재 여부, 정규 표현식 (regular-expression) 매칭 및 그 횟수와 같이 문맥을 보지 않고도 결정할 수 있는 항목들만 러너 (runner) 내부의 로컬 함수로 구현된 결정론적 체크로 옮겼고, 의미의 일관성, 논리, 그리고 장(chapter) 구조는 CLI를 통해 LLM을 리뷰어로 호출하는 레이어에 맡겼습니다. 빠르고 저렴한 판단은 결정론적 체크로 가고, 문맥이 필요한 판단은 리뷰어로 가며, 이 둘은 하나의 러너 안에서 공존합니다.
오류를 품질 판단과 분리하기
나는 리뷰어에게 내가 제공한 기준만을 사용하여 합격(pass) 또는 불합격(fail)을 판단하고, 모든 위반 사항을 누락 없이 나열하라는 고정된 지침을 주었으며, 응답을 판단, 위치, 이유를 포함하는 구조화된 데이터 (structured data)로 강제했습니다. 이는 위반 사항이 하나씩 지적됨에 따라 수정 작업이 길어지는 것을 방지하기 위함입니다. 조판용 백슬래시 (backslashes)가 응답에 섞여 들어오면 구조화된 출력 (structured output)의 파싱 (parsing)이 실패하므로, 이를 교정하고 파싱을 다시 실행하는 단계도 추가했습니다.
세 가지 가치 중 세 번째인 오류 (error)는 인증 만료, 타임아웃 (timeout), 또는 응답 파싱 실패로 인해 리뷰어를 전혀 실행할 수 없는 상태를 의미합니다. 나는 이를 텍스트의 품질 판단과 분리하여 유지하며, 불합격 (fail)에 섞지 않습니다. 만약 불합격 사례가 증가하는데 문제가 텍스트에 있는지 아니면 인프라 (infrastructure)에 있는지 구분할 수 없다면, 이 루프는 목적을 달성할 수 없습니다. 그것이 저의 견해였습니다.
실제로 리뷰어의 인증(authentication)이 만료되어 모든 리뷰어가 에러로 건너뛰어지는 사고가 발생한 적이 있었는데, 모든 테스트가 통과된 것처럼 보였음에도 불구하고 의미 체크(meaning check)는 단 한 번도 실행되지 않았습니다. 이 시점에서 명확해진 교훈은 모든 테스트가 통과한다고 해서 의미 체크가 실제로 실행되었다는 것을 보장하지는 않는다는 점입니다.
관찰 범위 내에서 단언(assertion)을 통과시키고, 다수결로 변동성(variance) 억제하기
판단의 축 중에서 단언(assertion)을 처리하는 부분이 가장 많은 튜닝(tuning)을 필요로 했습니다. 관찰된 규모와 상관없이 단언하는 모든 것을 일률적으로 실패 처리하던 기간에는, 텍스트가 그저 모호한 표현들로만 가득 차게 되어 어떤 주장도 제대로 서지 못했습니다. 그래서 저는 얼마나 많은 사람과 얼마나 많은 사례가 관찰되었는지와 같은 관찰 규모를 리뷰어의 지침(instruction)에 삽입하였고, 그 범위를 벗어나는 일반화만을 실패 처리하도록 했으며, 범위를 유지하는 합리적인 단언은 그대로 통과하도록 재작업했습니다. 이와 함께, 실패 시에는 리뷰어가 그것이 무엇인지 항상 알려주도록 했습니다. 즉, 문구 수정만으로 충분한지, 아니면 다시 데이터를 수집하러 돌아가야 하는지를 알려주는 것입니다.
리뷰어의 변동성(variance)을 억제하기 위해, 첫 번째 실행이 통과하면 거기서 멈추고, 통과하지 못했을 때만 두 번 더 판단하게 하여 총 세 번의 실행 중 다수결(majority vote)을 따르도록 구현했습니다. 그렇긴 하지만, 이는 속도에 무게를 둔 타협안이며, 최대 세 번의 시도 내에서 동일한 추상적 내용에 대해 동일한 판단을 얻는 재현성(reproducibility)이 실무에서 사용하기에 충분한지 여부는 측정하지 않았습니다. 단일 리뷰어 실행은 판단에 변동성이 있으며 안정적인 합의를 위해서는 반복적인 시도가 필요하다는 지적을 보긴 했으나, 필요한 시도 횟수를 뒷받침하는 1차 문헌(primary literature)을 아직 확인하지 못했습니다. 현재로서는 속도와 재현성 사이에서 균형을 맞추었다는 것 외에 더 드릴 말씀은 없습니다.
CI에는 결정론적 체크(deterministic check)를, 로컬 루프에는 리뷰어를
도입 단계에서 실무에 사용할 수 있는 단계에 이르기까지 본질적으로 이틀이 걸렸으며, 그 후 약 2주 동안 논문을 작성하는 방법에 관한 주요 문헌(primary literature)과 저 자신의 피드백을 바탕으로 체크 항목들을 조금씩 추가했습니다. 장(chapter) 단위의 체크는 한 장과 다음 장 사이의 간극을 보이지 않게 만들기 때문에, 나중에는 질문에서 주장까지 하나의 맥락이 이어지는지 확인하기 위해 모든 장을 가로질러 읽는 전체 작업 테스트(whole-piece test)도 추가했습니다.
또한 운영 방식의 분할을 결정했습니다. 결정론적 체크(deterministic check)만이 CI에서 매번 자동으로 실행되며, 의미 판단을 처리하는 리뷰어는 제가 수동으로 실행하는 루프(loop)를 위해 남겨두었습니다. 저는 하나의 초록(abstract)을 바탕으로 메커니즘을 구축했으며, 동일한 루프를 다른 주제의 초록으로 확장했습니다.
기계가 제공할 수 있는 것은 판단을 위한 재료에 불과했다
지금까지 이를 실행해 본 결과, 기계가 감당할 수 있는 범위가 시야에 들어왔습니다. 기계가 할 수 있는 일은 제가 반복해서 만들어온 코멘트들을 판단을 위한 재료(material for judgment)로서 제공하는 수준에 그쳤습니다. 테스트 주도 방식(test-driven work)이 초록 자체의 품질을 높였는지에 대해서는 단언할 수 없는데, 실패 횟수가 시간이 지남에 따라 어떻게 변했는지에 대한 기초 데이터(primary data)를 취하지 않았기 때문입니다. 실행을 통해 가시화된 것은 딱 거기까지이며, 그 이상의 효과를 이 도구의 결과라고 선언하는 것은 삼가고자 합니다.
기계에 맡길 수 있는 판단과 오직 사람만이 계속할 수 있는 판단 사이의 경계는 분명 앞으로도 계속 이동할 것이며, 그 경계가 얼마나 이동할지는 이 작업을 계속 수행하면서 파악해 나가고 싶은 부분입니다.
원문은 The Future of Humans, AI, and the Web에 게시되었습니다. 이 사이트는 저의 연구와 개발 내용이 인간과 AI에 의해 기록되고 분석되는 곳입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기