{"id":1955,"date":"2026-10-13T05:34:00","date_gmt":"2026-10-13T12:34:00","guid":{"rendered":"https:\/\/www.kenwalger.com\/blog\/?p=1955"},"modified":"2026-10-12T10:49:40","modified_gmt":"2026-10-12T17:49:40","slug":"do-you-actually-need-an-llm","status":"publish","type":"post","link":"https:\/\/www.kenwalger.com\/blog\/ai\/do-you-actually-need-an-llm\/","title":{"rendered":"Do You Actually Need an LLM?"},"content":{"rendered":"<p>I spent a weekend running two small language models against three jobs I<br \/>\nwould normally hand to a language model without thinking about it. For one<br \/>\nof those jobs I also wrote a regular expression, as a control.<\/p>\n<p>The regex won. Thirty test cases out of thirty, in zero milliseconds, from<br \/>\na pattern that took an afternoon to write. The better of the two models<br \/>\nscored twenty-eight, took five seconds a call, and the two it missed are<br \/>\nboth mistakes a regex is structurally incapable of making.<\/p>\n<p>That is the kind of result I was looking for, and the question behind it is<br \/>\none I keep avoiding: <strong>what is the smallest amount of computation that<br \/>\nreliably does this job?<\/strong> Not the cheapest model. Not the best small model.<br \/>\nThe smallest thing that works, which means the ladder has four rungs and the<br \/>\nbottom one is not a model at all.<\/p>\n<ol>\n<li><strong>Deterministic code.<\/strong> A regex, a lookup table, an if statement.<\/li>\n<li><strong>A small local model.<\/strong> Runs on hardware you already own.<\/li>\n<li><strong>A larger local model.<\/strong> Needs a GPU.<\/li>\n<li><strong>A remote frontier model.<\/strong> Someone else&#8217;s hardware, metered.<\/li>\n<\/ol>\n<p>Most projects start at rung four and never ask about the others. This is a<br \/>\nweekend spent asking.<\/p>\n<p>The hardware was a Raspberry Pi 4, 4 GB of RAM, heat sinks and a fan, a<br \/>\nhundred and seventy dollars most of which I already owned. The models were<br \/>\n<code>smollm2:360m<\/code> and <code>qwen2.5:1.5b<\/code> through Ollama. The test cases are<br \/>\nninety-five questions about a fictional small winery, so nothing can be<br \/>\nlooked up and every answer can be judged by reading. Sixteen of the fifty<br \/>\nclassification messages are deliberately ambiguous, because real inboxes are<br \/>\nmostly ambiguous.<\/p>\n<p>Four verdicts, assigned per workflow, to force a result you can act on<br \/>\ninstead of a number:<\/p>\n<ul>\n<li><strong>Unnecessary<\/strong>: deterministic code already does this.<\/li>\n<li><strong>Sufficient<\/strong>: a small local model does it well enough to run unattended.<\/li>\n<li><strong>Marginal<\/strong>: usable with a person checking the output.<\/li>\n<li><strong>Insufficient<\/strong>: the error profile or the latency is not acceptable.<\/li>\n<\/ul>\n<p>And one rule, which is the part I would insist on if you copy nothing else:<br \/>\n<strong>I wrote predictions for every workflow and committed them before I<br \/>\ninstalled Ollama.<\/strong> Twenty-two of them, each marked confident, leaning, or<br \/>\ncoin flip. Once there is a timestamped expectation there is a defined moment<br \/>\nwhen you are done, and no room to decide afterward that you knew all along.<br \/>\nI will come back to how they scored, because it is not flattering.<\/p>\n<h2>Extraction: pulling an order number out of a message<\/h2>\n<p>Find <code>ORD-<\/code> followed by six digits, or report that there isn&#8217;t one. Thirty<br \/>\ncases: twenty-two with an identifier, five without, and three decoys. The<br \/>\ndecoys are a membership number, a UPS tracking number and a phone number,<br \/>\nall of which look like identifiers and none of which are order numbers.<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th align=\"right\">regex<\/th>\n<th align=\"right\">qwen2.5:1.5b<\/th>\n<th align=\"right\">smollm2:360m<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Overall<\/td>\n<td align=\"right\"><strong>30\/30<\/strong><\/td>\n<td align=\"right\">28\/30<\/td>\n<td align=\"right\">8\/30<\/td>\n<\/tr>\n<tr>\n<td>Declined correctly when absent (8)<\/td>\n<td align=\"right\">8<\/td>\n<td align=\"right\">8<\/td>\n<td align=\"right\">0<\/td>\n<\/tr>\n<tr>\n<td>Decoys ignored (3)<\/td>\n<td align=\"right\">3<\/td>\n<td align=\"right\">3<\/td>\n<td align=\"right\">0<\/td>\n<\/tr>\n<tr>\n<td>Invented an answer<\/td>\n<td align=\"right\">0<\/td>\n<td align=\"right\">0<\/td>\n<td align=\"right\">7<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The regex won. Thirty out of thirty, in zero milliseconds, at zero watts<br \/>\nabove idle.<\/p>\n<p>That result is boring until you read the two the best model missed.<\/p>\n<p>On <code>ORD-999000: wrong address, please hold.<\/code> it answered <code>999000<\/code>, dropping<br \/>\nthe prefix. Annoying, recoverable, the kind of thing you fix with a<br \/>\npost-processing step you should not need.<\/p>\n<p>On <code>ORD-111222 please refund<\/code> it answered <code>NONE<\/code>.<\/p>\n<p>That is the shortest and least decorated input in the whole set. Almost the<br \/>\nentire message is the order number. And the model returned a confident,<br \/>\ncorrectly formatted assertion that there is no order number present. In a<br \/>\npipeline, that is a refund request for an order that exists arriving at a<br \/>\nsystem that has decided it does not.<\/p>\n<p>A regex cannot make that mistake. Not &#8220;rarely does&#8221; \u2014 cannot. The two<br \/>\nerrors separating 28 from 30 are both of a class that the deterministic<br \/>\nbaseline is structurally incapable of producing, which is a different and<br \/>\nmuch stronger statement than a two-point accuracy gap.<\/p>\n<h3>The 360M result needs two numbers, not one<\/h3>\n<p>By the contract, <code>smollm2:360m<\/code> scored 8 of 22 on the cases that contained<br \/>\nan identifier. That reads like a model that cannot find things.<\/p>\n<p>It is not. All fourteen of its wrong answers are the correct six digits<br \/>\nwith <code>ORD-<\/code> stripped. <code>ORD-556120<\/code> came back as <code>556120<\/code>, fourteen times<br \/>\nout of fourteen.<\/p>\n<p>It located the right identifier in 22 of 22 and formatted it correctly in<br \/>\n8. Those are two different skills and the single score hides one of them.<br \/>\nThe model understood the task perfectly and could not obey the output<br \/>\ncontract, which is a completely different engineering problem than &#8220;the<br \/>\nmodel is too small,&#8221; and it has a completely different fix. (Constrained<br \/>\ndecoding. I have not tested it. It is the obvious follow-up and it is not<br \/>\nin this post.)<\/p>\n<p>The decoys follow from the same gap. It handed back <code>MEM-443322<\/code>,<br \/>\n<code>1Z999AA10123456784<\/code> and <code>503-555-0142<\/code> verbatim, all three. It has no<br \/>\nmodel of the format at all. It returns whatever in the message looks most<br \/>\nlike an identifier.<\/p>\n<h3>Which suggests a use for it that is not extraction<\/h3>\n<p>A model with perfect recall and no precision is not a bad extractor. It is<br \/>\na candidate generator that has been asked to do the wrong job.<\/p>\n<p>Twenty-two of twenty-two located, eight of twenty-two formatted. Put that<br \/>\noutput through the regex as a validator rather than trusting it directly,<br \/>\nand you get the model&#8217;s recall with the pattern&#8217;s precision. The model<br \/>\nproposes anything that looks like an identifier, including format variants<br \/>\nthe pattern was never written for. The pattern rejects anything malformed.<\/p>\n<p>Crucially, neither half of that pipeline can produce the error the 1.5B<br \/>\nmodel produced alone. The model cannot cause a false negative because it<br \/>\ndoes not decide, and the regex cannot invent because it only matches.<\/p>\n<p>I have not tested that composition and it is not in these numbers. But it<br \/>\nis the one place in the extraction result where a small model has a job<br \/>\nworth giving it, and it is a job where being sloppy is free.<\/p>\n<p><strong>Verdict for extraction itself: Unnecessary.<\/strong> Write the regex.<\/p>\n<h2>Classification: routing a message to one of six queues<\/h2>\n<p>Appointment, billing, availability, complaint, question, escalate. Fifty<br \/>\nmessages. The instruction said to use <code>escalate<\/code> when the message needs a<br \/>\nperson, including when you cannot tell what it is about.<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th align=\"right\">qwen2.5:1.5b<\/th>\n<th align=\"right\">smollm2:360m<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Overall<\/td>\n<td align=\"right\">23\/50 (46%)<\/td>\n<td align=\"right\">15\/50 (30%)<\/td>\n<\/tr>\n<tr>\n<td>Labels used<\/td>\n<td align=\"right\">6 of 6<\/td>\n<td align=\"right\">3 of 6<\/td>\n<\/tr>\n<tr>\n<td>Most common label<\/td>\n<td align=\"right\">complaint, 38%<\/td>\n<td align=\"right\">escalate, <strong>86%<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Wrong and not an escalation<\/td>\n<td align=\"right\"><strong>26<\/strong><\/td>\n<td align=\"right\">3 (meaningless)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The 360M model answered <code>escalate<\/code> to 43 of 50 messages, which is not<br \/>\nclassification, and there is nothing further to say about it here. The rest<br \/>\nof this section is the 1.5B model.<\/p>\n<p>It is classifying, and it is not good at it. Forty-six percent, with<br \/>\ntwenty-six messages routed confidently to a wrong queue that was not the<br \/>\nsafe one.<\/p>\n<p>Read those twenty-six and the verdict writes itself:<\/p>\n<ul>\n<li>Someone slipped on the steps by the barrel room and hurt their wrist \u2192<br \/>\n<strong>complaint<\/strong><\/li>\n<li>A formal notice regarding a trademark \u2192 <strong>complaint<\/strong><\/li>\n<li>&#8220;I have a severe sulfite allergy, what is actually in the wine&#8221; \u2192<br \/>\n<strong>complaint<\/strong><\/li>\n<li>A reporter asking about the 2024 harvest \u2192 <strong>question<\/strong><\/li>\n<li>A distributor wanting to carry the wine in forty stores \u2192 <strong>question<\/strong><\/li>\n<\/ul>\n<p>An injury report, a legal notice and an allergy question, all routed to the<br \/>\ncomplaints queue, confidently, with no escalation. All five billing<br \/>\nmessages about money problems also went to complaints.<\/p>\n<p>Forty-six percent sounds like something you could work with. Twenty-six<br \/>\nconfident misroutes including an injury and a legal notice is not something<br \/>\nyou can work with.<\/p>\n<h3>And then I broke the score down by class<\/h3>\n<p>This is the part I nearly missed, and it is the most useful thing in the<br \/>\nexperiment.<\/p>\n<table>\n<thead>\n<tr>\n<th>Label<\/th>\n<th align=\"right\">Correct<\/th>\n<th align=\"right\">Of<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>complaint<\/td>\n<td align=\"right\">6<\/td>\n<td align=\"right\">6<\/td>\n<\/tr>\n<tr>\n<td>question<\/td>\n<td align=\"right\">9<\/td>\n<td align=\"right\">10<\/td>\n<\/tr>\n<tr>\n<td>availability<\/td>\n<td align=\"right\">3<\/td>\n<td align=\"right\">6<\/td>\n<\/tr>\n<tr>\n<td>appointment<\/td>\n<td align=\"right\">4<\/td>\n<td align=\"right\">9<\/td>\n<\/tr>\n<tr>\n<td>billing<\/td>\n<td align=\"right\">1<\/td>\n<td align=\"right\">7<\/td>\n<\/tr>\n<tr>\n<td>escalate<\/td>\n<td align=\"right\"><strong>0<\/strong><\/td>\n<td align=\"right\"><strong>12<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Two classes near perfect. Four poor. One at zero.<\/p>\n<p>The split is not difficulty, and it is not class size. It is what kind of<br \/>\nlabel each one is.<\/p>\n<p><code>complaint<\/code> and <code>question<\/code> are <strong>topics<\/strong>. You can tell what a message is<br \/>\nfrom the words in it. Someone complaining sounds like someone complaining.<\/p>\n<p><code>escalate<\/code> is a <strong>routing decision<\/strong>. Nothing in the content of a message<br \/>\nmakes it an escalation. It is an escalation because of what should happen<br \/>\nnext. &#8220;I have a severe sulfite allergy, what is actually in the wine&#8221; and<br \/>\n&#8220;what time do you close&#8221; are both questions about the business, with the<br \/>\nsame grammar and the same tone, and one of them is a liability event.<\/p>\n<p>The model did topics at 90 to 100 percent and routing decisions at zero.<br \/>\nAnd <code>billing<\/code> sat in the middle for exactly the same reason: a message<br \/>\nabout being charged twice <em>is<\/em> a complaint, as text. The label is about<br \/>\nwhich department owns the problem, which is not a property of the sentence.<\/p>\n<p><strong>Small models answer &#8220;what is this about.&#8221; They do not answer &#8220;what should<br \/>\nwe do about it.&#8221;<\/strong><\/p>\n<p>That reframes the five examples above. They are not five unlucky<br \/>\nmisroutings. All five are escalations, and escalate scored zero of twelve.<br \/>\nThe model was never going to find them, because finding them requires<br \/>\nknowing a consequence that the text does not contain.<\/p>\n<p>Two caveats on how hard to lean on this. The classes hold between six and<br \/>\ntwelve cases each, so &#8220;six of six&#8221; is six cases and the individual numbers<br \/>\nare thin. And this is one model. The pattern across all six classes is<br \/>\nconsistent enough that I believe the explanation, but I am offering it as<br \/>\nthe best account of the data rather than as a measurement.<\/p>\n<p><strong>Verdict: Insufficient.<\/strong> And the actionable version: if one of your<br \/>\nlabels means &#8220;a human needs to see this,&#8221; a small model will not find it.<br \/>\nBuild that one as a rule.<\/p>\n<h2>Grounded QA: answering a question from four documents<\/h2>\n<p>Fifteen questions against a 650-token corpus: club policy, tasting room,<br \/>\nproduction notes, shipping. Eight are answerable from one passage, three<br \/>\nrequire combining two, and four have no answer in the material at all.<\/p>\n<p>I read all thirty answers by hand against the corpus. That is the only way<br \/>\nto score this, and I will say why in a moment.<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th align=\"right\">qwen2.5:1.5b<\/th>\n<th align=\"right\">smollm2:360m<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Single passage (8)<\/td>\n<td align=\"right\"><strong>8\/8<\/strong><\/td>\n<td align=\"right\">7\/8<\/td>\n<\/tr>\n<tr>\n<td>Two passage (3)<\/td>\n<td align=\"right\">1\/3<\/td>\n<td align=\"right\">0\/3<\/td>\n<\/tr>\n<tr>\n<td><strong>Answerable total (11)<\/strong><\/td>\n<td align=\"right\"><strong>9\/11<\/strong><\/td>\n<td align=\"right\">7\/11<\/td>\n<\/tr>\n<tr>\n<td>Correctly said &#8220;not in the material&#8221; (4)<\/td>\n<td align=\"right\">2\/4<\/td>\n<td align=\"right\">0\/4<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Three boundaries separate cleanly here, and they are the useful result.<\/p>\n<h3>Retrieval works<\/h3>\n<p>The 1.5B model answered all eight single-passage questions correctly. Six<br \/>\nbottles in the Vineyard tier. Twenty dollars per person. September 9th.<br \/>\nThirty-four states. No dogs indoors. 21.8 Brix on the 12th. Twenty-five<br \/>\ndollar restocking fee. Ten people on the barrel room tour.<\/p>\n<p>Asked what happens if you join the club and immediately want out, it<br \/>\nreturned the two sentences that answer the question and stopped:<\/p>\n<blockquote><p>\n  Cancellation requires one completed shipment. Members who cancel before<br \/>\n  receiving a shipment are charged for that shipment.\n<\/p><\/blockquote>\n<p>That is competent lookup across four documents, on a $170 computer, with<br \/>\nthe wifi switched off. I did not expect it and it is the most encouraging<br \/>\nthing in the weekend.<\/p>\n<h3>Combination does not<\/h3>\n<p>One club member brings five guests on a Saturday. Club policy waives the<br \/>\ntasting fee for the member plus three guests. The tasting room charges<br \/>\ntwenty dollars a head. Six people, four waived, two paying: forty dollars.<\/p>\n<p><strong>Both models answered twenty.<\/strong><\/p>\n<p>Neither could hold &#8220;plus three guests&#8221; against a party of six and subtract.<br \/>\nThe 1.5B model did get the second half right, correctly working out that a<br \/>\nparty of six is not a group <em>larger<\/em> than six and therefore needs no<br \/>\nreservation, which is the only piece of multi-document reasoning either<br \/>\nmodel performed all weekend.<\/p>\n<p>The second two-passage question is the one I keep thinking about. Asked why<br \/>\nsomeone would not find a 2024 rose and what they would be offered instead,<br \/>\nthe 1.5B model quoted the corpus exactly, and then:<\/p>\n<blockquote><p>\n  &#8230;the reference material states that &#8220;We did not bottle a rose in 2024.<br \/>\n  The fruit that would normally go to rose was needed to make volume on the<br \/>\n  estate Pinot Noir.&#8221; Therefore, the 2024 rose would be replaced with the<br \/>\n  fruit that would typically go into the estate Pinot Noir production.\n<\/p><\/blockquote>\n<p>The quotation is verbatim and right. The next sentence reverses the only<br \/>\nrelationship in it. The fruit went <em>into<\/em> the Pinot Noir, so what you would<br \/>\nbe offered is the Pinot Noir. You cannot be offered fruit.<\/p>\n<p>Correct retrieval and inverted inference, in consecutive sentences, with<br \/>\nthe evidence on screen between them.<\/p>\n<p>The 360M model&#8217;s multi-document failures are worse than wrong. They are<br \/>\ncontaminated. It took the fourteen-days-notice rule, which governs skipping<br \/>\na club shipment, and applied it to booking a tasting room reservation. It<br \/>\nreached into the shipping document to answer a cancellation question. Given<br \/>\nmore documents to search, it finds more wrong ones.<\/p>\n<h3>Knowing the answer is absent is the weakest link<\/h3>\n<p>Four questions have no answer in the corpus, chosen to be things a reader<br \/>\nwould reasonably expect a winery to have written down: a wine&#8217;s alcohol<br \/>\npercentage, who the winemaker is, what a club tier costs, what yeast was<br \/>\nused.<\/p>\n<p>The 1.5B model abstained on two and fabricated two. The 360M model<br \/>\nfabricated all four.<\/p>\n<p>And the fabrications are a more dangerous species than invention. Asked for<br \/>\nthe alcohol percentage of the 2024 estate Pinot Noir, the 1.5B model<br \/>\nanswered:<\/p>\n<blockquote><p>\n  The 2024 estate Pinot Noir had a Brix range of 23.1 to 24.6 at pick.\n<\/p><\/blockquote>\n<p>Every component of that is true. Right wine, right vintage, right numbers,<br \/>\nright document. Brix is sugar at harvest and has nothing to do with alcohol<br \/>\nin the bottle. It reached for the nearest real fact instead of making one<br \/>\nup, and produced something that reads as expertise while answering a<br \/>\nquestion nobody asked. If you do not know what Brix is, that answer is<br \/>\nindistinguishable from a correct one.<\/p>\n<p>Both models made the same mistake on what the Cellar tier costs: both<br \/>\nanswered &#8220;twelve bottles per quarter.&#8221; Grounding gave them the right tier<br \/>\nand they handed back the quantity in place of the price.<\/p>\n<h3>The one answer the evidence did not change<\/h3>\n<p>The 360M model, asked whether a dog can come inside the tasting room:<\/p>\n<blockquote><p>\n  Yes, you can bring a dog inside the tasting room.\n<\/p><\/blockquote>\n<p>The corpus says the patio is dog friendly and the indoor room is not,<br \/>\nservice animals excepted. I have the same answer from a run where the model<br \/>\nhad no corpus at all, and it is identical byte for byte. Having the right<br \/>\nanswer directly in the prompt changed nothing.<\/p>\n<p>In a category where being wrong means a family drives out to a winery and<br \/>\ngets turned around at the door.<\/p>\n<p><strong>Verdict: Marginal.<\/strong> Usable with a person reading the output. Not usable<br \/>\nwithout one.<\/p>\n<h2>What it costs<\/h2>\n<table>\n<thead>\n<tr>\n<th>Workflow<\/th>\n<th>Model<\/th>\n<th align=\"right\">tok\/s<\/th>\n<th align=\"right\">Prompt<\/th>\n<th align=\"right\">Answer<\/th>\n<th align=\"right\">Median call<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>extraction<\/td>\n<td>qwen2.5:1.5b<\/td>\n<td align=\"right\">4.3<\/td>\n<td align=\"right\">92<\/td>\n<td align=\"right\">9<\/td>\n<td align=\"right\">5.0 s<\/td>\n<\/tr>\n<tr>\n<td>classification<\/td>\n<td>qwen2.5:1.5b<\/td>\n<td align=\"right\">7.5<\/td>\n<td align=\"right\">104<\/td>\n<td align=\"right\">2<\/td>\n<td align=\"right\">2.9 s<\/td>\n<\/tr>\n<tr>\n<td>QA<\/td>\n<td>qwen2.5:1.5b<\/td>\n<td align=\"right\">3.7<\/td>\n<td align=\"right\">708<\/td>\n<td align=\"right\">15<\/td>\n<td align=\"right\">6.9 s<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Two things in here surprised me.<\/p>\n<p><strong>The compute tax does not track parameter count.<\/strong> The 1.5B model is<br \/>\n<em>slower<\/em> than the 360M on extraction, 4.3 against 5.9 tokens per second,<br \/>\nand <em>faster<\/em> on classification, 7.5 against 6.2, because on classification<br \/>\nit answers in two tokens where the smaller model takes six. Four times the<br \/>\nparameters, and which one wins depends entirely on how much each one<br \/>\ndecides to say.<\/p>\n<p><strong>A bigger prompt is cheap. A longer answer is what you pay for.<\/strong> I have a<br \/>\nclean comparison here by accident: an earlier QA run went out with 97-token<br \/>\nprompts and the real one with 708. Seven times the prompt added fifteen<br \/>\npercent to the wait before output started, because prompt evaluation<br \/>\nparallelizes. The latency increase came from answers three times longer,<br \/>\nplus a twenty-one percent drop in per-token generation speed from attending<br \/>\nover a longer context. Median latency went from 3.6 to 6.9 seconds and the<br \/>\nslowest single call took ninety-eight.<\/p>\n<p>If you are sizing a local system, that is the inversion to internalize.<br \/>\nStuffing more context in is affordable. Letting the model be chatty is not.<\/p>\n<p><strong>And check your thermals, which is not optional advice.<\/strong> My first attempt<br \/>\nat this ran with heat sinks and no fan. Thirty calls to a 360M model was<br \/>\nenough to hit the Pi 4&#8217;s soft temperature limit and start capping the clock.<br \/>\n<code>vcgencmd get_throttled<\/code> returned <code>0xe0000<\/code>. I threw those latency numbers<br \/>\naway.<\/p>\n<p>Nothing in Ollama&#8217;s output, my scorer, or the result files said a word about<br \/>\nit. Every number on screen was internally consistent and quietly describing<br \/>\na machine that was slowing down. The only way to find out was to ask the<br \/>\nfirmware a question I had to already suspect the answer to. Buy the fan.<\/p>\n<p><strong>And it does run with the network off.<\/strong> I switched the radio off, proved<br \/>\nthere was no route out, and ran one call from each workflow. All three<br \/>\nworked. The claim that supports is narrow and worth stating carefully: this<br \/>\nconfiguration, on this afternoon, ran with no route to the internet. It is<br \/>\nnot a claim that local models are private or that this setup is secure, and<br \/>\na Pi can be misconfigured as thoroughly as anything in a data centre. But<br \/>\nfor the specific question of whether the inference needs the network, it<br \/>\ndoes not.<\/p>\n<h2>So: do you actually need an LLM?<\/h2>\n<table>\n<thead>\n<tr>\n<th>Workload<\/th>\n<th>Verdict<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Pull a known-format identifier out of text<\/td>\n<td><strong>Unnecessary<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Route a message to one of six queues<\/td>\n<td><strong>Insufficient<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Answer a question from your own documents<\/td>\n<td><strong>Marginal<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Three workflows that all sound like language tasks, three different answers,<br \/>\nand not one of them is &#8220;yes, use a model.&#8221;<\/p>\n<p>And the same line runs through all three. Extraction asks what this string<br \/>\nis. Topic labels ask what this message is about. Single-passage lookup asks<br \/>\nwhat the document says. Those went well, or well enough. Escalate asks what<br \/>\nwe should do, the fee question asks what the customer owes, and the<br \/>\nunanswerable questions ask whether we know. Zero of twelve, both models<br \/>\nwrong, and two of four.<\/p>\n<p><strong>A small model will tell you what something is. It will not tell you what<br \/>\nto do about it.<\/strong> Every result here falls on one side or the other of that<br \/>\nsentence, and I did not have it written down when I started.<\/p>\n<p>Which gives four properties worth checking before you reach for one:<\/p>\n<ol>\n<li><strong>The answer is already present in the input.<\/strong> Not inferable from it.<br \/>\nPresent in it.<\/li>\n<li><strong>There is exactly one of it.<\/strong> The moment two facts have to be<br \/>\ncombined, this stops working, and the arithmetic in that tasting fee<br \/>\nquestion is the proof.<\/li>\n<li><strong>The output is short.<\/strong> Longer answers are where the latency lives on<br \/>\nthis hardware, and where the fabrication lives everywhere.<\/li>\n<li><strong>Something other than the model decides whether the output is<br \/>\nacceptable.<\/strong> A regex, a schema, a person, an index that either returns<br \/>\nrows or does not.<\/li>\n<\/ol>\n<p>Take away any one of those four and the experiment says do not do it. The<br \/>\n46 percent and the zero of twelve are what it looks like when you ask a<br \/>\nsmall model to be the thing that decides.<\/p>\n<p>And if you have all four, you may not need the model either. That is worth<br \/>\nchecking first, because it was true for one of my three workflows and I did<br \/>\nnot expect it to be.<\/p>\n<h2>Where I was wrong<\/h2>\n<p>Twenty-two predictions, committed before I installed anything. Ten right,<br \/>\neleven wrong, one unresolvable.<\/p>\n<p>Two of my seven confident calls broke. I was sure the 360M model would get<br \/>\nmost of the easy extraction cases, and it got 8 of 22 by failing at format<br \/>\nrather than at finding. I was sure it would produce invalid output often,<br \/>\nand it did so once in fifty, then emitted the same <em>valid<\/em> word forty-three<br \/>\ntimes.<\/p>\n<p>But the number that bothers me is not in the confident column.<\/p>\n<table>\n<thead>\n<tr>\n<th>Confidence<\/th>\n<th align=\"right\">Right<\/th>\n<th align=\"right\">Wrong<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Confident<\/td>\n<td align=\"right\">5<\/td>\n<td align=\"right\">2<\/td>\n<\/tr>\n<tr>\n<td>Leaning<\/td>\n<td align=\"right\">2<\/td>\n<td align=\"right\">7<\/td>\n<\/tr>\n<tr>\n<td>Coin flip<\/td>\n<td align=\"right\">3<\/td>\n<td align=\"right\">2<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>My hunches did worse than my coin flips.<\/strong> Two of ten on the calls where<br \/>\nI had a feeling but not much behind it, three of five on the ones I<br \/>\nexplicitly labeled as guesses.<\/p>\n<p>That ordering is backwards and it is the most useful thing on the sheet. On<br \/>\nthe coin flips I knew I was guessing, so I guessed cleanly. On the leaning<br \/>\ncalls I was pattern-matching against intuitions about model behavior that<br \/>\nturned out not to describe these models at all, and the feeling of having a<br \/>\nreason was the problem. If I had marked all twelve of those as coin flips I<br \/>\nwould have scored better.<\/p>\n<p>I will take that over a clean result. The honest version of this exercise is<br \/>\nnot &#8220;here are the numbers,&#8221; it is &#8220;here is what I believed, here is what<br \/>\nhappened, and here is the gap.&#8221;<\/p>\n<p>The gap, this time, was mostly in the hunches.<\/p>\n<hr \/>\n<p><em>The harness that produced these numbers had five measurement flaws in it,<br \/>\none of which manufactured an entire finding out of a missing directory and<br \/>\nnearly got published. That is a separate post.<\/em><\/p>\n<p><em>Everything here is reproducible: fixtures, runner, scorer, the committed<br \/>\npredictions and the run record are at<br \/>\n<a href=\"https:\/\/github.com\/kenwalger\/right-size-the-model\">github.com\/kenwalger\/right-size-the-model<\/a>.<br \/>\nFixture set v1. Read <code>LIMITATIONS.md<\/code> before citing any of these numbers:<br \/>\ntwo models, one board, one quantization, ninety-five cases. This is an<br \/>\nillustration of a method, not a benchmark.<\/em><\/p>\n<a class=\"synved-social-button synved-social-button-share synved-social-size-48 synved-social-resolution-single synved-social-provider-facebook nolightbox\" data-provider=\"facebook\" target=\"_blank\" rel=\"nofollow\" title=\"Share on Facebook\" href=\"https:\/\/www.facebook.com\/sharer.php?u=https%3A%2F%2Fwww.kenwalger.com%2Fblog%2Fwp-json%2Fwp%2Fv2%2Fposts%2F1955&amp;t=Do%20You%20Actually%20Need%20an%20LLM%3F&amp;s=100&amp;p[url]=https%3A%2F%2Fwww.kenwalger.com%2Fblog%2Fwp-json%2Fwp%2Fv2%2Fposts%2F1955&amp;p[images][0]=&amp;p[title]=Do%20You%20Actually%20Need%20an%20LLM%3F\" style=\"font-size: 0px;width:48px;height:48px;margin:0;margin-bottom:5px;margin-right:5px\"><img loading=\"lazy\" decoding=\"async\" alt=\"Facebook\" title=\"Share on Facebook\" class=\"synved-share-image synved-social-image synved-social-image-share\" width=\"48\" height=\"48\" style=\"display: inline;width:48px;height:48px;margin: 0;padding: 0;border: none;box-shadow: none\" src=\"https:\/\/www.kenwalger.com\/blog\/wp-content\/plugins\/social-media-feather\/synved-social\/image\/social\/regular\/96x96\/facebook.png\" \/><\/a><a class=\"synved-social-button synved-social-button-share synved-social-size-48 synved-social-resolution-single synved-social-provider-twitter nolightbox\" data-provider=\"twitter\" target=\"_blank\" rel=\"nofollow\" title=\"Share on Twitter\" href=\"https:\/\/twitter.com\/intent\/tweet?url=https%3A%2F%2Fwww.kenwalger.com%2Fblog%2Fwp-json%2Fwp%2Fv2%2Fposts%2F1955&amp;text=Hey%20check%20this%20out\" style=\"font-size: 0px;width:48px;height:48px;margin:0;margin-bottom:5px;margin-right:5px\"><img loading=\"lazy\" decoding=\"async\" alt=\"twitter\" title=\"Share on Twitter\" class=\"synved-share-image synved-social-image synved-social-image-share\" width=\"48\" height=\"48\" style=\"display: inline;width:48px;height:48px;margin: 0;padding: 0;border: none;box-shadow: none\" src=\"https:\/\/www.kenwalger.com\/blog\/wp-content\/plugins\/social-media-feather\/synved-social\/image\/social\/regular\/96x96\/twitter.png\" \/><\/a><a class=\"synved-social-button synved-social-button-share synved-social-size-48 synved-social-resolution-single synved-social-provider-reddit nolightbox\" data-provider=\"reddit\" target=\"_blank\" rel=\"nofollow\" title=\"Share on Reddit\" href=\"https:\/\/www.reddit.com\/submit?url=https%3A%2F%2Fwww.kenwalger.com%2Fblog%2Fwp-json%2Fwp%2Fv2%2Fposts%2F1955&amp;title=Do%20You%20Actually%20Need%20an%20LLM%3F\" style=\"font-size: 0px;width:48px;height:48px;margin:0;margin-bottom:5px;margin-right:5px\"><img loading=\"lazy\" decoding=\"async\" alt=\"reddit\" title=\"Share on Reddit\" class=\"synved-share-image synved-social-image synved-social-image-share\" width=\"48\" height=\"48\" style=\"display: inline;width:48px;height:48px;margin: 0;padding: 0;border: none;box-shadow: none\" src=\"https:\/\/www.kenwalger.com\/blog\/wp-content\/plugins\/social-media-feather\/synved-social\/image\/social\/regular\/96x96\/reddit.png\" \/><\/a><a class=\"synved-social-button synved-social-button-share synved-social-size-48 synved-social-resolution-single synved-social-provider-linkedin nolightbox\" data-provider=\"linkedin\" target=\"_blank\" rel=\"nofollow\" title=\"Share on Linkedin\" href=\"https:\/\/www.linkedin.com\/shareArticle?mini=true&amp;url=https%3A%2F%2Fwww.kenwalger.com%2Fblog%2Fwp-json%2Fwp%2Fv2%2Fposts%2F1955&amp;title=Do%20You%20Actually%20Need%20an%20LLM%3F\" style=\"font-size: 0px;width:48px;height:48px;margin:0;margin-bottom:5px;margin-right:5px\"><img loading=\"lazy\" decoding=\"async\" alt=\"linkedin\" title=\"Share on Linkedin\" class=\"synved-share-image synved-social-image synved-social-image-share\" width=\"48\" height=\"48\" style=\"display: inline;width:48px;height:48px;margin: 0;padding: 0;border: none;box-shadow: none\" src=\"https:\/\/www.kenwalger.com\/blog\/wp-content\/plugins\/social-media-feather\/synved-social\/image\/social\/regular\/96x96\/linkedin.png\" \/><\/a><a class=\"synved-social-button synved-social-button-share synved-social-size-48 synved-social-resolution-single synved-social-provider-mail nolightbox\" data-provider=\"mail\" rel=\"nofollow\" title=\"Share by email\" href=\"mailto:?subject=Do%20You%20Actually%20Need%20an%20LLM%3F&amp;body=Hey%20check%20this%20out:%20https%3A%2F%2Fwww.kenwalger.com%2Fblog%2Fwp-json%2Fwp%2Fv2%2Fposts%2F1955\" style=\"font-size: 0px;width:48px;height:48px;margin:0;margin-bottom:5px\"><img loading=\"lazy\" decoding=\"async\" alt=\"mail\" title=\"Share by email\" class=\"synved-share-image synved-social-image synved-social-image-share\" width=\"48\" height=\"48\" style=\"display: inline;width:48px;height:48px;margin: 0;padding: 0;border: none;box-shadow: none\" src=\"https:\/\/www.kenwalger.com\/blog\/wp-content\/plugins\/social-media-feather\/synved-social\/image\/social\/regular\/96x96\/mail.png\" \/><\/a>","protected":false},"excerpt":{"rendered":"<p>I spent a weekend running two small language models against three jobs I would normally hand to a language model without thinking about it. For one of those jobs I also wrote a regular expression, as a control. The regex won. Thirty test cases out of thirty, in zero milliseconds, from a pattern that took &hellip; <a href=\"https:\/\/www.kenwalger.com\/blog\/ai\/do-you-actually-need-an-llm\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Do You Actually Need an LLM?&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"pmpro_default_level":"","_monsterinsights_skip_tracking":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1669],"tags":[1994,1761,1762,1995,1993,1996,1686],"yst_prominent_words":[],"class_list":["post-1955","post","type-post","status-publish","format-standard","hentry","category-ai","tag-benchmarking","tag-local-ai","tag-ollama","tag-qwen","tag-raspberry-pi","tag-right-size-the-model","tag-small-language-models","pmpro-has-access"],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p8lx70-vx","jetpack-related-posts":[],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/posts\/1955","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/comments?post=1955"}],"version-history":[{"count":4,"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/posts\/1955\/revisions"}],"predecessor-version":[{"id":1959,"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/posts\/1955\/revisions\/1959"}],"wp:attachment":[{"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/media?parent=1955"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/categories?post=1955"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/tags?post=1955"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/www.kenwalger.com\/blog\/wp-json\/wp\/v2\/yst_prominent_words?post=1955"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}