EN below · Wersja polska poniżej każdej sekcji.
AI on Ordinary Hardware: a Micro‑Prompt Architecture for Reliable LLM Extraction on a CPU
AI na zwykłym sprzęcie: architektura micro‑prompt do niezawodnej ekstrakcji LLM na CPU
TL;DR (EN). You do not need a GPU farm to put a Large Language Model to useful work. On a 4‑core CPU with 15 GB of RAM and no GPU, a single “do everything” prompt times out and hallucinates. The fix is architectural, not hardware: split the job into micro‑prompts (one field, one small call), feed the model only the narrow text window located by phrase anchors, bound every query parameter, and let plain code answer whatever the LLM does not have to. The result: 240‑second timeouts became ~20‑second calls, and garbage output became coherent, validated data.
TL;DR (PL). Nie potrzebujesz farmy GPU, by zaprząc duży model językowy do realnej pracy. Na 4‑rdzeniowym CPU z 15 GB RAM i bez GPU pojedynczy prompt „zrób wszystko” timeoutuje i halucynuje. Lekarstwo jest architektoniczne, nie sprzętowe: podziel zadanie na micro‑prompty (jedno pole = jeden mały call), podawaj modelowi tylko wąskie okno tekstu znalezione przez kotwice‑frazy, ogranicz każdy parametr zapytania i pozwól zwykłemu kodowi odpowiadać na to, czego LLM nie musi. Efekt: timeouty 240 s zmieniły się w ~20‑sekundowe calle, a bełkot — w spójne, zwalidowane dane.
1. The problem: AI on ordinary hardware / Problem: AI na zwykłym sprzęcie
EN. The reference machine in this article is deliberately modest: a 4‑core desktop‑class CPU (AVX2, no AVX‑512), 15 GB of dual‑channel DDR4 memory, an SSD, and no GPU at all. A 7‑billion‑parameter model quantized to 4‑bit fits in RAM and generates at roughly 5–6 tokens per second once warm. That is enough — if you respect the constraint instead of fighting it. CPU inference is memory‑bandwidth‑bound, so the winning move is to make the model do less: shorter prompts, fewer output tokens, and only the work that genuinely needs a language model.
PL. Maszyna referencyjna jest celowo skromna: 4‑rdzeniowy CPU klasy desktop (AVX2, bez AVX‑512), 15 GB dwukanałowej pamięci DDR4, dysk SSD i brak GPU. Model 7‑miliardowy w kwantyzacji 4‑bit mieści się w RAM i — po rozgrzaniu — generuje ~5–6 tokenów na sekundę. To wystarczy, jeśli uszanujesz ograniczenie, zamiast z nim walczyć. Inferencja na CPU jest ograniczona pasmem pamięci, więc wygrywa strategia „mniej”: krótsze prompty, mniej tokenów wyjścia i tylko ta praca, która naprawdę wymaga modelu językowego.
2. Why one big prompt fails / Dlaczego jeden wielki prompt zawodzi
EN. The intuitive design — paste the whole document and ask the model for one large JSON with every field at once — is exactly the design that breaks on a CPU. Generation time scales with the number of output tokens, and a rich JSON object is hundreds of tokens. At 5 tok/s, 700 output tokens is ~140 seconds before counting prompt evaluation; under any contention it crosses a request timeout and returns nothing. Worse, a small model loaded with a 6,000‑character prompt loses the thread and produces incoherent text. We measured this precisely: the monolithic call hit a 240‑second timeout on essentially every document.
PL. Intuicyjny projekt — wklej cały dokument i poproś o jeden wielki JSON ze wszystkimi polami naraz — to dokładnie ten projekt, który pada na CPU. Czas generacji rośnie z liczbą tokenów wyjścia, a bogaty obiekt JSON to setki tokenów. Przy 5 tok/s 700 tokenów to ~140 s zanim doliczymy ewaluację promptu; przy jakiejkolwiek kontencji przekracza timeout i nie zwraca nic. Gorzej: mały model obciążony promptem 6000 znaków gubi wątek i produkuje niespójny tekst. Zmierzyliśmy to: monolityczny call uderzał w timeout 240 s praktycznie na każdym dokumencie.
# ANTI-PATTERN — one giant call, unbounded output. Times out on CPU.
prompt = f"Analyze this tender and return JSON with ALL fields:\n{document[:6000]}"
resp = ollama.generate(model="mistral:7b", prompt=prompt, format="json") # >240s → timeout
3. The micro‑prompt architecture / Architektura micro‑prompt
EN. Borrow the idea from microservices: one prompt, one responsibility. Instead of asking for ten fields at once, ask ten times for one field each. Every call is tiny, focused, independently retryable, and bounded to a handful of output tokens. The model’s full attention lands on a single, clear question. This is the single most important change — it turns an unbounded generation into a predictable, short one.
PL. Pożycz pomysł z mikroserwisów: jeden prompt, jedna odpowiedzialność. Zamiast prosić o dziesięć pól naraz, zapytaj dziesięć razy o jedno pole. Każdy call jest malutki, skupiony, niezależnie powtarzalny i ograniczony do kilkudziesięciu tokenów wyjścia. Cała uwaga modelu trafia w jedno, jasne pytanie. To najważniejsza zmiana — zamienia nieograniczoną generację w przewidywalną, krótką.
# PATTERN — one field per call, bounded output, low temperature.
async def micro_prompt(field: str, window: str) -> str | None:
question = QUESTIONS[field] # a precise, single-field instruction
prompt = f"{question}\n\nText:\n---\n{window[:CHUNK_LIMIT[field]]}\n---"
out = await ollama_generate(
model="mistral:7b",
prompt=prompt,
format="json",
options={"num_predict": MAX_TOKENS[field], # e.g. 20–100, not 700
"temperature": 0.1,
"num_ctx": 2048}, # sized to the window, not the model max
)
return validate(field, parse_json(out)) # returns a clean value or None
4. Find the needle: phrase‑anchor localization / Znajdź igłę: lokalizacja po kotwicach‑frazach
EN. A micro‑prompt is only fast if its context is small. Do not send the whole document — locate the relevant sentence first with cheap regular‑expression “anchors”, then send a 200–600 character window around the match. A quality check confirms the window actually contains a proof of value (a date, an amount, a keyword) and a negative check rejects look‑alikes (e.g. a submission deadline when you want a completion date). This “search, then ask” step is what keeps prompts under a strict character budget.
PL. Micro‑prompt jest szybki tylko wtedy, gdy jego kontekst jest mały. Nie wysyłaj całego dokumentu — najpierw zlokalizuj właściwe zdanie tanimi „kotwicami” (wyrażeniami regularnymi), a potem wyślij okno 200–600 znaków wokół trafienia. Kontrola jakości potwierdza, że w oknie jest dowód wartości (data, kwota, słowo kluczowe), a kontrola negatywna odrzuca podszywaczy (np. termin składania ofert, gdy szukasz terminu zakończenia robót). Ten krok „najpierw szukaj, potem pytaj” trzyma prompty w twardym budżecie znaków.
ANCHORS = {
"deadline": [r"completion\s+date[:\s]+\d", r"within\s+\d+\s+months", r"no\s+later\s+than\s+\d"],
}
NEGATIVE = { # veto: looks like the field but is not
"deadline": r"submission\s+deadline|bid\s+opening|tender\s+closing",
}
def locate(text: str, field: str, max_window: int = 600) -> str | None:
for rx in ANCHORS[field]:
m = re.search(rx, text, re.I)
if not m:
continue
window = text[max(0, m.start()-120): m.start()+max_window]
if NEGATIVE.get(field) and re.search(NEGATIVE[field], window, re.I):
continue # reject the look-alike, keep scanning
return window
return None # nothing solid → do not guess
5. The parameters that decide speed / Parametry, które decydują o prędkości
EN. Three numbers dominate latency on a CPU:
num_predict(output cap). This is the single biggest lever — generation time is linear in output tokens. Cap each field to what it needs (a date is ~20 tokens, not 700).num_ctx(context window). A 4096‑token context reserves a KV‑cache and prompt‑evaluation budget you do not use when your window is ~600 tokens. Right‑size it (≈2048) and prompt evaluation gets cheaper.- A hard prompt‑length guard. One choke‑point truncates any prompt above a strict character limit and logs a warning — so no future code path can accidentally smuggle a long prompt onto weak hardware.
Add keep_alive so the model stays warm in RAM between calls (no cold reloads), and keep one in‑flight request at a time (single‑slot queue) — on a memory‑bandwidth‑bound CPU, concurrency only causes thrashing.
PL. Trzy liczby rządzą latencją na CPU:
num_predict(limit wyjścia). Największa dźwignia — czas generacji jest liniowy w tokenach wyjścia. Ogranicz każde pole do tego, czego potrzebuje (data to ~20 tokenów, nie 700).num_ctx(okno kontekstu). Kontekst 4096 tokenów rezerwuje KV‑cache i budżet ewaluacji, których nie używasz przy oknie ~600 tokenów. Dobierz rozsądnie (≈2048), a ewaluacja promptu tanieje.- Twardy strażnik długości promptu. Jeden punkt‑dławik obcina każdy prompt powyżej limitu znaków i loguje ostrzeżenie — żeby żadna przyszła ścieżka kodu nie przemyciła długiego promptu na słaby sprzęt.
Dodaj keep_alive, by model został ciepły w RAM między callami (bez zimnych przeładowań), i trzymaj jeden request naraz (kolejka jednoslotowa) — na CPU ograniczonym pasmem pamięci równoległość tylko powoduje thrashing.
MAX_PROMPT_CHARS = 3000 # the line in the sand for this hardware
def guard(prompt: str) -> str:
if len(prompt) > MAX_PROMPT_CHARS:
log.warning(f"prompt too long ({len(prompt)}); truncating — micro-prompt policy")
return prompt[:MAX_PROMPT_CHARS]
return prompt
6. Don’t ask the model what code already knows / Nie pytaj modelu o to, co kod już wie
EN. The cheapest LLM call is the one you never make. A fast heuristic (regex + a tiny scorer) runs first and, when confident above a per‑field threshold, fills the value directly — the model is skipped entirely. When the heuristic is unsure, its best guess is passed to the model as a hint (“confirm or correct this”), which is far easier than extracting from scratch. And whole decisions that are deterministic — a yes/no “is this a fit for us” verdict and a numeric score — are computed by rules, not generated. The model is reserved for the one thing it is uniquely good at: turning a located fragment of natural language into a clean value or a short justification.
PL. Najtańszy call do LLM to ten, którego nie wykonasz. Szybka heurystyka (regex + drobny scorer) działa pierwsza i, gdy jest pewna powyżej progu dla danego pola, wpisuje wartość bezpośrednio — model jest pomijany. Gdy heurystyka nie jest pewna, jej najlepsze przypuszczenie idzie do modelu jako podpowiedź („potwierdź albo popraw”), co jest znacznie łatwiejsze niż ekstrakcja od zera. A całe decyzje deterministyczne — werdykt „czy to dla nas” i liczbowy wynik — liczą reguły, nie generacja. Model jest zarezerwowany do jednej rzeczy, w której jest wyjątkowo dobry: zamiany zlokalizowanego fragmentu języka naturalnego w czystą wartość lub krótkie uzasadnienie.
# Hybrid: rules decide the verdict + score; the model only writes one sentence.
optimal = (category in PREFERRED) and profile_score >= 40 and risk_score < 70
score = round(0.6 * profile_score + 0.4 * (100 - risk_score))
reason = await micro_prompt_reason(title, category, profile_score, risk_score) # ~1 short call
7. Make it robust: parsing, retries, validation, NULL discipline / Niezawodność: parsowanie, retry, walidacja, dyscyplina NULL
EN. Small models occasionally truncate JSON or wrap it in prose. A forgiving parser recovers the value from a cut‑off object; a one‑shot “return valid JSON” retry fixes format slips; a per‑field validator enforces the expected shape (a parseable date, an amount with a unit). Above all, enforce NULL discipline: if the evidence is not there, return nothing rather than a confident hallucination — and never let a NULL overwrite a value already extracted. Reliability comes from these layers, not from a bigger model.
PL. Małe modele czasem ucinają JSON albo otaczają go prozą. Wyrozumiały parser odzyskuje wartość z uciętego obiektu; jednorazowy retry „zwróć poprawny JSON” naprawia wpadki formatu; walidator per pole wymusza oczekiwany kształt (parsowalna data, kwota z jednostką). Przede wszystkim egzekwuj dyscyplinę NULL: jeśli dowodu nie ma, zwróć nic, a nie pewną siebie halucynację — i nigdy nie pozwól, by NULL nadpisał już wyciągniętą wartość. Niezawodność bierze się z tych warstw, nie z większego modelu.
8. Choosing a model on a CPU: discipline beats prose / Wybór modelu na CPU: dyscyplina bije prozę
EN. The instinct is to pick the model with the nicest prose or the best leaderboard score. For extraction on a CPU that instinct is wrong. What you need is format discipline — does it obey “return YYYY‑MM‑DD or null” and resist hallucinating a relative term when no date exists? We benchmarked candidates with a fixed trap suite (offer‑deadline vs completion‑date, “only an offer deadline → must return null”, noisy dates). A proven 7B model scored 7/8 — it picked the works date over the bid date and correctly returned null when only a bid date was present. A newer, “better‑written” 4.5B model scored 0/8: it ignored the date format and invented “N months from contract” where explicit dates existed. The lesson: evaluate every candidate on both axes — accuracy on traps and speed — and discard the ones that fail. Also mind the hardware envelope: a 30B Mixture‑of‑Experts “flash” model needs ~24 GB of RAM to hold all its experts, so it simply does not fit a 15 GB box regardless of how few parameters are active per token.
PL. Instynkt podpowiada wybrać model z najładniejszą prozą albo najlepszym wynikiem w rankingu. Dla ekstrakcji na CPU ten instynkt jest błędny. Potrzebujesz dyscypliny formatu — czy słucha „zwróć RRRR‑MM‑DD albo null” i nie halucynuje terminu względnego, gdy daty nie ma? Przetestowaliśmy kandydatów stałym zestawem pułapek (termin oferty vs termin robót, „tylko termin oferty → musi być null”, daty‑szum). Sprawdzony model 7B uzyskał 7/8 — wybrał datę robót zamiast oferty i poprawnie zwrócił null, gdy była tylko data oferty. Nowszy, „lepiej piszący” model 4.5B uzyskał 0/8: ignorował format daty i wymyślał „N miesięcy od umowy” tam, gdzie były jawne daty. Wniosek: oceniaj każdego kandydata na dwóch osiach — trafność na pułapkach oraz prędkość — i odrzucaj te, które nie zdają. Pilnuj też koperty sprzętowej: 30‑miliardowy model MoE „flash” potrzebuje ~24 GB RAM, by pomieścić wszystkich ekspertów, więc po prostu nie wejdzie na maszynę z 15 GB, niezależnie od tego, jak mało parametrów jest aktywnych na token.
# A trap-based accuracy test is worth more than a leaderboard.
VARIANTS = [
("plain date", "Completion date: 2026-11-30.", "2026-11-30"),
("TRAP offer+works", "Bid deadline 2026-06-23. Works completion: 2026-11-30", "2026-11-30"),
("TRAP only offer → null", "Bid submission deadline: 2026-07-10.", None),
("relative term", "to be completed within 18 months of signing.", "18 months"),
]
# Run every candidate through this AND a tok/s benchmark. Keep only those that pass both.
9. Results / Wyniki
EN. The same hardware, re‑architected: per‑field calls of ~2–20 seconds instead of a 240‑second timeout; coherent, validated values instead of garbage; a deep “is this worth our time” assessment produced in ~20–30 seconds from rules plus a single short generation; and zero silent failures on fresh documents over a full processing cycle. No new GPU, no bigger model — just an architecture that respects the machine.
PL. Ten sam sprzęt, przebudowana architektura: calle per pole ~2–20 s zamiast timeoutu 240 s; spójne, zwalidowane wartości zamiast bełkotu; głęboka ocena „czy warto” wyliczona w ~20–30 s z reguł plus jedna krótka generacja; i zero cichych błędów na świeżych dokumentach przez pełny cykl przetwarzania. Bez nowego GPU, bez większego modelu — tylko architektura, która szanuje maszynę.
10. Principles to take away / Zasady na wynos
- One field, one call. / Jedno pole, jeden call.
- Locate, then ask — never paste the whole document. / Zlokalizuj, potem pytaj — nigdy nie wklejaj całego dokumentu.
- Bound everything: output tokens, context, prompt length. / Ogranicz wszystko: tokeny wyjścia, kontekst, długość promptu.
- Let code answer what code can; reserve the model for language. / Niech kod odpowiada na to, co potrafi; model zostaw do języka.
- NULL over a guess. / NULL zamiast zgadywania.
- Pick models for discipline, prove it with traps, measure speed and quality together. / Wybieraj modele za dyscyplinę, udowadniaj pułapkami, mierz prędkość i jakość razem.
EN. None of this requires exotic infrastructure. The art of running AI on ordinary hardware is the art of asking the model less, and asking it well.
PL. Nic z tego nie wymaga egzotycznej infrastruktury. Sztuka uruchamiania AI na zwykłym sprzęcie to sztuka pytania modelu o mniej — i pytania dobrze.