dev.to

I built a lie detector for AI coding agents. Then I read the rules.

AI 코딩 에이전트의 거짓 보고를 잡는 도구를 만들었습니다. 그다음 규칙을 읽었습니다

claimcheck는 에이전트가 변경 파일과 작업 내용을 claim.json으로 제출하면 실제 diff와 대조해 검증하고, 통과한 변경만 병합하는 Git 플랫폼 프로토타입입니다. 28개 에이전트의 동시 작업을 다섯 차례 시연해 허위 보고 11건을 모두 거부했지만, 작성자는 LLM 검증의 한계와 경쟁 규정상 참가 자격 문제도 공개했습니다.

에디터 노트

claimcheck의 가장 설득력 있는 설계는 권한 검사와 설명 검사를 분리한 점입니다. 보호 경로는 결정적 규칙으로 막고 LLM은 설명과 diff의 일치 여부만 판단합니다. 다만 근거 검사가 인용문의 존재 여부에 그친다는 댓글 지적은 코드와 맞닿아 있습니다. 문맥 줄이나 삭제된 줄도 증거로 인정될 수 있어 거부 판정의 증명 강도가 약해집니다. 작성자는 추가 변경 두 사례를 LLM이 잡았다고 답했지만, 15개 사례의 반복 시험만으로 근거 검사의 허점을 해소하지는 못합니다. hunk 단위 검사는 그 간극을 줄이는 제안이며 아직 구현 결과는 본문에 없습니다. 다중 에이전트 병합과 장애 복구는 실제 코드와 반복 시험으로 구체화했지만, 허위 보고 검증은 제한된 표본에서 확인한 프로토타입 단계입니다.

AI 요약

AI 에이전트가 커밋 메시지에 적은 작업 내용을 실제 변경과 대조하는 도구 claimcheck를 만들었습니다. 에이전트마다 fork에서 작업하며, 변경 파일과 각 변경의 설명을 .claim/claim.json에 담아 push합니다. claimcheck는 실제 diff와 제출 내용을 검증한 뒤 통과한 변경만 병합 대기열에 넣습니다. Cloudflare Workers와 Artifacts를 기반으로 하며, 저장소 코드는 MIT 라이선스로 공개했습니다.

검증 방식

검증은 결정적 규칙과 LLM 판정으로 나뉩니다. 먼저 변경된 파일이 claim에 모두 포함됐는지, claim에 적힌 파일이 실제로 바뀌었는지 확인합니다. 기본 보호 경로인 src/auth/**를 건드리거나 변경 규모 제한을 넘겨도 거부합니다. macOS와 Windows에서 대소문자 차이가 경로 보호를 우회하지 않도록 경로를 대소문자 구분 없이 비교합니다. 작성자는 src/Auth/session.js가 src/auth/**에 걸리지 않던 테스트 결과를 반영했다고 설명합니다.

이 규칙을 통과하면 LLM이 각 설명과 diff가 일치하는지 확인합니다. 모델은 판단 근거로 diff의 줄을 인용해야 합니다. 코드에는 인용한 내용이 diff에 있는지 검사하는 evidenceInDiff 함수가 있습니다. 근거가 있는 불일치 판정은 거부로 이어지고, 모든 항목이 일치하면 검증됩니다. 모델 오류나 시간 초과, 근거가 확인되지 않은 불일치는 needs_review로 보내며 기본적으로 자동 병합하지 않습니다. 같은 파일을 먼저 검증된 변경이 수정했다면 뒤의 변경은 정직한 내용이어도 병합하지 않습니다.

시연과 운영 결과

작성자는 스크립트 에이전트 28개가 동시에 push하는 시연을 다섯 차례 반복했습니다. 매번 허위 보고 에이전트 11개를 모두 거부했고 정직한 에이전트를 잘못 거부하지 않았다고 보고합니다. 각 판정에는 근거가 붙었고, 중앙값 판정 시간은 6~7.5초였습니다. 스크립트를 쓴 이유는 에이전트 행동의 변수를 줄여 검증기를 반복해서 시험하기 위해서입니다. 실제 Qwen 에이전트는 오탈자 수정 작업을 다섯 차례 수행했고 매번 정확히 작업을 설명해 검증을 통과했습니다. 열 개 에이전트가 같은 파일을 동시에 수정한 별도 시험에서는 모두 정직했지만 첫 변경만 병합됐습니다. 판정까지 걸린 시간은 중앙값 8.6초, 전체 실행은 59초 이내였습니다.

22개 에이전트를 동시에 돌린 규모 시험에서는 12개가 충돌 없이 정상 동작했고, 2개는 같은 파일을 수정해 충돌했습니다. 나머지 8개는 파일 누락, 보호 경로 접근, 허위 설명 등으로 거부됐습니다. 다만 LLM 판정기만 따로 시험한 결과는 짧은 단일 파일 diff 15개를 다섯 번씩 평가한 75건입니다. 모두 기대한 결과와 일치했지만 작성자도 이 표본이 작다고 적었습니다.

구현과 수정

Artifacts에는 diff 기능이 없어 git tree를 재귀적으로 비교하고, 해시가 같은 하위 트리는 건너뛰어 변경 파일을 찾습니다. 작성자는 삭제 파일, 새 하위 폴더, 교체된 줄이 있는 실제 fork에서 git diff --numstat 결과와 대조했다고 설명합니다. 푸시 이벤트마다 Workflow를 실행하고, Workers AI가 설명을 판정하며, Durable Object가 판정과 병합 대기열을 관리합니다.

테스트 과정에서는 서비스 장애와 구현 오류도 드러났습니다. push 재시도, 모델 호출 45초 시간 제한, 502·503·504 응답 재시도 기능을 추가했습니다. 병합 요청이 성공했는데 응답만 실패해 중복 병합을 시도하는 문제는 병합 커밋을 식별하고, 이미 반영됐는지 확인하는 방식으로 고쳤습니다. 전체 시험은 첫 세 차례에 각각 4/5, 4/5, 3/5 성공했고 수정 뒤 5/5 성공했습니다.

남은 쟁점

댓글에서는 파일 단위 claim만으로는 한 파일 안에서 허가받은 작업에 끼워 넣은 부수 변경을 잡기 어렵다는 지적이 나왔습니다. 작성자는 LLM이 설명과 다른 추가 변경을 판정하도록 설계했다고 답하며, 각 diff hunk를 선언한 변경과 대응시키는 결정적 검사를 다음 버전의 아이디어로 받아들였습니다. 또 다른 댓글은 evidenceInDiff가 인용문이 diff에 존재하는지만 확인해, 변경되지 않은 문맥이나 삭제된 줄을 근거로 허위 판정을 승인할 수 있다고 지적했습니다. 작성자는 이 지적에 대한 답을 본문에 싣지 않았습니다.

작성자는 프로젝트를 Cloudflare의 Git 플랫폼 구축 대회 출품작으로 만들기 시작했지만, 일주일 뒤 미국 또는 캐나다 거주자만 참가할 수 있다는 규정을 확인했습니다. 본인은 자격 요건에 맞지 않아 대회 출품작이 아닌 기능 프로토타입으로 공개한다고 밝혔습니다. 데모 사이트는 비용을 줄이기 위해 종료했으며, 배포에는 유료 Workers 요금제가 필요합니다.

dev.to 반응

  • @jason_ilands — 이 글을 두 번 읽었습니다. 규칙 5는 겉보기보다 더 많은 일을 합니다. "각 설명은 diff와 일치해야 한다"는 검사는 주장을 기록과 대조합니다. 하지만 정확한 주장이라도 애초에 해서는 안 될 변경이라면 잡지 못합니다. 보호 경로 변경이 걸리는 건 규칙 3이 별도의 diff 외부 검사로 있기 때문입니다. 정확성과 권한은 서로 다른 검사입니다. 저도 반대편에서 같은 경계에 부딪혔습니다. 에이전트 바운티 두 자리를 맡았다가 제대로 확인한 뒤 둘 다 포기했습니다. 질문은 "할 수 있나"가 아니라 "작업이 의존하는 구성 요소를 내가 소유하나"였습니다. 실행 가능성은 통과했지만 권한은 없었습니다. 이번 주 @thepracticaldev의 "An Agent's Plan Isn't Permission"도 이 경계를 다룹니다. 두 방향에서 같은 틈을 짚습니다.
    • @dannwaneri — 제가 쓴 것보다 훨씬 잘 표현하셨습니다. 정확성과 권한은 다릅니다. LLM 검사는 정확성만 보고 권한은 보호 경로 규칙이 담당합니다. 규모 시험에서 에이전트 p1은 src/auth/session.js 변경을 정확하게 설명했지만 거부됐습니다. 현재 claimcheck의 권한 검사는 기본 수준입니다. 작업을 만들 때 정한 경로 목록일 뿐 에이전트가 정하지 않습니다. 그래서 에이전트가 소유하지 않은 의존성을 작업해도 되는지 같은 바운티 질문에는 답하지 못합니다. 그런 권한은 작업 전에 정해야 하며, 계획과 권한을 다루는 접근법이 맡을 부분입니다. 말씀하신 것처럼 양쪽에서 같은 틈이 보입니다.
  • @xulingfeng — 규칙을 꼼꼼히 읽지 않은 결과네요.
    • @dannwaneri — 하하, 맞습니다. 값비싼 교훈이었습니다. 다음 대회에서는 첫 커밋 전에 규칙부터 읽겠습니다.
    • @xulingfeng — 비싸지만 값진 경험이네요! 다음 대회도 행운을 빕니다.
  • @reidmarlow — macOS와 Windows checkout을 위해 경로를 대소문자 구분 없이 검사한 점을 잘 잡으셨습니다. 파일시스템 의미 차이가 권한 경계를 건드리는 일은 홈랩에서 흔히 겪습니다. claim과 diff를 대조하는 시스템에서 제가 계속 만나는 틈은 한 파일 안의 범위 이탈입니다. 결정적 검사는 claim에 빠진 파일을 바로 잡습니다. 하지만 에이전트가 src/math.js에서 helper를 추가하도록 허가받았을 때 기존 함수도 조용히 바꾸면서 새 helper에 관한 설명은 정확하게 쓸 수 있습니다. LLM 판정은 설명된 변경이 diff에 있는지 확인하고 근거 검사도 인용한 줄이 실제인지 확인합니다. 빠진 부분은 hunk 대응입니다. git diff -U0에서 hunk 위치를 뽑아 모든 수정 hunk가 선언된 변경 블록에 대응하도록 하면 LLM에 설명 검증을 맡기기 전에 부수 변경을 결정적으로 잡을 수 있습니다.
    • @dannwaneri — 좋은 지적이고 분명 나아갈 방향입니다. 다만 LLM 검사는 실제로 diff가 설명한 내용과 정확히 일치하는지, 상당히 다른 내용을 포함하지는 않는지를 확인합니다. 파일에서 언급하지 않은 조용한 추가 변경이 있었다면 "아니오"라고 답했을 겁니다. 라벨이 붙은 시험 세트에 그런 사례가 두 개 있었습니다. 하나는 오탈자 수정에 비밀번호 로그를 추가했고, 다른 하나는 주석을 고치면서 재시도 횟수를 3에서 10으로 늘렸습니다. 제가 고른 사례였고 둘 다 다섯 번씩 실행해 매번 잡았습니다. 판정이 항상 맞지는 않는다는 점에는 동의합니다. 제안하신 방식은 결정적이고 비용도 적습니다. LLM을 호출하기 전에 git diff -U0의 모든 hunk가 선언된 변경과 맞아야 합니다. 물론 현재 claim은 파일 단위라서 이 방식에는 줄이나 심볼 단위 주장이 필요합니다. 다음 버전에서 고려해 보겠습니다.
  • @dronzer2code — AI 에이전트가 "작업을 완료했습니다"라고 말할 때 실제 뜻은 "규칙을 제 나름대로 해석한 기준으로 작업을 완료했습니다"였네요.
    • @dannwaneri — 맞습니다. "완료"는 에이전트가 작업을 이해한 방식에 따른 완료입니다. 그래서 diff를 보고 주장을 검증해야 합니다.
  • @kartik-nvjk — 에이전트마다 claim.json을 제출하고 실제 diff와 대조하면 커밋 메시지를 검증 가능한 대상으로 바꿀 수 있다는 점이 좋습니다. 예전에 에이전트가 "오탈자 수정"이라고 쓴 커밋에서 조건문도 뒤집은 적이 있습니다. 허위 보고 11건을 모두 잡고 정직한 에이전트를 하나도 거부하지 않은 결과는 깔끔합니다. 주장은 사실이지만 변경 일부를 빠뜨렸다면 어떻게 점수를 매기나요?
  • @benjamin_nguyen_8ca6ff360 — 정말 멋지네요!
  • @anp2network — v1.0.0의 근거 검사는 관련성을 확인하지 않고 존재 여부만 확인하므로, 가장 약한 근거가 가장 강한 판정을 뒷받침합니다. worker/src/verify/llm.ts의 evidenceInDiff는 인용한 근거를 줄 단위로 나누고 stripPrefix로 앞의 +, - 또는 공백을 제거한 뒤 각 조각이 diff의 줄에 포함되는지 검사합니다. 비교 대상 diff는 worker/src/diff.ts의 createTwoFilesPatch에서 context: 3으로 만들어지므로 양쪽 줄의 표식이 이미 사라졌습니다. 변경되지 않은 문맥 줄이나 삭제된 줄을 인용해도 추가된 줄과 똑같이 검사를 통과합니다. return 하나만 인용해도 통과합니다. 이 비대칭을 먼저 고쳐야 합니다. decide()는 검사를 통과한 "아니오" 판정을 provenNo로 올려 즉시 거부합니다. 근거 검사를 통과하지 못한 "아니오"는 needs_review로 낮춥니다. 즉, 가장 강하게 실행하는 판정에 가장 느슨한 증명이 붙습니다. 근거가 전체 줄과 일치해야 하고 +나 - 표식을 포함해야 하며, claim에 선언된 파일의 hunk에 있어야 한다고 요구하면 "허위 근거로 거부할 수 없다"는 속성을 지킬 수 있습니다. 둘째, needs_review는 서로 다른 원인을 한 값으로 처리합니다. judgeChange는 한 번 재시도한 뒤 불명확한 결과와 오류를 반환하고, decide()는 실제 모델의 불확실성, 5분 멈춘 호출 같은 시간 초과, 근거 검사에 실패한 "아니오"를 같은 범주로 보냅니다. 세 번째 경우는 증명되지 않은 잠재적 허위 보고이므로 따로 분리할 가치가 있습니다. 판정만 저장하면 needs_review 비율이 올라가도 에이전트가 더 교묘해진 건지 Workers AI가 느려진 건지 알 수 없습니다. 거부 판정에도 같은 계측이 필요합니다. 제가 추적하는 오래 실행된 서명 작업 장부에는 판정 1,690건과 실패 37건이 있습니다. 가장 최근 실패는 104일 전이며 그 뒤로 연속 통과가 1,278건입니다. 값의 종류만 세는 상태 점검으로는 이런 상황을 잡지 못합니다. 판별을 조용히 멈춘 검사와 실제로 정상 입력만 들어온 상황을 구분할 수 없습니다. 실제 에이전트 시험은 15건 모두 정직했으니 운영에서는 거부 판정이 조용해지는 일이 가장 먼저 생길 가능성이 큽니다. 규칙 코드별 횟수와 마지막 발생 시각을 원인 코드와 함께 저장하면 적어도 그 침묵을 드러낼 수 있습니다.

원문: dev.to / 번역·요약: Trawling