Local-LLM Site Concierge
in progressThe askAI page on this site, answered by a small language model running on an old desktop in my house. No cloud inference, no API key, no data centre.
The plumbing is the hard part, not the model. My desktop sits behind NAT with nothing open to the internet, and I wasn’t willing to forward a port to run a toy. So the connection only ever runs outward: the Vercel route pushes a job onto an Upstash Redis queue, the desktop long-polls that queue over HTTPS, runs the question against a local Ollama model, and writes the answer back under the job’s id. Nothing ever reaches in. The desktop also publishes a heartbeat with a short TTL — if it goes stale the site knows the node is down and says so, in plain language, with links, instead of hanging.
Making a public-facing model behave is most of the remaining work, and it’s layered rather than clever:
- A regex pre-filter refuses instruction-extraction and override attempts before the model is ever called, so the cheapest attacks cost nothing to defend.
- A hardened system prompt answers about me only from a single grounding document, in third person, and is told to treat missing sections as “I don’t know” rather than fill them in.
- Deterministic rails catch two specific failure modes a prompt alone won’t reliably prevent: impersonating me, and making commitments on my behalf — replying to an offer, agreeing to a date, accepting anything. Those are intercepted rather than negotiated with.
- Everything fails closed. A model error, a relay error, a malformed job — all of them produce no answer, which surfaces as the honest offline message. A wrong answer about a real person is worse than no answer.
Speed, without pretending. A 3B model on four CPU cores takes 10–20 seconds a question, and no amount of tuning changes that. What does change it is not running the model at all: the box is idle almost all the time, so an idle worker precomputes answers to likely questions, generates several candidates, and scores them against a formal-register rubric — contractions, filler, fourth-wall leaks, sentence count — keeping only the best. Cache matching is normalised-exact on purpose. No embeddings, no fuzzy similarity: a near-miss would serve a confidently incorrect answer about a real person, which is far worse than being slow. The cache is keyed by a fingerprint of the system prompt plus the grounding doc, so editing either retires every stale entry automatically.
What the visitor sees. The /ask page shows which model is loaded, the machine it’s running on, your position in the queue, and whether the desktop is even awake. When the desktop is off, the page still shows the last numbers it reported and says when that was, instead of an empty panel. There’s a passphrase fast-lane that jumps the queue for people I’ve given it to. It’s deliberately un-magical: the point is that a small model on a normal computer in a house can do this, and you can watch it happen.
Built with Next.js API routes and Upstash Redis on the site side; a single-file Python poller, Ollama and systemd on the desktop side.
It must be noted with such a machine running it hard indefinitely is a lot, so it often shuts down and auto-reboot systems fail. I’m working to improve this, which may include switching from Linux Mint, which I am now comfortable enough to do, to something with even less overhead. A headless distro may be in order for such a project.
- Local LLMs
- System Design
- Security Design