<?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: ryuya</title>
    <description>The latest articles on DEV Community by ryuya (@nanakii610).</description>
    <link>https://dev.to/nanakii610</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%2F4141587%2F430120f4-592d-42fa-8d9b-981bda9b87fb.png</url>
      <title>DEV Community: ryuya</title>
      <link>https://dev.to/nanakii610</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nanakii610"/>
    <language>en</language>
    <item>
      <title>I swapped LLM scoring for a non-generative model. The score now moves by 0.01 out of 5</title>
      <dc:creator>ryuya</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:35:53 +0000</pubDate>
      <link>https://dev.to/nanakii610/i-swapped-llm-scoring-for-a-non-generative-model-the-score-now-moves-by-001-out-of-5-5cd6</link>
      <guid>https://dev.to/nanakii610/i-swapped-llm-scoring-for-a-non-generative-model-the-score-now-moves-by-001-out-of-5-5cd6</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I replaced LLM-based scoring with a split: &lt;strong&gt;judgment = Jev, arithmetic = code, feedback text = LLM&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Run the same answer 10 times and the score moves by &lt;strong&gt;0.01 on a 0-5 scale&lt;/strong&gt; (SD, mean of 30 items)&lt;/li&gt;
&lt;li&gt;Median latency &lt;strong&gt;249 ms&lt;/strong&gt;. Cost per 1,000 scorings dropped from &lt;strong&gt;$0.16 to $0.045&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;All numbers below come from one log: 30 answers x 10 runs = 300 scorings on &lt;code&gt;jev-1.13.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why not an LLM
&lt;/h2&gt;

&lt;p&gt;I run a small vocabulary app. It asks you to write a short English answer, then scores it and estimates a CEFR level.&lt;/p&gt;

&lt;p&gt;For scoring, being &lt;em&gt;stable&lt;/em&gt; matters more than being &lt;em&gt;right&lt;/em&gt;. A learner who submits the same sentence twice and sees two different levels stops trusting the app immediately.&lt;/p&gt;

&lt;p&gt;LLM scoring gave me three problems.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What it looked like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scores drift&lt;/td&gt;
&lt;td&gt;Lowering temperature and adding few-shot examples did not stop it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scores are coarse&lt;/td&gt;
&lt;td&gt;Asked for 0-5, got a pile at 3 and 4. No difference between 2.6 and 3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow&lt;/td&gt;
&lt;td&gt;Five criteria in five calls meant five round trips. One call made the criteria drag each other around&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jev is a System One model from TypeSafe AI. It does not generate text. You send state and typed questions, and it returns typed answers with probabilities and a confidence value.&lt;/p&gt;

&lt;p&gt;It has three question types: Choice, Score and Noul. &lt;strong&gt;Score&lt;/strong&gt; takes a rubric with ordered levels and returns a continuous value. You can ask for several criteria in a single round trip, which killed all three problems at once.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;Five criteria, scored on every answer:&lt;/p&gt;

&lt;p&gt;grammatical accuracy / lexical range / syntactic complexity / task achievement / naturalness&lt;/p&gt;

&lt;p&gt;The weights are tuned for my content, so I am keeping those private. The structure is the part worth sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;The rule I settled on: &lt;strong&gt;Jev judges, code computes, the LLM writes prose.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A[Learner answer] --&amp;gt; B["Jev&amp;lt;br/&amp;gt;score 5 criteria"]
    A --&amp;gt; E["LLM&amp;lt;br/&amp;gt;write feedback"]
    B --&amp;gt; C["App code&amp;lt;br/&amp;gt;weighted sum&amp;lt;br/&amp;gt;map to CEFR band"]
    C --&amp;gt; D["Shown instantly&amp;lt;br/&amp;gt;~0.2 s"]
    E --&amp;gt; F["Appended&amp;lt;br/&amp;gt;a few seconds later"]&lt;/code&gt;&lt;/pre&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;systemOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jev-1.13.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answerText&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;primitives&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grammar_accuracy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;rubric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RUBRIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grammar&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vocabulary_range&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;rubric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RUBRIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vocabulary&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;syntactic_complexity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rubric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RUBRIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;syntax&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;task_achievement&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="na"&gt;rubric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RUBRIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;naturalness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="na"&gt;rubric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RUBRIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalness&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="c1"&gt;// the sum and the band mapping stay in code, always&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;weightedSum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;band&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toBand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mapping a score to a CEFR band is deterministic. There is no reason to spend a model call on arithmetic, and every reason not to: code gives you the same answer forever.&lt;/p&gt;

&lt;p&gt;Scoring and feedback are separate endpoints. Scoring returns in about a quarter of a second, so the learner sees a number long before the prose arrives.&lt;/p&gt;




&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;I wrote 30 reference answers, 5 per CEFR level from A1 to C2, and ran each one 10 times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do the levels come out in order
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;SD&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.103&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1.79&lt;/td&gt;
&lt;td&gt;0.127&lt;/td&gt;
&lt;td&gt;1.60&lt;/td&gt;
&lt;td&gt;1.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B1&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;3.09&lt;/td&gt;
&lt;td&gt;0.102&lt;/td&gt;
&lt;td&gt;2.90&lt;/td&gt;
&lt;td&gt;3.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;4.13&lt;/td&gt;
&lt;td&gt;0.130&lt;/td&gt;
&lt;td&gt;3.91&lt;/td&gt;
&lt;td&gt;4.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C1&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;4.63&lt;/td&gt;
&lt;td&gt;0.086&lt;/td&gt;
&lt;td&gt;4.51&lt;/td&gt;
&lt;td&gt;4.78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C2&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;4.84&lt;/td&gt;
&lt;td&gt;0.052&lt;/td&gt;
&lt;td&gt;4.72&lt;/td&gt;
&lt;td&gt;4.89&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of the 5 question sets climbed from A1 to C2 without a single inversion.&lt;/p&gt;

&lt;p&gt;The gaps between neighbouring levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;Distributions overlap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A1 - A2&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2 - B1&lt;/td&gt;
&lt;td&gt;1.30&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B1 - B2&lt;/td&gt;
&lt;td&gt;1.04&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2 - C1&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C1 - C2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.21&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;C1 and C2 do not separate. Advanced writing pins every criterion near the ceiling: C2 grammar scores landed between 4.92 and 4.98 across all 50 runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does the same answer move
&lt;/h3&gt;

&lt;p&gt;This is the part I cared about.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SD of the total, mean over 30 answers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0105&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SD of the total, worst answer&lt;/td&gt;
&lt;td&gt;0.0215&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max minus min, mean&lt;/td&gt;
&lt;td&gt;0.032&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max minus min, worst&lt;/td&gt;
&lt;td&gt;0.070&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;0.01 on a 0-5 scale. I never got close to that with an LLM.&lt;/p&gt;

&lt;p&gt;Worth saying plainly: &lt;strong&gt;not one of the 30 answers returned an identical value 10 times out of 10.&lt;/strong&gt; Jev is not deterministic. It barely moves, which is a different property.&lt;/p&gt;

&lt;p&gt;Per criterion the movement is larger:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Mean SD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Grammatical accuracy&lt;/td&gt;
&lt;td&gt;0.0166&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lexical range&lt;/td&gt;
&lt;td&gt;0.0181&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syntactic complexity&lt;/td&gt;
&lt;td&gt;0.0190&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task achievement&lt;/td&gt;
&lt;td&gt;0.0194&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naturalness&lt;/td&gt;
&lt;td&gt;0.0211&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Around 0.02 each, and 0.0105 once they are weighted and summed. The criteria wobble in uncorrelated directions and the sum cancels most of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency
&lt;/h3&gt;

&lt;p&gt;300 calls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;249 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;419 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95&lt;/td&gt;
&lt;td&gt;580 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max&lt;/td&gt;
&lt;td&gt;1,956 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Under 300 ms&lt;/td&gt;
&lt;td&gt;236 / 300 (79%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fast enough that the score is on screen before the learner looks up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three things that bit me
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Short correct sentences scored too well
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;I like apples.&lt;/code&gt; was getting a high score. Nothing is wrong with it, and that was the problem: my rubric asked "is this grammatically correct".&lt;/p&gt;

&lt;p&gt;I rewrote the rubric levels from &lt;strong&gt;"what is being measured"&lt;/strong&gt; to &lt;strong&gt;"what difficulty was attempted, and was it pulled off"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To check it, I wrote pairs of answers to the same prompt, one deliberately simple (S) and one deliberately complex (C):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Simple (S)&lt;/th&gt;
&lt;th&gt;Complex (C)&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;1.68&lt;/td&gt;
&lt;td&gt;2.37&lt;/td&gt;
&lt;td&gt;+0.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2&lt;/td&gt;
&lt;td&gt;1.87&lt;/td&gt;
&lt;td&gt;2.70&lt;/td&gt;
&lt;td&gt;+0.83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;td&gt;1.87&lt;/td&gt;
&lt;td&gt;2.08&lt;/td&gt;
&lt;td&gt;+0.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4&lt;/td&gt;
&lt;td&gt;1.66&lt;/td&gt;
&lt;td&gt;2.32&lt;/td&gt;
&lt;td&gt;+0.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5&lt;/td&gt;
&lt;td&gt;1.98&lt;/td&gt;
&lt;td&gt;2.25&lt;/td&gt;
&lt;td&gt;+0.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.54&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every prompt moved the right way.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A stable score still produces an unstable label
&lt;/h3&gt;

&lt;p&gt;The score moves by 0.01, and yet &lt;strong&gt;2 of the 30 answers changed band across the 10 runs&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Band flip&lt;/th&gt;
&lt;th&gt;Score range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;An A2 answer&lt;/td&gt;
&lt;td&gt;A2.1 to A2.2&lt;/td&gt;
&lt;td&gt;1.88 - 1.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A C2 answer&lt;/td&gt;
&lt;td&gt;C1 to C2&lt;/td&gt;
&lt;td&gt;4.72 - 4.77&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both sit almost exactly on a band boundary. A 0.01 wobble is irrelevant to the score and decisive to the label the learner reads.&lt;/p&gt;

&lt;p&gt;The fix is not in the model. I added hysteresis: moving up and moving down use different thresholds, so the displayed level stops flickering at a boundary. I have not re-run the full calibration since adding it, so treat that as a design change rather than a measured result.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Pin the model version
&lt;/h3&gt;

&lt;p&gt;If you point at a floating latest tag, everyone's score shifts on the day the weights change, and your history becomes incomparable. For a learning app that is fatal. I pin &lt;code&gt;jev-1.13.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On top of that, a script replays the reference set and fails the build if any answer lands in a different band than the recorded baseline.&lt;/p&gt;




&lt;h2&gt;
  
  
  When it breaks
&lt;/h2&gt;

&lt;p&gt;A wrong score is worse than a missing one, so everything degrades toward showing nothing.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Answer received] --&amp;gt; B{Written in the target language?}
    B -- no --&amp;gt; X[Skip scoring]
    B -- yes --&amp;gt; C{Jev responded?}
    C -- no --&amp;gt; Y["Hide the score&amp;lt;br/&amp;gt;show feedback only"]
    C -- yes --&amp;gt; D{Any low-confidence criterion?}
    D -- yes --&amp;gt; E["Hide that criterion&amp;lt;br/&amp;gt;keep it in the total"]
    D -- no --&amp;gt; F[Show everything]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Confidence comes back from Jev and drops when its judgment is spread out. Across the 300 calls (1,500 criterion scores) &lt;strong&gt;45, or 3%, were hidden&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the other reason scoring and feedback are separate endpoints. Scoring can fail without taking the feedback down with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Per 1,000 scorings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scoring with Jev&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.045&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asking an LLM for the score&lt;/td&gt;
&lt;td&gt;~$0.16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speech recognition&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt; (on-device Web Speech API)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jev returns numbers, so there is no output token bill. The whole 300-call calibration run cost about one US cent, which is why re-running everything 10 times per answer was an easy decision.&lt;/p&gt;

&lt;p&gt;My billing is in yen, so these are converted at roughly ¥155 to the dollar.&lt;/p&gt;

&lt;p&gt;Before this I capped how often a free user could be scored. That cap is gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Text that varies between runs is fine. A score that varies between runs is not.&lt;/p&gt;

&lt;p&gt;I was asking one model to do both and getting the worse half of each. Splitting the judgment out into Jev and keeping the arithmetic in code fixed it. If you have an LLM producing numbers in production, it is worth looking at.&lt;/p&gt;




&lt;p&gt;I build &lt;a href="https://chunkbook.net/lp/" rel="noopener noreferrer"&gt;Chunkbook&lt;/a&gt;, the vocabulary app this scoring runs in. Originally published in Japanese on Qiita.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
