Files
kebab/docs/components/eval
th-kim0823 af8c162e09 docs(components): per-group contributor reference (12 그룹)
docs/components/<group>/README.md 12 페이지 + 인덱스 작성. 각 그룹
페이지가 구성 crate 표 + 구조 mermaid + data flow mermaid + 주요
type/trait/함수 시그니처 + 외부 의존 + 핵심 결정 (HOTFIXES + spec
의 "왜" 통합) + 관련 spec/HOTFIXES 링크. 인덱스가 그룹 wiring
다이어그램 + 진입 가이드 보유.

ARCHITECTURE.md 의 ASCII crate 의존 그래프를 mermaid flowchart 로
교체 (등가 정보, Gitea/GitHub 자동 렌더). docs/components/ 진입
링크 추가.

이 layer 는 contributor 향 — 사용자 향 grand picture 는 README.md
의 logical-architecture diagram 그대로 유지. 진척도는 HANDOFF.md,
per-task spec 은 tasks/INDEX.md 가 기존대로 source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:05:32 +09:00
..

Eval

Golden query 회귀 평가. fixture YAML 로드 → kebab-app facade 통해 실행 → 결과 + 메트릭을 SQLite + per_query.jsonl 로 저장. 두 run 간 비교 (compare). LLM-as-judge 안 함 — rule-based must_contain.

구성 crate

Crate 역할
kebab-eval golden fixture loader + run executor + 메트릭 computer + run-vs-run comparison + markdown 리포트. retrieval/embedding/LLM crate 직접 의존 금지 — 모두 kebab-app facade 통해서만 (P5-1 inheritance).

구조

classDiagram
    class GoldenQuery {
        id: String
        query: String
        mode: SearchMode
        k: usize
        must_contain: Vec~String~
        expected_doc_ids: Option~Vec~String~~
        ask: bool
    }
    class QueryResult {
        query_id
        chunks_returned: u32
        top_score
        contains_all: bool
        ask_grounded: Option~bool~
        ask_refusal_reason
        per_query_jsonl_path
    }
    class EvalRun {
        run_id: String
        started_at
        finished_at
        config_snapshot_json: String
        results: Vec~QueryResult~
    }
    class AggregateMetrics {
        recall_at_k: HashMap~k,f32~
        precision_at_k: HashMap~k,f32~
        no_hit_rate: f32
        refusal_rate: f32
        ask_grounded_rate: Option~f32~
    }
    class CompareReport {
        baseline: EvalRun
        candidate: EvalRun
        per_query: Vec~QueryComparison~
        kind: ComparisonKind
    }
    class ComparisonKind {
        <<enum>>
        Better
        Same
        Regressed
    }
    EvalRun --> QueryResult
    EvalRun --> AggregateMetrics
    CompareReport --> EvalRun
    CompareReport --> QueryComparison

Data flow — eval run

flowchart LR
    YAML["fixtures/golden_queries.yaml"]
    Load["load_golden_set"]
    QSet["Vec~GoldenQuery~"]
    RunId["uuid v7 simple<br/>(timestamp-ordered)"]
    Snapshot["5-version snapshot<br/>(parser/chunker/embedding/<br/>index/prompt_template)"]
    Loop["per query 루프"]
    Search["kebab_app::search_with_config"]
    Ask["kebab_app::ask_with_config<br/>(query.ask == true 시)"]
    MustContain["must_contain<br/>contains_all 검사"]
    Result["QueryResult"]
    Aggregate["compute_aggregate<br/>recall@k / precision@k /<br/>no_hit_rate / refusal_rate /<br/>ask_grounded_rate"]
    SQLiteRow["eval_runs row +<br/>eval_query_results rows"]
    Jsonl["runs_dir/&lt;run_id&gt;/<br/>per_query.jsonl"]
    YAML --> Load --> QSet --> Loop
    RunId --> Snapshot --> SQLiteRow
    Loop --> Search --> Result
    Loop --> Ask --> Result
    Result --> MustContain --> Aggregate
    Aggregate --> SQLiteRow
    Result --> Jsonl

Data flow — compare two runs

flowchart LR
    Base["baseline run_id"]
    Cand["candidate run_id"]
    LoadRows["eval_runs + eval_query_results<br/>rows 로드"]
    Diff["per query 비교<br/>(contains_all flip,<br/>top_score delta,<br/>refusal flip)"]
    Kind["ComparisonKind<br/>(Better / Same / Regressed)"]
    MD["render_report_md<br/>(human-friendly)"]
    Base --> LoadRows
    Cand --> LoadRows --> Diff --> Kind --> MD

주요 type / trait / 함수

Loader (kebab-eval::loader):

  • load_golden_set(path: &Path) -> Result<Vec<GoldenQuery>>serde_yaml 위 YAML 파싱.
  • GoldenQuery { id, query, mode, k, must_contain, expected_doc_ids, ask } — fixture 한 entry. must_contain 가 case-sensitive substring 검사.

Runner (kebab-eval::runner):

  • run_eval(opts: EvalRunOpts) -> Result<EvalRun> / run_eval_with_config(cfg, opts) -> Result<EvalRun>.
  • EvalRunOpts { golden_path, ask_enabled, k_override, ... }.
  • 각 query → kebab_app::search_with_config 호출 (모든 retrieval 은 facade 통해). ask=true 시 추가로 ask_with_config.
  • run_id = UUID v7 simple (timestamp-ordered, lowercase hex).
  • config_snapshot_json = 5 version 모두 (parser_version / chunker_version / embedding_version / index_version / prompt_template_version) 한 번에 capture → 후일 compare 가 정확히 같은 environment 인지 검증.
  • runs_dir/<run_id>/per_query.jsonl 로 raw search hit + answer 저장 — SQLite 의 eval_query_results 는 메트릭 + 요약만, full payload 는 JSONL.

Metrics (kebab-eval::metrics):

  • AggregateMetrics { recall_at_k, precision_at_k, no_hit_rate, refusal_rate, ask_grounded_rate }.
  • TOP_K_VARIANTS — 표준 k 값 set (1, 3, 5, 10, ...). 한 run 에서 multiple k 측정.
  • compute_aggregate(run: &EvalRun) -> AggregateMetrics / _with_config companion.
  • store_aggregate(...) — SQLite eval_runs.aggregate_json 컬럼에 저장.
  • LLM-as-judge 안 함. ask_grounded_rateAnswer.refusal_reason.is_none() && answer.citations.len() > 0 rule.

Compare (kebab-eval::compare):

  • compare_runs(baseline_id, candidate_id) -> Result<CompareReport> / _with_config.
  • CompareOpts { include_unchanged, k_focus }.
  • QueryComparison { query_id, baseline_result, candidate_result, deltas: ContainsFlipped/TopScoreDelta/RefusalFlipped }.
  • ComparisonKind { Better, Same, Regressed } — overall verdict.
  • render_report_md(&CompareReport) -> String — markdown 본문 (PR 리뷰 첨부 용도).

외부 의존

  • crate dep: kebab-core + kebab-config + kebab-app (facade only) + kebab-store-sqlite (SQLite 직접 read/write — eval_runs / eval_query_results 측). retrieval / embedding / LLM crate 직접 import 금지.
  • 외부 lib: serde_yaml (golden YAML), serde_json, uuid (v7), time, tracing, anyhow.
  • 외부 서비스: 없음 (facade 가 가져옴).

핵심 결정

  • runner 가 kebab-app facade 통해서만 실행 (직접 retrieval 금지). : facade rule (P5-1 inheritance). retrieval/embedding/LLM 의 swap 가 eval 의 contract 깨면 안 됨. 같은 facade 호출이 production 에서도 → eval 결과가 production 동작과 등가.

  • rule-based must_contain, LLM-as-judge 거부. : LLM judge 가 stochastic + 비용 발생 + 모델 swap 시 baseline 회귀. must_contain substring 검사가 deterministic + 무료 + 사용자가 fixture 작성 시 이미 의도 명시. spec §11 의 비-목표 명시.

  • 5-version config_snapshot_json. : 두 eval run 비교 시 environment drift 잡음. parser_version / chunker_version 한 단계라도 다르면 비교 무의미 (다른 chunk_id, 다른 retrieval 결과). compare 가 mismatch 시 경고.

  • per_query.jsonl off-disk + SQLite metrics-only. : SQLite row 가 raw search hit + answer 본문 다 보관하면 row 가 거대해짐 + FTS5 인덱스 노이즈. JSONL 은 개별 파일 → 디스크 저렴, append-only 안전, jq / fzf 로 ad-hoc 분석 가능. SQLite 는 메트릭 + 요약만.

  • UUID v7 run_id. : timestamp 순 정렬 (v4 random 은 정렬 안 됨) + collision-free + lowercase hex. runs_dir/<run_id>/ 가 자연 정렬 시 시간 순.

  • ask_grounded_rate = refusal == None && citations > 0. : "grounded" 정의가 spec §3.8 — refusal 아니고 citation 보유. LLM 의 답변 품질 (hallucination 여부) 평가 LLM-judge 없이는 불가능, 가까운 proxy 가 grounded rate.

  • compare 의 ComparisonKind overall verdict. : PR review 가 "regressed 인가?" 한 줄 답이 핵심. per-query delta 모두 봐야 verdict 가 나오면 leverage 안 남. Better / Same / Regressed 단일 verdict + 세부 delta 가 backing.

  • eval 자체 cancellable 안 함. : 50-query suite 가 ~5 분. 중도 cancel 시 partial run 가 baseline 으로 쓰이면 회귀 검출 부정. CLI Ctrl-C 면 hard exit (소실 OK), partial state 저장 안 함.

관련 spec / HOTFIXES