feat(experiments): A/B routing — serve the candidate to a fraction of users (Step 5.7c) - #146
Merged
Merged
Conversation
… users (Step 5.7c) Add ABRouter (rag_gateway.experiments) — deterministic, request_id-hashed variant assignment that *serves* the candidate retrieval config to the assigned fraction of users (the first 5.7 slice that can change a response). Wired into the served /v1/query + /v1/retrieve path: the candidate arm routes inline via _route_query (not degrade-open — a candidate failure surfaces as 502, never a silent fallback that would corrupt the experiment), the retrieval cache is partitioned by variant so control/candidate never share an entry, and the served arm's outcome is recorded once per fresh retrieval. Responses carry a new frozen ExperimentAssignment (experiment/variant/ is_candidate) on QueryResponse/RetrieveResponse (REST-only — gRPC proto mirror deferred like corpus_decision). Gated separately by cfg.experiments.routing_* (independent of shadow_enabled); ShadowCandidateConfig generalised to the shared CandidateConfig. ragctl ab drives assign -> serve -> record -> analyze. dist/schemas + dist/openapi + dist/rag.schema regenerated. All gates green (ruff, mypy --strict 292 files, RAG001, schema/openapi-drift, proto-compat, policy-coverage); ~25 new tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 5.7c — A/B routing: deterministic variant assignment that actually serves the candidate retrieval config to a fraction of live users. This is the first 5.7 slice that can change which response a user receives (5.7a was observe-only analyzer/tracker; 5.7b shadow mode is observe-only background fan-out).
ABRouter(rag_gateway.experiments, besideShadowRunner) —assign(request_id)hashes deterministically to a variant (hash(request_id) < routing_sample_rate ? candidate : control); alsois_candidate/candidate/record/tag./v1/query+/v1/retrieve: the candidate arm routes inline via_route_query(one retrieval, no added latency vs. a normal query). Not degrade-open — a candidate failure surfaces as a normalRetrievalError(→ 502) rather than silently swapping back to control, which would corrupt the experiment and hide a broken candidate. The served arm'soutcome_metric(mean retrieval score) is recorded once per fresh retrieval (skipped on a cache hit, same apples-to-apples discipline as shadow).compute_plan_hash/compute_params_hashfold in the assigned variant, so control and candidate never share a cache entry. Byte-identical key when routing is off.ExperimentAssignmentwire tag (experiment/variant/is_candidate) onQueryResponse/RetrieveResponse(experimentfield, additive + defaultedNone). REST-only for now — the gRPCRagServicepredates A/B routing, so the proto mirror is deferred exactly likecorpus_decision. Served variant also rides the existinggateway.query_completelog.ShadowCandidateConfiggeneralised to the sharedCandidateConfig(used byshadow_candidate+ the newrouting_candidate);cfg.experimentsgainsrouting_enabled/routing_sample_rate/routing_experiment/routing_candidate. Gated separately fromshadow_enabledbecause routing can change a response; both feed one tracker under distinct experiment ids.HybridRetriever.retrieve(the canonicalread_chunkPDP site), so ACLs are enforced identically and the policy coverage linter needs no new entry.build_appleavesab_routerunset;build_app_from_configbuilds it only whenenabledandrouting_enabled. Production injects a candidate over real backends viabuild_app(ab_router=…).ragctl abdrives the full assign → serve → record → analyze flow against in-process stubs (e.g.ragctl ab --rate 0.5 --lift 0.25).dist/schemas(+ExperimentAssignment.json),dist/openapi.*, anddist/rag.schema.*regenerated.Documentation
ragctl abTest plan
ruff check+ruff format --checkcleanmypy --strictclean (292 source files)experimentallowlisted with rationale)ABRouterunit (assignment/record/tag), gateway integration (candidate served + tagged, control not calling candidate,/v1/retrieve, dashboard sees served experiment, inert default), variant cache-partition (test_cache_keys), config wiring +routing_*(test_experiments_config)ragctl abran end-to-end (52% candidate split, control mean ≈0.50 vs candidate ≈0.75, lift +50%, significant)🤖 Generated with Claude Code