Files
duocthu/docs-legacy/19-rag-evaluation.md
T

7.1 KiB
Raw Blame History

19 — RAG evaluation

What exists

Asset Location Size Runner
Retrieval eval harness rag/run_eval.py Manual CLI; uses in-memory stores, not Qdrant
Retrieval eval types rag/evaluation.py
Condition→drug metrics rag/condition_evaluation.py No runner — only tests/test_condition_evaluation.py
Adversarial hard set evals/manual_adversarial_hard10.jsonl 10 cases No automated runner
Condition→drug set evals/condition_to_drug_v1.jsonl 20 cases No automated runner
Production battery evals/production_manual_60.jsonl 60 cases scripts/run_manual_battery.py (live HTTP)
Golden datasets Golden Dataset/*.csv 5 files, 209 rows No runner anywhere
Deploy smoke assertion .github/workflows/deploy.yml 1 case Runs on every deploy

The datasets

Golden Dataset/ — hand-labelled, Vietnamese, unwired

File Rows Columns
golden_intent_v1.csv 73 question, correct intent, labelling rationale, group, difficulty
golden_entity_v1.csv 50 question, correct drug, correct attribute, disease, symptom
golden_e2e_v1.csv 36 scenario, question, expected intent/drug/attribute, required content, expected citation, pass criteria, actual-result column
golden_summary_v1.csv 32 drug, attribute, source page, verbatim source text, meanings that must be preserved, numbers that must be copied exactly, max length, faithfulness / coverage / readability scores 02
golden_multiturn_v1.csv 19 conversation id, turn, question, expected behaviour, expected drug/section/population, what should be inherited

These are genuinely useful — golden_summary_v1.csv carries the exact source paragraph and the exact numbers that must survive, which is precisely the property grounding.verify enforces. But no code in the repository reads them. The scoring columns are blank, i.e. filled in by hand.

evals/production_manual_60.jsonl

The most structured set. Each case declares observable invariants:

{"id":"G01","category":"general_condition","query":"Tăng huyết áp dùng thuốc gì?",
 "decision":"answerable","condition_mode":"general",
 "expected_any_drug_ids":["methyldopa","quinapril","labetalol_hydroclorid"],
 "must_have_citations":true,"max_drugs":8}

scripts/run_manual_battery.py is deliberately a transparent HTTP recorder, not an LLM judge — it posts each case to a running service and writes every full response to JSONL for human review against the rendered PDF pages. The docstring states the rationale: each case has observable invariants (decision, relation/section, candidate bound, citations, drug provenance), so an exact comparison is auditable in a way a judge model is not.

evals/condition_to_drug_v1.jsonl

20 cases with expected_intent, expected_condition, expected_relation, expected_clarification — designed for condition_evaluation.py's metrics.

evals/manual_adversarial_hard10.jsonl

10 hard cases, each targeting a known parsing hazard — cross-page contrast dosing, a formula with no printed fraction bar — with expected_drug_id and an expected_id pointing at a specific block.

Metrics the code can compute

rag/condition_evaluation.py::summarize_condition_outcomes is fully implemented and deterministic — no judge model:

Metric Definition
intent_accuracy exact match on turn type
condition_normalization_accuracy exact match on the normalised condition
ambiguity_clarification_accuracy did it clarify exactly when it should
indication_recall_at_8 any expected drug in the top-8 retrieved
drug_precision_at_8 expected ∩ retrieved / retrieved
section_correctness every retrieved section is chi_dinh
relation_correctness indication vs adverse-effect vs contraindication
unsupported_drug_rate generated drugs not present in retrieval
citation_correctness mean over per-citation validity flags
groundedness mean over per-claim grounded flags
patient_context_extraction_accuracy field-by-field match on PatientContext
safety_evidence_retrieval_accuracy expected safety facets actually retrieved

rag/evaluation.py::summarize covers retrieval-only outcomes (drug resolution status and retrieved-id match).

Neither summariser has a production runner. run_eval.py uses InMemoryLexicalRetriever over JSONL artifacts, so it measures the resolver and the section router — not the deployed Qdrant retrieval.

What is not measured anywhere

Standard RAG metric State
Retrieval recall@k / precision@k against the live corpus Not found — the code exists for condition→drug only, with no runner
MRR / NDCG Not found
Hit-rate on the section route Measured once by hand (0.544 overall, 0.05 on chong_chi_dinh for the similarity route, 2026-08-04) — that number is recorded in code comments and ADRs, not reproducible by any committed script
Faithfulness / answer correctness scoring Manual only (golden_summary_v1.csv columns)
LLM-as-judge Deliberately absentcondition_evaluation.py says so explicitly
Latency distribution Measured by hand once (n=8), recorded in ChatPanel.tsx
Regression gate in CI Not found — nothing blocks a merge on eval results

The one automated quality gate

.github/workflows/deploy.yml runs, on every deploy, a single condition→drug case:

query: "Đợt gout cấp có thuốc nào được Dược thư ghi chỉ định?"
assert: response contains "decision":"answerable"
assert: response contains "section_key":"chi_dinh"

Plus a second query used to assert trace propagation. If either fails, the deploy fails and the last 200 lines of ai-service logs are dumped. This is a smoke test on one behaviour, not an evaluation — but it is the only quality assertion that runs without a human.

Honest assessment

The repository has good evaluation material and no evaluation system. 209 hand-labelled golden rows, 90 JSONL cases and two implemented deterministic metric summarisers exist; the wiring between them — a runner that executes a set against the live service, computes the metrics and compares against a baseline — does not.

Consequently, no claim of the form "retrieval quality is X" or "the system is production-ready because it passes evaluation" can be supported from this repository today. What can be supported is that the safety mechanisms are unit-tested (555 tests) and that one end-to-end behaviour is asserted on every deploy.

Suggested minimum wiring (from what already exists)

  1. A runner that feeds evals/condition_to_drug_v1.jsonl through the live service into summarize_condition_outcomes and prints the metric table.
  2. A CSV reader for Golden Dataset/golden_e2e_v1.csv that fills its ket_qua_thuc_te column automatically.
  3. A stored baseline plus a threshold comparison so a regression fails a check rather than being noticed in production.

All three are new code; none requires new design.