Files
kebab/docs/release-notes/v0.30.1-draft.md
altair823 cea390d437 chore: bump version 0.30.0 → 0.30.1 + v0.30.1 release notes
후속 배치 3건 (PR #211/#212/#213):
- pdf-page-v1.2: PDF 페이지 oversize 분할 (chunker_version v1.1→v1.2).
- rag-v4: RAG provenance 라벨 + 신뢰도 우선·귀속 2규칙 (prompt_template_version
  기본 rag-v3→rag-v4, search_hit.v1 additive source_id/trust_level).
- config [ingest.chunking] budget floor 검증.

rag-v4 LLM-judge 도그푸딩(gemma3:4b@R9700, competing 66): override 0/0 (실패
모드 미재현), v4 무해 + trust-steering 효과 미입증. NB: chunker_version·
prompt_template_version 변경은 §Versioning 상 minor 트리거지만 사용자 재량 patch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 06:01:13 +00:00

12 KiB

kebab v0.30.1 — pdf-page-v1.2 / rag-v4 / config budget floor 검증

v0.30.0(md-heading-v2 — 거대 청크 oversize 분할) 후속 patch release. v0.30.0 에서 markdown 청커에만 들어갔던 oversize 분할을 PDF 청커로 확장하고(pdf-page-v1.2), RAG 답변 생성 측에 출처/신뢰도(provenance) 라벨을 도입하며(rag-v4), config 청킹 예산의 깨진 조합을 load 시점에 거부하는 검증을 추가한다. 세 변경 모두 사용자는 새 바이너리로 교체 후 다음 kebab ingest / kebab ask 한 번이면 자동 적용되며, 새 명령·플래그·필수 config 키는 없다.

NB (버전 결정). chunker_version(pdf-page-v1.1v1.2) + prompt_template_version(rag-v3rag-v4, multi-hop rag-multi-hop-v1v2) 변경은 CLAUDE.md §Versioning 규칙상 본래 minor(0.31.0) 트리거다. 다만 사용자가 받는 검색·RAG 결과의 실질적 변화가 없고(PDF 는 거대 페이지 분할만, RAG 는 무해한 라벨 + 효과 미입증 가드레일), 도그푸딩에서 회귀 0 을 확인했기에 사용자 재량으로 patch(0.30.1) 로 컷한다. 단, --json wire 출력에서 answer.v1.prompt_template_version(기존 required 필드)의 값이 rag-v3rag-v4(multi-hop 답변은 rag-multi-hop-v1rag-multi-hop-v2)로 바뀐다 — schema **shape 는 불변(v2 bump 아님)**이지만 이 필드를 파싱·assert 하는 기존 소비자(eval snapshot 포함)는 값 변화를 관측한다. "additive-only / 완전 무영향"이 아니라 "기존 필드 값 변경, schema 호환"으로 구분한다.


1) pdf-page-v1.2 — PDF 페이지 oversize 분할

변경 사실

v0.30.0 의 oversize 분할 primitive(예산 초과 청크를 줄→UTF-8 문자 경계로 재분할)를 crates/kebab-chunk/src/oversize.rs 공유 모듈로 추출하고, PDF 청커를 pdf-page-v1.1pdf-page-v1.2 로 올려 같은 분할을 적용했다. v1.1 의 페이지 청킹은 문장/문단 경계로만 잘랐기 때문에, 경계가 거의 없는 거대 페이지(빽빽한 scanned page 가 한 줄로 OCR 된 경우)가 통째로 한 청크가 되어 임베더 입력 한도를 넘기는 hole 이 PDF 에 남아 있었다(markdown 과 동형의 문제). v1.2 는 2-tier 로 동작한다 — tier-1(문장/문단 greedy + overlap) 후, 한 조각이라도 max_chunk_tokens(공유 config, byte/3 근사, default 4000)를 넘으면 tier-2 가 줄/문자 경계로 재분할 → 모든 PDF 청크가 예산 이하가 된다. 분할된 sub-piece 의 chunk_id 는 #c{start}s{i}, 미분할(작은) 페이지는 v1.1 과 byte-identical(bare #c{start} 유지).

Trade-off

  • 거대 PDF 페이지 한 장이 여러 검색 hit 로 나뉜다. 이전엔 그 페이지가 임베딩 실패로 벡터 검색에서 아예 빠지거나 조용히 잘린 채 한 hit 였다. 이제는 여러 조각으로 정상 색인되어 같은 페이지가 검색 상위에 여러 번 잡힐 수 있다.
  • byte/3 휴리스틱. 토큰 수는 실제 토크나이저가 아니라 바이트/3 근사다. CJK 는 과대추정(더 안전), 영문은 실토큰과 비슷 — strict 임베더에 대해 보수적인 방향이다.

Mitigation

  • 결과·CLI·wire 포맷 불변. --json 스키마, exit code, citation 모양 모두 동일. 내부적으로 거대 페이지가 분할될 뿐이다.
  • markdown / 코드 청커 무영향. max_chunk_tokens 는 PDF 청커의 policy_hash 에만 fold 되므로 markdown 청커는 출력 byte-identical(parity 테스트 통과), 코드 청커도 재청크되지 않는다. chunker_version 은 청커별 라벨이라 markdown(md-heading-v2)·코드 청커의 버전은 unchanged 다.
  • span drift 없음. 분할 조각의 citation span 은 부모 segment 범위(segment-granular)를 그대로 쓴다 — 줄 분할 시 구분자 소실로 인한 offset drift 를 원천 차단하며 회귀 테스트로 잠갔다.
  • strict 임베더 호환. oversize 입력을 truncate 가 아닌 거부(예: AMD Lemonade 500 too large)하는 백엔드에서도 PDF 색인이 깨지지 않는다 — 이것이 본 변경의 직접 동기다.

업그레이드 절차

  1. 새 바이너리로 교체.
  2. kebab ingest — PDF 청커의 chunker_versionpdf-page-v1.1pdf-page-v1.2 로 바뀌었으므로 기존 PDF 자산이 1회 자동 재청크된다(--force-reingest 불필요). markdown / 코드 자산은 버전이 그대로라 영향 없음.
  3. 대용량 PDF 코퍼스라면 첫 ingest 가 PDF 재청크로 평소보다 길어질 수 있다(1회성). embedding 은 파생물 캐시 히트로 대부분 재계산을 피하고, 분할된 거대 페이지의 새 조각만 새로 임베드된다.

도그푸딩 evidence. 실험 KB(scanned PDF + paddle-onnx OCR + arctic@Lemonade, tier-2 를 강제하려 budget 200). 625/625 문서 errors=0, scanned_page1.pdf 1→3 청크(max 190 ≤ 200), scanned_page2.pdf 3→7 청크(max 197 ≤ 200), 둘 다 chunker_version=pdf-page-v1.2 스탬프. 전 코퍼스(markdown + 이미지 OCR + PDF) 10215 청크 전부 ≤ 200, 초과 0.


2) rag-v4 — RAG provenance(출처/신뢰도) 라벨

변경 사실

RAG 컨텍스트의 각 [근거] 청크 머리에 [#n] source={id} trust={primary|secondary|generated} 라벨을 렌더하고, system prompt 에 2개 규칙을 추가했다 — (a) 신뢰도 우선: trust=primarysecondary/generated 와 충돌할 때 primary 를 우선하고 저신뢰 단독 근거에 기댈 때는 불확실성을 명시하도록 모델에게 지시한다(모델이 실제로 그렇게 따르는지 — trust-steering 효과 — 는 본 릴리스에서 미입증, 아래 evidence 참조); (b) 귀속: 사실 주장을 [#번호] 로 출처에 귀속하도록 지시한다. config 의 prompt_template_version 기본값이 rag-v3rag-v4 로 바뀌며(rag-v3 도 config 로 계속 선택 가능), multi-hop 합성 prompt 도 rag-multi-hop-v1v2 로 같은 2규칙을 받는다. 출처 필터(--source/--trust-min)가 retrieval 단계에서 못 거르는 생성(generation) 측 실패 — 저신뢰 청크가 답변에서 권위 청크를 덮어쓰는 것 — 을 겨냥한 가드레일이다.

Trade-off

  • 답변이 약간 더 간결해질 수 있다. 도그푸딩에서 평균 인용 수가 5.14 → 4.23 으로 줄었다(유일하게 관측된 실질 델타). 단 원인은 미상이다 — 품질 지표가 전부 표본오차(±1) 내라 이 감소를 trust-steering(저신뢰 인용 억제)으로 귀속할 근거는 없다.
  • 컨텍스트에 라벨 토큰이 추가된다. 청크당 한 줄 라벨이 들어가므로 프롬프트가 소폭 길어진다 — 라벨 포맷은 bare(source=… trust=…)로 토큰을 아꼈다.

Mitigation

  • wire 는 schema-호환. search_hit.v1source_id / trust_leveloptional 필드로 추가됐다(required 아님, None 이면 직렬화 생략) — 기존 --json 소비자는 무영향이며 v2 bump 가 아니다. 한편 answer.v1 의 기존 required 필드 prompt_template_version 은 schema shape 는 그대로이되 값이 rag-v3rag-v4(multi-hop 답변은 rag-multi-hop-v1rag-multi-hop-v2)로 바뀐다 — 이 필드를 파싱·assert 하는 통합(eval snapshot 포함)은 값 변화를 받는다(상단 NB 참조).
  • 재색인 불필요. prompt_template_version 은 answer 생성 시점에만 쓰이므로 DB migration 이 없다(source_id/trust_level 컬럼은 기존 migration 으로 이미 존재). 라벨 렌더는 버전 무관 항상 동작하는 무해한 metadata 이고, 2규칙(discount 지시)만 rag-v4 에서 추가된다.
  • single-hop 은 opt-out 가능, multi-hop 은 불가(비대칭). config 로 prompt_template_version = "rag-v3" 핀을 잡으면 single-hop 답변은 이전 8규칙 prompt 로 즉시 되돌아간다(라벨은 보이되 discount 지시만 빠짐). 단 multi-hop 합성 prompt 에는 prompt_template_version opt-out 경로가 없어 rag-v3 핀을 잡아도 multi-hop 은 rag-multi-hop-v2 로 강제된다 — multi-hop 답변에서 이전 동작을 완전히 복원하는 방법은 본 릴리스에 없다.
  • 출처 주입 방지. source_id 가 RAG 헤더에 렌더되므로 char-set 검증([A-Za-z0-9._-] 만 허용)을 추가해 newline/bracket 주입을 차단했다.

업그레이드 절차

  1. 새 바이너리로 교체.
  2. kebab ask 를 다시 실행하기만 하면 자동으로 rag-v4 prompt 가 적용된다 — 재색인·재임베딩 불필요. single-hop 의 이전 동작이 필요하면 config 에 prompt_template_version = "rag-v3" 를 명시하면 된다(multi-hop 은 위 비대칭에 따라 rag-multi-hop-v2 로 고정).
  3. search_hit.v1 의 신규 필드는 optional 이므로 기존 --json 통합은 코드 변경 없이 동작한다(필요하면 source_id/trust_level 를 새로 읽으면 된다). 다만 answer.v1.prompt_template_version 을 파싱·검증하는 통합은 값이 rag-v3rag-v4(multi-hop 은 rag-multi-hop-v1v2)로 바뀌는 것을 반영해야 한다.

도그푸딩 evidence — 정직한 보고(무해 + 효과 미입증). 실험 KB(wiki 문서 + jira 이슈 혼합), gemma3:4b + snowflake-arctic-embed2, R9700 GPU ollama 에서 competing 질의 전체 66건(wiki-auth 34 + jira-auth 32)으로 rag-v3 vs rag-v4 를 LLM-judge 비교했다.

  • 라벨 도달은 확인됨. competing 쿼리에서 wiki(primary)·jira(secondary)가 둘 다 retrieval 되고 각 hit 가 정확한 source_id/trust_level 로 채워져, 라벨이 올바른 출처에서 프롬프트까지 도달함을 end-to-end 입증했다.
  • 타깃 실패 모드는 애초에 재현되지 않았다. 저신뢰 jira 가 권위 wiki 를 wiki 인용 없이 "덮어쓰는" 실패는 두 버전 모두 0/34(두 버전이 동일 = 발생 자체가 없음, v4 의 억제 효과가 아님) — 이 모델/코퍼스 조합에서 고칠 대상 자체가 나타나지 않았다.
  • 품질 지표는 표본오차(±1) 내라 통계적으로 구분 불가. wiki-grounded 32 vs 31, jira-correct 29 vs 28, 거부 4 vs 5. 유일한 실질 델타는 위에 적은 평균 인용 5.14 → 4.23(더 간결, 원인 미상).

결론. rag-v4무해(회귀 0) 하고 라벨이 프롬프트에 도달함은 확인됐으나, 헤드라인 효과(trust-steering 으로 override 억제)는 이 설정에서 실패 모드 자체가 재현되지 않아 입증되지 않았다.rag-v4이 모델/코퍼스가 노출하지 않는 실패 모드를 위한 가드레일이다. 더 큰 모델이나 wiki↔jira 가 실제로 모순되는 코퍼스에서 의미를 가질 수 있으나, 본 릴리스는 그 효능을 주장하지 않는다. "rag-v4 가 출처 신뢰도를 개선한다 / override 를 줄인다"는 주장은 본 릴리스에서 하지 않는다.


3) config [ingest.chunking] budget floor 검증

Config::from_filevalidate_chunking() 를 추가해, 청킹 예산의 명백히 깨진 조합을 load 시점에 명확한 에러로 거부한다. 규칙: target_tokens ≥ 16, overlap_tokens < target_tokens, max_chunk_tokens ≥ target_tokens. 이전엔 max_chunk_tokens = 0 같은 오설정이 청커 내부 클램프에 흡수돼 3-byte 청크 폭주(인덱스 bloat, 에러 없이) 처럼 downstream 에서 뒤늦게 터졌다. 이제 깨진 config 만 load 가 실패하며, valid config 는 동작·결과가 완전히 불변이다(새 키·migration 없음).