Repository navigation
eval: per-client pipeline validation workflow (onboarding step) #127
Description
Why
Pipeline tuning is not a one-time activity — this is a multi-client product, and
each client brings their own contract corpus, their own ERPNext, their own
deployment. For every onboarding we need to arrive at knob values that work for
that client's data.
But we can't just re-run #113 per client:
- Hand-authoring ~150 grounded questions (the eval: build the reference tuning set (~150 Q, dev/test split) + candidate generator #112 approach) does not scale to N
clients. - We must not run the sweep against a client's live
contractcollection — the
chunk_size/ embedding sweeps rebuild the collection and would disrupt their
users. - We often can't hold a client's contract data on our own infra (sensitive).
The model: tune once, validate per client
Tune the pipeline once (#113) on the reference corpus (#112) → produces the
shipped defaults in config.py / pipeline/constants.py and the baseline
thresholds. Most knobs are corpus-independent, so this is the expensive
statistical work done a single time.
Validate per client (this issue) → a small, mostly-automated check that the
shipped defaults hold for the client's corpus, with a targeted re-tune only when
a slice is genuinely weak.
Knob taxonomy
| Knob | Per-client? |
|---|---|
chunk_size / chunk_overlap |
No — document structure (contract clauses) is ~constant across procurement clients |
RRF k |
No — "expect no move" per docs/PIPELINE_TUNING.md |
RERANKER_MODEL, OPENAI_MODEL, system prompt |
No — model / prompt choices |
QUERY_REWRITE_STRATEGY |
No — intent-dependent, not corpus-dependent |
RERANK_TOP_N |
Weakly — precision vs. context budget |
RETRIEVAL_TOP_K |
Yes-ish — recall knee shifts with corpus size / redundancy; generous default (20) absorbs most |
metadata filter vocab (_DOCTYPE_KEYWORDS, _STATUS_KEYWORDS) |
Per-client, but that's config, not tuning |
Scope
scripts/generate_eval_set.py— corpus → candidate{question, ground_truth_answer, ground_truth_contexts, case_class, split}triples, in the same schema asevaluation/test_dataset.json. LLM-generated candidates grounded in realdocname:chunk_indexkeys pulled from the client's Qdrant collection; emits a review file for a human to spot-check a sample before use. Same tool is used to draft the reference set in eval: build the reference tuning set (~150 Q, dev/test split) + candidate generator #112.- Per-client validation run — ~20–40 questions on the client's corpus, ingested into a separate
{QDRANT_COLLECTION}_evalcollection (populated from their ERPNext during the pre-cutover window),evaluate.py --splitagainst it, compared to the reference baseline thresholds from eval: tune the pipeline on the reference corpus (docs/PIPELINE_TUNING.md pass) #113. - Targeted re-tune — if a
case_classslice is below threshold, sweep only the implicated knob (usuallyRETRIEVAL_TOP_Kor the system prompt), not the full eval: tune the pipeline on the reference corpus (docs/PIPELINE_TUNING.md pass) #113 pass. Record the per-client override in their.env. - Onboarding artifact — the per-client eval set + validation results + any knob overrides go into the client's deployment record; add the step to
docs/CLIENT_DEPLOYMENT_RUNBOOK.md.
Where it runs
A separate eval collection on the client's infra during the pre-cutover window —
never the live contract collection. Our infra only if contractually
allowed to hold their contract data.
Depends on
- eval: build the reference tuning set (~150 Q, dev/test split) + candidate generator #112 — reference set format + the candidate generator (shared tooling)
- eval: tune the pipeline on the reference corpus (docs/PIPELINE_TUNING.md pass) #113 — the reference defaults and baseline thresholds this validates against
Related
docs/PIPELINE_TUNING.md,docs/CLIENT_DEPLOYMENT_RUNBOOK.md- feat(evaluation): act on RAGAS results — threshold gating + visible notification #49 (CI regression gate — same per-
case_classnumbers) - eval: run the golden-dataset RAGAS eval as a Langfuse Dataset + Experiment #109 (Langfuse Datasets/Experiments — could host per-client sets)
Area: Evaluation
Activity
docs: add per-client knob taxonomy + reach order to PIPELINE_TUNING.md
feat(eval): reference tuning set — 92 entries, dev/test split, candid…
docs(eval): record the decision to skip corpus expansion (#129)
chore(eval): #113 tuning-pass enabling changes + retrieval-knob close…
feat(onboarding): metadata-filter vocab as per-client config + guided…
arunjoyt commentedon Aug 31, 2026
Closed via
fe72f01(merge6755e22).Most of #127 shipped incrementally already:
scripts/generate_eval_set.py— per-client candidate generator (eval: build the reference tuning set (~150 Q, dev/test split) + candidate generator #112)docs/templates/guided_review.md,evaluation/client/gitignored dir, runbook Phase 5 (feat(onboarding): metadata-filter vocab as per-client config + guided-review worksheet #136)docs/PIPELINE_TUNING.md§ Per-client tuningRETRIEVAL_TOP_K/RERANK_TOP_N/QUERY_REWRITE_STRATEGY) — PR chore(eval): #113 tuning-pass enabling changes + retrieval-knob close-out #133This close-out added:
evaluation/evaluate.py --collectionflag (mirrorsgenerate_eval_set.py) — a per-client run targets the collection explicitly instead of via aQDRANT_COLLECTION=env prefix; the collection is recorded inresults.json'sconfigblock.CLIENT_DEPLOYMENT_RUNBOOK.mdPhase 5 +PIPELINE_TUNING.md.Scope change — the separate
{client}_evalcollection is dropped. Post-#113 the only per-client knobs (RETRIEVAL_TOP_K→ prompt →RERANK_TOP_N) are all query-time — nothing rebuilds the collection — and the runbook's staging RAG stack strategy already isolates validation from the client's production users. A bespoke eval collection was a third isolation layer for a risk already covered. Validation runs read-only against the staging stack's collection (or the prod collection pre-go-live).250 tests pass, ruff clean.