Intent classification is usually presented as a choice between two heavy options: prompt an LLM, or fine-tune a supervised classifier on a large labeled dataset. Prototype embeddings offer a third path that is closer to zero-shot than either. You define each intent with a small set of reference sentences, embed those sentences once with a sentence transformer, and classify every new query by measuring similarity to the reference embeddings. No training data, no GPU, no API call — and the whole thing runs in the browser.
This is the technique behind intent routing in ReLU.chat, a free, browser-based, privacy-first chatbot suite whose intent classification compares a query embedding against 19 prototypes per intent category. Because every embedding is computed locally, intent classification never sends the query anywhere.
How Prototype Classification Works
The idea is simple: an intent is a region of embedding space. A sentence transformer like all-MiniLM-L6-v2 maps sentences to 384-dimensional vectors in which semantically similar sentences land close together. If you embed a handful of canonical phrases for each intent, any new query that means roughly the same thing will embed near one of those reference points.
For each intent category you maintain a list of prototypes — embeddings of representative phrases. For a game-theory chatbot, the "ask for an example" intent might have prototypes like "give me an example", "can you illustrate this", and "show me a concrete case". For a "compare concepts" intent, prototypes might be "what is the difference between X and Y" and "how do these two ideas compare". The exact wording matters less than the semantics: the embedding collapses paraphrase variation.
At query time, three steps happen:
- Encode the query with the same sentence transformer to get a 384-dimensional vector.
- Compute the cosine similarity between the query vector and every prototype.
- Combine the per-intent similarities into a score, then pick the highest-scoring intent.
Cosine similarity is the standard measure because it is scale-invariant — the length of the embedding vector does not affect the result:
cos(q, p) = (q · p) / (||q|| · ||p||)
A minimal implementation in Python:
import numpy as np
def cosine(a, b):
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
def classify(query_embedding, prototypes):
scores = {}
for intent, protos in prototypes.items():
# best prototype match for this intent
scores[intent] = max(cosine(query_embedding, p) for p in protos)
return max(scores, key=scores.get), scores
Building Prototypes Without Training
The "zero-shot style" comes from how prototypes are built. There is no gradient descent and no labeled corpus. You write between a handful and a couple of dozen reference phrases per intent, embed them once at load time, and you are done. Changing the intent taxonomy is an edit to a list, not a retraining run.
A few practical tips for writing prototypes:
- Cover the main paraphrase families: imperative ("explain X"), interrogative ("what is X?"), and noun-phrase ("X definition").
- Keep prototypes short; sentence transformers are tuned on sentence-level inputs, and very long prototypes dilute the signal.
- Include domain jargon alongside plain language, because embeddings treat them as different neighborhoods.
- If an intent is broad, use more prototypes; the per-intent score should be the maximum over its prototypes, not the average, so one good match is enough.
For embedding you need a sentence encoder small enough to run on-device. ReLU.chat uses the all-MiniLM-L6-v2 sentence transformer, quantized to ONNX at roughly 22 MB, running in-browser via ONNX Runtime and transformers.js. At 384 dimensions, a single prototype vector is only about 1.5 KB of float32 data, so even a few hundred prototypes are trivial to keep in memory.
From Similarity to a Calibrated Confidence
Raw cosine similarity is not a probability, and max-over-prototypes alone makes two problems: borderline queries can flip intents on a hair, and the score distribution is not comparable across intents with different numbers of prototypes. ReLU.chat addresses both with a three-step scoring pipeline:
- Per-intent best-vs-average blend: the intent score combines the best prototype match (70%) with the average over that intent's prototypes (30%). The average acts as a stabilizer — an intent whose many prototypes all match moderately is treated as more likely than one with a single lucky match.
- Softmax calibration with temperature 1.5. A temperature above 1 flattens the softmax distribution, so the model stays uncertain when scores are close and only becomes confident with a clear winner.
- The calibrated distribution is used downstream — to decide the response mode, to pick retrieval strategies, or simply to present a fallback question when no intent clears the bar.
The softmax with temperature is:
P(i) = exp(s_i / T) / Σ_j exp(s_j / T)
With T = 1.5, the exponent is divided by 1.5, compressing the differences between scores. In JavaScript:
function softmax(scores, temperature = 1.5) {
const scaled = scores.map(s => Math.exp(s / temperature));
const sum = scaled.reduce((a, b) => a + b, 0);
return scaled.map(s => s / sum);
}
When Prototypes Win, and When They Do Not
Prototype classification is the right tool when the intent set is small and stable, labeled data is unavailable, and you need fast, interpretable predictions. Its advantages:
- No training run; prototypes are human-readable and auditable.
- One embedding lookup per query — a single matrix multiply against the prototypes.
- Easy to debug: you can inspect exactly which prototype matched.
- Naturally extensible: add an intent by appending phrases.
Its limits are equally clear. Coverage is bounded by the phrases you wrote; a query that is semantically far from every prototype will be misclassified with high uncertainty, which is why a confidence threshold matters. Embeddings capture semantics, not syntax or logic, so "reverse the order of the arguments" and "swap the arguments" may land in the same neighborhood even when the intent is different. And for intents that depend on conversation state rather than query text — whether the user is following up, correcting, or switching topics — text-only prototype matching is not enough.
In ReLU.chat's architecture, prototype-based intent classification works alongside a trained policy network: the prototype layer provides the fast, explainable intent signal on the query, while a small MLP policy (25 inputs, 128 and 64 hidden units, 6 action heads covering mode, intent, topic count, fragment count, creativity, and tone) decides the broader action parameters, and a 15-rule heuristic fallback covers the cold start before any weights are loaded.
Key Takeaway
Prototype embeddings give you zero-shot-style intent classification with three ingredients: a sentence encoder that runs on-device, a curated list of reference phrases per intent, and cosine similarity with softmax calibration. It is not a replacement for a trained classifier on hard taxonomies, but for the common case — a small, stable set of intents, no labeled data, and a strict privacy budget — it is accurate, auditable, and free.