AI agents

Classifying Requests Before They Reach the Router

Antonio Evans11 min read

A conveyor belt carries simple rectangular request cards toward a fork in the path.
Key points

Key points

  1. 1A small classifier placed before the router turns raw request text into two labels (intent and a risk tier) plus a confidence score, so routing becomes a policy decision over structured tags instead of a judgement made inside a prompt.
  2. 2The label schema is the product decision: five to ten intents, three risk tiers keyed to whether an action reads, writes, or moves money or personal records, and a calibration band that decides when the classifier abstains.
  3. 3Labelled data comes from two cheap sources: sampled production logs labelled by a fixed rubric, and a few hundred hand-written examples aimed at the tail that logs never show.
  4. 4Drift shows up first as a gap between the classifier's score and what actually happened downstream, so log both the routing decision and the outcome and alert on their divergence.

Why the router needs tags, not just text

A model router reads the incoming request and picks a model. Most of the routing policies in circulation are static: a request class goes to a fixed model, and only the newest designs decide per request. The distinction is worth keeping because static routing is where most of the safe savings live, while the learned layer sits on top of it (the routing guide from MLflow). If your router has no idea whether a request is a read-only lookup or an instruction to move money, it cannot make a safe decision even with a good model behind it.

The argument for putting a classifier in front is economic before it is architectural. Price spreads between model tiers are large: the cost optimisation summary from NeuralTrust notes that one small model costs a fraction of a frontier model per million tokens and handles most classification, summarisation, and routing tasks just as well (NeuralTrust). A classifier that costs a sliver per call and keeps a measurable share of traffic off the frontier tier pays for itself long before you tune the router.

The second argument is safety. Risk is a property of the action, not of how hard the request is to answer. "Summarise this invoice" and "pay this invoice" are both short, both easy to classify, and both completely different in consequence. A text-only router sees the same shape in both. A tag of risk=high on the second one lets the policy force a stronger model, require a confirmation step, or refuse to route at all.

The third argument is operational. Every routing event becomes an auditable record: this request, this intent, this risk, this confidence, this model. When someone asks why a particular request reached a high-cost model, the answer is a row, not a reconstruction. The routing guide makes the same point about telemetry: capture the routing decision as a first-class event, not just the model's output (MLflow). The classifier is what makes that event meaningful.

Designing the label schema

The schema is where most classification projects fail, and it is the part you cannot fix later with more data. Keep it small enough that two careful people agree on a label in under ten seconds.

Intent. Pick five to ten values that map to distinct downstream behaviour. If two intents would route to the same model, same tools, and same prompt template, merge them. A working set for a customer-support surface looks like this: account_lookup, billing_question, refund_request, technical_troubleshooting, policy_question, data_export, human_handoff. Seven values, each with a different handling path. The eight-value lists people start with usually contain two pairs that are the same intent wearing different names.

Risk. Three tiers, defined by the action rather than the topic.

TierDefinitionExampleDefault routing consequence
lowRead-only. Nothing is written or sent."What is my current plan?"Cheapest capable model, aggressive caching
mediumWrites data or sends something to a third party"Change the shipping address on order 4412"Mid-tier model, no caching, tool call logged
highMoves money, touches personal records, or is irreversible"Refund the last three invoices"Strongest model, confirmation step, escalation hook

The example column matters more than the definition column. Labelers disagree about abstractions and agree about examples.

Confidence and the abstain band. Your classifier should emit a score per label and a single decision: assign, or abstain. An abstention sends the request to the router's default (usually the strong model) plus a log entry for review. Set the abstain threshold by cost, not by accuracy: if a wrong cheap route costs more than an unnecessary frontier call, bias the threshold toward the strong model. That asymmetry is the one rule that survives every case study in the routing literature (the routing analysis from Adnan Masood).

Do not add a topic label. Teams consistently want one, for dashboards. Build it from the intent and risk pair instead. Every extra output head is another thing to label, calibrate, and monitor.

Getting labelled data without a big annotation budget

You need roughly a few thousand examples to train a small encoder well. The cheap path is a blend: half sampled from logs, half written by hand.

Start from logs that already carry an outcome. Requests you have already served include the tool call the agent made, the model that answered, and whether a human escalated. Those outcomes are weak labels. If the agent called issue_refund, the intent was refund_request and the risk was high. Weak supervision is a genuinely useful bootstrap, though a controlled comparison found that weakly supervised classifiers lose on precision relative to LLM-assisted labelling even when they win on recall (the SIGIR study on weak supervision versus LLMs). Use the weak labels to fill the head of the distribution, then correct a sample by hand.

Sample by stratum, not at random. Pull 150 examples per intent and 100 per risk tier. A uniform sample of logs gives you thousands of policy_question rows and perhaps fifteen data_export rows, and the classifier will learn to predict the majority class and nothing else.

Write the tail by hand, in pairs. For each intent, write ten examples with the obvious phrasing and ten with the phrasing a frustrated user actually types. Pairs where only one word differs (a negation, a number, a name) teach the model what does not matter. Few-shot work on intent classification consistently finds that data augmentation and contrastive objectives help most when examples per class are scarce, which is exactly the tail (Microsoft's few-shot intent study).

Label in a fixed order, with a rubric. Intent first, then risk, then a short note if the two labelers disagreed. Disagreements are the most valuable rows in the set: they tell you which boundary in your schema is under-specified. Fix the schema, not the labelers.

Hold out a challenge set. Twenty percent of the hand-written tail goes into a set you never train on. Report accuracy on it separately from accuracy on the log sample. The gap between the two numbers is your real quality estimate, and it is the number to quote when someone asks whether the classifier is ready.

Training and calibrating the small model

A small fine-tuned encoder is usually the right shape. It is cheap per call, fast enough to sit in the request path, and easy to version. The work is in calibration, not architecture.

Split the job into two heads rather than one joint label space. Intent is a multi-class problem over five to ten values; risk is a three-class problem. Two heads means you can tune the risk threshold without disturbing intent predictions, and a request can legitimately be account_lookup at high risk when it carries an unusual identifier.

Calibrate each head separately. Raw softmax scores from a fine-tuned encoder are usually overconfident, which is fatal for abstention. Fit a temperature on the held-out challenge set, then check the reliability of the abstain band directly: of the requests scored between 0.55 and 0.75, what share were actually correct? If the answer is 0.6 when the score says 0.7, widen the abstain band.

A minimal training and scoring loop looks like this:

# two heads over one shared encoder; calibrate on the challenge split
encoder = load_encoder("small-encoder")
intent_head = Linear(encoder.dim, len(INTENTS))
risk_head = Linear(encoder.dim, 3)

for batch in loader(train):
    h = encoder(batch.text)
    loss = ce(intent_head(h), batch.intent) + ce(risk_head(h), batch.risk)
    loss.backward(); step()

calibrator_intent = fit_temperature(intent_head, loader(challenge))
calibrator_risk = fit_temperature(risk_head, loader(challenge))

def classify(text, tau=0.70):
    h = encoder(text)
    p_intent = calibrator_intent(intent_head(h))
    p_risk = calibrator_risk(risk_head(h))
    i, c = p_intent.max(dim=-1), p_risk.max(dim=-1)
    if c_i < tau or c_r < tau:
        return {"abstain": True, "reason": "low confidence"}
    return {"intent": INTENTS[i], "risk": RISK[c]}

Measure the classifier the way you will use it. Overall accuracy hides the errors that matter. Report intent accuracy per class, risk recall at the high tier (you care about high-risk misses far more than high-risk false alarms), and the abstain rate. If high-risk recall is below your bar, lower the risk threshold and accept more abstentions; the abstention path is the strong model, which is where you wanted high-risk traffic anyway.

Keep a durable record per call: the text hash, the model version, both labels, both calibrated scores, the abstain flag, the routing decision, and the model that served the request. That row is the raw material for everything in the next section.

Catching drift by comparing with real outcomes

Labels arrive late, which is why drift detection for this classifier has to lean on proxies. Data drift is a change in the distribution of inputs; prediction drift is a change in the distribution of outputs; both are useful signals when ground truth is delayed (the Evidently guide to data drift). For a pre-router classifier, three monitors cover almost everything.

Label mix drift. Track the share of each intent and risk tier per day, per tenant, per entry point. A sudden jump in billing_question is usually a product change, a marketing campaign, or a new client integration. Alert on a shift of more than a few percentage points relative to a trailing baseline.

Abstain rate drift. A rising abstain rate is the earliest signal that input text has changed in ways the encoder does not recognise. It is cheap to compute and it fires before accuracy numbers move, because accuracy needs labels. Watch it per intent, not just in aggregate: an abstain spike confined to technical_troubleshooting points at a vocabulary change in one product area.

Score-to-outcome divergence. This is the monitor most teams skip. For every routed request, log what actually happened: did the agent call the tool the intent predicted, did a human escalate, did the user rephrase and ask again within a minute, did the request fail validation. Then compare. If the classifier said risk=low and the downstream tool call wrote a record, the risk head was wrong. If requests labelled policy_question escalate to a human at twice the rate they did last month, the intent definition has drifted from reality.

The comparison is what makes the classifier auditable, and it catches the failure mode that labels alone never will: a schema that no longer matches the business. It also gives you a free label pipeline. The downstream outcomes are labels you did not pay for, arriving hours or days after the request, ready to fold into the next training set.

Restrict what the comparison covers. A tool call is a reliable weak label for intent. Escalation is a reliable weak label for "this needed a stronger model or a human", but it is noisy about which. Do not treat user rephrasing as ground truth for anything except "the first answer failed", which is itself useful. Pair the tooling with per-request token and cost logging, since attribution is the prerequisite for knowing whether the classifier is saving what you think it is (Worklytics on token tracking).

A worked example with numbers

Take a support assistant serving 1,000,000 requests a month. The router has two tiers: a small model and a frontier model. Per million tokens, the small model costs a fraction of the frontier model, and the gap is on the order of an order of magnitude or more (NeuralTrust on model right-sizing). Set the exact per-token figures from your provider's current price list before you run the arithmetic; the shape of the calculation is what transfers.

Assume the average request is 800 input tokens and 300 output tokens, so 1.1 billion tokens a month in and out combined. Suppose sending all of it to the frontier model costs 1,000 cost units. The classifier sends 70 percent of traffic to the small tier at roughly a tenth of the unit price, and 30 percent to the frontier tier: the blended bill lands near 0.37 of the original on model spend. The classifier itself, running on a small encoder over a short request, adds a small fraction of a unit per call, and you should measure it rather than assume it.

Now the part the spreadsheet misses. The classifier will be wrong on some share of requests, and wrong cheap routes can cost more than unnecessary expensive ones. Model that explicitly: if 2 percent of the 70 percent low-tier traffic gets a wrong cheap answer that a user has to retry, you pay for the retry plus the trust. The rule that survives: bias the abstain threshold toward the frontier tier until production evidence says otherwise, then move it down one notch at a time and watch the divergence monitor.

One more number worth computing before you build: the share of traffic that is genuinely high risk. In most support surfaces it is small, single digits. If your sampled logs say 40 percent, your risk definition is too broad and the tier has stopped discriminating. Narrow it until high risk means "a human should probably see this".

Once the tags exist, the routing policy is configuration, and the classifier is doing the job a gateway is meant to do: applying one set of decisions across every provider without touching application code. If you would rather not run the model layer yourself, Quanta Inference gives you text, image, video, audio and embedding models plus a hosted way to run classification through one OpenAI-compatible API key, which keeps the classifier and the downstream models on the same bill.

Operating the classifier in production

Treat the classifier as a product with a version, an owner, and a rollback.

Version everything together. Encoder, calibration temperatures, thresholds, intent list, risk rubric, and routing policy travel as one artifact. When someone asks why a request went to the frontier model in March, you should be able to reload the exact policy that was live.

Shadow before you enforce. Run the classifier on live traffic for a week, log its tags, and route everything to the default. Compare the tags against the downstream outcomes before you let them change a single routing decision. This step catches rubric problems that no offline evaluation will.

Review abstentions on a schedule. A weekly sample of 50 abstained rows, read by a person, will surface schema gaps faster than any metric. When the same kind of abstention shows up three weeks running, it is a new intent, not noise.

Retrain on a cadence, not on a hunch. Monthly is usually enough for a small classifier on a stable domain. Each retrain takes the previous month's outcome-labelled traffic, re-runs the challenge set, and ships only if the challenge-set numbers hold. If they drop, the schema changed and no amount of retraining will fix it.

Give the policy a circuit breaker. If the abstain rate crosses a hard ceiling in any hour, bypass the classifier and route everything to the default tier. A classifier outage should degrade cost, never safety.

Keep the human in the loop for high risk. The classifier's job on a risk=high request is to make the escalation obvious, not to decide it. Route it, log it, and let the confirmation step live in the application layer where it can be tested independently.

Teams that run this well end up with three assets they did not have before: a routing policy that can be reasoned about, a labelled dataset that grows for free from outcomes, and an audit trail that answers the question every cost review eventually asks. If you want the plumbing and the classifier hosted together, the Quanta platform takes a described system and builds it with you; if your team would rather have someone review the schema and the monitoring plan before you commit, a consulting call covers that.

FAQ

FAQ

How many labelled examples do I actually need?

A few thousand is the practical target for a small fine-tuned encoder with five to ten intents and three risk tiers, split roughly half from sampled logs and half hand-written to cover the tail. The hand-written half matters more than the volume of log rows, because logs are dominated by a few frequent intents. Start with 150 examples per intent and 100 per risk tier, then grow whichever slice your challenge set shows is weakest.

Can I skip the classifier and let the router decide from the prompt?

You can, and many routers do decide from prompt features such as length, keywords, or embedding similarity (MLflow). What you lose is the risk dimension: prompt shape does not tell you whether an action writes data or moves money, so the router cannot enforce a different handling path for consequences. A small classifier that emits a risk tier is the cheapest way to make that distinction explicit, and it gives you an auditable record of the decision.

What should happen when the classifier is not confident?

Send the request to the router's default path, which should be the stronger model, and log the abstention with both scores. Set the threshold by comparing the cost of a wrong cheap route against the cost of an unnecessary strong route, and bias toward the strong route until production evidence supports moving it. Review a sample of abstentions weekly; recurring patterns are usually a missing intent rather than model weakness.

How do I detect drift before accuracy numbers drop?

Watch three proxies: the label mix per day, the abstain rate per intent, and the divergence between predicted tags and what actually happened downstream. Data drift tooling can flag changes in the input distribution, but for this classifier the outcome comparison is the stronger signal because it reflects the business, not just the text (Evidently). Logging per-request cost and token usage alongside helps you tell a quality problem from a spend problem (Worklytics).

Should the classifier run on the same model as the router's default?

No. Keep the classifier small, fast, and cheap so it can run on every request without becoming a cost centre itself. The router's default is the strong fallback for requests the classifier abstains on, and mixing the two roles makes both harder to tune and to monitor. Keeping them separate also means a classifier failure degrades cost rather than correctness.

Sources

Sources

  1. LLM Routing in Production: Four Stages, Mapped to MLflow
  2. LLM Cost Reduction: 12 Proven Strategies - NeuralTrust
  3. Right-Sizing the Frontier: A Guide to LLM Routing ...
  4. Comparing Weak Supervision and LLMs for Short Query Intent ... - arXiv
  5. PDF Strategies to Improve Few-shot Learning for Intent Classification ...
  6. What is data drift in ML, and how to detect and handle it
  7. How to Track LLM Token Usage and Cost | Guide - Worklytics
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles