Files
kebab/docs/components/eval/README.md
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

172 lines
8.2 KiB
Markdown

# 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). |
## 구조
```mermaid
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
```mermaid
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
```mermaid
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_rate``Answer.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
- frozen 설계 §5.7 (eval_runs / eval_query_results), §6.3 (runs_dir), §11 (비-목표 = LLM-as-judge 금지), §9 (5-version cascade): [`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`](../../superpowers/specs/2026-04-27-kebab-final-form-design.md)
- task spec:
- runner: [`tasks/p5/p5-1-eval-runner.md`](../../../tasks/p5/p5-1-eval-runner.md)
- metrics + compare: [`tasks/p5/p5-2-eval-metrics.md`](../../../tasks/p5/p5-2-eval-metrics.md)
- HOTFIXES (P5-1 facade-inheritance 결정, P5-2 metric definition tweaks): [`tasks/HOTFIXES.md`](../../../tasks/HOTFIXES.md)