fix: 28.4k 문서 도그푸딩이 잡은 결함 3건 + eval 회귀 게이트 #233

Merged
altair823 merged 2 commits from fix/lance-compaction into main 2026-08-16 05:56:48 +00:00
Owner

요약

나무위키 18,282 + Apache Jira 10,145 = 28,427 문서 / 600,808 청크 코퍼스로 종단 도그푸딩을 돌렸다. 기존 최대 규모(620 문서)의 46배라 그 규모에서는 드러날 수 없던 결함 3개가 나왔고, 셋 다 여기서 고친다. 더불어 골든셋이 실제로 무언가를 막을 수 있도록 eval compare 에 회귀 게이트를 붙였다.

상세 evidence 와 수치는 tasks/HOTFIXES.md 의 2026-08-16 엔트리에 있다.

1. Lance fragment 무한 누적 (이슈 #230 제안 2)

upsert 가 asset 1건당 merge_insert 를 1회 호출하고 Lance 는 그때마다 새 테이블 버전 + fragment 를 만든다. manifest 는 그 시점의 전 fragment 를 나열하므로 N번째 쓰기가 N줄짜리 manifest 를 새로 쓴다 — 쓰기 비용이 문서 수에 비례하고 manifest 총량은 제곱으로 는다. 코드베이스 전체에 optimize / compact_files / cleanup_old_versions 호출이 0건이었다.

수정 전 (16,828 문서) 수정 후 (28,427 문서)
fragment / manifest 16,814 336
_versions/ 12.2 GB 6.6 MB
실제 벡터 데이터 1.7 GB 1.7 GB
ingest 속도 30.7 → 4.3 문서/분 (단조 하락) 32~36 문서/분 (평평)

이슈 #230 이 11,229 문서에서 잰 메타/실데이터 비율은 2.5배였는데 16,828 문서에서는 7.2배였다 — 제곱 증가가 두 지점으로 확인됐다. 기존 테이블 1회 압축은 153초에 15 GB → 1.7 GB(행 365,991 보존)로 끝나므로, #230 본문의 "회복 수단이 reset --vector-only 밖에 없다"는 서술은 사실이 아니다.

같은 이슈의 삭제 경로 배치화(제안 1) · geodatafusion(제안 3) · doctor 지표(제안 4)는 미해결이라 #230 은 열어 둔다.

2. 묶음 인용 마커가 answer.v1 에서 조용히 사라짐

마커 추출 정규식이 괄호 하나에 마커 하나만 인정해서, 모델이 한 주장에 여러 근거를 다는 [#2, #10] 형태를 못 잡았다. citations 는 추출 마커와 packed entry 의 교집합이라 그 근거들이 배열에서 빠지고, 본문에는 [#2] 가 남아 사용자도 agent 도 해소할 수 없는 인용이 된다. SYSTEM_PROMPT_RAG_V4[#번호] 로 귀속하라고만 하고 한 괄호에 하나씩 쓰라고 지시한 적이 없으므로 모델 잘못이 아니다.

실측(28.4k KB, "Spark shuffle OOM"): 본문 인용 1,2,6,7,8,9,10 vs citations 1,8,9,10 → 3건 끊김. 수정 후 7/7 해소.

기존 엄격함(vec![1] · [1] · [ #1 ] · [#foo] · [#1234] 불인정)은 유지했다 — 답변에 Rust 코드가 섞일 때의 오탐 방지가 원래 목적이고 기존 테스트가 그걸 고정한다.

3. ollama 요���이 max_tokens 를 전송하지 않음

GenerateRequest::max_tokens 를 RAG 파이프라인이 계산해 넘기는데 OllamaOptionstemperature/seed/num_ctx/stop 만 직렬화하고 그 값을 버렸다. ollama 기본 num_predict 는 -1(무제한)이고 컨텍스트가 차면 창을 밀어 계속 생성한다. 연결에 바이트가 계속 흐르므로 request_timeout_secs 로도 못 막는다.

eval run --with-rag 216 질의가 1시간 45분에 21개만 끝냈고, 붙잡고 있던 질의 하나가 13 MB 를 받은 상태였다(15초에 142 KB 유입, ollama runner 198% CPU). num_predict 전송 후 같은 216 질의가 31분에 완료됐다.

4. eval compare --fail-under (신규)

이전에는 delta 만 출력하고 exit code 가 항상 0 이라 "회귀했는가"를 기계가 판정할 수 없었다. empty_result_rate 는 반대 방향으로 검사하고, A 에서 재던 지표가 B 에서 NaN 이 되면 위반으로 잡는다 — 골든셋이 ground truth 를 잃은 경우가 정확히 그 모양이라 조용히 통과시키면 안 된다.

검증

  • 워크스페이스 테스트 186 결과 전부 통과, 실패 0
  • 신규 회귀 테스트 7개: compaction 1 · 마커 그룹 1 · num_predict 1 · --fail-under 4
  • 마커 테스트와 compaction 테스트는 수정 전 상태에서 실제로 실패하는 것을 확인한 뒤 고쳤다
  • 실 KB(28,427 문서)로 종단 확인: 검색 3모드 · 소스 필터 · RAG 인용 정상, citation_coverage 1.0
  • clippykebab-parse-code 의 기존 question_mark 지적으로 red 인데 main 도 동일하며 이 PR 이 건드리지 않은 크레이트다 (rust 1.97.0 clippy 기준)

시험 항목 (Test Plan)

  • cargo test --workspace --no-fail-fast
  • cargo clippy -p {kebab-store-vector,kebab-rag,kebab-llm-local} 통과
  • cargo fmt --check — 변경 크레이트에 신규 지적 0 (kebab-rag 는 main 기준 이미 62건 있어 재포맷하지 않음)
  • 28.4k 문서 실 KB ingest → search → ask 종단
  • eval compare --fail-under 통과/실패 양쪽 경로

비범위

  • 이슈 #230 의 삭제 경로 배치화 · geodatafusion · doctor 지표
  • refusal_correctness 0.1667 — 코퍼스에 없는 질의 6개 중 1개만 거절한다. grounded 가 "마커를 달았는가"만 보기 때문이고, 방어 장치인 rag.nli_threshold 는 기본값이 0(꺼짐)이다. NLI 를 켜고 재측정하는 것이 후속 과제
  • 버전 bump — CLAUDE.md 규약상 bump 는 release commit 과 같은 커밋이라 여기서 하지 않았다. --fail-under 는 신규 CLI flag 이므로 다음 release 는 minor bump 대상

Assisted-by: Claude Code

## 요약 나무위키 18,282 + Apache Jira 10,145 = **28,427 문서 / 600,808 청크** 코퍼스로 종단 도그푸딩을 돌렸다. 기존 최대 규모(620 문서)의 46배라 그 규모에서는 드러날 수 없던 결함 3개가 나왔고, 셋 다 여기서 고친다. 더불어 골든셋이 실제로 무언가를 막을 수 있도록 `eval compare` 에 회귀 게이트를 붙였다. 상세 evidence 와 수치는 `tasks/HOTFIXES.md` 의 2026-08-16 엔트리에 있다. ### 1. Lance fragment 무한 누적 (이슈 #230 제안 2) `upsert` 가 asset 1건당 `merge_insert` 를 1회 호출하고 Lance 는 그때마다 새 테이블 버전 + fragment 를 만든다. manifest 는 그 시점의 **전 fragment 를 나열**하므로 N번째 쓰기가 N줄짜리 manifest 를 새로 쓴다 — 쓰기 비용이 문서 수에 비례하고 manifest 총량은 제곱으로 는다. 코드베이스 전체에 `optimize` / `compact_files` / `cleanup_old_versions` 호출이 0건이었다. | | 수정 전 (16,828 문서) | 수정 후 (28,427 문서) | |---|---|---| | fragment / manifest | 16,814 | 336 | | `_versions/` | 12.2 GB | 6.6 MB | | 실제 벡터 데이터 | 1.7 GB | 1.7 GB | | ingest 속도 | 30.7 → 4.3 문서/분 (단조 하락) | 32~36 문서/분 (평평) | 이슈 #230 이 11,229 문서에서 잰 메타/실데이터 비율은 2.5배였는데 16,828 문서에서는 7.2배였다 — 제곱 증가가 두 지점으로 확인됐다. 기존 테이블 1회 압축은 153초에 15 GB → 1.7 GB(행 365,991 보존)로 끝나므로, #230 본문의 "회복 수단이 `reset --vector-only` 밖에 없다"는 서술은 사실이 아니다. 같은 이슈의 삭제 경로 배치화(제안 1) · geodatafusion(제안 3) · doctor 지표(제안 4)는 **미해결**이라 #230 은 열어 둔다. ### 2. 묶음 인용 마커가 `answer.v1` 에서 조용히 사라짐 마커 추출 정규식이 괄호 하나에 마커 하나만 인정해서, 모델이 한 주장에 여러 근거를 다는 `[#2, #10]` 형태를 못 잡았다. `citations` 는 추출 마커와 packed entry 의 교집합이라 그 근거들이 배열에서 빠지고, 본문에는 `[#2]` 가 남아 사용자도 agent 도 해소할 수 없는 인용이 된다. `SYSTEM_PROMPT_RAG_V4` 는 `[#번호]` 로 귀속하라고만 하고 한 괄호에 하나씩 쓰라고 지시한 적이 없으므로 모델 잘못이 아니다. 실측(28.4k KB, "Spark shuffle OOM"): 본문 인용 `1,2,6,7,8,9,10` vs `citations` `1,8,9,10` → 3건 끊김. 수정 후 7/7 해소. 기존 엄격함(`vec![1]` · `[1]` · `[ #1 ]` · `[#foo]` · `[#1234]` 불인정)은 유지했다 — 답변에 Rust 코드가 섞일 때의 오탐 방지가 원래 목적이고 기존 테스트가 그걸 고정한다. ### 3. ollama 요���이 `max_tokens` 를 전송하지 않음 `GenerateRequest::max_tokens` 를 RAG 파이프라인이 계산해 넘기는데 `OllamaOptions` 가 `temperature`/`seed`/`num_ctx`/`stop` 만 직렬화하고 그 값을 버렸다. ollama 기본 `num_predict` 는 -1(무제한)이고 컨텍스트가 차면 창을 밀어 계속 생성한다. **연결에 바이트가 계속 흐르므로 `request_timeout_secs` 로도 못 막는다.** `eval run --with-rag` 216 질의가 1시간 45분에 21개만 끝냈고, 붙잡고 있던 질의 하나가 13 MB 를 받은 상태였다(15초에 142 KB 유입, ollama runner 198% CPU). `num_predict` 전송 후 같은 216 질의가 31분에 완료됐다. ### 4. `eval compare --fail-under` (신규) 이전에는 delta 만 출력하고 exit code 가 항상 0 이라 "회귀했는가"를 기계가 판정할 수 없었다. `empty_result_rate` 는 반대 방향으로 검사하고, **A 에서 재던 지표가 B 에서 NaN 이 되면 위반**으로 잡는다 — 골든셋이 ground truth 를 잃은 경우가 정확히 그 모양이라 조용히 통과시키면 안 된다. ## 검증 - 워크스페이스 테스트 **186 결과 전부 통과, 실패 0** - 신규 회귀 테스트 7개: compaction 1 · 마커 그룹 1 · `num_predict` 1 · `--fail-under` 4 - 마커 테스트와 compaction 테스트는 수정 전 상태에서 **실제로 실패하는 것을 확인**한 뒤 고쳤다 - 실 KB(28,427 문서)로 종단 확인: 검색 3모드 · 소스 필터 · RAG 인용 정상, `citation_coverage` 1.0 - `clippy` 는 `kebab-parse-code` 의 기존 `question_mark` 지적으로 red 인데 **main 도 동일**하며 이 PR 이 건드리지 않은 크레이트다 (rust 1.97.0 clippy 기준) ## 시험 항목 (Test Plan) - [x] `cargo test --workspace --no-fail-fast` - [x] `cargo clippy -p {kebab-store-vector,kebab-rag,kebab-llm-local}` 통과 - [x] `cargo fmt --check` — 변경 크레이트에 신규 지적 0 (kebab-rag 는 main 기준 이미 62건 있어 재포맷하지 않음) - [x] 28.4k 문서 실 KB ingest → search → ask 종단 - [x] `eval compare --fail-under` 통과/실패 양쪽 경로 ## 비범위 - 이슈 #230 의 삭제 경로 배치화 · geodatafusion · doctor 지표 - `refusal_correctness 0.1667` — 코퍼스에 없는 질의 6개 중 1개만 거절한다. `grounded` 가 "마커를 달았는가"만 보기 때문이고, 방어 장치인 `rag.nli_threshold` 는 기본값이 0(꺼짐)이다. NLI 를 켜고 재측정하는 것이 후속 과제 - 버전 bump — CLAUDE.md 규약상 bump 는 release commit 과 같은 커밋이라 여기서 하지 않았다. `--fail-under` 는 신규 CLI flag 이므로 다음 release 는 minor bump 대상 Assisted-by: Claude Code
altair823 added 1 commit 2026-08-16 05:09:51 +00:00
나무위키 18,282 + Apache Jira 10,145 = 28,427 문서 / 600,808 청크로 종단
도그푸딩. 기존 최대 규모(620 문서)의 46배라 그 규모에서 안 보이던 결함이
드러났다. 상세 evidence 는 tasks/HOTFIXES.md 2026-08-16 엔트리.

1) Lance fragment 무한 누적 (이슈 #230 제안 2)

   upsert 가 asset 1건당 merge_insert 를 1회 호출하고 Lance 는 그때마다 새
   버전 + fragment 를 만든다. manifest 는 그 시점의 전 fragment 를 나열하므로
   N번째 쓰기가 N줄짜리 manifest 를 새로 쓴다 — 쓰기 비용이 문서 수에 비례,
   manifest 총량은 제곱. 코드베이스에 optimize/compact/cleanup 호출이 0건이었다.

   16,828 문서 시점: fragment 16,814 · _versions 12.2 GB (실데이터는 1.7 GB) ·
   ingest 30.7 → 4.3 문서/분으로 단조 하락.
   COMPACT_EVERY_N_UPSERTS=512 마다 Compact + Prune 추가 후: manifest 336 ·
   _versions 6.6 MB · 28,427 문서 끝까지 32~36 문서/분 유지.

   기존 테이블 1회 압축은 153초에 15 GB → 1.7 GB (행 365,991 보존).
   같은 이슈의 삭제 경로 배치화 / geodatafusion / doctor 지표는 미해결.

2) 묶음 인용 마커가 answer.v1 에서 조용히 사라짐

   마커 정규식이 괄호 하나에 마커 하나만 인정해서, 모델이 한 주장에 여러
   근거를 다는 [#2, #10] 형태를 못 잡았다. citations 는 추출 마커와 packed
   entry 의 교집합이라 그 근거들이 배열에서 빠지고, 본문에는 [#2] 가 남아
   해소 불가능한 인용이 됐다. 프롬프트는 [#번호] 로 귀속하라고만 하고 한
   괄호에 하나씩 쓰라고 지시한 적이 없으므로 모델 잘못이 아니다.

   실측: 본문 1,2,6,7,8,9,10 vs citations 1,8,9,10 → 3건 끊김. 수정 후 7/7.
   기존 엄격함(vec![1] · [1] · [ #1 ] · [#foo] · [#1234] 불인정)은 유지.

3) ollama 요청이 max_tokens 를 안 보냄

   GenerateRequest::max_tokens 를 RAG 파이프라인이 계산해 넘기는데
   OllamaOptions 가 그걸 직렬화하지 않았다. ollama 기본 num_predict 는
   -1(무제한)이고 컨텍스트가 차면 창을 밀어 계속 생성한다. 연결에 바이트가
   계속 흐르므로 request_timeout_secs 로도 못 막는다.

   eval run --with-rag 216 질의가 1시간 45분에 21개만 끝냈고, 붙잡고 있던
   질의 하나가 13 MB 를 받은 상태였다. num_predict 전송 후 같은 216 질의가
   31분에 완료.

4) eval compare --fail-under (신규)

   이전에는 delta 만 출력하고 exit code 가 항상 0 이라 "회귀했는가"를 기계가
   판정할 수 없었다. empty_result_rate 는 반대 방향으로 검사하고, A 에서 재던
   지표가 B 에서 NaN 이 되면 위반으로 잡는다 — 골든셋이 ground truth 를 잃은
   경우가 정확히 그 모양이라 조용히 통과시키면 안 된다.

테스트: 워크스페이스 186 결과 전부 통과. 신규 회귀 테스트 7개
(compaction 1 · 마커 그룹 1 · num_predict 1 · fail-under 4).
clippy 는 kebab-parse-code 의 기존 question_mark 지적으로 red 인데 main 도
동일하며 이 PR 이 건드리지 않은 크레이트다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
altair823 added 1 commit 2026-08-16 05:56:31 +00:00
리뷰에서 나온 지적 중 실제로 고칠 값이 있는 4건 반영.

1) 압축 트리거를 인메모리 카운터 -> Lance 테이블 version 으로

   `upserts_since_compact: AtomicU64` 는 store 인스턴스 수명 동안만 살아
   있는데, `kebab ingest-file` 과 MCP `ingest_file`/`ingest_stdin` 은 호출마다
   새 App(따라서 새 store)을 연다. 한 번에 수만 건 넣는 ingest 에서만 512 에
   닿고, 한 건씩 넣는 경로에서는 카운터가 매번 0 으로 되돌아가 압축이 영영
   안 돌았다 — PR 이 잡겠다던 fragment 누적이 그 경로에 그대로 남는 셈.

   Lance 의 `table.version()` 은 테이블에 저장돼 프로세스를 넘어 단조 증가
   하므로 그걸 기준으로 바꿨다. 부수적으로 struct 에서 카운터 필드와
   AtomicU64 import 가 사라졌다.

2) `--fail-under` -> `--max-drop`

   관용적으로 `--fail-under 0.9` 는 "지표가 0.9 미만이면 실패" 로 읽히는데
   실제 의미는 "0.9 이상 떨어지면 실패" 였다. 그대로 두면 CI 에 하한이라고
   믿고 적은 값이 아무것도 안 막는다 — recall 0.95 -> 0.10 붕괴도 낙폭
   0.85 < 0.9 라 통과. 새로 노출되는 표면이라 지금이 바꿀 수 있는 시점이다.
   음수·nan 은 시작 시점에 거부한다(그대로 두면 게이트가 무력화됨).

3) `chrono` 직접 의존 제거

   lancedb 가 `chrono::Duration` 을 이미 re-export 한다
   (lancedb-0.23.1/src/table.rs:85). 앞 커밋이 Cargo.toml 에 적은
   "lancedb 는 chrono 를 re-export 하지 않는다" 는 사실이 아니었다.

4) 안전성 근거 주석 정정 + 측정치 통일

   `delete_unverified: true` 의 근거를 "kebab ingest 는 단일 동기 프로세스"
   라고 단언했으나, 이 리포는 `kebab mcp` 를 preferred 통합 표면으로 배포하고
   그 서버는 ingest 까지 노출한다. 락도 없다. 단언 대신 전제와 그 전제가
   코드로 강제되지 않는다는 사실, 그리고 이 플래그 없이는 신규 색인에서
   아무것도 회수되지 않는다는 trade-off 를 그대로 적었다.
   압축 소요는 사본 측정치(102초) 대신 실 테이블 측정치(153초)로 통일.

`is_multiple_of` 는 MSRV(1.85) 보다 높은 1.87 안정화라 `%` 로 대체.

검증: 워크스페이스 186 결과 전부 통과 · 실패 0. `--max-drop` 3경로 실증
(통과 exit 0 / 잘못된 값 exit 2 / 회귀 exit 1). 회귀 경로에서 "측정 불가"
가드가 실제 run 쌍으로 발동하는 것도 확인.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
altair823 merged commit aeb65e6698 into main 2026-08-16 05:56:48 +00:00
altair823 deleted branch fix/lance-compaction 2026-08-16 05:56:50 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#233