어제 하루 동안 저희 코드에서 문서에 적힌 규칙과 실제 코드가 어긋난 곳을 다섯 군데 찾았습니다. 전부 AI에게 맡겨서 작업한 부분이었고, 전부 "완료했습니다"라는 보고를 받은 뒤였습니다.
가장 뼈아팠던 건 이겁니다. 저희 문서 두 곳에 /me/threads 엔드포인트를 써야 한다고 분명히 적혀 있었는데, 정작 게시 엔진만 옛날 주소를 쓰고 있었습니다. 그래서 Threads 게시가 한 번도 성공한 적이 없었습니다. 아무도 몰랐고요.
그래서 이 주제를 정리하게 됐습니다. AI 에이전트의 완료 보고를 어떻게 검증할 것인가.
AI는 왜 안 하고 했다고 할까
마침 이 문제를 구조적으로 다루는 글이 있어서 잘 읽었습니다. Dr. Cha Tech Blog의 unlazy 스킬 가이드입니다. 원인을 두 가지로 짚습니다.
- 주의력 분산 — 대화창이 채워질수록 모델이 초반 지시를 놓칩니다.
- 비용을 아끼려는 성질 — 토큰과 계산량을 줄이려는 보상 구조가 내재돼 있습니다.
그 결과가 "모든 요청 작업을 마쳤습니다"라는 보고와, 실제로는 건너뛴 파일들입니다. 이걸 게으름(Laziness), 조기 종료(Premature Completion)라고 부릅니다.
여기서 중요한 건 프롬프트로는 안 고쳐진다는 점입니다. "빠짐없이 다 해줘"라고 아무리 강조해도, 그 문장 역시 길어진 대화 속 한 줄일 뿐입니다. 저희가 다섯 번 놓친 것도 전부 "문서에 적어둔" 규칙이었습니다.
이런 소식, 남들보다 먼저 받으실 분?
직접 돌려보고 되는 것만 골라 매주 보내드립니다. 광고성 하이프는 거릅니다.
unlazy의 답 — 텍스트 말고 명령어로 확인한다
unlazy는 개발자 Leonxlnx가 공개한 오픈소스 스킬입니다. 접근이 명확합니다. 완료를 텍스트로 받지 않고, 실제 명령어를 돌려서 받습니다.
① Depth Tree — 작업을 잘게 찢는다
과제를 트리 구조로 분해하고, 각 조각마다 깨끗한 컨텍스트와 독립된 에이전트를 붙입니다. 대화가 길어져서 집중력이 떨어지는 문제 자체를 없애는 방식입니다.
② Gates — 게이트를 통과해야 다음으로
GATES.md라는 원장 파일에 목표·검증 명령어·예상 출력값·실제 증거를 적습니다. AI가 스스로 체크박스를 채워도 소용없습니다. 검증 스크립트가 명령어를 직접 실행해서 예상 출력이 나와야만 게이트가 열립니다.
표로 보면 차이가 분명합니다.
| 구분 | 보통의 방식 | 게이트 방식 |
|---|---|---|
| 완료 판정 | "완료했다"고 적으면 인정 | 명령어 실행 결과로 판정 |
| 컨텍스트 | 계속 누적 → 집중도 저하 | 조각마다 독립 |
| 누락 대응 | 어려운 과제는 스킵 가능 | 미통과 시 다음 단계 진입 불가 |
npx skills add Leonxlnx/unlazy
# 사용
/unlazy tree 2-3 [소규모 모듈 구현]
/unlazy tree 5 [대규모 리팩토링]
순차 실행은 대형 작업에서 3~4시간이 걸리는데, Claude Code의 서브 에이전트 병렬 처리를 붙이면 1시간 안쪽으로 줄어든다고 합니다. 자세한 설정은 원문 가이드를 보세요.
저희는 아직 unlazy를 실제 프로젝트에 붙여보지 않았습니다. 그래서 이 글에서는 써봤다고 말하지 않겠습니다. 대신 같은 문제를 저희가 어떻게 풀고 있는지를 적겠습니다 — 그건 매일 돌리고 있는 것이라 자신 있게 말할 수 있습니다.
저희가 쓰는 방법 — 규칙을 문서가 아니라 코드에 둔다
저희가 얻은 결론은 한 줄입니다.
문서에만 적힌 규칙은 지켜지지 않습니다. 기계가 검사해야 합니다.
같은 얘기를 반대로 하면, 규칙을 지키게 하고 싶으면 그 규칙을 어겼을 때 빨간불이 켜지는 스크립트를 만들어야 한다는 뜻입니다. 게이트와 같은 발상이고, 훨씬 소박한 형태로 만들 수 있습니다.
예시 — 광고 고지 검사기
저희는 제휴 링크가 들어간 글에 공정위 고지를 넣어야 합니다. 문서에 규칙을 적어뒀는데, 어느 날 확인해보니 양쪽으로 다 어긋나 있었습니다. 제휴 링크가 하나도 없는 글 두 편에 고지가 붙어 있었고(허위표시도 위반입니다), 반대 경우도 있었습니다.
그래서 60줄짜리 스크립트를 만들었습니다. 핵심은 이 부분입니다.
// 대가를 받는 링크의 표시. 새 제휴처가 생기면 여기 추가한다.
const AFFILIATE = /3ha\.in|coupa\.ng|\/invitation\/|[?&]ref=|referral/i;
// 화면에 실제로 보이는 고지만 센다. CSS 규칙만 남아 있는 건
// 아무것도 표시하지 않으므로 위반이 아니다 — 이걸 세면 오탐이 4건 난다.
const SHOWN = /<p class="ad-disclosure"/;
function checkBlog(slug) {
const html = fs.readFileSync(path.join(BLOG, slug, "index.html"), "utf8");
const shown = SHOWN.test(html);
const aff = AFFILIATE.test(html);
if (shown && !aff) problems.push({ slug, why: "고지가 있는데 제휴 링크가 없다 (허위표시)" });
if (aff && !shown) problems.push({ slug, why: "제휴 링크가 있는데 고지가 없다" });
}
// 불일치가 있으면 종료코드 1 — CI·훅에 걸기 좋게
process.exit(problems.length ? 1 : 0);
보시면 알겠지만 특별할 게 없습니다. 정규식 두 개와 if 두 줄입니다. 중요한 건 이게 사람의 기억이 아니라 종료코드로 판정된다는 점입니다. 게시 전에 한 번 돌리면 끝입니다.
검사기를 만들 때 배운 것
위 코드의 주석 한 줄을 눈여겨봐 주세요. "이걸 세면 오탐이 4건 난다". 처음 만들었을 때 CSS 규칙만 남아 있는 파일까지 위반으로 잡았습니다. 검사기가 틀리면 사람이 검사기를 무시하기 시작합니다. 그러면 없느니만 못합니다.
그래서 새 검사기를 만들면 반드시 이 순서로 확인합니다.
- 지금 통과해야 하는 것들에 돌려서 0건이 나오는지 (오탐 확인)
- 일부러 어긴 것을 만들어서 잡히는지 (미탐 확인)
- 메시지에 어디가 왜 틀렸는지가 적히는지
3번이 특히 중요합니다. "검사 실패"만 뜨는 검사기는 결국 안 쓰게 됩니다.
이 글을 쓰다가 바로 겪었습니다. 위 코드를 본문에 붙이고 검사기를 돌렸더니 이 글이 위반으로 잡혔습니다. 코드 안에 적힌 리퍼럴 패턴 문자열을 검사기가 진짜 제휴 링크로 읽은 겁니다. 처음엔 코드 블록만 검사에서 뺐는데 그것도 부족했습니다 — 본문에서 그 단어를 한 번 더 언급하니 또 걸리더군요. 결국 HTML 전체가 아니라 href 값만 보도록 바꿨습니다. 글자로 적는 건 링크가 아니니까요. 그리고 위의 세 단계를 다시 돌렸습니다 — 65개 글에서 오탐 0건, 일부러 어긴 파일은 양방향 다 잡힙니다.
어제 이 방식으로 잡힌 것들
문서 두 곳에 올바른 엔드포인트가 적혀 있었지만 엔진만 안 고쳐져 있었습니다. 문서-코드 대조를 스크립트로 돌리면서 발견했습니다.
파서가 같은 이름의 구역을 두 번 읽어서, 나중 것이 앞의 것을 덮어쓰고 있었습니다. 왕복 테스트(저장 → 다시 읽기 → 비교)를 붙이니 바로 나왔습니다.
카드마다 유형(표지·본문·마무리)을 붙이는 코드가 어디에도 없어서, 여섯 장이 전부 본문으로 그려지고 있었습니다. 배지와 강조 문구가 통째로 빠진 채로요.
세 개 다 "완료했습니다"를 받은 작업이었습니다. 그리고 세 개 다 사람이 눈으로 봐서는 못 찾았을 것들입니다. 결과물이 그럴듯하게 나왔으니까요.
정리 — 오늘 바로 할 수 있는 것
unlazy처럼 잘 만들어진 프레임워크를 붙이는 것도 좋은 선택입니다. 다만 그 전에, 규모에 상관없이 지금 당장 할 수 있는 게 있습니다.
- AI에게 작업을 시킬 때 검증 명령어를 같이 정하세요. "다 하면
npm test를 돌려서 출력을 붙여줘"가 "빠짐없이 해줘"보다 백 배 낫습니다. - 지키고 싶은 규칙이 있으면 검사 스크립트로 만드세요. 30줄이면 충분합니다. 문서에 적는 건 그다음입니다.
- 대화가 길어지면 새로 시작하세요. 컨텍스트가 쌓일수록 초반 지시가 흐려지는 건 구조적인 문제라, 참고 계속하는 게 답이 아닙니다.
결국 개발자의 일이 코드를 타이핑하는 것에서 AI가 내놓은 결과를 어떻게 검증할 것인가로 옮겨가고 있다는 이야기입니다. 저희도 그렇게 바뀌고 있고요.
그대로 가져가세요
위에서 쓴 검사기 뼈대입니다. 프로젝트에 맞게 정규식만 바꾸면 바로 돌아갑니다.
import fs from "node:fs";
import path from "node:path";
const TARGET_DIR = "./content"; // 검사할 폴더
const MUST_HAVE = /지켜야 할 패턴/; // 있어야 하는 것
const TRIGGER = /이 조건일 때만/; // 위 규칙이 필요해지는 조건
const problems = [];
for (const name of fs.readdirSync(TARGET_DIR)) {
const file = path.join(TARGET_DIR, name);
if (!fs.statSync(file).isFile()) continue;
const text = fs.readFileSync(file, "utf8");
const need = TRIGGER.test(text);
const has = MUST_HAVE.test(text);
// 양방향으로 본다 — 빠뜨린 것도, 필요 없는데 붙인 것도 둘 다 문제다
if (need && !has) problems.push(`${name}: 조건에 맞는데 규칙이 빠졌습니다`);
if (!need && has) problems.push(`${name}: 조건이 아닌데 규칙이 붙어 있습니다`);
}
if (problems.length) {
console.log(`불일치 ${problems.length}건`);
for (const p of problems) console.log(" " + p);
process.exit(1); // 종료코드로 말한다
}
console.log("불일치 없음 ✓");
쓰는 법: node check-rule.mjs — 게시나 배포 전에 한 번 돌립니다. 종료코드가 1이라 package.json의 prepublish나 git 훅에 그대로 걸 수 있습니다.
카드로 보기
실측 위주 AI 뉴스레터 — 직접 돌려보고 되는 것만 골라 매주 보내드립니다.
출처 · unlazy 스킬 소개와 설치·설정은 Dr. Cha Tech Blog의 "AI의 환각과 게으름을 원천 차단하는 unlazy 스킬 완벽 가이드"를 참고했습니다. 스킬 원본은 github.com/Leonxlnx/unlazy입니다.