feat(rag): fb-41 PR-3a HopRecord wire + RagCfg multi-hop knobs #168
Reference in New Issue
Block a user
Delete Branch "feat/fb-41-pr-3-dynamic-decide-loop"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
fb-41 multi-hop RAG 의 PR-3a (PR-3 의 분할 첫 PR). wire additive (
HopRecord+HopKind+Answer.hopsfield) +RagCfg의multi_hop_*3 노브. RAG pipeline 동작 미변경 — 모든 Answer literal 의hops = None. PR-3b (후속) 가ask_multi_hop의 happy path 에서 dynamic decide loop 구현 + hops trace 채움.설계: docs/superpowers/specs/2026-05-25-p9-fb-41-multi-hop-rag-design.md
계획: docs/superpowers/plans/2026-05-25-p9-fb-41-multi-hop-rag.md (PR-3a 단락)
분할 이유 (사용자 결정 2026-05-25)
원래 PR-3 = wire + cfg + decide loop + ScriptedLm + helper refactor + 5+ tests 단일 PR. ~1500 줄 단일 patch 가 review 부담 + 회기 위험 ↑. PR-3a (additive foundation) + PR-3b (decide loop + ScriptedLm + tests) 로 분리.
변경
kebab_core::HopRecord(iter,kind,sub_queries,context_chunks_added,forced_stop,llm_call_ms) +HopKind(Decompose/Decide/Synthesize) — wire-additive shape, snake_case serde tag.kebab_core::Answer.hops: Option<Vec<HopRecord>>—#[serde(default, skip_serializing_if = "Option::is_none")]. single-pass / refusal path = None, PR-3b 의 multi-hop happy path = Some.kebab_config::RagCfg에 3 신규 노브:multi_hop_max_depth: u32(default 3) —KEBAB_RAG_MULTI_HOP_MAX_DEPTHmulti_hop_max_sub_queries_per_iter: u32(default 5) —KEBAB_RAG_MULTI_HOP_MAX_SUB_QUERIES_PER_ITERmulti_hop_max_pool_chunks: u32(default 30) —KEBAB_RAG_MULTI_HOP_MAX_POOL_CHUNKShops: None명시 (kebab-rag/src/pipeline.rs×6 +kebab-cli/src/main.rs+kebab-tui/tests/ask.rs+kebab-eval/src/metrics.rs). exhaustive field check 자동 guard.검증
cargo build -p kebab-core -p kebab-config -p kebab-rag -p kebab-store-sqlite -p kebab-tui -p kebab-cli -p kebab-eval -j 1— clean.cargo test -p kebab-config -p kebab-core -p kebab-rag -j 1— 163 test 모두 녹색 (50 config + 57 core + 31 rag unit + 19 pipeline + 3 prompt + 3 streaming).cargo clippy -p kebab-config -p kebab-core -p kebab-rag -p kebab-store-sqlite -p kebab-tui -p kebab-cli -p kebab-eval --all-targets -j 1 -- -D warnings— clean.-j 1).시험 항목 (Test Plan)
default_multi_hop_max_depth_is_3default_multi_hop_max_sub_queries_per_iter_is_5default_multi_hop_max_pool_chunks_is_30env_overrides_multi_hop_knobs— 3 env var 모두 overridelegacy_config_without_multi_hop_knobs_uses_defaults—LEGACY_PRE_TIMEOUT_TOMLfixture 공유hops: None추가 후 기존 RAG / TUI / CLI / eval test 자동 회귀 핀Wire 영향
answer.v1의 optionalhops필드.skip_serializing_if = None이라 single-pass response 에 emit 안 됨 → 기존 single-pass consumer (CLI / MCP / TUI / agent) 자동 backward-compat. JSON Schema 갱신은 PR-3b 또는 PR-4 (실제 multi-hop happy path 가Some채우는 시점).비범위 (PR-3b)
multi_hop_decompose의 latency 측정 +multi_hop_decidehelper 신규ask_multi_hop의 dynamic decide loop (iter 0 decompose → iter 1+ retrieve+decide → iter N synthesize, max_depth cap, max_pool_chunks cap, forced_stop signaling)ScriptedLmhelper (per-call 다른 response 반환) — happy-path multi-hop integration test 가능multi_hop_decide_stop_triggers_synthesize,multi_hop_decide_continue_adds_more_chunks,multi_hop_max_depth_force_stops,multi_hop_pool_chunks_dedup_by_chunk_id,multi_hop_decide_parse_failure_falls_through_to_synthesizeMULTI_HOP_DECOMPOSE_USER_TEMPLATE의format!named arg 교체 (PR-2 회차 1 carry-over)ask+ask_multi_hop의 §4-§9 helper 추출, PR-2 회차 1 carry-over) — 별 PR 후속 가능성다음 단계
PR-3a 머지 후 PR-3b 시작. branch 명:
feat/fb-41-pr-3b-decide-loop권장 (현재 branchfeat/fb-41-pr-3-dynamic-decide-loop는 본 PR-3a 의 misnamed branch, 머지 후 cleanup).Assisted-by: Claude Code
회차 1 — PR-3a 의 wire additive + cfg 노브 foundation 깔끔 ship. RAG pipeline 미변경 + skip_serializing_if 가 backward-compat invariant 유지. actionable 0 (모두 PR-3b carry-over) + suggestion 3 + question 1 (self-resolved) + 칭찬 1:
suggestion (PR-3b carry-over):
MULTI_HOP_MAX_SUB_QUERIES_DEFAULTconst vs cfg field reconcile — PR-3b 가 const 제거 또는 default fn 으로 변경[rag.multi_hop]) — 사용자 선호 결정 (PR-3b 또는 별 PR)question (resolved): wire schema
additionalPropertiesdefaulttrue→ PR-3b emit 시 wire breaking 아님.칭찬: Answer.hops 의
skip_serializing_ifannotation 가 backward-compat invariant 정확 표현.머지 후 PR-3b (decide loop) 진행 권장. branch rename:
feat/fb-41-pr-3b-decide-loop또는 현재 misnamed branch 삭제 후 fresh.@@ -183,1 +183,4 @@pub max_context_tokens: usize,/// p9-fb-41: hard ceiling on the number of multi-hop iterations/// (decompose iter + decide iters). When the LLM keeps returning/// `continue` past this depth the pipeline cuts to `synthesize`Suggestion —
RagCfg.multi_hop_*field 의 flat vs hierarchic naming 결정:현재 채택: flat (
rag.multi_hop_max_depth,rag.multi_hop_max_sub_queries_per_iter,rag.multi_hop_max_pool_chunks).대안: hierarchic
[rag.multi_hop]sub-section:장점:
KEBAB_RAG_MULTI_HOP_MAX_DEPTH그대로 일관 (현재 flat 와 같음)단점:
RagCfg.multi_hop: MultiHopCfg형태로 한 단계 더multi_hop_max_depth를multi_hop.max_depth로 자동 마이그레이션 안 함flat 채택은 옛 config (현재 fb-41 spec 시점) 호환 + 단순성. 향후 multi-hop 노브 더 늘어나면 cluster 가능성 ↑. 사용자 선호 확인 권장 — flat 유지 vs PR-3b 에서 hierarchic 으로 migrate (V008 config schema bump).
PR-3a 의 cleanup 으로 flat 유지가 default — 의도된 staging 이면 OK, 단순 default 였다면 PR-3b 또는 별 PR 에서 결정.
@@ -32,0 +33,4 @@/// Each entry records one hop (`decompose` / `decide` / `synthesize`)/// — the LLM call category, the sub-queries emitted, retrieval/// counts, and a `forced_stop` flag for cap-driven termination./// Wire-additive: `answer.v1` schema_version unchanged; consumers칭찬 —
Answer.hops: Option<Vec<HopRecord>>의#[serde(default, skip_serializing_if = "Option::is_none")]annotation 가 backward-compat invariant 정확히 표현. (1) None 일 때 emit 안 됨 → 옛 single-pass consumer wire 변동 0, (2)serde(default)로 옛 wire 가 본 schema 로 deserialize 시 None 자동, (3)Option<Vec<HopRecord>>가 "hops 정보가 없을 수도 있다" 와 "hops 가 있지만 비어 있다" 를 type-level 로 구분 (multi-hop 일 때만 Some, refusal 이라도 trace 가 1+ entry 보존 시Some(vec![...])가능).@@ -58,0 +83,4 @@/// hop this is empty (no sub-queries — just the final answer).#[serde(default)]pub sub_queries: Vec<String>,/// Number of *new* chunks the retrieval round contributed to theSuggestion —
HopRecord.sub_queriesdoc 의 "this iter" ambiguity (PR-3b 시점에 보강):"emit at this iter" 의 의미가 다음 둘 중:
PR-3a 단계에서는 실 emit 안 됨 — PR-3b 가 채움. 실 동작 확정 후 doc 더 정확하게:
PR-3b commit 에서 같이 정정 권장. 본 PR 의 doc 은 generic 한 상태 유지 가능.
Suggestion —
MULTI_HOP_MAX_SUB_QUERIES_DEFAULT(const) vsRagCfg.multi_hop_max_sub_queries_per_iter(cfg) reconciliation:PR-2 에서 const = 5 (kebab-rag) 도입, 본 PR-3a 에서 cfg field = 5 (kebab-config) 도입. 둘이 동일 의미 (decompose / decide 의 sub-query 개수 cap), 다만 const 는 compile-time, cfg 는 runtime.
현재
multi_hop_decompose가 const 사용:PR-3b 의 decide loop 도입 시 cfg field 가 runtime knob 로 사용 (user 가 env / config 로 override). const 와 default 가 일치해야 wire 의 LLM 입장에서 일관된 cap 신호. 두 옵션:
multi_hop_decompose+multi_hop_decide둘 다 cfg field 만 사용. const 는 backward-compat 안 됨.default_multi_hop_max_sub_queries_per_iter()만 유지, kebab-rag 가 그 default 또는 cfg 값 사용. 명확.PR-3b 책임 — 본 PR 명시만.
Wire schema
additionalProperties확인 결과:docs/wire-schema/v1/answer.schema.json가additionalProperties명시 안 함 → JSON Schema defaulttrue(unknown field 자동 허용). PR-3b 의 multi-hop happy path 가Some(hops_trace)채워answer.v1JSON 에hopskey 등장해도 wire breaking 아님 (additive minor).그래도 PR-3b 가 schema 의
properties.hops명시 추가 + description 갱신을 같은 PR 에 포함하는 게 schema consumer 측 발견성 ↑. 본 PR-3a 는 schema 변경 없이 ship 가능 —skip_serializing_if = None가 single-pass wire 무변 보장.(Question 자체는 self-resolved — 본 inline 의 답변. PR-3b 의 schema sweep checklist 에 명시 권장.)
회차 2 — 회차 1 의 actionable 0 (모두 PR-3b carry-over) 확인. 본 PR 의 코드 변경 불필요. APPROVE.
회차 1 의 suggestion 3 + question 1 모두 본 PR scope 외:
회차 1 등록 자체가 over-careful 였음 — 본 PR 의 직접 변경 필요한 actionable 0 였다면 회차 1 가 APPROVE 합리적. 회차 2 즉시 APPROVE 로 cadence 정정. plan 의 PR-3b 단락에 carry-over 항목 이미 명시됨 (회차 1 commit 의 plan 정정 부분).
PR 분할 자체 (PR-3a / PR-3b) 가 sequencing 안전성 ↑ — wire / cfg additive foundation 가 동작 변경 없이 ship, PR-3b 가 그 위에서 dynamic loop. 머지 후 PR-3b 시작 (사용자 의향 재확인 권장 — 작업 시간 큰 PR).