Chunking diagnostics
Chunking diagnostics
목적
RFP 문서는 heading, 요구사항 목록, 제출조건 같은 구조가 검색 품질에 직접 영향을 준다. 이 저장소의 CLI 기본값은 naive baseline 재현을 위해 fixed-size chunking을 사용하고, section-aware metadata는 --chunking_strategy auto 또는 section으로 명시해 비교한다.
인덱스 schema
각 chunk에는 아래 진단 필드가 포함된다.
section_id/parent_section_id: parent section과 child chunk를 연결한다.section_path: heading 계층을 보존한다. 공개 fixture smoke 문서는 1단계 heading을 사용한다.chunk_seq_in_section: 같은 parent section 안에서 child chunk 순서를 나타낸다 (1-indexed).total_chunks_in_section: 같은 parent section을 구성하는 child chunk의 총 수. issue #73 진단 필드 —chunk_seq_in_section과 함께 보면 “section을 N등분 중 M번째를 가져왔다”가 evidence 단계에서 바로 읽힌다.chunking_strategy: 실제 적용된 전략이다. 값은section또는fixed이다.regions/page_span: visual parsing v2 입력에서만 포함되는 page/bbox 근거 위치 metadata다.
index.json의 parent_sections에는 parent section text와 metadata가 저장된다. visual parsing v2 문서라면 parent section에도 regions와 page_span이 보존된다. build.chunking에는 요청 전략, chunk_max_chars, overlap, 문서별 실제 전략, parent section 수, chunk 수가 기록된다.
동작 방식
baseline 인덱싱 명령은 다음 옵션과 같다.
python3 scripts/build_index.py \
--input_dir eval/fixtures/smoke_rfp/raw \
--output_dir data/index \
--chunking_strategy fixed \
--chunk_max_chars 520 \
--chunk_overlap_sentences 1
fixed: 문서 전체를 parent section으로 묶고 fixed-size child chunk를 만든다. 현재 CLI 기본값이며naive_baseline의 기준 chunking이다.auto: 문서에 여러 section이 있거나 heading 구조가 있으면section을 사용한다.section: section 경계를 강제로 유지한다.
질의 기본값은 flat retrieval이다. --retrieval_mode hierarchical은 child chunk를 먼저 점수화한 뒤 parent_section_id 기준으로 section text를 재조립해 evidence로 반환한다.
공개 fixture smoke 평가 결과
2026-04-30에 hashing embedding으로 동일 평가셋을 비교했다.
| Index strategy | Chunks | Parent sections | Retrieval | Accuracy | Groundedness | Citation | Abstention | Retry |
|---|---|---|---|---|---|---|---|---|
| auto | 13 | 13 | flat | 1.000 | 1.000 | 1.000 | 1.000 | 0.250 |
| auto | 13 | 13 | hierarchical | 1.000 | 1.000 | 1.000 | 1.000 | 0.250 |
| fixed | 4 | 4 | flat | 1.000 | 1.000 | 1.000 | 1.000 | 0.250 |
| fixed | 4 | 4 | hierarchical | 1.000 | 1.000 | 1.000 | 1.000 | 0.250 |
현재 공개 fixture smoke 문서는 짧고 heading이 명확해 fixed와 section-aware의 품질 차이가 지표로 드러나지 않는다. 대신 section-aware 인덱스는 chunk별 section_path와 parent-child 연결을 제공해 citation 해석과 긴 문서 디버깅에 더 유리하다.
해석 기준
- 기본 flat retrieval은 naive baseline과 agentic full pipeline 모두의 기본 retrieval mode다.
- hierarchical retrieval은 긴 section이 여러 child chunk로 나뉠 때 주변 문맥을 함께 확인하는 실험 옵션이다.
- 품질 비교는
reports/eval_summary.json의hierarchicalablation run과 fixed/auto 임시 인덱스 평가 결과를 함께 본다. - 사람용 local board가 필요하면
python3 scripts/render_chunking_diagnostics_board.py를 실행해reports/retrieval/chunking_diagnostics.html을 본다. 이 HTML은 Phase 2 chunking ablation, real100 chunk health, multi-chunk evidence failure aggregate를 한 화면에 모으는 reviewer view이며, chunking winner나 성능 개선 claim이 아니다.
Chunk-boundary probe set (issue #73)
공개 fixture smoke 문서는 모두 1 chunk 안에 들어갈 만큼 짧아서 chunk-boundary 실패 모드(real-data taxonomy C3)를 자연스럽게 노출시키지 못한다. 이 격차를 메우기 위해 의도적으로 multi-chunk로 분할되는 probe fixture와 probe query를 별도로 추가했다.
Probe fixture: eval/fixtures/smoke_rfp/raw/rfp_agency_d_spectrometer_probe.json
- 기관 D · 분광기 시스템 운영 (현존하지 않는 가상 기관 — 다른 eval case와 metadata 충돌 없음)
- 두 개 본문 section: 사업 개요 (~1100자) + 운영 자동화 세부 요구사항 (~750자)
- 기본
max_chars=520+auto/section전략에서 각 section이 2 chunk로 분할되어 총 4 chunk 생성
Probe queries (eval/config.yaml, hardcase_categories: chunk_boundary):
| Probe id | 답이 위치한 chunk | 무엇을 잡는가 |
|---|---|---|
chunk_probe_external_audit_period |
section 1, chunk 2/2 | 첫 chunk 너머에 있는 fact retrieval — 첫 chunk의 metadata 토큰 밀도가 더 높아도 정답 chunk를 surface해야 함 |
chunk_probe_report_storage |
section 2, chunk 2/2 | section 경계를 넘는 retrieval — 섹션 1만 보지 않고 섹션 2를 골라야 함 |
chunk_probe_calibration_overlap |
section 1, chunk 1/2와 2/2 모두 (overlap region) | DEFAULT_CHUNK_OVERLAP_SENTENCES=1 메커니즘이 살아있는지 — overlap된 문장이 양쪽 chunk에서 retrieve 가능해야 함 |
진단 활용: 위 probe가 실패하면 outputs/answer.json의 evidence[*].chunk_seq_in_section / total_chunks_in_section 값을 본다.
- evidence가 비어있으면 → upstream retrieval 실패 (entity / metadata filter 문제, C1/C2)
- evidence는 있는데
chunk_seq_in_section이 정답 chunk 번호와 다르면 → chunking failure (C3) 자체 - 두 chunk가 같은 section에서 나왔는데 답에 필요한 chunk만 빠져 있으면 → top-k 산정 또는 score 균형 문제
이 분리는 real-data taxonomy C3의 “chunk boundary, masked by upstream miss”를 풀기 위한 첫 단계다. 자연 real-data에서는 이 두 실패 모드가 한 case에서 동시에 일어나기 쉬우므로 합성 probe로 분리해서 본다.
확장 가이드: 새 chunking 전략을 비교할 때 같은 probe set을 양쪽 인덱스에 돌리면 되돌아오는 chunk_seq_in_section / total_chunks_in_section 분포로 분할 정책 차이를 한눈에 본다. 답 텍스트를 변경하지 않은 채 수치 격차만 비교 가능하다는 점이 핵심이다.
Strategy ablation (issue #62)
issue #73의 probe set이 갖춰진 뒤, chunking 전략 차이를 정량적으로 비교 가능해졌다. scripts/run_chunking_ablation.py는 동일 코퍼스(eval/fixtures/smoke_rfp/raw/)를 fixed / section / auto 세 전략으로 인덱싱한 뒤 chunk_boundary probe queries에 대한 top-evidence score를 표로 출력한다.
python3 scripts/run_chunking_ablation.py
2026-05-11 측정 결과 (hashing backend, max_chars=520, overlap_sentences=1):
| Probe | fixed | section | auto |
|---|---|---|---|
| chunk_probe_external_audit_period | ✓ 0.7951 (2/3) | ✓ 0.849 (2/2) | ✓ 0.849 (2/2) |
| chunk_probe_report_storage | ✓ 0.7342 (3/3) | ✓ 0.7084 (1/2) | ✓ 0.7084 (1/2) |
| chunk_probe_calibration_overlap | ✓ 0.7913 (1/3) | ✓ 0.7995 (2/2) | ✓ 0.7995 (2/2) |
| mean score Δ vs fixed | — | +0.012 | +0.012 |
해석:
- section / auto가 평균적으로 약 +0.012 score gain. 3개 probe 중 2개에서 fixed보다 높은 top-score를 달성한다 (
external_audit,calibration_overlap). 모든 probe가 정답 doc + 정답 term을 포함하는 evidence를 returns. - section은 자연 경계를 보존한다. fixed는 한 doc을 단일 parent로 묶고 character cap에서 자른다. section은 heading 단위로 분리하므로 같은 사업의 여러 측면(개요 vs 자동화)이 다른 chunk로 분리된다.
- report_storage probe는 fixed가 약간 더 좋다 (0.7342 vs 0.7084). 정답이 마지막 section의 후반부에 있을 때, fixed는 더 큰 chunk에 답이 포함되어 dense 매칭에 유리. section은 같은 답을 더 작은 chunk로 좁혀 노이즈는 줄지만 score는 약간 낮아진다.
- 현재 CLI 기본값은
fixed(ADR 0001 — naive_baseline 재현성). 위 결과는 multi-section RFP 코퍼스에서는--chunking_strategy auto를 명시적으로 사용할 때 chunk_boundary slice 평균이 미약하게 개선됨을 시사한다. 명시적 옵션으로 두고 default는 변경하지 않는다 (베이스라인 보호).
언제 strategy를 바꿀지 가이드:
- 짧은 단일 section RFP가 다수일 때 →
fixed가 합리적 (chunk 수 최소화) - 긴 multi-section RFP가 많고 chunk_boundary slice 점수가 낮을 때 →
auto또는section을 ablation으로 검증 후 선택 - 비교 ablation은
python3 scripts/run_chunking_ablation.py한 번이면 충분