← Notes

테스트가 통과했다고 완료는 아니다

AI 에이전트 생태계 구축기· 3 / 10
  1. 1 — 완료는 실측으로만 말한다
  2. 2 — 같은 함정, 세 번째는 다른 얼굴로
  3. 3 — 테스트가 통과했다고 완료는 아니다
  4. 4 — 속 빈 강정 — 함수는 있는데 왜 안 불렸나
  5. 5 — 하루 만에 지은 나의 두 번째 뇌
  6. 6 — 조직보다 자산 먼저 — 두 번 실패하고 남은 3원칙
  7. 7 — AI의 판단력은 복리로 자라지 않는다
  8. 8 — 큐는 비우고, raw는 지우지 않는다
  9. 9 — 하나의 비서를 세 개의 뇌로 나눈 이유
  10. 10 — 허위 적격 0 — 애매하면 통과시키는 필터

“테스트 통과”는 함수가 격리된 조건에서 옳은 값을 낸다는 증명이고, “완료”는 그 함수가 실사용 경로에서 실제로 호출된다는 증명이다.

이 둘은 서로 다른 명제인데, 하나를 확인했다고 나머지 하나도 확인됐다고 착각하기 쉽다. agent-system(지크의 개인 AI 에이전트 생태계 프로젝트)의 자가학습·검증 기능을 만들면서 이 차이를 두 번 데어서 알았다.

간판 기능을 결정론 함수 셋으로 설계하다

agent-system의 간판 차별점은 자가학습과 적대적 검증으로 복리 성장한다는 것이다. 자가학습은 명시 교훈(A)·자동 감지(C)·승격(SL-02)·메타루프(SL-04) 4층, 검증은 검토(A)·게이트(B)·백그라운드 루프(VF-06) 3종으로 설계됐다. 이 중 승격·메타루프·검증루프 세 함수가 이번 사건의 주인공이다.

  • 승격 로직(promote.mjs): 같은 실수가 3회 이상 반복돼야 승격 후보로 뽑는다. 한 번의 우연을 규칙으로 굳히는 과잉일반화를 막는 임계값이다.
  • 메타루프(learning-meta.mjs): 검출율 시계열의 기울기를 계산해, 최근 검출율이 목표치(0.9) 밑이면 합격선을 0.05씩 올린다.
  • 검증루프(verify-loop.mjs): 작업자와 검증자를 최대 3라운드까지 반복시키고, 그래도 통과 못 하면 “미해결”로 에스컬레이션한다.

세 함수 모두 LLM 호출 없이 입력에 대해 항상 같은 출력을 내는 결정론 함수다. 코드 자체는 잘 짜였다 — 설계 결함이 아니다.

골든 픽스처가 낸 PASS

세 함수의 검증 방식은 test-spec이었다. 골든 픽스처(정답이 미리 적힌 표본 데이터)를 함수에 직접 주입해 출력이 정답표와 일치하는지 대조하고, 판정은 항상 이 대조로만 한다(“자가채점 금지”). 이 방식으로 세 함수 모두 PASS를 받았고, 교훈 유무에 따라 haiku 실행자의 오류율이 달라지는지까지 잰 SL-03-EFFECT 항목도 PASS를 받았다. TASK-007~011이 2026-07-03 하루에 몰려 만들어졌고, work-log는 이 시점을 “1차 완성”으로 기록했다.

grep 한 줄이 갈라놓은 것

2026-07-07, 검증 방법 자체를 바꿔 재확인했다. 골든 픽스처 대조 대신, 각 함수가 런타임 경로(server.ts·drop-core·텔레그램/터미널 리스너·백스테이지 잡)에서 실제로 호출되는지를 확인하는 방식이다.

함수/모듈골든 픽스처 측정런타임 호출처
승격 로직 (promote.mjs)PASS0건
메타루프 (learning-meta.mjs)PASS0건
검증루프 (verify-loop.mjs)PASS0건
그 외 7개 모듈(verify-roi.mjs·verify-run.mjs·blog-draft.mjs·token-guard.mjs·dev-modes.mjs·lint-links.mjs·scan-skill.mjs)PASS0건

세 함수를 실제로 부르는 코드는 딱 세 곳, 그것도 각자의 측정 스크립트 자신이었다.

import { promotionCandidates, shouldPromote, PROMOTE_THRESHOLD } from './promote.mjs'; // sl02-measure.mjs
import { trendSlope, adjustThreshold } from './learning-meta.mjs'; // sl04-measure.mjs
import { runVerifyLoop } from './verify-loop.mjs'; // vf06-measure.mjs

즉, 이 함수들을 부르는 코드는 “자신을 검증하기 위해 자신을 부르는 코드”뿐이었고, 실제 서비스 경로에는 한 줄도 배선돼 있지 않았다. 이 기준으로 08-honest-status.md는 기능 전체를 다시 나눴다. 던지기→분류→라우팅·지원금 실 API 매칭·자동 브리핑·서재 컴파일·텔레그램 상시 채널은 ✅(실제로 도는 것), 자가학습 심화·검증 루프·비용감시·개발팀 모드·링크 Lint·스킬 스캐너는 🔴(껍데기)로 갈렸다.

배선은 됐는데 데이터가 없는 층도 있었다

같은 재검증에서 반대 사례도 나왔다. 자가학습 기본 기능(명시 교훈·자동 교정 감지)은 실제로 배선돼 있었다 — server.ts가 교정을 감지해 교훈을 쓰고, drop-core.mjs가 그 교훈을 비서 프롬프트에 주입한다.

  • 배선은 있다: 감지→저장→주입 경로가 코드로 존재한다.
  • 데이터가 없다: memory/lessons/에 저장된 교훈은 0건이다. 배선됐지만 실사용된 적이 없어 주입할 것도 없고, 순효과도 0이다.
  • 정합 구멍도 있다: 교훈을 캡처하는 로직은 옛 server.ts(앱 채널)에만 있다. 채널중립 엔진은 주입만 담당해, 텔레그램·터미널로 들어온 교정은 애초에 교훈으로 저장되지 않는다. 3채널 통합 리팩터 때 캡처 로직이 따라오지 못한 흔적이다.

SL-03-EFFECT의 PASS는 이 간극을 정확히 가리는 지점이었다. 교훈 전용 픽스처로 잰 “교훈 유무에 따른 오류율 차이”는 함수 수준 증명이지, “실제로 교훈이 쌓여 시스템이 성장한다”는 운영 수준 증명이 아니다. 운영 수준에선 교훈이 0건이니 효과도 0이다. 측정 PASS와 실동작 사이의 이 간극이 완료 오보의 정확한 메커니즘이다.

완료의 정의를 고치다

지크가 두 번 낙담한 근본 원인은 악의가 아니라 “완료”의 정의가 잘못됐기 때문이다. test-spec의 통과 기준은 골든 픽스처를 주입해 함수 출력이 정답표와 맞는지 보는 것이었다. 이건 함수가 격리 조건에서 올바른 값을 낸다는 것만 증명한다. 그 함수가 실제 파이프라인에 배선돼 실사용되는지는 test-spec이 보지 않았다. 배선 안 된 껍데기가 PASS를 받아 1차 완성에 합산됐다 — PASS는 실동작이 아니었다.

해결은 검증 방법을 하나 추가하는 것이었다.

  • 기존: 함수 출력 검증(test-spec, 골든 픽스처 대조).
  • 추가: 런타임 배선 확인(grep으로 실사용 경로 호출 여부 확인).
  • 판정 규칙: 측정 스크립트만 호출하면 껍데기.

즉, 테스트가 통과한다는 것은 함수가 격리 조건에서 옳은 값을 낸다는 것만 증명하고, 그 함수가 실제로 호출되는가는 완전히 다른 질문이다. 어떤 정교한 골든 픽스처 대조도 이 질문에는 답하지 않는다. 런타임 배선 확인은 사후 감사가 아니라 완료 정의의 일부여야 한다. 그리고 배선됐다고 다 끝난 것도 아니다 — 존재한다, 배선됐다, 쓰이고 있다는 세 개의 다른 질문이고, 그중 하나만 확인하고 나머지를 가정하는 순간 완료는 다시 오보가 된다.

이 원칙은 agent-system 한 프로젝트에 갇힌 교훈이 아니다. 어떤 코드베이스든 “테스트가 초록불이다”와 “기능이 켜져 있다” 사이에는 같은 틈이 열릴 수 있고, 그 틈을 닫는 방법은 결국 같다 — 완료 정의에 실사용 경로 확인 한 줄을 박아 넣는 것이다.

목차