<?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: Lajos Bencz</title>
    <description>The latest articles on DEV Community by Lajos Bencz (@lajosbencz).</description>
    <link>https://dev.to/lajosbencz</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%2F4030908%2F8dd822fd-c301-4760-8bd4-c0dad4de68cc.png</url>
      <title>DEV Community: Lajos Bencz</title>
      <link>https://dev.to/lajosbencz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lajosbencz"/>
    <language>en</language>
    <item>
      <title>Running an LLM agent entirely in your browser</title>
      <dc:creator>Lajos Bencz</dc:creator>
      <pubDate>Wed, 15 Jul 2026 20:59:54 +0000</pubDate>
      <link>https://dev.to/lajosbencz/running-an-llm-agent-entirely-in-your-browser-5foe</link>
      <guid>https://dev.to/lajosbencz/running-an-llm-agent-entirely-in-your-browser-5foe</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I fine-tuned LiquidAI's LFM2.5 (230M and 350M) into a generic front-end agent that runs &lt;em&gt;entirely in the browser&lt;/em&gt; - no server, no API key, no cloud costs.&lt;br&gt;
It doesn't just chat; it calls real tools to browse a catalog, answers grounded questions, and manages a cart.&lt;br&gt;
The trick: it's trained on interaction &lt;em&gt;patterns&lt;/em&gt;, not domain facts, so the same weights drive a coffee store, an absurdist emporium, or a corner grocer - with zero retraining.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lajosbencz.github.io/frontend-agent/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt; on Github pages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;Most "AI assistant" features are a text box wired to a frontier model in someone's data center.&lt;br&gt;
That's fine, but it means a network round-trip per turn, a bill per token, and your users' inputs leaving the device.&lt;/p&gt;

&lt;p&gt;I wanted the opposite: an agent small enough to ship &lt;em&gt;with the page&lt;/em&gt;.&lt;br&gt;
Load it once, run it on the user's own hardware (WebGPU if available, CPU/WASM otherwise via &lt;a href="https://github.com/ngxson/wllama" rel="noopener noreferrer"&gt;wllama&lt;/a&gt;), and let it actually &lt;em&gt;do things&lt;/em&gt; in the UI instead of just describing them.&lt;/p&gt;

&lt;p&gt;The bet was that a small model can't hold a useful amount of world knowledge, but it &lt;em&gt;can&lt;/em&gt; learn a compact set of behaviors well enough to be useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns, not knowledge
&lt;/h2&gt;

&lt;p&gt;The model does &lt;strong&gt;not&lt;/strong&gt; know what a "BrewCraft Pico" is, or that it costs $699.&lt;/p&gt;

&lt;p&gt;It knows how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pick the right tool for an intent&lt;/li&gt;
&lt;li&gt;bind arguments and item ids correctly&lt;/li&gt;
&lt;li&gt;resolve references ("the second one", "a dozen of those")&lt;/li&gt;
&lt;li&gt;ground an answer in retrieved text, and &lt;em&gt;refuse&lt;/em&gt; when the text doesn't contain the answer&lt;/li&gt;
&lt;li&gt;steer back politely when asked something off-topic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything domain-specific is injected &lt;strong&gt;at runtime&lt;/strong&gt;.&lt;br&gt;
Each turn, the host app hands the model a compact context: the items currently on screen (with ids and prices), the cart, and any retrieved knowledge.&lt;br&gt;
The model grounds strictly in that. Swap the store, swap the injected context - the same weights work.&lt;/p&gt;

&lt;p&gt;That's why the demo ships three storefronts on one model.&lt;br&gt;
And critically, they were &lt;strong&gt;held out of training entirely&lt;/strong&gt;.&lt;br&gt;
If the model can run a store it never saw, the generalization works.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;Three design choices carry most of the weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A frozen tool roster.&lt;/strong&gt;&lt;br&gt;
Early on I tried teaching the model to read &lt;em&gt;arbitrary&lt;/em&gt; tool schemas - variable tool names and arguments per training example so it wouldn't memorize a fixed set.&lt;br&gt;
For a 230M model, that was too much to ask; it garbled calls.&lt;br&gt;
So the roster is now &lt;strong&gt;fixed&lt;/strong&gt;: eight tools with stable names (&lt;code&gt;list_items&lt;/code&gt;, &lt;code&gt;get_item&lt;/code&gt;, &lt;code&gt;search_knowledge&lt;/code&gt;, &lt;code&gt;add_to_cart&lt;/code&gt;, &lt;code&gt;remove_from_cart&lt;/code&gt;, &lt;code&gt;clear_cart&lt;/code&gt;, &lt;code&gt;checkout&lt;/code&gt;, &lt;code&gt;navigate&lt;/code&gt;) that the model learns by name.&lt;br&gt;
A small, memorizable action space.&lt;br&gt;
The one place variety survives is the &lt;em&gt;filter set&lt;/em&gt; on &lt;code&gt;list_items&lt;/code&gt;, which the model reads from the injected schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAG as a tool, not a pipeline.&lt;/strong&gt;&lt;br&gt;
Retrieval is just &lt;code&gt;list_items&lt;/code&gt; (catalog) and &lt;code&gt;search_knowledge&lt;/code&gt; (guides, policies).&lt;br&gt;
The model decides when to call them and grounds its reply in the results.&lt;br&gt;
The backend is swappable - BM25, vector, hybrid - because only the &lt;em&gt;result shape&lt;/em&gt; is the contract.&lt;br&gt;
The demo uses in-browser BM25; nothing leaves the machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grammar-constrained decoding.&lt;/strong&gt;&lt;br&gt;
Tool calls are decoded against a GBNF grammar, so every call is syntactically valid and - the important part - every id the model emits is one that actually exists in the injected context.&lt;br&gt;
It literally cannot hallucinate a product id.&lt;br&gt;
That single constraint removes a whole class of failures that would otherwise sink a model this small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training it
&lt;/h2&gt;

&lt;p&gt;The pipeline is synthetic-data distillation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Defined ~18 interaction &lt;em&gt;recipes&lt;/em&gt; (add-to-cart, browse, compare, price lookup, knowledge Q&amp;amp;A, refusal, off-scope steering, small talk, ...).
Each recipe generates short, bounded exchanges with an example runtime context attached.&lt;/li&gt;
&lt;li&gt;A teacher model writes the natural-language parts (the customer's phrasing, the grounded reply); the &lt;em&gt;structure&lt;/em&gt; is deterministic and generated in code, so the tool calls and ids are always correct.&lt;/li&gt;
&lt;li&gt;Fine-tune the base model on ~30M tokens of this.
Full fine-tune, fits on a single 16GB GPU.&lt;/li&gt;
&lt;li&gt;Evaluate on verticals the model never saw - plus a demo-faithful "does it survive the real UI" harness.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One deliberate choice worth flagging for anyone doing the same: &lt;strong&gt;train on the &lt;em&gt;pattern&lt;/em&gt;, not a blocklist.&lt;/strong&gt;&lt;br&gt;
For "handle an off-topic request," it's tempting to enumerate a fixed list of off-topics.&lt;br&gt;
Don't - the model just memorizes those strings.&lt;br&gt;
Instead, generate a genuinely different off-topic examples every time (thousands of distinct ones) so it learns the &lt;em&gt;behavior&lt;/em&gt; of steering, not fifteen banned phrases.&lt;/p&gt;

&lt;p&gt;Some LLM providers might cache requests; this would duplicate training data, where we expect variety. To avoid this, seed each request appropriately.&lt;/p&gt;

&lt;p&gt;The quality of the teaching model is of course extremely important; I limited to only Apache 2.0 licensed ones, and the best bang for buck I found at the time of writing was &lt;a href="https://openrouter.ai/qwen/qwen3-30b-a3b-instruct-2507" rel="noopener noreferrer"&gt;Qwen3 30B through Openrouter.ai&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  So... does it work?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Well, yes, but actually no.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What works:&lt;/strong&gt;&lt;br&gt;
Structured tool calls are reliable. Add an item by name or by position, ask a price, get a grounded answer, check out - the core loop holds up, including on domains it never trained on.&lt;br&gt;
For a 150 MB model running on your laptop with no server, that still feels a lot like magic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What doesn't (yet):&lt;/strong&gt;&lt;br&gt;
230M parameters spread across many domains is &lt;em&gt;thin&lt;/em&gt;.&lt;br&gt;
During training, generalized use-cases compete for the same limited capacity.&lt;br&gt;
Multi-turn fidelity is the weakest spot - over a long conversation it can drift toward the shape it was trained on rather than the specific thing you just said.&lt;br&gt;
Because of this, the training regime limits to only 2 turns of conversation patterns, so does the JS runtime with a sliding window; the agent never sees more than 2 turns of conversations.&lt;br&gt;
The 350M variant buys real headroom at ~1.5x the footprint, and you can switch between them in the demo to feel the difference.&lt;/p&gt;

&lt;p&gt;I think this is an interesting frontier: instead of "can a giant model do this" (obviously), we ask &lt;em&gt;how small can you go&lt;/em&gt; and still be genuinely useful on-device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why on-device at all
&lt;/h2&gt;

&lt;p&gt;Beyond the cool factor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt; - inputs never leave the device. For a lot of use-cases that's not a nice-to-have, it's the requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline&lt;/strong&gt; - it keeps working with no connection after the first load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; - zero inference bill; you ship weights, not a GPU fleet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt; - paired with proper ARIA roles and semantic HTML, an on-device agent can make a complex UI navigable by voice or intent.
(Speech-to-text and text-to-speech in the demo are on-device models too - nothing is sent anywhere.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it / take it apart
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://lajosbencz.github.io/frontend-agent/" rel="noopener noreferrer"&gt;https://lajosbencz.github.io/frontend-agent/&lt;/a&gt; (three storefronts, one model, all in your tab)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/lajosbencz/frontend-agent" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; - Apache-2.0 client library + the Nuxt demo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; &lt;a href="https://huggingface.co/lazos/lfm2.5-230m-frontend-agent" rel="noopener noreferrer"&gt;230M&lt;/a&gt; ·
&lt;a href="https://huggingface.co/lazos/lfm2.5-350m-frontend-agent" rel="noopener noreferrer"&gt;350M&lt;/a&gt; on Hugging Face&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/lazos/frontend-agent-sft" rel="noopener noreferrer"&gt;frontend-agent-sft&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built on &lt;a href="https://huggingface.co/LiquidAI/LFM2.5-230M" rel="noopener noreferrer"&gt;LiquidAI LFM2.5&lt;/a&gt;.&lt;br&gt;
Model weights inherit the LFM Open License v1.0; code and this post are Apache-2.0 / CC-BY&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>localllm</category>
      <category>webdev</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
