<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Take Sunbath</title>
    <description>The latest articles on DEV Community by Take Sunbath (@5ak3t).</description>
    <link>https://dev.to/5ak3t</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172723%2Fa49c51f1-9103-4e78-b9ba-d68e0ac386d7.png</url>
      <title>DEV Community: Take Sunbath</title>
      <link>https://dev.to/5ak3t</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/5ak3t"/>
    <language>en</language>
    <item>
      <title>Not every AI call needs to generate text: ej, an 11 MB model for typed decisions</title>
      <dc:creator>Take Sunbath</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:42:51 +0000</pubDate>
      <link>https://dev.to/5ak3t/not-every-ai-call-needs-to-generate-text-ej-an-11-mb-model-for-typed-decisions-3ko9</link>
      <guid>https://dev.to/5ak3t/not-every-ai-call-needs-to-generate-text-ej-an-11-mb-model-for-typed-decisions-3ko9</guid>
      <description>&lt;p&gt;A lot of "AI" in production pipelines isn't writing anything. It's deciding.&lt;/p&gt;

&lt;p&gt;Which queue does this ticket go to? Does this message need a human? How urgent is this request: low, medium or high? The&lt;br&gt;
answer is one item from a list you already know. Yet the usual way to get it is to send the text to a language model,&lt;br&gt;
ask it to reply in JSON, parse the reply, retry when the JSON is broken, and hope the word "high" means the same thing&lt;br&gt;
every time.&lt;/p&gt;

&lt;p&gt;That works, but it's an odd fit. You pay for generation you don't need, you get text where you wanted a number, and you&lt;br&gt;
have no clean way to say "only automate this when the model is at least 90% sure."&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;ej&lt;/strong&gt; to do that one job differently. It's an open model that takes a piece of text plus a set of typed&lt;br&gt;
questions, and returns &lt;strong&gt;one probability distribution per question, in one forward pass, without generating a single&lt;br&gt;
token&lt;/strong&gt;. The whole model is one 11 MB file that runs on a CPU.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code (Apache-2.0): &lt;a href="https://github.com/5ak3t/ej" rel="noopener noreferrer"&gt;https://github.com/5ak3t/ej&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Weights (CC BY-SA 4.0): &lt;a href="https://huggingface.co/5ak3t/ej" rel="noopener noreferrer"&gt;https://huggingface.co/5ak3t/ej&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pip install ejai&lt;/code&gt; (the import name is &lt;code&gt;ej&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is version 0.0.1. Below is what it does, how I measured it, and, just as important, where it is weak.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a "typed decision" looks like
&lt;/h2&gt;

&lt;p&gt;You give ej a &lt;code&gt;state&lt;/code&gt; (free text, or a JSON object written as text) and any number of questions. Each question has one&lt;br&gt;
of three types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;You give&lt;/th&gt;
&lt;th&gt;You get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;choice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;instructions + a list of option texts, chosen when you call it&lt;/td&gt;
&lt;td&gt;a probability for each option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noul&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a yes/no statement&lt;/td&gt;
&lt;td&gt;P(false), P(true)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;score&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;instructions + ordered levels&lt;/td&gt;
&lt;td&gt;a distribution over the levels&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The options are plain text you write at call time. There is no fixed label set baked in at training.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ej&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ej&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5ak3t/ej&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# downloads model.ejpack and checks its SHA-256
&lt;/span&gt;
&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The blender arrived with a cracked jug. Replace it before Friday.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;questions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;route&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instructions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Which team should handle this request?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;returns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Returns and replacements for damaged or wrong items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                              &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Billing, invoices and payment problems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs_human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noul&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instructions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The customer is upset enough that a human agent should reply.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ej&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NOUL_OPTIONS&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urgency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instructions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How urgent is this request?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Low: can wait a week&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Medium: answer within two days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;High: answer today&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;route&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;        &lt;span class="c1"&gt;# [p_returns, p_billing], sums to 1
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs_human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# [p_false, p_true]
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urgency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;      &lt;span class="c1"&gt;# [p_low, p_medium, p_high]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three questions are answered in the same pass. Because the output is a distribution, the routing logic in your code&lt;br&gt;
is a threshold, not a string parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;route&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;auto_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;route&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;send_to_human&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Pick that threshold on labelled examples of &lt;em&gt;your&lt;/em&gt; workflow. More on why below.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use an LLM, or an existing classifier?
&lt;/h2&gt;

&lt;p&gt;There are three common ways to make this kind of decision today. ej is a different trade-off from each, not a strict&lt;br&gt;
upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompting an LLM.&lt;/strong&gt; It's flexible and strong on new tasks. But it generates text you have to parse, it needs either an&lt;br&gt;
API call (cost, latency, your data leaving your servers) or a local model that is hundreds of MB to several GB, and its&lt;br&gt;
reply isn't a probability you can threshold. ej skips generation entirely and is small enough to ship inside a service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NLI zero-shot classifiers&lt;/strong&gt; (the "zero-shot-classification" pipeline). These are the closest relatives. They usually&lt;br&gt;
run one forward pass per (text, option) pair, so cost grows with the number of options, and each question type needs its&lt;br&gt;
own setup. ej reads the text once and answers choice, yes/no and score questions together. To be clear, though, a good&lt;br&gt;
NLI model beats ej on workflows ej has never seen (numbers below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fine-tuning your own classifier&lt;/strong&gt; (BERT, SetFit and friends). It's accurate on its own task, but the label set is fixed&lt;br&gt;
at training, and every change means new labelled data and a retrain. With ej the options are input. It also has an&lt;br&gt;
&lt;code&gt;adapt&lt;/code&gt; method that takes a handful of labelled records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;adapted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;examples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;labelled&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# per-option offsets; the weights don't change
&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it fits in 11 MB
&lt;/h2&gt;

&lt;p&gt;Briefly (a technical report with the full method is coming):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The text encoder is a distilled version of &lt;code&gt;e5-small-v2&lt;/code&gt;, quantised to &lt;strong&gt;2-bit weights and 3-bit attention&lt;/strong&gt;, with a
trimmed vocabulary.&lt;/li&gt;
&lt;li&gt;On top sit a set of small expert heads, combined per question type, plus a selective head that knows when it isn't
sure.&lt;/li&gt;
&lt;li&gt;Bigger "teacher" models were used only during training and are distilled into those heads. They don't ship.&lt;/li&gt;
&lt;li&gt;Everything, including the tokenizer, is packed into one file, &lt;code&gt;model.ejpack&lt;/code&gt;: &lt;strong&gt;11,384,312 bytes&lt;/strong&gt;, bit-packed codes
with a SHA-256 check on every section. It contains no pickles, so loading it never executes code from the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the test box (a shared 4-core Xeon, one thread), a warm call takes roughly &lt;strong&gt;200 to 360 ms per record&lt;/strong&gt;, depending on&lt;br&gt;
the run and the inputs, and the process peaks at about &lt;strong&gt;558 MB&lt;/strong&gt; of RAM. Most of that is torch itself: the model's own tensors peak at 128 MB, or 35 MB with&lt;br&gt;
&lt;code&gt;ej.load(..., low_memory=True)&lt;/code&gt;, which is about 1.5x slower and gives identical predictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results, including the bad ones
&lt;/h2&gt;

&lt;p&gt;I tried hard to make these numbers trustworthy rather than flattering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every final test suite was &lt;strong&gt;scored once&lt;/strong&gt;, for this release. Development suites were used to choose between
candidates, so their numbers are optimistic, and they aren't reported as results.&lt;/li&gt;
&lt;li&gt;Every number has a 95% confidence interval, and I only claim "A beats B" when the paired interval excludes zero.&lt;/li&gt;
&lt;li&gt;Six other models were benchmarked on the same blind inputs, each with a flag for whether it might have seen the test
data in training.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test suite&lt;/th&gt;
&lt;th&gt;ej 0.0.1 accuracy [95% CI]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typed Decisions, workflows seen in training&lt;/td&gt;
&lt;td&gt;.721 [.695, .746]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MASSIVE intents (leak-free split)&lt;/td&gt;
&lt;td&gt;.832 [.808, .857]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support tickets (in-house)&lt;/td&gt;
&lt;td&gt;.712 [.679, .746]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support tickets, new writing styles&lt;/td&gt;
&lt;td&gt;.683 [.639, .726]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;105 workflows never seen in training&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;.419 [.379, .458]&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One held-out Typed Decisions workflow (also used during model selection)&lt;/td&gt;
&lt;td&gt;.422 [.388, .452]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How that compares with the six rivals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where ej is good:&lt;/strong&gt; on the two ticket suites, ej's log loss (how good its probabilities are, not just its top pick)
is lower than every rival's. Bear in mind that ej was trained on tickets from the same synthetic source, and the
rivals weren't. On seen Typed Decisions workflows it is level with or above five of the six rivals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where ej is behind:&lt;/strong&gt; on MASSIVE it trails four of the six. On the held-out Typed Decisions workflow it is below all
six.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The big caveat:&lt;/strong&gt; on workflows it has never seen, ej gets about 42% right. That's the number to expect on a
brand-new task before you adapt it. During development, a 71M-parameter NLI model scored about six points higher on
this kind of task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few more things you should know before using it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calibration is measured per suite, not promised.&lt;/strong&gt; The probabilities were well calibrated on the two ticket suites
and not on the other four. Check on your own data before trusting "0.9" to mean 90%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptation is modest.&lt;/strong&gt; Eight labelled records lift accuracy on unseen workflows by about three points, and the
interval includes zero. Mostly what it learns is how common each label is in your workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ticket data is synthetic.&lt;/strong&gt; The in-house tickets were written by Claude Haiku, not by real customers, and that
corpus isn't released.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It's English only.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small differences aren't meaningful.&lt;/strong&gt; Six training runs of the identical recipe differ by several points on some
suites, so I don't read anything into differences that size.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who should try it
&lt;/h2&gt;

&lt;p&gt;ej makes sense if all of these are true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You make the &lt;strong&gt;same kinds of decisions over and over&lt;/strong&gt; on text records: routing, triage, yes/no checks, priority
levels.&lt;/li&gt;
&lt;li&gt;You'd rather get a &lt;strong&gt;probability you can set a threshold on&lt;/strong&gt; than a string you have to parse.&lt;/li&gt;
&lt;li&gt;You need it to run &lt;strong&gt;on CPU, on your own servers, or offline&lt;/strong&gt;, where hosting or calling an LLM is awkward.&lt;/li&gt;
&lt;li&gt;You can label a few dozen of your own records to &lt;strong&gt;check accuracy and pick a threshold&lt;/strong&gt; before automating anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your task is brand-new and you have no labelled data at all, a prompted LLM or an NLI zero-shot model will probably&lt;br&gt;
serve you better today. I'd rather say that here than have you find out in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and break it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ejai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ej&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ej&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5ak3t/ej&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;revision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository also contains the training code (&lt;code&gt;python -m ej.train&lt;/code&gt;), the evaluator that produced every number above&lt;br&gt;
(&lt;code&gt;python -m ej.eval&lt;/code&gt;), and the benchmark runner with the rival adapters, so you can check my numbers or train your own&lt;br&gt;
pack.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/5ak3t/ej" rel="noopener noreferrer"&gt;https://github.com/5ak3t/ej&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hugging Face: &lt;a href="https://huggingface.co/5ak3t/ej" rel="noopener noreferrer"&gt;https://huggingface.co/5ak3t/ej&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Benchmark details: &lt;a href="https://github.com/5ak3t/ej/blob/main/BENCHMARKS.md" rel="noopener noreferrer"&gt;https://github.com/5ak3t/ej/blob/main/BENCHMARKS.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run it on your own workflow, I'd love to hear what accuracy you get, good or bad. Results on real workflows are&lt;br&gt;
exactly what version 0.0.2 needs.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>opensource</category>
      <category>nlp</category>
    </item>
  </channel>
</rss>
