v0.18 cut 전 fb-41 multi-hop RAG 도그푸딩에서 발견된 **safety regression**
fix. 자세한 도그푸딩 결과는 `tasks/HOTFIXES.md` 의 2026-05-25 fb-41
pre-v0.18 entry + `/build/cache/dogfood-v018/results/SUMMARY.md` 참조.
## 문제 (S7)
Query: `What is the chemical formula of caffeine?` (KB 에 없는 fact).
- Single-pass `kebab ask`: retrieve top score 가 default `rag.score_gate
= 0.30` 미만 → `refuse_score_gate` → 안전한 refusal.
- Multi-hop `kebab ask --multi-hop`: **`grounded = true`**, 본문
`"카페인의 화학식은 C₉H₁₅N₃O 입니다 [#6]"` (hallucination — 실제
C₈H₁₀N₄O₂) + `[#6]` 가 Adam optimizer chunk 의 `g_t = ∂L/∂θ_i` 본문을
인용 (시각적 short structured token 매칭 trigger).
원인: `ask_multi_hop` 의 score-gate 검사가 *pool 의 top_score* 만 봤다.
multi-hop 의 pool 은 5 sub-queries 의 union — 한 sub-query 의 top score
가 gate 위면 다른 chunks 가 원본 query 와 무관해도 gate 통과 + synth →
LLM hallucinate.
## Fix
`ask_multi_hop` entry 에 **pre-decompose probe** 추가:
1. *원본 query* 로 retrieve 한 번 (LLM call 0회, ~ms).
2. probe empty → `refuse_no_chunks(None)` (decompose 안 함, hops=None).
3. probe top_score < gate → `refuse_score_gate(None)` (decompose 안 함).
4. probe pass → 기존 decompose / decide / synthesize flow 그대로.
Multi-hop 의 safety floor 가 single-pass 와 정확히 일치 — multi-hop 은
*원본 query 가 이미 KB 범위 내* 일 때만 cross-doc reasoning 추가.
비용: 한 번의 retrieve (수 ms), LLM call 없음. multi-hop 의 LLM-dominated
latency 대비 무시 가능.
## Tests
신규 3 회귀 핀 (`crates/kebab-rag/tests/multi_hop.rs`):
- `multi_hop_below_probe_gate_refuses_before_any_llm_call` — **S7 직접
회귀 핀**. low-score chunk + empty LM script → score_gate refusal, LM
calls 0회, hops=None. fix revert 시 즉시 panic.
- `multi_hop_empty_probe_pool_refuses_before_any_llm_call` — empty
retrieve 시 NoChunks refusal, LM calls 0회.
- `multi_hop_above_probe_gate_proceeds_to_decompose` — probe pass 시
full multi-hop flow 정상 (decompose + decide + synth).
기존 7 multi-hop test 의 `ScriptedRetriever` 에 *probe-pass entry*
prepend + `retriever_handle.calls()` expectation +1. test 2 / test 4
처럼 entry 두 개였던 곳도 prepend (3 entries).
`multi_hop_refuse_no_chunks_preserves_hops_trace` /
`multi_hop_refuse_score_gate_preserves_hops_trace` 의 의미 좁힘 — 이제
*decompose-driven* refusal (probe pass 후 sub-query retrieve 가 empty
또는 below-gate) 만 검증. *probe-driven* refusal 은 hops=None
(decompose 안 함) — 신규 test 가 그 path 핀.
## 검증
- `cargo test -p kebab-rag -j 1` — 10 multi-hop (7 갱신 + 3 신규) + 19
pipeline + 31 unit + 3 prompt_template + 3 streaming 모두 통과. 회귀
없음.
- `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.
- 단일 crate 직렬 build (16 GB RAM 제약).
## 변경 없음
- Wire schema — `Answer.hops` shape 동일, `refusal_reason` enum 동일.
- 다른 도그푸딩 발견 (synthesize citation 일관성, latency, binary path
confusion) — v0.18.1 또는 별 PR 의 책임. HOTFIXES 의 "다른 도그푸딩
발견" 절에 명시.
## 다음
PR-7 머지 후:
1. Workspace `Cargo.toml` version 0.17.2 → 0.18.0 (minor bump).
2. HANDOFF.md / INDEX.md 갱신 + frozen design §3.8 multi-hop sub-section.
3. `gitea-release v0.18.0 --auto-notes`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test 7 의 `i32_below_gate_chunk` helper rename → `seed_low_score_chunk` +
반환 shape 을 `(chunk_id, doc_id)` tuple 로 확장. `i32` prefix 가 Rust
integer 타입과 충돌하던 가독성 문제 해소 + 호출자가 `id32("d_low")` 를
재계산하지 않도록 id 페어를 single source of truth 로 통합.
검증
- `cargo test -p kebab-rag -j 1 --test multi_hop` — 7 모두 통과.
- `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(A) ScriptedLm doc 의 `Arc<Vec<String>>` 표기 → 실제 구현 (`Vec<String>` +
`AtomicUsize`, 외부에서 `Arc::new(ScriptedLm::new(...))` 로 wrap)
반영.
(B) ScriptedLm::new doc 의 미존재 `with_*` builder 언급 제거.
(C) refuse path 의 hops 보존 회귀 핀 2 건 추가 (`tests/multi_hop.rs`):
- `multi_hop_refuse_no_chunks_preserves_hops_trace`: empty pool →
`refuse_no_chunks(Some(hops))` → Answer.hops = Some([Decompose,
Decide]).
- `multi_hop_refuse_score_gate_preserves_hops_trace`: top score 0.10
< 0.30 gate → `refuse_score_gate(Some(hops))` → 같은 shape.
refuse_* widening + ask_multi_hop 의 forwarding wiring 이 reverting
되면 두 test 가 회귀 잡음.
(D) test 5 의 redundant `assert_ne!(.., Some(MultiHopDecomposeFailed))`
제거 — `assert_eq!(.., None)` 이미 함의. 메시지에 의도 통합.
검증
- `cargo test -p kebab-rag -j 1 --test multi_hop` — 7 (5+2) 모두 통과.
- `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>