Skip to main content
Every answer your engine gives is kept in the store, under its exact input. After a few thousand of them, the store holds something valuable: a labelled dataset for that exact question, in your data, with your reviews on top. hunch distill turns it into a small model that runs on your machine in about 10 milliseconds, with no API call. A small model may be less accurate than the engine that taught it. Give it the rows on which it clears your act threshold, and send the rest to the original engine. Test both routes on held-out rows before deciding how much work to keep local. A local student answers first. At or above the confidence threshold its answer is used; below it, Jev answers instead. Both paths are measured on held-out rows.

Distill

The BANKING77 spec sorts bank customers’ messages into 77 intents. Jev had answered 3,000 unlabelled messages, kept in the store. Distilling asks nothing:
The student’s frozen MiniLM encoder runs on your CPU. A small layer for each question learns from your gold where you have it and from the engine’s stored answers elsewhere. The output is a model folder, 194 KB here, that you can commit with the spec. Distillation also fits a calibration temperature using predictions on rows held out from each training fold. This helps its reported confidence track observed accuracy; hunch test still checks that claim on fresh rows. If the held-out answers contain too few mistakes to fit a temperature, confidence is left as it is. The command reports the rarest answer because a handful of examples may not teach the student that class well. It needs the distill extra: uv add "hunch-ai[distill]", which brings fastembed and its runtime (onnxruntime, tokenizers and about 20 other packages). The encoder downloads once, about 90 MB, to ~/.cache/hunch/encoders (or FASTEMBED_CACHE_PATH).

Measure it like any engine

A distilled model is an engine, distilled:<folder>, so every command measures it. On the 385 held-out messages, which it never saw:
On its own it is right 91.2% of the time, where Jev is right 97.9%. It knows which answers those are: at 0.9 and above it answers three messages in four, and gets about one in a hundred wrong. distill holds back a fifth of any gold rows it could have learned from and records what it trained on, so test and diff grade a distilled model only on rows it never saw. (Never saw means never saw that exact text: 3 of these 385 messages differ from a training message only in case or punctuation.)

Put the engine behind it

intent_distilled.yml is intent.yml with the student in front and Jev behind it:
intent_distilled.yml
Jev alone, measured the same way, is estimated at 95.8% (95% CI 88.7%–97.9%); on plain accuracy the two are 97.9% and 97.7%, one message apart. Nearly three messages in four (282 of 385) were answered on this machine. Behind a distilled model, an escalated answer always replaces the student’s: the teacher is the better model, so every doubt goes to it. The share that stays local grows with the answers you keep. Students trained on more of Jev’s answers, measured on the same holdout with a script that calls hunch’s own training, kept more: Every run and every review adds to the store, so distilling again later keeps more of the work local.

When not to use it

A student learns an answer from examples of it. When one answer is rare and missing it is costly, a few thousand rows aren’t enough. The command guard is that case: 2% of shell commands need a person, so its student saw only 11 destructive commands. Measured by a script on commands the panel had labelled and the student never saw, the student with Jev behind it (act 0.9) let through 2 of 6 destructive commands and 1 of 12 that send data out, where Jev alone let through none. distill flags the rare answers; hunch test --model distilled:… shows the misses on the dial’s “act on no” side. For a guard like that, keep the engine until the store holds many more of the rare answer. A distilled model answers only the questions it was trained on, word for word. Change a question and it refuses until you distill again, and hunch diff against the old engine shows what the new student changed.

Speed and cost

Inside a running app, a new answer takes about 11 ms: the encoder and the linear layer, on the CPU. A fresh process pays about 0.3 s to load the encoder first, so a per-command hook gains no speed, only cost and network. Distilled answers cost nothing and are stored like any other, so a row seen before comes from the store.