How LLMs Work Under the Hood: The Autocomplete Trap

The Illusion of Intelligence
When a chatbot answers a question fluently, it feels like talking to something that understands you. Architecturally, it is far simpler and far stranger: a large language model (LLM) is a very sophisticated autocomplete engine. It reads a sequence of text and predicts what comes next, one small piece at a time.
Understanding this single fact separates engineers who build fragile AI demos from engineers who ship systems that scale.
What an LLM Actually Returns
Tokens, Not Words
An LLM never sees raw sentences. Text is split into tokens, small chunks of roughly 3-4 characters. The model maps each token to a vector and runs it through layers of self-attention.
A Probability Distribution
The output of a single inference step is not an answer. It is a probability distribution over the entire vocabulary:
P(t_{n+1} \mid t_1, t_2, \ldots, t_n) = \mathrm{softmax}(W \cdot h_n)Given the prompt "2 + 2 = ", the token "4" simply carries an overwhelming probability. The model is not calculating; it is recalling a statistical pattern.
Autoregressive Generation
The chosen token is appended to the input, and the whole process repeats. This loop is called autoregressive generation, and it explains why asking a model to think step-by-step improves accuracy: each reasoning token becomes new context that steers the next prediction toward logical consistency.
Why AI Has Zero Memory
The Stateless API
An LLM API call is a pure function: text goes in, text comes out, and nothing is retained on the server between calls. The "memory" you experience in a chat product is an application feature.
The Compounding Context Problem
To fake memory, applications resend the entire conversation with every new message. Total tokens processed across a session grow quadratically:
T_{total} = \sum_{i=1}^{n} (S + i \cdot m) \approx n \cdot S + \frac{m \cdot n^2}{2}Here S is the system prompt size, m is the average tokens per turn, and n is the number of turns. At turn 3 nobody notices. By turn 10 latency has tripled and the monthly bill shows runaway growth.
The Chassis vs. The Engine
It helps to separate two layers:
- The Foundational LLM Engine: trained weights, the embedding layer and the self-attention matrices. It only predicts tokens.
- The AI Application Chassis: the session database, authentication, tool calling, guardrails, observability and budget controls.
Almost every production problem (cost, latency, safety, reliability) lives in the chassis, not the engine.
Building a Scalable Conversation Manager
Strategy
Three techniques keep context bounded:
- Sliding window: keep only the most recent turns.
- Summarization: compress older turns into a short running summary.
- Context caching: reuse the static prefix (system prompt, policies) across calls.
Reference Implementation
from dataclasses import dataclass, field
from typing import Callable, List, Dict
def rough_token_count(text: str) -> int:
# ~4 characters per token is a safe planning heuristic.
return max(1, len(text) // 4)
@dataclass
class ProductionConversationManager:
system_prompt: str
summarizer: Callable[[List[Dict[str, str]]], str]
max_context_tokens: int = 3000
keep_recent_turns: int = 6
summary: str = ""
history: List[Dict[str, str]] = field(default_factory=list)
def add_message(self, role: str, content: str) -> None:
self.history.append({"role": role, "content": content})
self._enforce_budget()
def _current_tokens(self) -> int:
total = rough_token_count(self.system_prompt) + rough_token_count(self.summary)
total += sum(rough_token_count(m["content"]) for m in self.history)
return total
def _enforce_budget(self) -> None:
# Compress the oldest turns once we exceed the token budget.
while (
self._current_tokens() > self.max_context_tokens
and len(self.history) > self.keep_recent_turns
):
overflow = self.history[: -self.keep_recent_turns]
self.summary = self.summarizer(
[{"role": "system", "content": self.summary}] + overflow
)
self.history = self.history[-self.keep_recent_turns:]
def build_payload(self) -> List[Dict[str, str]]:
messages = [{"role": "system", "content": self.system_prompt}]
if self.summary:
messages.append(
{"role": "system", "content": "Conversation so far: " + self.summary}
)
return messages + self.historyWith this manager, the payload size stays roughly constant instead of growing every turn, so cost per turn becomes predictable.
Agents Need Circuit Breakers
The same stateless, loop-driven design is dangerous for autonomous agents. An agent that retries a failing test forever can burn hundreds of dollars in minutes. Production agents need:
- A hard budget ceiling per task.
- A maximum iteration count.
- A circuit breaker that halts on repeated identical failures.
- Human-in-the-loop escalation.
Key Takeaways
- An LLM predicts the next token; it does not remember, calculate or search.
- Memory is an application-layer feature, and naive memory compounds cost.
- Bound your context with windows, summaries and caching.
- Wrap every autonomous loop in budgets and circuit breakers.