AI가 자신의 코드를 직접 리뷰하게 하는 것을 그만둔 이유
요약
AI 모델이 작성한 코드를 동일한 모델이 리뷰할 때 발생하는 '동기화된 추론'과 사각지대 문제를 다룹니다. 모델 간의 독립적인 판단력을 확보하기 위해 리뷰 대신 두 모델을 병렬로 실행하여 결과를 비교하는 '경주(Race)' 방식을 제안합니다.
핵심 포인트
- 동일 모델의 코드 리뷰는 논리적 오류를 방어하는 경향이 있음
- 리뷰어는 작성자와 다른 사전 지식(priors)을 가져야 함
- 모델 간 가중치 공유를 피하기 위해 서로 다른 모델 사용 권장
- 리뷰 대신 두 모델을 병렬 실행하여 직접 비교하는 방식 제안
사각지대 문제 (The blind spot problem)
나는 Claude에게 API 엔드포인트에 입력 유효성 검사 (input validation)를 추가하도록 시켰다. Claude는 깔끔하고 관용적인 (idiomatic) TypeScript 코드를 작성했다. 나는 Claude에게 변경 사항(diff)을 리뷰해달라고 요청했다. Claude는 이를 승인했다. 테스트도 통과했다. 나는 그대로 배포했다.
이틀 후, 한 동료가 유효성 검사 로직이 빈 문자열 (empty strings)을 조용히 허용하고 있다는 점을 지적했다. 이는 원래 명세서 (spec)에서 명시적으로 금지했던 사항이었다. Claude는 검증기 (validator)를 작성했고, 스스로 승인했으며, 그 과정에서 어느 쪽도 그 간극을 잡아내지 못했다.
다시 돌아가서 Claude에게 왜 그것을 놓쳤는지 물었을 때, 답변은 본질적으로 다음과 같았다: "요구사항을 '비어 있지 않음(non-empty)'이 아닌 'null이 아님(non-null)'으로 해석했으며, 이는 합리적인 해석입니다."
그것이 바로 문제다. 미묘하게 잘못된 코드를 작성한 모델은 버그를 잡아내지 못할 뿐만 아니라, 자신의 해석을 적극적으로 방어한다. 모델은 자신의 출력물에 대해 독립적인 판단력을 갖지 못한다. 대신 동기화된 추론 (motivated reasoning)을 한다.
인간의 코드 리뷰 (code review)가 작동하는 이유는 리뷰어가 서로 다른 사전 지식 (priors)을 가지고 오기 때문이다. 코드를 작성하지 않은 시니어 엔지니어는 작성한 사람과는 다른 질문을 던진다. 그들은 다른 것을 포착한다. 그들은 이미 특정 해석에 몰입해 있지 않다.
동일한 원리가 LLM (Large Language Models)에도 적용된다. 함수를 작성한 모델과 그것을 리뷰하는 모델은 가중치 (weights)를 공유해서는 안 된다.
내가 처음에 시도했던 것
명백한 해결책은 리뷰를 위해 두 번째 모델을 사용하는 것이다. Claude가 작성하게 하고, GPT-5나 Codex가 리뷰하게 하는 방식이다.
이 방법은 도움이 된다. 하지만 완전한 해결책은 아니다.
문제는 대부분의 오케스트레이션 (orchestration) 설정이 동일한 출력물에 대해 이러한 모델들을 순차적으로 실행한다는 점이다. Claude가 diff를 생성하면, 당신은 그 diff를 Codex로 전달하여 리뷰를 요청한다. Codex는 Claude가 놓친 논리적 오류를 잡아낼 수 있다.
하지만 이제 다른 문제에 직면하게 된다: Codex는 타인의 구현 방식 (implementation choices)을 리뷰하고 있는 것이다. Codex는 타당한 결정을 버그로 표시하거나, Claude가 코드를 구조화한 방식에 특화된 문제를 놓칠 수도 있다. 결국 노이즈가 섞인 리뷰를 받게 되고, 당신의 사용 사례에 어떤 모델이 실제로 더 나은 출력을 생성하는지에 대한 명확한 신호를 얻기 어려워진다.
나는 다른 무언가를 원했다. 나는 알고 싶었다: 이 특정 작업에서, 이 특정 코드베이스에서, 어떤 모델이 더 나은 코드를 작성하는가?
리뷰 대신 경주하기
내가 내린 결론은 동일한 작업에 대해 두 모델을 동시에 실행하는 것이었다.
Claude Code와 Codex에게 동일한 프롬프트 (Prompt)를 준다. 각 모델이 서로 간섭할 수 없도록 격리된 git 워크트리 (worktrees)에서 병렬로 전체 구현을 생성하게 한다. 그런 다음 두 출력을 나란히 비교하고 선택한다.
$ npx runoff run \
--prompt "Add formatRelativeTime() to src/utils/format.ts" \
--config pipeline.config.json
...
경주를 멈추는 것은 의도적인 것이다. 나는 시스템이 자동으로 승자를 결정하는 것을 원하지 않는다. 핵심은 내가 결정한다는 것이며, 왜냐하면 내 코드베이스에서 무엇이 중요한지 내가 알고 있기 때문이다. 함수가 Date 객체를 처리해야 하는가? 미래 날짜 지원이 추가적인 복잡성을 감수할 만큼 가치가 있는가? 오직 나만이 알 수 있다.
이것이 핵심적인 통찰이다: 경주는 어떤 모델이 객관적으로 더 나은지를 결정하려는 것이 아니다. 인간이 정보에 입각한 결정을 내릴 수 있도록 트레이드오프 (trade-offs)를 드러내려는 것이다.
50회 이상의 경주를 통해 배운 점
이 워크플로 (workflow)를 몇 주간 진행한 후, 몇 가지 패턴이 나타났다.
모델의 강점은 작업 및 코드베이스에 따라 다르다. 나의 TypeScript API 코드베이스에서는 Claude Code가 기존 스타일과 일치하는 더 관용적인 (idiomatic) 코드를 일관되게 생성한다. 하지만 Go 유틸리티 함수와 데이터 처리 스크립트의 경우, Codex/DeepSeek가 더 많은 엣지 케이스 (edge cases)를 처리하는 더 포괄적인 구현을 생성하는 경향이 있다. 어느 쪽이 보편적으로 더 낫다고 할 수는 없다.
나를 가장 놀라게 했던 경주가 가장 가치 있었다. Claude가 이길 것이라고 예상했을 때 Codex가 분명히 더 우수한 구현을 만들어냈다면, 그것은 단일 모델 워크플로에서는 얻을 수 없었을 정보였다. 그 반대의 경우도 마찬가지다.
프롬프트 문구(Prompt phrasing)는 예상치 못한 방식으로 모델의 강점과 상호작용합니다. "검증 로직을 추가해줘(Add validation)"라는 요청은 "빈 문자열, null, 그리고 255자를 초과하는 문자열을 거부하도록 검증 로직을 추가해줘(Add validation to reject empty strings, null, and strings over 255 characters)"라는 요청과는 매우 다른 상대적 결과를 만들어냅니다. 동일한 모델이라도, 사양(specs)이 달라지면 승자도 달라집니다.
시간이 흐르면서, 저는 어떤 유형의 작업에서 어떤 모델이 승리할지 예측하기 시작했습니다. 그러한 메타 지식(meta-knowledge)은 이제 개별 경기의 결과보다 저에게 더 가치 있는 것이 되었습니다.
메모리 관점 (The memory angle)
제가 발견한 패턴 — "Codex는 유틸리티 함수(utility functions)에서 더 철저한 경향이 있고, Claude는 API 핸들러(API handlers)에서 내 스타일을 더 잘 맞추는 경향이 있다" — 은 제가 시스템이 학습하고 활용하기를 원했던 종류의 정보였습니다.
이것이 바로 runoff의 Dream 시스템이 수행하는 역할입니다. 실행되는 모든 경기는 트레이스(trace)를 생성합니다. 사용자가 승자를 선택하면, 시스템은 어떤 제공자(provider)가 승리했는지, 어떤 종류의 작업이었는지, 어떤 파일들이 연관되었는지를 기록합니다. 시간이 지나면서 시스템은 사용자의 실제 선택을 바탕으로 패턴 라이브러리를 구축합니다.
새로운 경기를 시작하면, runoff는 관련 있는 과거 패턴을 검색합니다: "src/utils/를 포함하는 유사한 유틸리티 함수 작업에서, 당신은 9번 중 7번 Codex를 선택했습니다." 이것이 경기의 결과를 결정짓지는 않습니다. 사용자는 여전히 두 가지 차이점(diffs)을 직접 보고 결정합니다. 하지만 이는 자신의 이력으로부터 맥락(context)을 제공합니다.
이 검색(retrieval)은 다중 전략 접근 방식(의미론적 유사성(semantic similarity), 키워드 매칭(keyword matching), 파일 경로 그래프 점프(file-path graph hops), 엔티티 매칭(entity matching))을 사용하며, 사용자의 선택과 실제로 상관관계가 있는 패턴을 기반으로 시스템이 조정하는 가중치 랭킹(weighted ranking)과 결합됩니다. 충분한 경기가 진행된 후에는, 시스템이 사용자의 코드베이스에 어떤 검색 전략이 가장 잘 작동하는지 알게 됩니다.
이는 느린 축적의 과정입니다. 10번의 경기를 거치면 약한 신호를 얻게 됩니다. 50번을 거치면 유용한 무언가를 얻게 됩니다. 100번을 거치면 놀라울 정도로 사용자의 취향을 잘 반영하는 모델을 갖게 됩니다.
다른 접근 방식과의 비교
단일 모델 리뷰(single-model review)와 비교 시: 근본적인 한계는 가중치 공유(shared-weights) 문제입니다. 자신의 코드를 스스로 리뷰하는 모델은 동기화된 추론(motivated reasoning)을 하게 됩니다. 반면 경주(Racing)는 진정으로 독립적인 평가를 제공합니다.
vs. Vibe Kanban / parallel agent dashboards: 이러한 도구들은 처리량(throughput)을 높이기 위해 서로 다른 작업에 대해 서로 다른 에이전트(agents)를 병렬로 실행합니다. 그것은 규모(scale)와 속도(speed)라는 다른 문제입니다. runoff는 품질을 개선하기 위해 동일한 작업에 대해 서로 다른 에이전트를 실행합니다. 두 목표는 직교(orthogonal)합니다.
vs. Cadence의 역할 분할(role-split) 방식: Cadence는 소프트웨어 개발 생명주기(SDLC)의 각 단계마다 서로 다른 모델을 사용합니다. Claude는 작성하고, Codex는 리뷰하며, Gemini는 아키텍처 위원회(architectural council) 역할을 수행합니다. 이는 스마트한 방식입니다. 단계(phase) 수준에서 공유 가중치(shared-weights) 문제를 해결하기 때문입니다. 차이점은 runoff가 서로 다른 단계의 *역할(roles)*을 비교하는 것이 아니라, 동일한 단계의 *출력물(outputs)*을 비교한다는 점입니다. 이를 통해 단순히 어떤 모델이 일반적인 리뷰에 더 뛰어난지가 아니라, 귀하의 특정 작업 유형에 대해 어떤 모델이 더 나은 초안 구현(first-pass implementations)을 생성하는지 배울 수 있습니다.
vs. 두 모델을 수동으로 실행하기: 당연히 수동으로도 할 수 있습니다. 한 터미널에는 Claude Code를, 다른 터미널에는 Codex를 열고 동일한 프롬프트를 입력한 뒤 직접 차이점(diffs)을 비교하면 됩니다. runoff는 격리(별도의 워크트리(worktrees), 교차 오염 방지), 병렬 실행, 차이점 노출(diff surfacing), 추적 로깅(trace logging), 그리고 패턴 축적(pattern accumulation)을 자동화합니다. 워크플로우는 동일하지만, 오버헤드(overhead)는 훨씬 낮습니다.
실질적인 설정
runoff는 MCP 서버로 작동하므로, IDE를 벗어나지 않고도 Claude Code, Cursor, Claude Desktop과 통합됩니다:
{
"mcpServers": {
"runoff": {
...
경주(race) 설정은 파이프라인 JSON 내의 단순한 배열입니다:
{
"pipeline": {
"implement": [["claude-code", "opencode"]],
...
[["claude-code", "opencode"]] 구문은 "두 모델을 병렬로 실행하고 판사(judge)의 결정이 있을 때까지 일시 중지하라"는 의미입니다. 단일 문자열을 사용하면 해당 제공자(provider)를 순차적으로 실행합니다. 파이프라인은 선택 후에 계속됩니다.
아직 모르는 것들
제가 여전히 고민하고 있는 몇 가지 미결 질문들이 있습니다:
축적된 패턴 메모리가 실제로 도움이 될까요? 경험상으로는 그렇습니다. 새로운 레이스(race)를 시작할 때 검색(retrieval)을 통해 관련 컨텍스트가 드러나는 것을 확인했습니다. 하지만 패턴 검색(pattern retrieval)이 있는 경우와 없는 경우의 레이스 결과를 비교하는 통제된 실험을 수행해 보지는 않았습니다.
적절한 레이스 주기(cadence)는 무엇일까요? 저는 모든 작업에 대해 레이스를 진행하지 않습니다. 그렇게 하면 토큰 소모가 두 배로 늘어나고 속도가 느려질 것이기 때문입니다. 저는 구현 방식에 대해 진정한 불확실성이 있거나, 이전에 모델의 사각지대(blind spots)로 인해 어려움을 겪었던 작업들에 대해서만 레이스를 진행합니다. 적절한 선택 기준을 찾는 것은 여전히 시스템이라기보다 직관에 의존하고 있습니다.
팀 규모에서도 작동할까요? 현재 설정은 단일 사용자용입니다. 패턴 메모리는 머신(machine) 단위로 관리됩니다. 레이스 이력을 팀 전체가 공유하는 것이 어떤 모습일지, 혹은 팀 패턴이 유용할지 아니면 그저 노이즈(noise)가 될지에 대해서는 아직 깊이 고민해 보지 않았습니다.
사용해 보기
npx runoff init --work-dir /path/to/your/repo
npx runoff run --prompt "your task here"
init 명령은 해당 리포지토리(repo)를 위한 pipeline.config.json을 생성합니다. 데모 모드(npm run demo)는 실제 백엔드(backend)를 연결하기 전에 레이스 메커니즘을 확인하고 싶을 경우 모의 프로바이더(mock providers)와 함께 실행됩니다.
출처: github.com/alexangelzhang/runoff
만약 이와 유사한 것을 만드셨거나 공유 가중치(shared-weights) 문제에 대해 다른 의견이 있으시다면, 기꺼이 듣고 싶습니다.
태그: ai, programming, productivity, claude
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기