정정합니다 — 라우터가 41%를 안 남긴 게 아니라 내가 못 읽은 거였다 2026

이 글은 같은 날 오전에 올린 내용을 정정한 버전입니다. 원래 이 자리에는 “내 라우팅 로그가 처리했다고 선언한 70건 중 29건, 41.4%를 기록하지 않았다”고 적혀 있었습니다. 틀렸습니다. 실제 누락은 0건입니다. 70건이 전부 기록돼 있었고, 못 읽은 건 제가 짠 감사 스크립트였습니다.

숫자를 고치는 걸로 끝나는 정정이 아니라 결론이 반대로 뒤집히는 정정이라, 원문을 지우는 대신 무엇을 어떻게 틀렸는지 남깁니다. 오판 경로 자체가 이 글에서 제일 쓸모 있는 부분이라고 생각합니다.

원래 하려던 일

TypeSafe AI의 System One 모델 Jev를 제 에이전트 파이프라인에 넣을지 검토하는 중이었습니다. 공식 모델 문서에 이런 문장이 있습니다. “Other languages, including CJK scripts, are handled but not equally well; test on your own content before relying on Jev for a non-English workload, and pay close attention to Confidence when routing.”

한국어는 CJK고 제 파이프라인 입력은 거의 전부 한국어입니다. 그래서 “내 콘텐츠로 먼저 테스트하라”가 선택이 아니라 전제 조건이었고, 테스트하려면 정답셋이 필요했습니다.

재료는 있다고 봤습니다. 제 볼트에는 아이폰 미리알림을 읽어 일정, 할 일, 데일리 로그, 위시, URL 요약, 잔류 중 하나로 보내는 라우팅이 매일 돕니다. 결과가 마크다운으로 남으니 사람이 승인한 라벨이 달린 데이터인 셈이었습니다. 12일치 로그를 긁어서 세어 봤고, 거기서 사고가 났습니다.

무엇을 잘못 셌나

캡처 로그는 헤더에 “미리알림 반입 (N건)”을 선언하고 그 아래에 항목이 붙습니다. 저는 선언한 N과 -로 시작하는 불릿 줄 수를 비교했습니다. 70건 선언에 41줄. 29건이 비는 것처럼 보였습니다.

문제는 이 로그에 규약이 셋 있는데 제가 하나만 알고 있었다는 겁니다.

규약 1. 처리된 항목은 불릿이 결과 줄로 교체된다. URL 요약이 끝나면 - <https://...> 줄이 사라지고 그 자리에 > URL 요약 완료 → [[260910_[요약] ...]]가 들어갑니다. 원문을 남기고 결과를 덧붙이는 게 아니라 교체입니다. 그래서 처리가 잘 끝난 항목일수록 제 카운터에서 사라졌습니다. 가장 정상적으로 처리된 건들이 “누락”으로 잡힌 겁니다.

규약 2. 결과 줄 하나가 여러 건을 대표할 수 있다. 같은 URL이 중복으로 들어오면 (중복 2건 병합)이 붙은 결과 줄 하나로 합쳐집니다.

규약 3. 정보 없는 메모는 블록 단위로 병합되고 그 사실이 적혀 있다. 받아쓰기가 실패해 제목이 “새로운 미리 알림”뿐인 항목들은 한 줄로 합치고, 하단에 “정보 없는 중복 메모 3건은 1건으로 병합해 잔류”라고 남깁니다.

세 규약을 다 반영해서 다시 세면 이렇게 됩니다.

날짜 선언 불릿 결과 줄 병합 보정 집계 차
08-28 1 1 0 0 1 0
08-29 1 1 0 0 1 0
08-31 6 1 3 2 6 0
09-01 4 4 0 0 4 0
09-02 4 1 3 0 4 0
09-10 24 9 13 2 24 0
09-15 7 1 3 3 7 0
09-17 1 1 0 0 1 0
09-18 5 5 0 0 5 0
09-19 2 2 0 0 2 0
09-20 12 12 0 0 12 0
09-21 3 3 0 0 3 0
합계 70 41 22 7 70 0

12개 파일 전부 차이가 0입니다. 로그는 완전했습니다.

코드를 봤으면 5분 만에 끝날 일이었다

정정의 계기는 다음 편을 쓰려고 반입 스크립트를 연 것이었습니다. reminders-capture.sh의 파일 쓰기 부분은 이렇게 생겼습니다.

COUNT="$(printf '%s\n' "$ITEMS" | wc -l | tr -d ' ')"
...
{
  echo "## ${NOW} 미리알림 반입 (${COUNT}건)"
  printf '%s\n' "$ITEMS" | sed 's/^/- /'
  echo ""
} >> "$CAPTURE"

헤더의 ${COUNT}와 아래 불릿 줄이 같은 $ITEMS 변수에서 나옵니다. 헤더가 24라고 적었으면 불릿도 24줄 나갑니다. 구조적으로 어긋날 수가 없는 코드입니다.

즉 “선언과 기록이 다르다”는 관찰이 나왔을 때, 가능한 원인은 처음부터 둘 중 하나였습니다. 쓴 뒤에 누가 고쳤거나, 내가 잘못 세고 있거나. 스크립트를 30초만 봤어도 앞쪽이 아니라는 게 나왔고, 그러면 자동으로 뒤쪽이 남습니다.

놓친 단서가 이미 출력에 있었다

더 뼈아픈 건 제 감사 스크립트가 단서를 이미 뱉고 있었다는 점입니다. 실행하면 마지막 줄에 이렇게 나왔습니다.

기록 누락률: 41.4% (29/70)
라벨 가능한 판정: 47건

항목이 41건인데 판정이 47건입니다. 41건에서 47개의 판정이 나올 수 없습니다. 이 불일치가 곧 “결과 줄이라는 게 따로 있다”는 신호였는데, 저는 조사하는 대신 원고에 이렇게 적었습니다. “한 항목이 여러 줄 결과를 남기는 경우가 있어서 생기는 차이입니다.”

그럴듯하게 들리는 추측 한 문장으로 모순을 덮은 겁니다. 숫자 두 개가 안 맞을 때 설명을 만들어내는 건 조사가 아닙니다. 이 문장을 쓰는 순간이 멈춰야 할 지점이었습니다.

고친 스크립트

capture_log_audit.py를 세 규약을 반영해 다시 썼습니다.

python3 .claude/scripts/capture_log_audit.py
python3 .claude/scripts/capture_log_audit.py --json

핵심은 불릿만 세지 않는 것입니다. 결과 줄을 세되 라우팅 완료, 보류 사유 같은 블록 메타 줄은 빼고, 병합 주석을 두 종류로 나눠 처리합니다.

# 규약 2: 결과 줄에 붙은 항목별 병합은 합산한다.
inline_merge = sum(int(n) - 1 for q in results for n in INLINE_MERGE_RE.findall(q))

# 규약 3: 블록 요약의 병합은 같은 사건이 두 군데 적힐 수 있으므로 최댓값만.
block_ns = [int(n) for s in block_sources for n in BLOCK_MERGE_RE.findall(s)]
block_merge = (max(block_ns) - 1) if block_ns else 0

규약 3에서 합산이 아니라 최댓값을 쓰는 이유는 실제로 당했기 때문입니다. 9월 15일 로그는 같은 병합 사건을 불릿 주석과 하단 라우팅 줄 양쪽에 적어 뒀는데, 처음엔 그걸 합산해서 7건짜리 블록을 10건으로 셌습니다. 정정하는 스크립트에서 또 같은 종류의 실수를 한 셈입니다.

그래서 정답셋은 만들 수 있다

원래 결론은 “표본이 편향돼서 정답셋을 만들 수 없다”였습니다. 반대입니다. 12일치 70건이 목적지 라벨과 함께 온전히 남아 있습니다. 처리 결과가 붙은 건이 22건(URL 요약 20, 위시 1, 데일리 로그 1)이고 나머지는 잔류 상태입니다.

다만 정답셋으로 쓰기엔 쏠림이 심합니다. 결과가 달린 22건 중 20건이 URL 요약인데, 이건 “http로 시작하는가”라는 규칙으로 갈리는 판단이라 애초에 모델이 필요 없는 구간입니다. Jev든 LLM이든 판단이 필요한 건 자유 텍스트 메모 쪽인데 거기 표본이 얇습니다. 이건 표본이 없어서가 아니라 라벨이 한쪽에 몰려서 생기는 문제라, 원래 글이 주장한 “기록이 사라졌다”와는 원인도 해법도 다릅니다.

해법도 단순해집니다. 기록 구조를 고칠 필요가 없으니 자유 텍스트 메모 구간의 판정이 쌓일 때까지 기다리면 됩니다. 목적지별 최소 20건을 기준으로 잡았고, 지금 속도면 몇 주가 걸립니다.

남는 교훈

로그 규약을 모르는 채로 그 로그를 감사하는 스크립트를 짜면, 스크립트는 조용히 틀린 숫자를 냅니다. 에러도 안 나고 경고도 안 뜹니다. 41.4%라는 숫자는 소수점 한 자리까지 찍혀 나와서 오히려 정밀해 보였습니다.

생각해 보면 이 로그 형식 자체는 잘못된 게 아닙니다. 불릿을 결과로 교체하는 건 사람이 파일을 열었을 때 “아직 안 끝난 건”만 불릿으로 남아 한눈에 보이게 하려는 설계입니다. 읽는 사람 입장에서는 좋은 선택입니다. 다만 그 대가로 기계가 세기에는 불친절해집니다. 원본과 결과가 같은 자리를 놓고 경쟁하니, 규약을 모르면 둘 중 하나만 보게 됩니다. 사람이 읽을 로그와 기계가 셀 로그는 요구가 다르고, 한 파일에 둘을 겹쳐 놓으면 이런 종류의 오독이 생깁니다.

그리고 측정 대상을 만든 코드를 먼저 읽는 게 순서였습니다. 저는 산출물만 보고 세다가 틀렸고, 생산 코드를 연 순간 5분 만에 답이 나왔습니다. 로그를 데이터로 쓰기 전에 그 로그를 쓰는 코드부터 읽는다는 걸 다음 편 전제로 삼겠습니다.

한계

여전히 Jev를 써 보지는 못했습니다. 얼리 액세스 대기열 단계라 API 호출을 못 했고, 따라서 한국어 정확도에 대해 이 글은 어떤 수치도 제시하지 않습니다. 공식 문서가 “동등하게 잘 되지는 않는다”고 말했다는 사실만 있습니다.

표본도 12일치로 짧고, 8월 28일 이전 로그는 형식이 달라 제외했습니다. 그리고 이번 집계가 또 틀렸을 가능성도 완전히 배제할 수는 없습니다. 다만 이번에는 12개 파일 전부에서 차이가 0으로 딱 떨어졌고, 정상적인 로그라면 그래야 하는 값과 일치합니다.

FAQ — 자주 묻는 질문

Q1. 원래 글의 41.4%는 어떻게 된 건가요?

A: 제 계산 오류입니다. 처리 완료된 항목은 불릿 줄이 결과 줄로 교체되는데, 불릿만 세서 처리된 건을 전부 누락으로 잡았습니다. 실제 누락은 0건입니다.

Q2. 그럼 로그 구조를 고칠 필요는 없나요?

A: 없습니다. 선언 건수와 기록이 12개 파일 전부 일치합니다. 오히려 병합한 사실까지 주석으로 남기고 있어서, 세는 쪽만 규약을 알면 재구성이 됩니다.

Q3. 그래도 JSONL 판정 로그는 필요하지 않나요?

A: 지금 마크다운으로도 재구성은 됩니다. 다만 신뢰도 값이나 판정 주체 같은 필드를 나중에 Jev와 비교하려면 별도 적재가 편합니다. 급한 문제는 아니게 됐습니다.

Q4. Jev 공식 스펙은 어떻게 되나요?

A: 응답 70~500밀리초, System One 작업에서 40~200배 빠르다고 적혀 있고, 입력 100만 토큰당 0.042달러에 출력은 무료입니다. 요청당 state와 전체 질문 합쳐 64k, state와 가장 긴 질문 하나 합쳐 32k 토큰 예산이며 텍스트 전용입니다.

Q5. 감사 스크립트를 제 볼트에도 쓸 수 있나요?

A: 로그 형식이 다르면 정규식을 바꿔야 합니다. 다만 이 글의 교훈대로, 쓰기 전에 그 로그를 생성하는 코드를 먼저 읽으시길 권합니다.

관련 글

참고 자료