Skip to content

Learning the system

Purpose. This document explains, from first principles, the concepts the rest of the codebase depends on — language models, tokens, embeddings, vector stores, LoRA, GGUF, quantization, and the evaluation gate. It is written as an educational reference: each machine-learning concept is introduced on its own terms and then related to an established software-engineering idea, so the complete ecosystem can be understood whether the goal is to operate the platform or to develop it. No prior exposure to machine-learning infrastructure is assumed; familiarity with ordinary backend systems is sufficient to follow every section.

Companion pages

Stack & decisions covers which components were chosen and why. This page covers how those components work. Read both for the full picture, or use the Architecture page as the map once the concepts are clear.

Most of this platform is built from conventional components: an HTTP service, a relational database, a job queue, and a background worker. The machine-learning-specific parts are comparatively few, and each one corresponds to a familiar infrastructure concept. The sections below introduce those parts individually, then assemble them into the full picture in §6.


1. What a language model is

Set aside the term "artificial intelligence" for a moment. Mechanically, a language model is a pure function:

f(text_so_far) → probability distribution over the next token

Given some text, the model returns how likely each possible "next chunk of text" is. Generating a sentence is therefore an iterative process: predict the next chunk, append it, and predict again. There is no database lookup and no separate reasoning engine — only a very large mathematical function evaluated repeatedly.

Comparable to predictive text, scaled up

A smartphone keyboard suggests the next word from the preceding few words. A language model performs the same operation, but conditioned on the entire conversation and trained on a very large corpus. "Generating a response" is that prediction run forward, one chunk at a time, until a designated "stop" token is produced.

The "very large function" is defined by its weights — billions of numbers fixed during training. Evaluating the function is called inference. A useful correspondence: compiling a program is expensive and performed once, whereas running it happens continuously. Training is analogous to compilation; inference is analogous to execution.

Tokens — the unit of text

Models do not operate on characters or whole words; they operate on tokens — sub-word fragments. A tokenizer splits text into these fragments, each mapped to an integer.

  • Common words are usually a single token: "the" is one token.
  • Rare or invented words are split into several. An invented word such as "Zorptania" might cost four tokens, because the model has never encountered it and must assemble it from smaller fragments.

Tokens resemble syllables; rare words resemble spelling aloud

A familiar word is read as a single unit, while an unfamiliar one is spelled out fragment by fragment. A model behaves the same way. This has practical consequences: token count drives cost, context limits, and — as §5 describes — how difficult an invented answer is to learn.

Every response in this platform reports prompt_tokens, completion_tokens, and total_tokens for precisely this reason: the token is the unit of accounting.

GGUF and quantization — the model as a deployable artifact

A model's weights must be stored in a file. This platform serves models in the GGUF format, executed by the llama-cpp engine.

  • GGUF is a single-file packaging of the weights together with metadata. It is comparable to a compiled binary of the model; llama-cpp is the runtime that loads and executes it, much as a virtual machine executes a compiled artifact.
  • Quantization stores each weight at lower numeric precision (for example, four bits instead of sixteen) to reduce file size and memory footprint, trading a small amount of accuracy for a large reduction in size. A model labeled Q4_K_M is quantized to four bits.

Quantization is comparable to lossy image compression

A raw photograph is large and pixel-perfect; a compressed (JPEG) version is a fraction of the size and nearly indistinguishable. Quantization applies the same principle to model weights: a three-billion-parameter model that would require roughly 12 GB at full precision fits in approximately 4 GB at four-bit precision, with only minor quality loss.

This is why a consumer GPU with 8 GB of memory can serve a model that nominally requires far more: it executes the compressed form rather than the full-precision one.

Inference: one chef, one cutting board

The llama-cpp model object maintains mutable internal state during generation (a "KV cache" — effectively a scratchpad that records the conversation so far). A single model instance is not safe to call concurrently.

One chef, one cutting board

Two cooks sharing a single cutting board mid-task will collide. Likewise, two requests reaching one model instance's scratchpad simultaneously corrupt each other, producing garbage output or a crash. The remedy is not to rewrite the engine but to require that callers take turns.

That is exactly what the implementation does: a per-model asyncio.Lock serializes generation on each model instance (adapta/core/model_manager.py, adapta/core/inference.py), generation itself runs on a bounded thread pool so it does not block the asynchronous event loop, and a timeout caps each call. The model cache is a bounded LRU, so a large number of endpoints cannot exhaust memory by each pinning a full model in RAM. None of this is specific to machine learning; it is ordinary concurrency engineering around a non-thread-safe resource, identical in spirit to guarding any shared mutable handle.


2. Two ways to specialize a model

A base model is a generalist, whereas most deployments require a specialist. There are two fundamentally different ways to obtain one, and conflating them is the most common conceptual error:

Question Give it knowledge (RAG) Change its behavior (adapter)
What it addresses "What do the documents say?" "How should the model act?"
Modifies the weights? No Yes (through an adapter)
Comparable to An open-book examination Practice until a behavior is habitual
Input Documents A LoRA adapter (imported and gated on CPU) and an eval set of prompt → response examples
Cost Seconds, CPU Training happens outside the platform; gating and serving are CPU
Reversibility Delete a file and re-index Swap the adapter

A working distinction

Facts the model should look up are a matter of knowledge (RAG). A manner in which the model should behave is a matter of an adapter. "Answer from a 500-page handbook" calls for RAG; "always reply in a terse, bulleted support voice" calls for an adapter.


3. Knowledge (RAG), from first principles

RAG stands for Retrieval-Augmented Generation. The model does not memorize the documents. Instead, at the moment a question is asked, the relevant passages are located and supplied to the model as context, and the model is asked to answer using them.

An open-book examination with a capable research assistant

Rather than memorizing a textbook, an examinee whose assistant instantly locates the three most relevant pages and places them in view can answer from what is on the page. The model is the examinee; retrieval is the assistant. The model's effective knowledge is whatever has been placed in front of it.

Making "locate the relevant passages" work requires two machine-learning building blocks.

Embeddings — coordinates for meaning

An embedding is a function that converts a piece of text into a vector (a list of roughly 384 numbers) such that texts with similar meaning are positioned near one another in that vector space.

Coordinates for meaning

Consider a map on which every sentence has a location and sentences about the same topic cluster together — "refund policy" and "money-back guarantee" end up adjacent even though they share no words. An embedding model assigns those coordinates. This is the opposite of a hash function, which deliberately scatters similar inputs to unrelated outputs; an embedding maps similar inputs to nearby outputs.

In this codebase, adapta/services/embeddings.py wraps a sentence-transformers model that produces these vectors, loaded lazily as a singleton.

Vector store — a database indexed by meaning

A conventional database index retrieves rows by exact value (for example, a B-tree on user_id). A vector store retrieves by nearest neighbor in meaning — "return the chunks whose embeddings are closest to this query's embedding." This platform uses ChromaDB, with one collection per project.

A catalog organized by topic rather than by title

A title index locates a book only when its exact name is known. A topic-organized catalog locates books about a subject, even when different words are used. The vector store is that topic catalog; the closeness search (approximate nearest-neighbor) is the act of walking to the correct shelf.

Hybrid retrieval — meaning and words

Nearest-neighbor search on embeddings has one predictable failure: it matches meaning, so an exact token that carries the whole question — an error code, a SKU, a surname — can be smoothed into the neighborhood of merely similar things and lost. The classical fix is the older technology: BM25, keyword scoring that rewards rare terms appearing often in a short passage.

The topic catalog and the index at the back of the book

The topic catalog finds books about a subject even in different words. The back-of-book index finds the exact term and nothing else. A researcher uses both, and trusts a passage that appears in both lists more than one that appears in either alone.

Retrieval here uses both, then adds a third opinion:

  1. Vector search fetches a wide pool of candidates (top_k × 4).
  2. BM25 scores that same pool by keyword relevance.
  3. Reciprocal Rank Fusion merges the two rankings. It deliberately ignores the scores and uses only the positions — a cosine similarity and a BM25 score are not on a shared scale, but "third in one list, first in the other" is meaningful without any calibration.
  4. A cross-encoder reranker re-scores the shortlist. The embedding model is a bi-encoder — it reads query and passage separately, which is what makes the vectors precomputable and search fast. A cross-encoder reads the pair together, so it can weigh them against each other. It is much more accurate and much slower, which is exactly why it runs last, on a handful of candidates rather than the whole collection.

The reranker is optional (ADAPTA_RAG_RERANKER_MODEL="" disables it, leaving the fused order), and the citation scores reported to the client come from whichever stage ranked last.

The complete RAG cycle

Combining these pieces (adapta/services/rag.py, adapta/services/chat.py):

INDEX TIME (once, when a file is uploaded):
  document → parse → split into chunks → embed each chunk → store vectors in Chroma

QUERY TIME (on every chat request):
  question → embed it → vector search in Chroma (top_k × 4 candidates)
           → BM25 score the same candidates → fuse both rankings (RRF)
           → cross-encoder rerank the shortlist → keep top_k
           → inject them as context
           → model generates an answer grounded in those chunks → return with citations

The citations follow directly from the mechanism: because the system selects which chunks to place in front of the model, it can return those same chunks. The model is not asked to invent sources; it is given them.

Two practical safeguards in the code are worth noting, both of which are ordinary engineering. Retrieval runs off the event loop and under a timeout, so a stalled vector store degrades to a 504 rather than freezing every request; and the assembled prompt is fitted to the model's context window, dropping the least-relevant chunks first if it would not otherwise fit, rather than silently truncating the question.


4. LoRA adapters, from first principles

Fine-tuning changes how a model behaves by adjusting its weights from examples. Fully retraining a model with billions of parameters is prohibitively expensive, however. LoRA (Low-Rank Adaptation) is the technique that makes the operation affordable.

Patches applied over the original, rather than rewriting it

Full fine-tuning rewrites the entire model to instill one new behavior, which is enormous and wasteful. LoRA leaves the original weights untouched and instead produces a small set of corrective adjustments applied on top of them. These adjustments — the "adapter" — are tiny, a few megabytes against a multi-gigabyte model, yet sufficient to shift behavior.

In software terms, the base model is a large read-only dependency, and a LoRA adapter is a patch or diff applied over it at load time — comparable to a plugin, a version-control patch, or a style override. The small patch is shipped, not a new copy of the whole model.

Bring your own adapter

Adapta does not train adapters: in-product training (and with it the CUDA worker) was removed on 2026-10-10. Training an adapter is done with a LoRA trainer elsewhere. What the platform owns is everything after: import the adapter (PEFT, converted to GGUF by adapta/core/adapter_conversion.py, or GGUF directly), gate it on CPU against the base on the project's eval set, and serve it with RAG. The import flow is live (E4.2–E4.4, 2026-10-10); the import guide says what an adapter must match.

The eval set — labeled examples

The eval set is an instruction dataset: a JSONL file in which each line is a {prompt, response} pair (optionally with a system message), validated by adapta/services/datasets.py against specs/schemas/eval_dataset.schema.json. It is labeled data — input paired with desired output — the same shape as any supervised-learning task. The platform can also synthesize one from indexed documents (converting chunks into question/answer pairs), removing the need to author it by hand.

The job queue and worker

Long-running work is never performed inside a request handler, for the same reason that resizing ten thousand images synchronously within an HTTP request would be unacceptable.

An order ticket on a kitchen rail

The app is the front of house taking orders: it writes a ticket (a job, pushed onto the Redis queue). The worker is the kitchen: it takes the next ticket (via BLPOP), looks up the handler for its kind, and updates the ticket's status. The front of house never blocks while the kitchen works.

The worker (the same image as app) keeps the loop — heartbeat, healthcheck, requeue of the in-flight job on SIGTERM — but its handler table is empty today; an unknown kind fails the job with a typed error.


5. The evaluation gate — continuous integration for a model

An adapter introduces a specific risk: it can silently make a model worse. Unlike a code change, the difference cannot be read in a diff — the change is a shift across billions of weights. The question, then, is how to determine whether an adapter is safe to serve.

The answer mirrors how code is determined to be safe to ship: a test suite is run, and deployment is refused if it fails. That is the role of the evaluation gate.

A taste test on a fresh portion before a dish leaves the kitchen

A cook does not judge a dish by the spoon used to prepare it but by a fresh portion. The gate scores the adapter on a held-out eval set it never trained on, comparable to assessing a student with questions that were not on the study sheet. Scoring on the training data would be equivalent to grading an examination with the answer key in view, which reveals nothing about generalization.

Status

The gate that shipped with in-product training was removed on 2026-10-10 (E4.1). Its CPU replacement scores adapter and base with a task metric (E4.3, services/eval_gate.py); the adapter job runs it on every import (E4.4). An adapter that fails it never backs an endpoint. The design below is the contract the rebuilt gate keeps.

How the score is computed:

  1. Held-out rows. Only rows the adapter never trained on are scored.
  2. Response-only loss. The score measures how well the model produces the target answer; the prompt tokens are masked, so the answer is graded rather than the question. The mask is the shared prefix of the tokenized prompt and the full text, so a single-token answer (a bare classification label) is still graded.
  3. Score = exp(-loss). This converts average per-token "surprise" (perplexity) into a value between 0 and 1, where higher is better. A score of 1.0 indicates that the held-out answers were predicted perfectly.
  4. The gate. An adapter is permitted to serve if either it clears an absolute bar (default 0.6) or it clears a low sanity floor and surpasses the base model on the same held-out split by a margin.

A gate is only meaningful if it scores the model on the conditioning it will actually be served with, so scoring and serving must render every prompt through the same function — adapta.core.chat_templates.render. Otherwise an adapter could pass the gate on a conditioning no request ever sends.

The reason for the second path is itself instructive about calibrating a gate to reality. A small base model cannot reach the absolute bar even on an ideal task (the invented answer tokens carry an irreducible per-token cost — see the discussion of tokens), yet an adapter that reliably outperforms its own starting point has demonstrably learned the target behavior. The gate therefore measures improvement, not only an absolute threshold, which makes it a stronger signal rather than a weaker one: a non-improving or degenerate adapter still fails both paths.

In practice the gate rewards the tasks LoRA is genuinely good at — a repeatable shape in the answer: structured extraction (text → a fixed JSON schema), classification (message → one label from a closed set), and fixed format/voice (replies in a house template). These are exactly the cases where the held-out answer is predictable enough for the response-only loss to drop. A dataset with no learnable pattern (every answer different, or facts the adapter would have to memorize) cannot move the held-out score and is correctly blocked. Two fixture adapters in tests/fixtures/adapters/ (classify-pass and garbage-control) are the pass and fail cases the rebuilt gate is checked against.

An adapter that fails the gate raises EvalGateFailed (422), is not registered, and cannot back an endpoint. This is the platform's central safety guarantee: an unverified adapter never serves.


6. The complete picture

With the preceding sections in place, the Architecture page can be read straightforwardly. The platform consists of:

  • a web service (app) performing CRUD operations over PostgreSQL, plus two specialized flows;
  • RAG, an open-book-examination model powered by embeddings (coordinates for meaning) and a vector store (a database indexed by meaning);
  • adapters, in which LoRA adapters — patches over the frozen base — are imported and gated by a continuous-integration-style evaluation before they may serve (import and gate: E4.2–E4.4); a Redis queue and worker handle background jobs;
  • a single OpenAI-compatible API for serving, with llama-cpp executing quantized GGUF models under a per-model lock.

Every machine-learning term in that summary now has a plain engineering meaning.

Operating versus developing the ecosystem

The same concepts support two activities, and the documentation separates them deliberately:

  • Operating the platform — creating projects, supplying documents or datasets, observing the evaluation gate, and issuing keys — is described in the User Guide. The operator's mental model is the left-hand column of the §2 table (knowledge vs. behavior) plus the gate in §5.
  • Developing the platform — changing the API, the schema, the adapter path, or the serving path — is described in the Workflow page and the Architecture page. The developer's mental model adds the internal mechanisms in §1, §3, and §4, and the contract-driven process that protects them.

Glossary — machine-learning term to engineering equivalent

Term Engineering equivalent
Inference Executing a compiled function; the hot path
Training Compilation — expensive, performed offline (outside Adapta)
Token A syllable or word fragment; the unit of text and of cost
Weights The billions of constants that define the model function
GGUF A compiled binary of the model; llama-cpp is its runtime
Quantization Lossy compression of weights — smaller, slightly less accurate
Embedding Coordinates for meaning (similar text yields nearby vectors)
Vector store (Chroma) A database indexed by meaning rather than exact value
BM25 Classical keyword scoring — rewards rare terms in short passages
RRF Merges two rankings by position, so their scores need no shared scale
Cross-encoder Reads query and passage together; slow but accurate — used to rerank a shortlist
RAG An open-book examination: retrieve the relevant passages, then answer
Chunk One indexed passage of a document (a "page" placed in view)
Citation The chunks supplied to the model, returned for transparency
Fine-tuning Practice until a behavior is habitual (modifies weights); done elsewhere, then the adapter is imported
LoRA adapter A small patch or diff applied over the frozen base weights
Instruction dataset Labeled examples: prompt → desired response (here, the eval set)
Held-out split The fresh portion that is tasted / the unseen examination questions
Perplexity Average per-token "surprise"; lower indicates a better fit
Evaluation gate Continuous integration for a model — it cannot ship unless it passes
Base model A large read-only dependency on which every project builds
Endpoint / slug A deployed model instance and the name applications use to call it

Related pages: Architecture for how these pieces are assembled, and Workflow for how they are changed safely.