← Notes

허위 적격 0 — 애매하면 통과시키는 필터

AI 에이전트 생태계 구축기· 10 / 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 — 애매하면 통과시키는 필터

정부지원 공고를 감지해 알림을 보내는 운영 도구 gov-radar를 만들면서, 결정론 코드와 LLM 판단이 한 파이프라인에 같이 놓이는 지점을 만났다. 수집한 공고를 프로필과 대조해 적격만 추리는 과정에서, “이 대조를 어디까지 코드가 확정하고 어디서부터 판단을 넘겨야 하나”라는 질문에 부딪혔다.

결정론 필터는 “적격이다”를 증명하지 않는다. 명백히 부적격이라는 근거가 나올 때만 거르고, 조금이라도 애매하면 무조건 통과시킨다.

이 원칙을 zero false positives(허위 적격 0)라 부른다. gov-radar의 src/match.mjs가 이 원칙을 코드로 옮긴 결과물이다.

결정론 하드필터 — 책임과 위임의 경계

파일 헤더가 이 모듈의 책임 범위를 미리 좁혀 선언한다. 책임과 위임의 경계는 이렇다.

  • 책임진다: 연령·지역·취업상태 3축에서 명백한 부적격이라는 증거가 나올 때만 거절 사유 문자열을 반환한다.
  • 넘긴다: 세 조건 중 하나라도 비어 있거나 애매하면 무조건 통과시킨다(null). 소프트판정(자격 텍스트를 읽는 LLM 판단)은 이 모듈에 없다 — 뒷단(헤르메스, 크론 잡 에이전트)의 몫으로 코드 밖에 남겨뒀다.

그 경계선의 코드 쪽 마지막 관문이 아래 결정론 하드필터다.

src/match.mjs
export function hardCheck(profile, f) {
  if (f.ageLmt && f.minAge && f.maxAge && (profile.age < f.minAge || profile.age > f.maxAge))
    return `연령 요건 만 ${f.minAge}~${f.maxAge}세 — 프로필 ${profile.age}세가 벗어남`;
  if (f.regionCodes.length && profile.region_code && !f.regionCodes.includes(profile.region_code))
    return `거주지역 한정 정책 — 프로필 지역(${profile.region} ${profile.region_code}) 미포함`;
  if (f.jobCds.length && !f.jobCds.includes("0013010") && profile.employment_code && !f.jobCds.includes(profile.employment_code))
    return `취업상태 요건 [${f.jobCds.map((c) => JOB_CODE[c] ?? c).join(", ")}] — 프로필(${JOB_CODE[profile.employment_code] ?? profile.employment_code}) 불포함`;
  return null;
}

세 체크 모두 조건 여러 개가 동시에 참일 때만 거절 문자열을 반환하는 연쇄 AND로 짜여 있다.

  • 연령: 제한 플래그가 있고, 상·하한이 둘 다 존재하고, 나이가 그 범위를 실제로 벗어날 때만 거절한다. 상한이나 하한 중 하나라도 비어 있으면 이 줄은 작동하지 않는다.
  • 지역: 한정 코드 목록이 비어 있지 않고, 프로필에 지역 코드가 있고, 그 코드가 목록에 없을 때만 거절한다. 빈 목록은 “전국”으로 해석한다.
  • 취업상태: 목록이 비어 있지 않고, “제한없음”을 뜻하는 코드(0013010)가 없고, 프로필 재직코드가 목록에 없을 때만 거절한다. 목록에 “제한없음”이 하나라도 섞여 있으면 이 줄은 통째로 무력화된다.

데이터가 하나라도 비어 있거나 애매하면 어느 줄도 걸리지 않고 함수는 null을 반환한다. 마감일을 현행·만료로 가르는 computeStatus도 같은 결정론 층에서 “계산으로 결정 가능한 것은 코드가 확정한다”는 원칙을 공유하는 이웃 함수다.

여기서 null을 어떻게 읽느냐가 핵심이다. null은 “적격이다”가 아니라 **“코드가 보기엔 명확한 결격 사유를 못 찾았다”**는 뜻이다. 즉, null은 판정의 완료가 아니라 판단 위임의 신호다.

비유 — 공항 검색대는 통과를 무해 증명으로 쓰지 않는다

공항 검색대는 위험물이라는 증거가 나올 때만 막는다. 가방 안에 애매한 형체가 잡히면 그 자리에서 무해하다고 단정하지 않고 정밀검사로 넘긴다. 검색대 통과가 “무해함의 증명”은 아니다 — 단지 “명백한 위험물을 발견하지 못했다”는 뜻일 뿐이다. 결정론 하드필터도 정확히 이 구조다. 통과는 적격 판정이 아니라 명백한 결격을 발견하지 못했다는 뜻이고, 애매한 case는 뒷단의 정밀검사(소프트판정)로 넘어간다.

실측이 확인한 비대칭 — 표본 만료 누수 0

  • 온통청년: 2,648건 중 만료 1,042건 컷, 하드필터가 다시 1,373건 컷 → 최종 후보 233건. 표본 만료 누수 0으로 확인됐다.
  • 마이홈: 270건 중 서울 접수중 6건(유효 1건)만 남았다.

이 233건이 곧바로 “적격”으로 확정되진 않는다. 통과한 뒤에도 헤르메스가 다시 소프트판정을 하는데, 이때도 같은 원칙이 반복된다 — 불확실하면 무조건 [확인 필요] 태그를 붙여 발송한다. 실패 모드를 허위 적격(위험한 실패)이 아니라 딱지 붙은 도착(안전한 실패)으로 강제하는 안전장치다. 결정론 층에서 시작된 비대칭이 다음 층까지 끊기지 않고 이어진다.

즉, 정직함은 거절하지 않음의 절제에 있다

즉, 이 설계의 정직함은 애매한 걸 다 통과시키는 관대함이 아니라, 확신이 서지 않으면 거절하지 않는다는 절제에 있다. “허위 적격 0”이 달성되는 이유는 코드가 똑똑해서가 아니라, 코드가 자기 확신의 한계를 정확히 알고 그 밖을 다음 층으로 넘기기 때문이다.

목차