AI engineering

A self-hosted LLM writes our blog drafts: how we keep it honest

A 4B model on a 6-watt mini PC with no GPU writes drafts for two small businesses of ours. What it is given, what it is refused, the eleven checks that use no model, and the four ways we failed to make it write a single link.

Short answer. A small model is enough for drafts when the work around it is strict. Ours runs qwen3:4b through Ollama on an Intel N100 mini PC. It writes only from facts that code selects, and code refuses to start without them. It writes a plan and then one section at a time. Code chooses the topic and inserts the links. Eleven checks that use no model gate every draft before a person sees it. A person decides whether it is true and presses Publish. The model is the least important part of the pipeline — which is the reason a small one works. It belongs to the two blogs in our local-business case study.

A 4B model on a 6-watt machine

The writer runs on a mini PC on our own network: an Intel N100, four cores, about six watts, no GPU. Ollama serves the model in its own container, reachable only from the writer’s internal network, because Ollama has no authentication of its own. The machine has no route to the code host on purpose; new versions arrive as a bundle over SSH and roll back to the previous image if they do not come up healthy.

The default model is qwen3:4b. A second compose file reserves an NVIDIA GPU and switches to qwen3:14b — about 9 GB of weights, sized for a 12 GB card — but the GPU is off by default, because not passing a device to the container is the only reliable way to force CPU. Measured on the N100, the model reads a prompt at about 14 tokens a second and generates at about 2.3. Each prompt is 2,300 to 2,700 tokens of facts and examples before the first word comes out, so the plan takes about six minutes, the first section about three, the rest about a minute and a half each, and a whole article about sixteen minutes. For a draft a day, that is plenty, and nothing the writer reads ever leaves the building.

Two settings that cost us days. The qwen3:4b tag points at a thinking build: on a tiny schema request it spent 82 seconds and 4,351 characters reasoning with thinking on, and answered in 1.7 seconds with it off. And Ollama’s default context window silently cuts the start of a long prompt — exactly where the instructions about voice sat — so the window is set explicitly to 16,384.

Facts before words, and a refusal without them

The writer reads the sites’ own repositories, mounted read-only: the published articles, the facts about each device model, the service or catalogue sections, and the list of pages. It reads them from disk rather than from the live sites, so a slow site or a stale cache cannot change what it knows.

The unit of a fact is a record, not a line — one record per service or section, one per device model — and only labelled fields are sent: what customers bring this model in for, what is checked first, typical problems, timing, price factors, FAQ. Code builds one ready sentence of specifications per model and labels it as safe to copy as is. Records are ranked by how many topic words they share, with a match on a record’s name worth ten times a match in its body, and sent until a budget of 6,000 characters per article and 2,500 per section is used.

The rule that matters most is the shortest one. If code finds no fact records for a topic, it stops before any prompt is written, with the message that writing without them means inventing. It replaced an instruction in the prompt to “write only what you are sure of”, which in practice the model read as permission to invent.

Code picks the topic, the model words it

Topic choice is mostly not a model’s job. Code picks the section of the blog with the fewest articles, breaking ties by the oldest last article. The model then proposes six topics for that section — each a concrete problem an owner has, not a review or a top-five list, using only the facts given and never repeating an existing title. Code drops any proposal whose title is too long, whose address is taken, or that shares 60% or more of its words with an existing title, and ranks the survivors by how many of their words appear in the facts. The schedule takes the first.

The honest limitation: the only signal is how many articles a section has. There is no demand data in the loop yet, which is the next thing an analytics agent could feed it.

A plan, then one section at a time

A model small enough to fit in 12 GB drifts on long answers, and a generation cut off halfway loses everything it wrote. So the writer asks for a plan first — title, description, lead and four to six sections, each with an intent and a shape (prose, list or steps) — and then writes each section in its own request, with its own facts between explicit “facts start” and “facts end” markers. A cut-off generation now costs one section, not the article.

Two prompt details earned their place. The no-invention rule is repeated at the very end of each section prompt, because the model reliably ignores instructions buried in the middle of a long one. And code cleans up after the model: it drops one-item lists, strips the numbering the model adds on top of list markup, removes duplicated blocks and keeps one note per section.

Internal links were the most instructive failure. The 4B model never inserted one. We tried four times: a soft rule, a hard rule, regenerating the section, and a dedicated request to insert one given link — which came back byte-for-byte unchanged. The cause turned out to be our own voice rule against advertising in articles: the model read a link to a service page as an ad.

Now code chooses two or three real pages whose titles share stems with the topic, and on one of the blogs it wraps words the model already wrote: it finds a paragraph of at least 120 characters without a link, looks for a service word or a word from the page’s title, and adds the markup without changing a character of the sentence. It refuses anchors that start with a preposition or end in a verb form, because a link on “to repair” reads worse than none. If no good anchor exists, it leaves the text alone and lets the check decide.

Eleven checks, and none of them is a model

Every check is arithmetic or a regular expression. A second model judging the first would share its blind spots and answer differently each run; counting does neither.

CheckWhat it measuresRejects at
LengthCharacters of the draft textunder 2,000 (warns over 12,000)
DuplicatesJaccard similarity of five-word shingles against every published article0.30 (warns from 0.18)
Stock phrasesHow many of 28 cliché patterns appear3 or more
Form of addressInformal “you” where the sites use the formal one2 or more hits
PricesA sum next to a currency or a cost wordany, on the site that publishes no prices
Known linksAny link that is not one of the site’s real pagesany
Inline linksLinks inside the text, per each site’s rulesnone where they are required
Address takenThe article’s address already publishedwarns, and marks it as a replacement
StructureHeadingsfewer than 3, or duplicates
GroundingSpecifications and model designations not found in the allowed sourcesany
SpecificsAmounts, periods, menu paths and terms not foundwarns only

Grounding is the check that matters. It pulls two kinds of hard claim out of the draft — a number with a unit such as Hz, mAh, W, mm, inch or MP, and a Latin-letter designation that contains a digit, which is what a device model looks like — and searches for each one in the allowed sources: every fact record in the topic’s scope (not only those sent to the model), all model names, all published articles, the topic and the chosen links. A number is never split off its word, so an invented “Model 26 Ultra” cannot pass on the strength of “Model” existing somewhere. Each section is checked as soon as it is written; if it carries hard claims nobody can find, it is rewritten once with those claims quoted back, and the second version is kept only if it is better.

Calibrated on people, not on the model. Every check is run against the human-written articles on both blogs. The first grounding run rejected about a quarter of them — the check, not the people, was wrong — so amounts, periods and menu paths were demoted from errors to warnings. Its honest limit is stated in the code: it checks where a value came from, not whether it is attributed to the right device. A draft once passed every check while claiming a laptop loses Wi-Fi from overheating “in 70% of cases”. Truth is left to the person on purpose.

The contract: what the site will accept

Early drafts passed every check and were then refused by the site: no category, a misspelled one, a 134-character title, an invalid address. The generator also counted length differently from the site. So before a person sees a draft, a contract check validates it against the site’s own publishing rules — the address pattern, title and description lengths, category, volume counted exactly as the site counts it — and a script reads those limits out of the sites’ validator code and fails if they ever disagree. A draft that fails the checks or the contract is saved aside with its findings and never shown to anyone.

A person presses Publish

A draft that passes arrives in a chat as a card — title, description, address, length, sections, links, any warnings under “look closer”, then the full text — with two buttons: Publish and Reject. A private review page shows the same draft with a fresh contract check; it is locked if no password is set, and it locks out repeated wrong guesses. The daily schedule only ever produces drafts. This is the human-in-the-loop part of the system, and it has no automatic fallback on purpose: if nobody presses a button, nothing is published.

Publish calls the site’s own endpoint with a token. The date is set at that moment, in the site’s time zone, because drafts that kept their creation date appeared backdated. An article whose address is already taken is sent only with an explicit replace flag, because one early publish silently overwrote a page that was already indexed. The site validates the article again, stores it, refreshes its cache and notifies search engines through IndexNow, and the result — published or replaced, and whether search engines were told — comes back to the same chat.

What went wrong on the way

When this makes sense, and what it costs

A pipeline like this makes sense when the content is factual and the facts already exist in your own systems — product data, service records, a catalogue — and when you want drafts without a per-token bill or your data leaving your network. It does not replace an editor; it gives one a draft that is already grounded, checked and shaped. One workflow like it, built into a system you already run, sits in our AI automation band, $15,000–$30,000 over 2–4 weeks. Related: how we evaluate grounded AI output before production.

Common questions

About running a small LLM for content.

Drafts, yes, when the work around it is strict. Ours is a 4B model on a mini PC with no GPU. It writes only from facts selected by code, one section at a time, and every draft passes eleven checks that use no model before a person sees it. A person still decides whether it is true and presses Publish. The quality comes from that structure far more than from the size of the model.
Less than people expect, if you accept the wait. Our writer runs qwen3:4b through Ollama on an Intel N100 mini PC — four cores, about 6 watts, no GPU — at roughly 14 tokens a second reading the prompt and 2.3 generating. A full article takes about sixteen minutes, which is fine for a daily schedule. A GPU override switches to a 14B model, sized for 12 GB of video memory.
Give it facts and refuse to run without them, then check its output against those facts with code. Our writer stops before any prompt if it finds no fact records for a topic, and a grounding check extracts every specification with a unit and every model designation from the draft and looks each one up in the allowed sources; an unknown one rejects the section, which is rewritten once with the invented claims quoted back. It checks where a value came from, not whether it is attributed to the right device, so a person still reads the draft.
Not as the only check. A second model shares the first one's blind spots and gives a different answer each time. Our eleven checks are arithmetic and regular expressions: length, similarity to published articles, stock phrases, links, structure and grounding. They are cheap, repeatable and calibrated against the human-written articles, which is how we found that the grounding check was too strict and demoted part of it to warnings.
No. The schedule produces drafts and stops at the approval button. A draft that fails a check is never shown to anyone; one that passes arrives in a chat with Publish and Reject buttons, or on a private review page. Publishing calls the site's own endpoint with a token, sets the date at the moment of publishing, and reports back whether search engines were notified.

Want drafts grounded in your own data?

Tell us where your facts live and who signs off on what is published. We will tell you whether a pipeline like this fits, and put a fixed scope and price in writing within 48 hours of the call.

Message received

We’ll review your enquiry and respond within one business day.

Related reading