The project's README from github.com/amitshekhariitbhu/ai-system-design. As one markdown file: README.md.
AI System Design - A complete guide to learn AI System Design step by step - from LLM inference, GPUs, KV Cache, and caching to RAG, Vector Databases, AI Agents, MCP, Multi-Agent Systems, Voice AI, Guardrails, Evaluation, Observability, Cost Optimization, and a step-by-step framework to crack any AI System Design interview. Everything in one place, explained in simple words, with detailed blogs for every deep dive.
This AI System Design guide is helpful for anyone who wants to become:
- AI Engineer
- Gen AI Engineer
- LLM Engineer
- Agentic AI Engineer
- AI Agent Engineer
- Forward Deployed Engineer
- AI Solutions Architect
- AI Platform Engineer
- Applied AI Engineer
- Machine Learning Engineer
- MLOps Engineer
- LLMOps Engineer
- Backend Engineer building AI products
Note: AI System Design is moving very fast, so this guide will continue to grow as I write more blogs on new topics. Bookmark it and come back whenever you want a refresher. Keep learning.
In this guide, we will learn about AI System Design, the discipline of putting GPUs, inference servers, caches, vector databases, AI agents, gateways, guardrails, and evals together into one system that is fast, cheap, reliable, and safe. We will also see how an LLM actually runs on a GPU, how prefill and decode shape latency, how caching, routing, and batching cut the cost, how RAG and AI Agents are built for production, how we keep the system safe and measurable, and a step-by-step framework to solve any AI System Design problem in an interview or in real production.
When we use a product like ChatGPT, Cursor, Perplexity, or Claude Code, we see a simple chat box and a streamed response. Behind that simple interface, there is a lot more happening - GPUs, inference servers, vector databases, agent loops, caches, gateways, guardrails, and a long list of design decisions that all have to work together.
This guide is everything we need in one place. We start with the basics like tokens and the inference server, build up through hardware, prefill and decode, scaling, caching, RAG, agentic systems, multi-agent systems, multimodal and voice systems, safety, observability, evaluation, and inference optimization, and finish with a step-by-step framework to solve any AI System Design problem.
Every section is:
This guide focuses on the System Design side of AI. To learn the complete AI Engineering path - Machine Learning, Deep Learning, Transformers, LLMs, Fine-Tuning, RAG, AI Agents, and more - check out the AI Engineering Course.
AI System Design is the discipline of designing the complete system around an AI model, especially a Large Language Model (LLM), so that it can serve real users in a way that is fast, cheap, reliable, safe, and measurable.
In simple words:
AI System Design = System Design + The new constraints of AI models.
The new constraints are GPUs, tokens, long and streamed responses, non-deterministic output, and a real cost on every single request. AI System Design is how we design caches, queues, databases, gateways, retrieval, agents, guardrails, and evals around these constraints.
Let's say we want to build a customer support chatbot. Calling an LLM API in a script takes ten lines of code. But serving 100,000 users with an answer that starts in under a second, stays grounded in our own documents, never leaks private data, and fits within a monthly budget - that is AI System Design.
This AI System Design guide is for:
In this AI System Design guide, we will learn:
flowchart TD
A[Foundations: Tokens, Inference Server, Hardware] --> B[LLM Inference: Prefill and Decode]
B --> C[Scaling, Estimation, and Load Balancing]
C --> D[Caching and LLM Routing]
D --> E[Embeddings, Vector Databases, and RAG]
E --> F[Context Window Management]
F --> G[Streaming, Queues, Rate Limiting, AI Gateway]
G --> H[AI Agents, Tools, MCP, and Memory]
H --> I[Multi-Agent Systems]
I --> J[Multimodal and Voice AI]
J --> K[Guardrails, Safety, and Privacy]
K --> L[Observability and Evaluation]
L --> M[Cost, Fine-Tuning, and Inference Optimization]
M --> N[How to Solve Any AI System Design Problem]
I am Amit Shekhar, Founder @ Outcome School, I have taught and mentored many developers, and their efforts landed them high-paying tech jobs, helped many tech companies in solving their unique problems, and created many open-source libraries being used by top companies. I am passionate about sharing knowledge through open-source, blogs, and videos.
I teach AI and Machine Learning at Outcome School.
Let's get started.
When most of us start with AI, we just pick an OpenAI or Anthropic API key, write a small script that calls the API, and get a response back. We feel that our AI app is built.
For a small project or a personal demo, this is good enough.
But the real world is very different.
In the real world, an AI product serves millions of users. Each user sends long prompts. Each response is streamed token by token. Models are slow. GPUs are expensive. Costs add up fast. Hallucinations creep in. Latency matters.
A single API call cannot handle all of this.
To make our AI product reliable, fast, cheap, and safe, we have to think about many things together. This is where AI System Design comes into the picture.
In regular system design, we deal with CPU, RAM, disk, databases, and network.
In AI System Design, we deal with all of these, and on top of that, we deal with:
So all the regular system design concepts (load balancers, caches, queues, databases) still apply. But we have to use them differently because the workload is different.
Before we go into AI System Design, let's quickly recap what an LLM is.
LLM stands for Large Language Model. It is a model that takes some text as input and predicts the next token. It does this again and again until the full response is generated.
The text is not directly given to the model. It is first broken into smaller pieces called tokens. Think of it like a chocolate bar. The full bar is the sentence. Each small part we break off is a token. The model processes multiple tokens at a time. Most modern LLMs use BPE (Byte Pair Encoding) to do this tokenization. One token is roughly 4 characters in English. So short common words like "AI" or "Hello" are one token. Longer words like "Bangalore" are 2 tokens. Compound names like "ChatGPT" are 2 to 3 tokens depending on the tokenizer.
LLMs generate text one token at a time. This is called autoregressive generation, which means the model keeps feeding its own output back as the new input. Let's say we give the model this input:
"I love"
The model looks at "I" and "love", and predicts the next token: "teaching". The full sequence becomes "I love teaching". The model now looks at "I", "love", and "teaching" and predicts the next token: "AI". The full sequence becomes "I love teaching AI". This process continues, one token at a time, until the model decides to stop.
At every step, the model does not know the next token for sure. It gives a probability to every possible token, and then one token is picked. Settings like Temperature and Top-k and Top-p Sampling control how this pick happens. This is also why the same prompt can give different outputs, which is one of the biggest reasons AI System Design is different from regular System Design.
Internally, the LLM is a Transformer - a stack of attention and feed-forward layers. The single most important idea inside it is the attention mechanism, where each token converts itself into three vectors - Query (Q), Key (K), and Value (V) - and uses them to figure out which previous tokens matter most for predicting the next one. We have a detailed blog on the math behind Attention - Q, K, and V that goes into the math step by step.
When we use an API like OpenAI, Anthropic, or Google, we send a prompt, and we get a streamed response back, one token at a time.
Examples of LLMs: GPT-5.5, Claude Opus 4.8, Gemini 3.5, Llama 4, Mistral.
If we want to go deep into LLM internals - tokenization, positional encodings, Q/K/V matrices, attention, transformer architecture, KV Cache, Paged Attention, Mixture of Experts - the AI and Machine Learning Program by Outcome School covers all of these from scratch.
This is enough recap. Now, let's move to the actual System Design part.
Before we go into the details, let's understand the big picture.
An AI system is just a regular system with one new component in the middle: the LLM. Everything else (load balancer, cache, database, queue) is still there. The LLM brings new constraints (slow, expensive, non-deterministic, streaming), and we have to design the rest of the system around those constraints.
In short, an AI system is a regular system with an LLM added in the middle, plus smarter caching, streaming, cost tracking, safety, and evals built around it.
If we keep this in mind, every AI architecture decision becomes easy to reason about.
In regular system design, a server runs our application code and returns a response.
In AI System Design, we have something called the Inference Server.
An inference server is a special kind of server that runs the LLM. It loads the model into the GPU memory and serves prediction requests.
Most teams do not write the inference server from scratch. They use famous open-source ones like:
When we use OpenAI or Anthropic API, we are not running the inference server. We are calling their inference server over the internet.
When we run our own LLM (like Llama or Mistral), we run vLLM or TGI on a GPU machine, and our backend calls this inference server.
So the architecture looks like below:
Client -> Backend Server -> Inference Server (with GPU) -> LLM
The inference server is the heaviest component of an AI system. It needs a GPU. GPUs are expensive. A single H100 GPU costs around $25,000 to $40,000 to buy (depending on the variant, like PCIe vs SXM, and vendor) or roughly $2 to $4 per hour to rent.
Here, the analogy is simple. A regular server is like a kitchen with a few cooks. An inference server is like a kitchen with one very expensive chef (the GPU). We do not want this chef sitting idle for even a second.
This is why in AI System Design, we put a lot of effort into:
Now, let's see which inference engine to use. There are four main inference engines we will see in production. Each one has a sweet spot.
Rule of thumb:
Now, we know which inference engine to use based on our use case.
The inference engine is the software. But the software runs on a chip, and the chip decides a lot of our speed and our cost. So, before jumping into latency, we must know the three chips that come up again and again in AI System Design.
Here, the most important lesson is this: while an LLM writes the answer, the bottleneck is memory, not math. To produce every single token, the chip must read the full model weights from memory. A GPU keeps the weights in HBM (High Bandwidth Memory), which sits next to the chip. An LPU keeps the weights in SRAM, inside the chip itself, so the data does not have to travel.
Let me tabulate the differences between a GPU and an LPU for your better understanding.
| Point | GPU | LPU |
|---|---|---|
| Built for | Any parallel math, training and inference | Running a trained language model |
| Where the weights live | HBM, next to the chip | SRAM, inside the chip |
| Memory speed | Around 3 TB per second | Around 80 TB per second |
| Memory size per chip | Tens of GB | Around 230 MB |
| Chips for a large model | A few | Hundreds |
| Speed for a single user | Moderate | Very high |
| Batching | Needed to get good total output | Not needed to get good speed |
| Training support | Yes | No |
| Flexibility | Very high | Low, everything is planned ahead of time |
So, based on our use case:
The bigger lesson goes beyond one chip. Once a workload becomes memory bound, the win comes from moving data less, not from computing more. We will see this same idea again in prefill vs decode, KV Cache, and Flash Attention.
In regular system design, we measure latency in milliseconds and throughput in requests per second.
In AI, we have the same metrics, but we add a few new ones because of how LLMs generate output token by token.
This is the time it takes from sending the prompt to receiving the very first token back.
Example: We send "Tell me a story" to ChatGPT. It takes 800 ms before the first word "Once" appears on screen. TTFT is 800 ms.
This matters a lot for user experience. If the first token shows up fast, the user feels the app is responsive even if the full answer takes 10 seconds to finish.
This is the speed at which the model generates tokens after the first one.
Example: After the first token shows up, the model generates 50 tokens per second. So a 500-token response takes 10 seconds to fully appear.
This depends on the model size and the GPU. Smaller models are faster. Bigger models are slower but more accurate.
The same speed is often measured the other way around, as TPOT (Time Per Output Token), which is the time taken to produce each token after the first one. 50 tokens per second means a TPOT of 20 ms.
This is the total number of tokens the inference server can produce per second across all users.
Example: If 100 users are using the system at the same time, and each one is getting 20 tokens per second, the total throughput is 2000 tokens per second.
A good AI system has:
These three are in tension with each other. If we batch many requests together, throughput goes up but TTFT goes up too. We will see this trade-off later in continuous batching.
Every request costs money based on the number of tokens.
Modern LLM APIs now bill on three categories of tokens:
Example: Suppose a model charges $3 per 1 million input tokens and $15 per 1 million output tokens. If a user sends 1000 input tokens and gets 500 output tokens back, the cost of that one request is:
(1000 / 1,000,000) * $3 + (500 / 1,000,000) * $15
= $0.003 + $0.0075
= $0.0105 (about 1 cent)
If we are using a reasoning model, we have to add the cost of reasoning tokens too, which can be much larger than the visible output for hard prompts.
In AI System Design, we watch cost the same way we watch latency. Every dashboard, every alert, every postmortem includes the dollar number.
In short:
Now, let's understand why TTFT and TPS behave so differently. The answer lies in how an LLM actually answers a request. Every request goes through two distinct phases.
In simple words, prefill is the model reading our question, and decode is the model writing the answer. We have a detailed blog on Prefill vs Decode that explains both phases step by step.
Both phases use the exact same model and the exact same weights. Only the way the two phases work is different, and that small difference changes everything.
Prefill is the phase where the model reads and processes our entire input prompt in one single pass and produces the very first output token.
Let's say a student walks into an exam hall. Before writing anything, the student silently reads the entire question paper. This reading-the-whole-paper part is prefill.
Because the whole prompt is already known up front, the model can process every input token at the same time. While reading the prompt, it also computes and stores the Key (K) and Value (V) of every input token. All of these stored Keys and Values together form the KV Cache (we will learn about it in detail soon). At the end of prefill, the model produces the very first output token.
Prefill is compute-bound. It multiplies big tables of numbers with big tables of numbers, which is exactly what GPUs are built for. The math units of the GPU stay very busy, often approaching 90 percent or more on large prompts.
Longer prompts mean longer prefill. So, faster prefill means sooner we see the first token (lower TTFT).
Decode is the phase where the model generates the output tokens one at a time, reusing the KV Cache that prefill prepared.
Continuing our exam example, the student now starts writing the answer, word by word. This slow, word-by-word writing is decode.
In decode, only one token is processed per step. The math for one token is tiny. But to produce even that one token, the GPU must still read the entire model weights and the whole growing KV Cache from its memory. So, the GPU spends most of its time waiting for data to arrive, and its math units sit only around 20 to 40 percent busy for a single request.
Decode is memory-bandwidth-bound. Think of a chef who must walk into a giant pantry, grab one ingredient, walk back, chop it, and repeat, one ingredient per trip. The walking back and forth (moving data from memory) takes far longer than the chopping (the tiny math).
For a typical request, prefill finishes in a fraction of a second, but decode can run for several seconds. So, faster decode means faster we see the rest of the answer (lower TPOT).
Let me tabulate the differences between Prefill and Decode for your better understanding.
| Aspect | Prefill | Decode |
|---|---|---|
| What it does | Processes the whole input prompt | Generates output tokens one by one |
| Parallelism | Parallel (all prompt tokens at once) | Sequential (one token at a time) |
| Steps | One big step | Many small steps |
| Bottleneck | Compute-bound (limited by math speed) | Memory-bandwidth-bound (limited by data movement) |
| GPU utilization | High (math units busy, can approach 90 percent or more) | Low (math units mostly idle, around 20 to 40 percent) |
| KV cache action | Writes the cache | Reads and extends the cache |
| Latency metric | Time To First Token (TTFT) | Time Per Output Token (TPOT) |
| Cost driver | Grows with prompt length | Streams full weights plus KV cache every step |
This split explains a lot of AI System Design. It is why input tokens are cheaper than output tokens. It is why a long prompt makes the first token slow. And it is why so many optimizations focus on the decode phase.
But, here is the catch. When prefill and decode run on the same GPU, they fight with each other. A very long prompt's prefill runs as one giant step. While that giant step runs, it hogs the GPU and freezes the token streaming of other users who are already in their decode phase. Their answers stutter.
Chunked prefill splits one long prompt's prefill into several smaller chunks, and slips small decode steps in between the chunks.
WITHOUT chunked prefill:
long prompt: [============= ONE BIG PREFILL =============]
other users: tok ....... (frozen, stuttering) ....... tok
WITH chunked prefill:
long prompt: [chunk 1][chunk 2][chunk 3][chunk 4][chunk 5]
other users: tok tok tok tok tok tok
^ a decode step slips in between the chunks
Here, we can see that one giant prefill freezes the other users, while chunked prefill lets their tokens keep flowing between the chunks. It is like a chef with one stove. If a customer orders a huge banquet, cooking it all at once freezes every other table's food. Instead, the chef cooks the banquet in small batches, slipping in the quick single dishes between them.
Note: Chunked prefill protects ongoing answers from stalling. It does not make prefill itself faster.
At large scale, we can go one step further. So, here comes Prefill-Decode Disaggregation to the rescue.
Prefill-Decode Disaggregation = Prefill + Decode + Disaggregation, where disaggregation simply means separating things that were joined together.
Prefill-Decode Disaggregation is the technique of running the prefill phase and the decode phase on separate GPUs, so that they never fight for the same hardware.
In simple words, we have one group of GPUs whose only job is to read prompts, and another group of GPUs whose only job is to write answers. There are four main parts:
The flow looks like below:
Request
|
v
[Router] --> picks a Prefill worker and a Decode worker
|
v
[Prefill worker] --> reads the full prompt
--> builds the KV Cache
--> produces the first token
|
v (KV Cache is sent over the network)
|
[Decode worker] --> loads the KV Cache
--> writes token 2, token 3, token 4 ...
|
v
Answer streamed to the user
Let's take one request with example numbers, just for the sake of understanding. A user sends a prompt of 4000 tokens and the answer will be 200 tokens long. The prefill worker processes all 4000 tokens in, say, 400 ms and produces the first token along with a KV Cache of, say, 2 GB. The user sees the answer start, so our TTFT is around 400 ms. The 2 GB KV Cache is copied to the decode worker in 20 to 50 ms. The decode worker then produces each token in, say, 20 ms, so the 200 tokens take about 4 seconds. The prefill worker has already moved on to the next user's prompt, and the decode worker is never interrupted by a big prefill.
So, the user gets a quick start (low TTFT) and a smooth flow (low TPOT), both at the same time.
Let me tabulate the differences between co-located serving and disaggregated serving for your better understanding so that you can decide which one to use based on your use case.
| Point | Co-located Serving | Disaggregated Serving |
|---|---|---|
| Where prefill and decode run | Same GPU | Separate prefill and decode workers |
| Interference between phases | Yes, prefill stalls decode | No |
| TTFT and TPOT | Trade-off, improving one hurts the other | Both can be tuned separately |
| Scaling | Scale both phases together | Scale each phase independently |
| KV Cache transfer | Not needed | Needed for every request |
| Network requirement | None | Fast interconnect is a must |
| Complexity | Low | High |
| Best for | Small scale, single GPU, short prompts | Large scale, many users, long prompts |
For a small setup, chunked prefill on a single GPU is simpler and good enough. For a large setup, disaggregation is the way to go. Serving systems like SGLang support it out of the box.
When traffic grows, we need to scale.
In regular system design, scaling means more CPU, more RAM, or more machines.
In AI System Design, scaling has more flavors because LLMs are huge. A model like Llama 70B does not even fit in a single GPU. We need to split the model across multiple GPUs.
Let's see the different ways to scale.
Use a bigger machine. In AI, this means a bigger GPU.
Example: Move from an A100 (80 GB) to an H100 (80 GB but faster) or to an H200 (141 GB).
This works only up to a point. The biggest GPU on the market is still limited. Big models do not fit.
Add more GPU machines and put a load balancer in front of them.
Example: We have 4 inference servers, each running Llama 8B on a single A100. The load balancer distributes user requests across these servers.
This is the most common form of scaling. It works perfectly when the model fits in one GPU.
When the model does not fit in one GPU, we split the model itself across multiple GPUs.
Example: Llama 70B in 16-bit precision needs around 140 GB. A single A100 has 80 GB. So we split the model across 2 A100s. Each GPU holds half of the model weights. When a forward pass happens, the GPUs talk to each other very fast over NVLink (a high-speed direct connection between NVIDIA GPUs that is much faster than going over a regular network) to compute the result.
This is called tensor parallelism because we split the tensors (weight matrices) of the model across GPUs.
In pipeline parallelism, we put different layers of the model on different GPUs.
Example: A model has 80 layers. We put layers 1 to 40 on GPU 1 and layers 41 to 80 on GPU 2. The token first goes through GPU 1, then to GPU 2.
This is good for very large models that have many layers.
Here is a side-by-side picture of the two parallelism styles:
Tensor Parallelism Pipeline Parallelism
(split each layer across GPUs) (split layers across GPUs)
Input Input
| |
v v
+--------+--------+ +---------+
| GPU 1 | GPU 2 | layer 1 | GPU 1 | layers 1..40
| weight | weight | (weights split) +---------+
| half A | half B | |
+--------+--------+ v
| +---------+
v | GPU 2 | layers 41..80
+--------+--------+ +---------+
| GPU 1 | GPU 2 | layer 2 |
| ... | ... | v
+--------+--------+ Output
|
v
Output
In tensor parallelism, the GPUs talk to each other inside every layer. In pipeline parallelism, each GPU finishes its layers before passing the token to the next GPU. Both can be combined for very large models.
We pick the scaling strategy based on our model size and traffic:
This is similar to regular auto scaling, but with one big difference.
In regular auto scaling, we add a new EC2 instance (a virtual server on AWS) when CPU usage crosses a threshold. This takes 30 seconds to 2 minutes.
In AI, when we add a new inference server, the GPU has to load the model first. A 70B model can take anywhere from 1 to 10 minutes to load into GPU memory, depending on whether the weights live on local SSD, network storage, or remote object storage. So we cannot just spin up a new GPU when traffic suddenly spikes.
So, here comes the warm pool to the rescue. We keep some inference servers always warm (always running, model already loaded). When traffic spikes, we route to these warm pools first, and in the background, we spin up more machines.
So in AI, auto scaling is slower and more careful. We always keep extra capacity ready because cold starts are very expensive.
In AI System Design interviews, we estimate three things:
Let's take an example. We are building a ChatGPT-like product. Suppose we have 10 million daily active users, and each user has 5 conversations per day with an average of 2000 input tokens and 500 output tokens per conversation.
Daily input tokens = 10 million * 5 * 2000 = 100 billion tokens per day
Daily output tokens = 10 million * 5 * 500 = 25 billion tokens per day
Total tokens per day = 125 billion tokens
Suppose one H100 GPU can produce 5000 tokens per second on a Llama-70B-class model.
Tokens per H100 per day = 5000 * 60 * 60 * 24 = 432 million tokens per day
Total H100 GPUs needed = 25 billion / 432 million = ~58 GPUs (just for output)
For input processing, we usually need fewer GPUs because input tokens are processed in parallel (we will see this in continuous batching).
So roughly 60 to 100 H100 GPUs are needed.
H100 cost per hour = $3 (rented)
Daily cost = 60 * 24 * 3 = $4320 per day
Monthly cost = ~$130,000 per month
This is for the inference alone. Storage, vector DB, observability, etc. add more cost.
This kind of estimation is what we do in AI System Design interviews. The numbers here are just examples, but the method is what matters.
In regular system design, we use round-robin or least-connection algorithms to distribute requests.
In AI, round-robin is a bad choice. Why?
Because LLM requests are not equal. One request can take 1 second (short answer) and another can take 60 seconds (long answer). If we use round-robin, one server can get stuck on long requests while another sits idle.
So in AI, we use smarter load balancing.
The load balancer tracks how many tokens each server is currently generating. The next request goes to the server with the fewest outstanding tokens. This is much more accurate than counting connections.
When two requests share a long common prefix (like a long system prompt), routing them to the same server is faster because the KV cache (we will learn about this soon) for that prefix is already warm.
Example: A coding assistant has the same 5000-token system prompt for every request. If we route all requests to different servers, each server has to recompute that prefix from scratch. If we route them to the same server, the prefix is already cached.
For a long conversation with the same user, we sometimes want to send all requests of that user to the same server, so the conversation history stays warm in cache.
So in AI, the load balancer is much smarter than in regular system design.
Caching in AI is much richer than caching in regular systems. The basic shape of any cache is the same. On every request, we check the cache first. If the data is there (a cache hit), we return it without calling the LLM. If not (a cache miss), we call the LLM, store the result in the cache, and return the answer.
Cache Hit (data already cached, skip the LLM)
Client ---> Server ---> Cache
|
| (found)
v
Client <--- Server <----- Cache
Cache Miss (data not in cache, call the LLM and store)
Client ---> Server ---> Cache
|
| (not found)
v
Server ---> LLM
|
v
Server ---> Cache (store for next time)
|
Client <--- Server <--------+
The win from caching is the size of the gap between a cache hit and a cache miss. In AI, that gap is huge - a hit costs us a few milliseconds in Redis, a miss costs us a full LLM call (seconds and tokens).
We have four major types of cache.
This is the most important cache in AI. We have a detailed blog on KV Cache that explains it from the ground up.
When an LLM generates one token at a time, for each new token, the model has to look at all the previous tokens. If we recompute everything from scratch for every new token, it is extremely slow.
Inside the attention layer, every token is converted into a Query (Q), a Key (K), and a Value (V). The current token uses its Query to compare against the Keys of all previous tokens, and then collects the Values from the relevant ones. So at every step, the model needs the Keys and Values of all previous tokens.
Without caching, the model would recompute Key and Value for every previous token at every step. Let's see this with our running example:
The model is repeating the same work for tokens it has already seen. This is wasted computation.
Do you see the problem? We are paying the GPU to redo work it has already done.
This stored memory is called the KV Cache. The idea is simple: compute the Key and Value for each token once, store them, and reuse them in every future step.
Think of it like taking notes in a meeting. Every time someone new speaks, instead of asking all the previous speakers to repeat what they said, we simply read from our notes and only listen to the new speaker. The notes are the cache.
To put this into perspective with real numbers - if the model is generating 100 tokens, without KV Cache the total Key and Value computations are 2 + 3 + 4 + ... + 100 = 5,049. With KV Cache, it is 2 + 1 + 1 + ... + 1 = 101. That is roughly 50 times fewer computations. The longer the sequence, the bigger the savings.
We cache only the Keys and Values, not the Queries. During autoregressive generation, the Query is only needed for the current token being generated. Once the prediction is done, that Query is no longer needed. But the Key and Value of every past token are needed at every future step.
KV Cache makes generation much faster, but it has to sit in memory the entire time the conversation is active. A long conversation can use gigabytes of KV cache. This is why we cannot serve too many long conversations at the same time on the same machine.
To manage the KV Cache memory more efficiently, modern inference servers use Paged Attention - the same idea operating systems use to manage memory. Instead of reserving one big block of memory upfront for each request (which wastes a lot of memory because we do not know in advance how long the response will be), Paged Attention allocates small fixed-size pages of KV cache as the response grows.
Think of it like a hotel where guests check in but do not know how many nights they will stay. The traditional approach reserves 30 rooms in a row for every guest, even if most stay only 2 nights. Hundreds of rooms sit empty. The paged approach gives guests one room at a time, as needed, wherever a room is available. The hotel keeps a list (the page table) of which rooms belong to which guest. The same idea applies to KV Cache - the system can serve far more users with the same amount of memory.
Paged Attention stops us from wasting memory. But the KV Cache itself is still huge. Let's see how huge with a model close to Llama 2: 32 layers, 32 attention heads, a Key and a Value of 128 numbers each per head, and each number stored in 2 bytes.
layers = 32
heads = 32
head_dim = 128
bytes_per_number = 2
# Key and Value, so multiply by 2
per_token = 2 * layers * heads * head_dim * bytes_per_number
print(per_token / (1024 * 1024)) # 0.5 MB
Here, we are multiplying the number of layers, the number of heads, the size of each head, and the bytes per number. We multiply by 2 because we store both the Key and the Value. So, one token needs 0.5 MB.
A conversation of 4,000 tokens needs 2 GB. A document of 100,000 tokens needs 50 GB - just for the cache of one user. A GPU has 24 GB to 80 GB, and a big part of it is already taken by the model weights. The KV Cache grows with the length of the text and with the number of users, and it eats the memory of the GPU.
So, here comes KV Cache Compression to the rescue. KV Cache Compression is the set of techniques that make the KV Cache smaller while keeping the quality of the model output almost the same.
Let me tabulate the four main approaches for your better understanding.
| Quantization | Token Eviction | Sharing across Heads | Low-Rank Compression | |
|---|---|---|---|---|
| How it saves memory | Fewer bits per number | Fewer tokens stored | Fewer Keys and Values per layer | Smaller vector per token |
| Needs retraining | No | No | Yes | Yes |
| Throws away tokens | No | Yes | No | No |
| Typical saving | 2x to 4x | Fixed cache size | 4x to 8x | 10x or more |
| Quality loss | Very less | Depends on the task | Very less | Almost none |
| Example | KIVI | H2O, StreamingLLM | GQA, MQA | MLA (DeepSeek) |
So, based on our use case:
Note: GQA (Grouped Query Attention) and MQA (Multi-Query Attention) let many attention heads share the same Keys and Values, and MLA (Multi-head Latent Attention) stores a small compressed vector per token instead of the full Keys and Values. We will see GQA in detail in Inference Optimization.
These approaches can be combined. A smaller KV Cache means more users per GPU, longer contexts, and faster decode, and that too without buying a bigger GPU.
Many apps send the same long system prompt with every request.
Example: A customer support bot has a 3000-token system prompt that explains the rules. Every user message goes with this same system prompt.
Without prompt cache, the inference server processes those 3000 tokens for every single request.
With prompt cache, the inference server processes the system prompt once and stores the result. For the next request with the same prompt, it reuses the stored result and only processes the new user message.
What is actually stored is the KV Cache of that shared beginning, which is called the prefix. This is why prompt caching is also called prefix caching, and why it cuts the prefill work and the TTFT. It is like a teacher who writes the same long instructions on the board at the start of every class. Instead of rewriting them each time, the teacher keeps a photo of the board and reuses it.
Note: Prompt caching only helps when requests share the exact same beginning. So, we must put the fixed parts (system prompt, tool definitions, long documents) at the start of the prompt, and the changing parts (the user message) at the end.
Both OpenAI and Anthropic support prompt caching. It can cut input cost by up to 90% and reduce TTFT massively. It makes our life easy.
This is unique to AI.
Suppose two users ask:
These are different sentences but mean the same thing. In a regular cache, we would call the LLM twice because the input strings are different.
In a semantic cache, we convert each query into an embedding (a vector). We store the embedding and the response. When a new query comes in, we convert it to an embedding and search for similar embeddings in the cache. If we find a close match, we return the cached response without calling the LLM.
This saves cost and latency.
But here is the catch. "Close enough" is not always "the same". We need to set a similarity threshold carefully.
0.99, almost nothing matches. Only an exact copy would pass, and we lose most of the benefit.0.50, very different questions start matching. "Capital of France" could wrongly match "Capital of Germany" and return the wrong city.A value around 0.85 to 0.95 works well for many apps, but the right value depends on our use case. We must test it with real questions from our users and adjust it.
A high threshold means fewer matches but safer answers. A low threshold means more matches but a higher chance of a wrong answer.
Note: Never share a semantic cache across tenants or users when the answer depends on private data. "What is my account balance?" must never be served from someone else's cache entry.
When we build a RAG system (we will see this soon), we convert lots of documents into embeddings. Computing embeddings is itself an expensive operation.
We cache the embeddings of common queries and documents in Redis or any key-value store. The key is the text (or its hash), and the value is the embedding vector. We have a detailed blog on how an Embedding Cache works that explains how the cache key is built.
Note: The cache key must include the name and version of the embedding model too. The same text gives different vectors in different models, so mixing them silently breaks the search.
Next time we see the same text, we read the embedding from cache instead of recomputing it.
In short:
In production AI, we rarely use one single model.
We have many models with different cost, speed, and quality:
For every user request, we ask: which model should we use?
This is model routing, also called LLM Routing.
LLM Routing is the practice of choosing the right LLM for each user query, instead of sending every query to the same LLM.
A simple router can decide based on the question:
The high-level picture looks like below:
+----------------+
user query --> | LLM Router |
+----------------+
|
+-----------+-----------+
v v v
+----------+ +----------+ +----------+
| Small | | Mid | | Frontier |
| LLM | | LLM | | LLM |
+----------+ +----------+ +----------+
A model router is just a regular function, a classifier, or a tiny model that runs before the actual LLM call. It can save 50% to 70% of cost without hurting quality much.
Note: The router itself must be very fast. A good rule of thumb is to keep the router's own latency under 10 percent of the chosen LLM's latency. If the chosen LLM takes 1 second, the router should add at most 100 ms.
There are five common routing strategies:
(query, best-LLM) pairs. It learns from real data but needs labeled data and retraining.Cascade routing is the most cost-saving strategy, so let's see it in code:
def cascade(query: str) -> str:
answer = small_llm(query)
if is_confident(answer):
return answer
answer = mid_llm(query)
if is_confident(answer):
return answer
return frontier_llm(query)
Here, each step either returns the answer or escalates to the next LLM. The is_confident check can be a self-rating from the LLM, a verifier model, or a simple heuristic like the presence of "I do not know". Most queries get solved by the cheapest LLM. The hard queries pay for two or three calls.
Let me tabulate the differences between the routing strategies for your better understanding so that you can decide which one to use based on your use case.
| Strategy | Speed | Cost | Flexibility | Needs Training |
|---|---|---|---|---|
| Rule-based | Very Fast | Very Low | Low | No |
| Classifier-based | Fast | Low | Medium | Yes |
| Embedding-based | Fast | Low | Medium | No |
| LLM-as-Router | Medium | Medium | High | No |
| Cascade | Slow on hard queries | Lowest on average | High | No |
There is no clear winner here. For a small system with a few clear query types, rule-based is enough. For a large product with mixed queries, cascade or LLM-as-router works better.
Common mistakes to avoid:
Note: LLM Routing is not the same as Mixture of Experts. LLM Routing picks one full LLM for a full query, outside the model. Mixture of Experts picks a few expert sub-networks for each token, inside one model. We will see Mixture of Experts later.
Tools like OpenRouter provide model routing as a service.
Regular databases store rows and columns. They are good for exact-match queries.
But in AI, we deal with embeddings. An embedding is a long list of numbers (typically 768, 1024, 1536, or 3072 numbers depending on the model) that represents a piece of text in a way that captures its meaning. Two pieces of text that mean similar things get numbers that sit close together. This is what lets us do Semantic Search, which means searching by meaning instead of by exact words. Images can be turned into embeddings too, and we have a detailed blog on how Image Embeddings work.
When we want to find similar pieces of text, we cannot use SQL WHERE. We have to compute the similarity between two embeddings using a math formula like cosine similarity (which measures how close the two vectors point in the same direction) or dot product (which multiplies and sums the numbers in the two vectors). Both give a single similarity score we can sort by.
A normal database is too slow for this. So we use a Vector Database.
A Vector DB is specially built to:
Examples of Vector DBs: Pinecone, Weaviate, Qdrant, Chroma. Some regular databases also have vector support: PostgreSQL with pgvector, MongoDB, and Redis.
Internally, Vector DBs use special algorithms called vector indexes to make similarity search fast even with millions or billions of vectors.
Comparing the query with every single stored vector one by one is exact, but far too slow at scale. So, vector indexes use Approximate Nearest Neighbor (ANN) search. ANN gives up a tiny bit of accuracy to become thousands of times faster. It finds vectors that are almost certainly the closest, without checking all of them.
Now, let's understand the different types of vector indexes. There are four indexes that come up again and again. Each one has a sweet spot based on dataset size and memory budget.
Rule of thumb:
For most apps, pgvector inside PostgreSQL with HNSW is enough. We do not always need a dedicated vector database.
LLMs do not know our company data. They were trained on public data with a cutoff date.
If a user asks our chatbot "What is the refund policy of our store?", the LLM does not know.
To solve this, we use RAG (Retrieval Augmented Generation).
RAG = Retrieval + Augmented + Generation. The idea is simple:
Let's see the flow.
Step 1: Indexing (offline, done once)
Step 2: Query time (online, done for every user query)
The architecture looks like below:
Indexing (offline)
Documents -> Chunker -> Embedding Model -> Vector DB
Query (online)
User -> Backend -> Embedding Model -> Vector DB (top-k chunks)
|
v
LLM <- (Prompt + chunks)
|
v
Response
The best way to think about RAG is with a real-world analogy. Suppose we want to find an answer in a big library. Standard RAG is like a librarian who fetches one book. We hand over the question. The librarian picks the book that matches the words best, gives it to us, and the LLM reads it to write the answer. One shot. Done.
The librarian flow is fast but cannot adapt to the question. For more complex questions that need many steps, we need a researcher instead of a librarian. That is Agentic RAG, where an AI Agent drives the retrieval process and decides every step. We will see Agentic RAG in detail below.
RAG is the most popular architecture for AI products today. ChatGPT search, Perplexity, customer support bots, code assistants - all of them use RAG in some form.
Now, let's discuss the part that most RAG tutorials skip: getting clean text out of real-world documents. In production, our source data is rarely clean Markdown. It is PDFs, scanned images, slides, web pages, spreadsheets, Word documents, and emails.
If we feed messy text into the pipeline, no amount of clever retrieval can save us. The model can only answer from what it was given.
A real document parsing pipeline does the following:
A common pattern is confidence-based routing: high-confidence parses flow straight through, medium-confidence get flagged for spot-check, low-confidence go to manual review.
Tools used: Unstructured, LlamaParse, Mistral OCR, Nemotron Doc Pipeline, AWS Textract, Azure Document Intelligence.
In real RAG projects, 60% to 80% of the engineering effort goes into this step. We must build the document pipeline with the same care we give to the LLM call itself.
The way we cut documents into chunks decides the quality of RAG. A chunk is a small piece of text that we cut out from a bigger document. We have a detailed blog on Chunking Strategies for RAG that explains each strategy with examples.
If chunks are too big, the LLM gets too much noise. If chunks are too small, we lose context and the chunks do not make sense on their own.
Common chunking strategies:
Chunk overlap means we repeat the last few lines of one chunk at the start of the next chunk, so that an idea cut at the boundary still appears whole in at least one chunk. A common setting is an overlap of about 10 to 20 percent of the chunk size. So, for a chunk of 500 tokens, an overlap of 50 to 100 tokens works well.
The right chunk size depends on the kind of answer:
We start with the default and then tune it with evals on our own data.
Vector search is great at finding meaning. But it is bad at exact-match queries like "Order ID 12345" or "Section 4.2.1".
For exact-match, the old keyword search (BM25 - Best Matching 25) is much better.
So in production RAG, we use Hybrid Search: run both vector search and BM25, combine the scores, and return the best results from both.
The two lists have scores on different scales, so we cannot just add them. A popular way to merge them is Reciprocal Rank Fusion (RRF), which ignores the raw scores and only uses the position of each chunk in each list. A chunk that ranks high in both lists ends up at the top.
Tools like Elasticsearch, OpenSearch, Weaviate, and Qdrant support hybrid search natively.
There is one more problem in retrieval. The user's question and the real answer often look very different. A question is short and question-like. The answer in our documents is long and answer-like. So, their embeddings do not always sit close together.
So, here comes HyDE into the picture.
HyDE stands for Hypothetical Document Embeddings. It is a technique where we first ask the AI model to write a fake answer to the question, and then we search using that fake answer instead of the question.
HyDE = Hypothetical + Document + Embeddings. The core idea is simple: an answer looks like an answer. So, if we want to find a real answer, we must search using something that looks like an answer.
We do not show this fake answer to the user. We do not even trust that it is correct. We only use it as a better search key. The final answer is still written from the real documents.
Just for the sake of understanding, let's say closeness is measured from 0 to 1:
Here, we can notice that the fake answer is much closer to the real answer, so the search picks the correct chunk more easily.
Advantage: Better chunks for short, vague, or technical questions, with no extra training.
Disadvantage: One extra LLM call, which means more latency and more cost. The fake answer can also drift off topic.
A good practice is to test both plain search and HyDE on the same set of questions, and keep the one that finds better chunks for our data.
The first retrieval is fast but not very accurate. We pull, say, the top 50 chunks. Then we use a smaller, slower, but very accurate model called a reranker to score these 50 chunks again. We keep only the top 5.
A reranker is usually a cross-encoder model. The difference from regular vector search is simple. Vector search encodes the question and the chunk separately and then compares the two vectors - this is fast but a bit shallow. A cross-encoder feeds the question and the chunk together into the model and produces a single relevance score - this is slower but much more accurate because the model can directly compare the words in the question against the words in the chunk.
The two-stage funnel looks like below:
User Question
|
v
+---------------------------+
| Stage 1: Retrieval |
| (vector + BM25) |
| Fast, cheap, ~ok recall |
+-------------+-------------+
|
| top 50 chunks
v
+---------------------------+
| Stage 2: Reranker |
| (cross-encoder model) |
| Slower, very accurate |
+-------------+-------------+
|
| top 5 chunks
v
LLM
This two-stage retrieval (cheap retrieval + accurate reranking) is the standard for high-quality RAG. Stage 1 casts a wide net cheaply. Stage 2 picks the best ones precisely.
Examples of rerankers: Cohere Rerank, BGE Reranker, Jina Reranker.
A reranker is one of the simplest changes that gives a big jump in RAG quality. It also helps with the Lost in the Middle problem (we will see it soon), because sending 5 good chunks is better than sending 50 average ones.
Now, the following question arises. The cross-encoder reranker is accurate but slow. Vector search is fast but blunt, because it squeezes a whole passage into one single vector and loses word-level detail. Can we get both?
So, here comes ColBERT to the rescue. ColBERT keeps one vector per token instead of one vector per passage, and matches them with a cheap step called late interaction.
1. Cross-encoder (accurate, slow)
query + document together -> BERT -> score
BERT runs once per pair, nothing can be stored ahead of time
2. Single-vector dual encoder (fast, blunt)
query -> BERT -> [ one vector ]
>-- one dot product -> score
document -> BERT -> [ one vector ]
3. Late interaction, ColBERT (accurate and fast)
query -> BERT -> [ v1 v2 v3 ]
>-- MaxSim -> score
document -> BERT -> [ v1 v2 v3 v4 v5 ]
Here, we can see the difference clearly. Because the document is encoded without seeing the query, every document's vectors can be computed once, offline. At query time, each query token finds its best matching document token (this is called MaxSim), and the scores are added up.
The price is storage. A 100-token passage needs 100 vectors instead of 1, which is 100 times more storage. So, ColBERT buys its accuracy with disk space. We use it as a strong reranker, or for end-to-end retrieval when quality matters more than storage cost.
Standard RAG works for simple questions. But the moment the question gets harder, it breaks down. The librarian only knows how to match words. The librarian cannot think, cannot ask follow-up questions, and cannot decide where to look.
Standard RAG fails when:
So, here comes Agentic RAG to the rescue. We replace the librarian with a researcher.
In simple words:
Agentic RAG = RAG + an AI Agent that controls the retrieval steps.
In Agentic RAG, the agent decides every step:
The Agentic RAG loop:
User Question
|
v
+------------------+
| AI Agent |
| (the LLM) |
+------------------+
|
| thinks: "Do I need to retrieve?
| Which tool? What query?"
v
+------------------+
| Retrieval Tool |
| (vector / SQL / |
| web / graph) |
+------------------+
|
| returns chunks
v
+------------------+
| AI Agent |
| evaluates: |
| "Good enough?" |
+------------------+
|
+--- No --> retrieve again with a new query
|
+--- Yes --> generate the final answer
Common patterns of Agentic RAG: ReAct-style RAG (think, act, observe loop), Self-RAG (the agent grades its own retrievals), and Corrective RAG (CRAG) (the agent recommends a fallback source when the main retrieval is bad).
The trade-off is real: Agentic RAG has higher latency, higher cost, and is harder to debug. We use Agentic RAG only when standard RAG cannot do the job.
Let me tabulate the differences between Standard RAG and Agentic RAG for your better understanding so that you can decide which one to use based on your use case.
| Aspect | Standard RAG | Agentic RAG |
|---|---|---|
| Flow | Fixed pipeline | Dynamic loop |
| Number of retrievals | One | Many, as needed |
| Tools used | One (usually vector search) | Many (vector, SQL, web, and etc.) |
| Query rewriting | No | Yes |
| Quality check on retrieved chunks | No | Yes |
| Multi-hop questions | Cannot handle | Can handle |
| Latency | Low | Higher |
| Cost | Low | Higher |
| Complexity to build | Simple | More complex |
Now, let's move to a more advanced form of RAG. Vector RAG is great when the answer lives in one chunk. It struggles when the answer requires connecting facts across many chunks. For questions like "How are these two people connected?" or "What policies apply to all products in this category?", vector search just pulls a few related chunks and the LLM has to guess the connections.
So, here comes GraphRAG to the rescue. It solves this problem by building a knowledge graph from the corpus first.
In simple words:
GraphRAG = Knowledge Graph + RAG. Retrieve relationships, not just chunks.
The flow has two stages.
Stage 1: Indexing (offline)
Stage 2: Query time
A small example of a knowledge graph extracted from a corpus looks like below:
+-----------+
| Jane Doe |
+-----------+
|
| is_CEO_of
v
+-----------+ acquires +-----------+
| Globex | -----------------> | Acme |
| | (in 2023) +-----------+
+-----------+
|
| has_report
v
+-----------+
| Q4 2023 |
| revenue |
+-----------+
Question: "Q4 revenue of the company that acquired Acme in 2023?"
Traversal: Acme <- (acquires) <- Globex -> (has_report) -> Q4 2023 revenue
The arrows read in active voice: Globex acquires Acme, Globex has_report Q4 2023 revenue, Jane Doe is_CEO_of Globex. To answer the question, we walk backward along the acquires edge from Acme (which gives us Globex), then forward along the has_report edge (which gives us Q4 2023 revenue).
Vector RAG would never make this connection in a single retrieval. GraphRAG walks the edges and finds the answer by following the relationships.
GraphRAG is much better at:
The trade-off: GraphRAG is much more expensive to index (every document goes through LLM extraction) and harder to update incrementally. We use it when relationships matter and the data is fairly stable.
Microsoft Research's GraphRAG project is the most well-known implementation. Other tools: LlamaIndex Knowledge Graph Index, Neo4j LLM Graph Builder.
In short:
In production, the two are often used together.
Now, let's see the opposite direction. Vector RAG has some real problems:
Clause 14.2.b and Clause 14.3.b look almost identical to an embedding model.So, here comes Vectorless RAG to the rescue.
Vectorless RAG = RAG without the vectors. Instead of searching by number similarity, we let the LLM itself look at the structure of the document and decide which part to open, exactly the way a human finds an answer in a book.
Suppose we buy a washing machine and want to know about the warranty. We get a 200-page manual. We do not measure the similarity of anything. We open the table of contents, see "Chapter 9: Warranty and Service", jump to page 154, and read it. That is the whole idea.
It works in two phases:
Let me tabulate the differences between Vector RAG and Vectorless RAG for your better understanding so that you can decide which one to use based on your use case.
| Vector RAG | Vectorless RAG |
|---|---|
| Needs an embedding model and a vector database. | Needs neither. |
| Documents are cut into fixed size chunks. | Documents are kept whole and read section by section. |
| Search by measuring distance between numbers. | Search by the LLM reading titles and summaries. |
| No reasoning during the search. | Reasoning at every step of the search. |
| Very fast and very cheap for each question. | Slower and costlier, because it needs a few LLM calls. |
| Hard to explain why a chunk was picked. | Easy to explain, because the LLM gives its reason. |
| Scales very well to millions of documents. | Works best on long and well structured documents. |
| Struggles with exact codes, numbers, and negation. | Handles exact codes, numbers, and negation well. |
We must use Vector RAG for a very large number of short documents where speed and low cost matter, like a help center with a million articles. We must use Vectorless RAG for long, well structured documents where a wrong answer is expensive, like insurance policies, legal contracts, annual reports, and technical manuals. Many strong systems use both: a cheap vector or keyword search narrows millions of documents down to twenty, and then tree search finds the exact section inside those twenty.
To learn RAG, Vector Databases, embeddings, and retrieval pipelines end to end with hands-on projects, check out the AI and Machine Learning Program by Outcome School.
Every LLM has a maximum context window. GPT-5.5 has up to 400k tokens. Claude Sonnet 4.6 and Opus 4.8 have up to 1 million tokens. Some models go even further. But all of them are finite.
In production, we hit context limits all the time:
But there is a deeper problem. The maximum context window is the size we are allowed to use, not the size we must use.
As we fill the context with more tokens, three things happen, and they all hurt quality.
1. Context Rot. As the number of tokens in the context grows, the model's ability to recall information from anywhere in that context drops. Long-context evaluations (such as Needle-in-a-Haystack style tests and the more recent RULER and LongBench benchmarks) show that even models that advertise very long context windows start losing accuracy well before they hit that limit. So a large advertised context does not mean an equally large effective context.
2. Lost in the Middle. Research by Liu et al. (2023) showed that models pay the most attention to the start and the end of the context, and the least attention to the middle. If we plot accuracy against the position of the fact, we get a U-shaped curve. So a critical fact placed in the middle of a long context is more likely to be ignored than the same fact placed at the top or bottom. This silently breaks RAG systems that dump 50 chunks into the prompt. The fix is to send less but send the right thing (use a reranker and keep the top few chunks), and to place the most relevant chunks at the start and the end.
3. RoPE Positional Decay. Modern LLMs use Rotary Position Embedding (RoPE) to encode the position of each token. RoPE makes the attention score between two tokens depend only on the relative distance between them, and as two tokens get farther apart, their attention tends to decay. Newer models have softened this, but no production model has eliminated it.
The practical implication: where we place information in the prompt matters as much as the information itself. Important context should go at the top of the system prompt or right next to the question, not buried in the middle.
Now, let's understand context engineering. Because of these effects, the discipline of Context Engineering has emerged.
Context Engineering is the practice of designing, organizing, and managing everything that goes into an LLM's context window so that the model can do its task reliably.
It is the next layer above prompt engineering. Prompt engineering is about the wording of one instruction. Context engineering is about everything the model sees: the system prompt, the retrieved chunks, the tool definitions, the tool results, the memory, and the conversation history. Context engineering decides:
A useful way to think about this: the context window is a small workspace, and we are the one deciding what goes on it. We choose what to put in, what to leave out, what to put at the top, and what to push to the bottom. The model only sees what we place there.
We need a strategy to manage the context window.
Drop the oldest messages when we are about to overflow. Simple but loses information.
When the conversation is long, summarize the older messages into a shorter summary and put that summary in the prompt. This is what most chat products do.
Keep only the last N messages and the system prompt. Forget everything in between. Works for chat where only the recent context matters. The same idea exists inside some models too, where each token only attends to a fixed window of recent tokens. We have a detailed blog on how Sliding Window Attention works.
Context compaction is the technique of shrinking the old conversation into a short summary, so the important facts stay while the context window gets free space again. For agent loops, we also compact tool results that are no longer needed. Keep the question, the decisions, and the important findings. Drop the verbose tool outputs. Good compaction never summarizes away the system instructions, the user's main goal, or the very latest messages. Only the messy middle gets compressed. For example, an hour of a coding agent's work can become a short note like "Goal: fix the login bug. Already changed auth.py and login.py. Tests are passing." This is how an agent works on a long task for hours without losing the plot.
Store the full history in a vector DB and only retrieve relevant parts when needed.
Choosing the right strategy depends on the use case. For chat, summarization works well. For agents, compaction works well. For Q&A, hierarchical memory works well.
To learn how to build an AI Coding Agent from scratch and implement different layers of compaction in it, check out the AI and Machine Learning Program by Outcome School.
In regular APIs, the server sends one response after the work is done.
In AI, we cannot wait for the full response. A long answer can take 30 seconds. The user cannot stare at a blank screen for 30 seconds.
So we stream the response. As soon as the LLM generates a token, we send it to the user. The user sees the text appear word by word. This is called Token Streaming.
The difference between a regular response and a streamed response looks like below:
Regular HTTP (no streaming)
Client Server
| |
| --- request -------------> |
| | (model generates 500 tokens)
| | ...
| | ~10 seconds later
| <-- full response -------- |
| (all 500 tokens) |
Streaming (SSE)
Client Server
| |
| --- request -------------> |
| <-- "Once" --------------- | ~800 ms (TTFT)
| <-- " upon" -------------- |
| <-- " a" ----------------- |
| <-- " time" -------------- |
| ... |
| <-- " end" --------------- | ~10 seconds total
| <-- [DONE] --------------- |
| |
In streaming, the user sees the first token at TTFT (under a second) and watches the response build up. In a regular HTTP response, the user stares at a blank screen for 10 seconds and then everything appears at once. Streaming is what makes a slow LLM feel fast. It works perfectly.
There are two main ways to stream. We have a separate detailed blog on HTTP Request, Long Polling, WebSocket, and SSE that explains these in depth.
The most popular way. The client opens an HTTP connection. The server keeps it open and writes chunks as they come. Each chunk is a small message that starts with data:, and the stream ends with a special [DONE] marker. This is what ChatGPT uses on the web.
A two-way streaming connection. Used when we also need to receive messages from the client mid-stream (like cancel a request or send more context).
Streaming changes a lot of things in our backend:
Some AI tasks are too long to stream. For example:
For these, we do not stream. We do async processing.
The flow happens in two stages:
Stage 1: Client kicks off the job and gets a job_id back
Client ---request---> Backend ---enqueue---> Queue
Client <--job_id, "processing"-- Backend
Stage 2: Worker runs the task in the background
Queue ---pull---> Worker ---run task---> Database
(saves result)
Stage 3: Client polls for the result (or gets a webhook)
Client ---poll job_id---> Backend ---read---> Database
Client <---result/status--- Backend
Step by step:
Step 1: The client sends the request.
Step 2: The backend immediately responds with { "job_id": "12345", "status": "processing" }.
Step 3: The backend puts the job in a message queue.
Step 4: A background worker picks up the job and runs it.
Step 5: When done, the worker stores the result in a database.
Step 6: The client polls the status using the Job ID, or gets a webhook callback.
This is the same pattern as regular async processing, but the tasks are AI tasks (LLM calls, embeddings, transcription, etc.).
Message queues are heavily used in AI for the same reasons as regular systems: decoupling, reliability, and retry.
But in AI, the messages are heavier (long prompts, large documents, audio files). And the workers are slower (LLM calls, transcription).
Common usage:
Tools used: AWS SQS, RabbitMQ, Apache Kafka, Redis Streams.
Kafka shines when many independent consumers need to read the same stream of messages (for example, an indexing worker and an analytics worker both reading every uploaded document). SQS and RabbitMQ shine when each message should be processed by exactly one worker (a classic job queue). We pick based on the consumption pattern, not the AI part.
In regular APIs, we rate-limit by requests per second. Like 100 requests per minute per user.
In AI, this is not enough. Two requests can be very different in cost. One request can be 100 tokens, another can be 100,000 tokens.
So in AI, we rate-limit on multiple dimensions.
We track how many tokens a user has used in the last minute and block them after a limit.
Same as before, but in addition to TPM.
How many requests is a user running at the same time? This matters because LLM requests are long-running.
In some apps, we directly track the dollar cost a user is consuming and apply a cost cap.
Tools like Kong, Tyk, and Portkey provide multi-dimensional rate limiting for AI workloads.
Here, we have rate-limited on tokens, requests, concurrency, and cost together. This is critical because a buggy client can otherwise burn through our entire budget in minutes. Now, our AI system can stay safe under sudden load and abusive traffic.
In real production, our app does not call OpenAI, Anthropic, or our self-hosted vLLM directly. We put a single layer in between, called the AI Gateway (also called LLM Gateway). I will highly recommend treating the AI Gateway as one of the first pieces of infrastructure to build, because every other concern (cost, rate limiting, caching, guardrails, observability) becomes much easier once we have it.
Put simply, an AI Gateway is a reverse proxy specialized for LLM traffic.
A regular API gateway routes HTTP requests. An AI Gateway also understands tokens, prompts, streaming, and cost. It is the single point where every LLM call in our company flows through.
A typical AI Gateway sits like this:
+--------+ +-------------+ +-------------------+
| App | ------> | AI Gateway | ------> | OpenAI / Claude |
+--------+ | | ------> | Gemini / Llama |
| | ------> | Self-hosted vLLM |
+-------------+ +-------------------+
What does the AI Gateway give us?
Tools used: Portkey, LiteLLM, Kong AI Gateway, Envoy AI Gateway, TrueFoundry, AWS Bedrock (acts as a gateway too).
A few things we must get right when designing an AI Gateway:
Once we have an AI Gateway, every other concern in this blog (rate limiting, cost, caching, guardrails, observability) gets a single home. This is why an AI Gateway is one of the first pieces of infrastructure any serious AI team builds.
For RAG and semantic search, we need to embed lots of documents.
If we have a million documents and each one has 10 chunks, we have 10 million embeddings to compute. This is a big batch job.
The embedding pipeline looks like below:
Source Data (S3, DB, web)
|
v
Chunker (split documents into smaller pieces)
|
v
Embedding Model (compute embedding for each chunk)
|
v
Vector DB (store embeddings with metadata)
This pipeline runs as a batch job in the background. We use:
We also handle re-embedding:
A good embedding pipeline is incremental. It only processes what is new or changed, not the whole dataset every time.
Now, let's pick the right embedding model. The embedding model decides the quality of our retrieval. A bad embedding model means even the best vector DB returns junk. Common production options in 2026:
A useful trick worth knowing. Matryoshka embeddings are trained so that we can truncate them to a smaller dimension without retraining. So we can store the full 3072-dimensional embedding once, but at query time use only the first 256 or 512 dimensions for a fast first-pass search, and then re-rank with the full vector.
This gives us a memory-vs-quality knob without having to maintain two embedding models. OpenAI text-embedding-3 and Cohere both support this.
A good rule: pick the embedding model based on the task type, the language, the budget, and the privacy needs. Switching the embedding model means re-embedding everything, so this decision has gravity.
An AI Agent is a system that can take a goal from us, figure out the steps to achieve it, use tools when needed, and keep working until the goal is done. It is not just an LLM that answers one question. It is an LLM wrapped in a loop, with tools and memory, where the LLM is the decision-maker - it decides what action to take next, picks the right tool, reads the result that the loop feeds back, and plans the next move.
In simple words:
AI Agent = An LLM + Instructions + Tools + Memory + A loop that runs until the goal is achieved.
Think of an AI Agent like a new intern on their first day. They do not do the whole job in one go. They read the task, think about what to do first, use a tool (open a file, send a message, run a query), check the result, and decide the next step. They repeat this until the job is done. An AI Agent does the same thing - just faster and without breaks.
A plain LLM gives us one answer and stops. A chatbot gives many answers. An AI Agent talks and acts, many times, until the task is done.
Every AI Agent, no matter how complex, has the same five core parts.
+------------------------------------------------+
| Loop |
| |
| Instructions ---> +-------+ |
| | | |
| Memory ---> | LLM | <-----+ |
| +-------+ | |
| | | |
| | pick | result |
| v | |
| +-------+ | |
| | Tools | ------+ |
| +-------+ |
+------------------------------------------------+
1. The LLM (the brain). This is the reasoning engine. It reads the current situation and decides the next action - either pick a tool for the loop to call, or return a final answer. Every meaningful decision in the agent goes through the LLM. The LLM does not call tools itself - it only recommends which tool to use and what inputs to pass. The loop is the one that actually calls the tool.
2. The Instructions (the system prompt). This tells the LLM what its job is, what tools it has, and what rules to follow. Without good instructions, the agent is confused.
3. The Tools (the hands). These are the actions the agent can take - search, calculate, read files, call APIs, send emails, run code. Each tool has a name, a description, and an input schema.
4. The Memory. This is what lets the agent keep track of progress and avoid repeating work. Short-term memory is the conversation history of the current run. Long-term memory is anything we want the agent to remember across runs - user preferences, past decisions, learned facts. We will go deeper on memory in the next section.
5. The Loop (the runtime). This is the code that keeps the agent going. Send prompt to LLM. Read the LLM's response. If the LLM picked a tool, run that tool. Feed the result back. Repeat until the LLM says "done." The loop is what makes it an agent, not just a chatbot. The loop is also where tool calls actually happen - the LLM only decides which tool to use.
Going back to the intern analogy - the LLM is the intern's brain, the Instructions are the job description the manager handed over, the Tools are the laptop and email and office systems they can use, the Memory is the notebook where they track progress, and the Loop is the workday itself that keeps them going from one task to the next.
Remove any one of these, and it is no longer an AI Agent.
+----------------+
| User's Goal |
+----------------+
|
v
+----------------+ +-------------+
| LLM |------->| Tool |
| (decides: | +-------------+
| pick a tool | |
| or return | v
| final answer)| +-------------+
| |<-------| Observation |
+----------------+ +-------------+
|
| when goal is achieved
v
+----------------+
| Final Answer |
+----------------+
The goal comes in at the top. The LLM looks at it and decides one of two things - which tool the loop should call next, or the final answer directly. When the LLM picks a tool, the loop runs it and produces an observation, which is fed back to the LLM. The LLM then decides again. This little loop between the LLM, the Tool, and the Observation keeps running until the LLM decides the goal is achieved and returns the final answer.
This loop is also called the agent loop (the think, act, observe cycle), and the code around the LLM that runs it is called the harness. We have a detailed blog on Harness Engineering in AI that explains the harness in depth.
The whole skeleton of an AI Agent looks like this in Python:
async def run_agent(user_goal, tools, max_steps=10):
messages = [{"role": "user", "content": user_goal}]
step = 0
while True:
# Safety stop: bail out if we hit the step limit
if step >= max_steps:
return "Reached step limit without completing the task."
step += 1
# Ask the LLM what to do next
response = await call_llm(messages, tools)
# If the LLM says "I am done", stop the loop and return the final answer
if response.is_done:
return response.final_answer
# Otherwise, run each tool the LLM picked and feed the result back
for tool_call in response.tool_calls:
result = await call_tool(tool_call.name, tool_call.arguments)
messages.append({
"role": "tool",
"name": tool_call.name,
"content": str(result),
})
That is the entire AI Agent in about 20 lines of Python. A while loop, an LLM call inside it, a check for the stop signal, and a tool-call branch that feeds results back. Everything else we add on top - memory, parallel tool calls, retries, logging - is polish around this core loop.
Not all AI Agents look the same. Most reuse a small set of patterns:
These are not mutually exclusive. A real system often combines patterns - for example, a ReAct agent that uses Agentic RAG for knowledge lookups, with a Reflection step at the end for quality.
Now, let's go deeper into computer-use agents. This new class of agents deserves its own note because it changes the system design in several ways.
A regular tool-calling agent talks to APIs. A computer-use agent has no APIs to call. It looks at the screen the way a human does and clicks the same buttons a human would click. This is how it can drive any web app, any desktop app, even systems that have no API at all.
The architecture looks like below:
+---------------+
| User's Goal |
+---------------+
|
v
+---------------+ screenshot +-----------------+
| LLM | <--------------- | Browser / |
| (vision + | click, type | Desktop / OS |
| reasoning) | ---------------> | |
+---------------+ +-----------------+
|
v
+---------------+
| Final Answer |
+---------------+
The model sees the screen as an image, decides on a UI action (click at coordinates, type text, scroll), the runtime executes that action, and the next screenshot comes back. Loop until done.
Design implications:
Computer-use agents work well for tasks like filling out forms, scraping data from web apps without APIs, navigating legacy enterprise UIs, and running back-office workflows. They replace traditional RPA (Robotic Process Automation) with something far more flexible.
AI Agents are powerful, but they fail in specific ways:
Production-grade AI Agents are mostly about handling these failure modes well. Getting a single demo to work is easy. Keeping it stable across thousands of varied real user requests is the hard part.
Now, the next big question is: do we even need an agent? Many real AI products do not.
AI Orchestration is the process of coordinating multiple AI components, such as LLMs, tools, data sources, and agents, to work together to finish a complex task. Think of it like a conductor in an orchestra. The conductor decides who plays when, who plays louder, and who stops.
The key difference is who controls the flow. In AI Orchestration, the developer defines the steps, and the LLM does the work inside each step. In an AI Agent, the LLM itself decides what to do next.
Let me tabulate the differences between AI Orchestration and AI Agents for your better understanding.
| Aspect | AI Orchestration | AI Agents |
|---|---|---|
| Who controls the flow | The developer | The LLM |
| Flow type | Fixed, defined upfront | Dynamic, decided at runtime |
| Predictability | High | Low |
| Best for | Known workflows | Open-ended tasks |
| Cost control | Easier | Harder |
| Debugging | Easier | Harder |
The common orchestration patterns are Sequential (step after step), Parallel (independent steps at the same time), Conditional (a branch based on the output), Loop (repeat until a check passes), and Orchestrator-Worker (one LLM splits the task and hands pieces to workers). A simple chain of steps like this is also called Prompt Chaining, where the output of one prompt becomes the input of the next prompt.
In real systems, we often use both together. We use AI Orchestration for the high-level flow, and an AI Agent inside one of the steps when that step is open-ended. Start with a fixed workflow. Reach for an agent only when the steps cannot be known in advance.
When we do use an agent, the loop is where most of the quality comes from. Loop Engineering is the practice of designing the repeating cycle that an AI agent runs, so that the agent keeps making real progress on a task and stops at the right moment with the right result.
These are the parts of the loop that we, the engineers, must decide:
test_login passes" is strong, because a machine can check it.src/mian.py, did you mean src/main.py" lets the agent recover on the very next turn.Here, we can see why the goal matters so much. AI works best when a machine can check whether the task is done. We have a detailed blog on why AI is only as good as our definition of done.
In short: Prompt Engineering fixes what the model hears, Context Engineering fixes what the model knows, and Loop Engineering fixes what the model is allowed to do over time.
Graph Engineering is the practice of designing an AI system as a graph, where every step of the work is a node and every path from one step to another step is an edge. A shared state travels through the graph, conditional edges take decisions, and cycles let the system do the work again when a check fails.
Let me tabulate the differences between Loop Engineering and Graph Engineering for your better understanding.
| Point | Loop Engineering | Graph Engineering |
|---|---|---|
| Who decides the next step | The model decides | Our code decides, using the model's output |
| Structure | One loop of think and act | Many nodes joined by edges |
| Path on every run | Different every time | Same path for the same situation |
| Testing | Hard, because the path keeps changing | Easy, we can test one node at a time |
| Best for | Open ended and unknown tasks | Known work with clear stages |
Frameworks like LangGraph are built exactly on this idea. LangChain gives us the building blocks (prompts, chains, retrievers, and output parsers) to connect an LLM with our data and tools.
To learn AI Agents, Agentic AI, Tool Use, Memory, MCP, Subagents, and Multi-Agent Systems with real projects (including building our own AI Agent), check out the AI and Machine Learning Program by Outcome School.
Tool calling is how the LLM tells us "please call this function for me".
The LLM gets a list of available tools as part of its prompt. Each tool has a name, a description, and an input schema.
Here is what a tool definition looks like in practice:
{
"name": "web_search",
"description": "Search the web for pages matching the query. Use this tool when the user asks for current information, news, or anything not available in the model's training data.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to run."
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return. Default is 5."
}
},
"required": ["query"]
}
}
Here, the LLM reads the name to identify the tool, the description to decide when to recommend it, and the input_schema to know what arguments to pass and in what format. The description is the most important field - every word matters, because this is what the LLM uses to pick between tools. A vague description leads to the wrong tool being picked. A precise description leads to the right one.
When the LLM decides to recommend a tool, it returns a structured output that names the tool and gives the arguments. Our code parses this, runs the actual search, and feeds the result back as the next message. It works perfectly.
This is called function calling or tool calling. Both names mean the same thing. It is the foundation of all agentic AI.
Note: The LLM never runs the tool itself. It only returns the name of the tool and the arguments. Our code runs the tool. This is an important security boundary, because it means our code can validate every argument and refuse any action before it happens.
In an agentic system, designing good tools is more important than the model choice. Bad tools (vague names, unclear parameters, noisy outputs) make even the smartest model behave badly.
In the section above, we wrote our own JSON tool definitions and our own loop to call them. That works for a small project. But what if we want our agent to talk to GitHub, Slack, Google Drive, our own database, and 20 other systems? We do not want to write 20 different tool integrations in 20 different ways.
So, here comes MCP (Model Context Protocol) to the rescue.
In plain words, MCP is a universal standard for connecting AI applications to tools and data sources.
The popular analogy is: MCP is the USB-C port for AI. Just as USB-C is one standard plug that any device can use, MCP is one standard interface that any AI application can use to plug into any tool or data source.
MCP was introduced by Anthropic in late 2024 and has since been adopted by OpenAI, Microsoft, and parts of the Google and AWS stacks. As of 2026, the ecosystem has thousands of MCP servers covering developer tools, business apps, databases, and search.
MCP has two sides:
+----------------+ +-------------+ +----------------+
| AI Agent | | | | GitHub Server |
| (MCP Client) | <----> | MCP | <----> | (MCP Server) |
+----------------+ | Standard | +----------------+
| | +----------------+
| | <----> | Slack Server |
| | | (MCP Server) |
| | +----------------+
| | +----------------+
| | <----> | Database |
+-------------+ | (MCP Server) |
+----------------+
When our agent wants to use a tool, it does not care whether the tool talks to GitHub, Slack, or a database. It just speaks MCP. The MCP server on the other side takes care of the actual integration. The problem is solved.
Without MCP, every team writes its own tool layer. With MCP:
MCP is now the default way to do tool calling in production AI systems. If we are designing a serious AI System Design problem in 2026, mentioning MCP is expected.
Note: An MCP server is code that runs with real permissions. We must only connect to MCP servers we trust, give each server the smallest set of permissions it needs, and treat every tool result as untrusted text (we will see why in Prompt Injection).
MCP gives the agent reach. But reach is not enough. The agent also needs to know our way of doing things - our steps, our format, our checklists. Stuffing all of these into the system prompt fills the context window fast.
So, here comes Agent Skills to the rescue.
An Agent Skill is a folder of instructions, and optionally scripts and reference files, that an AI agent loads by itself only when the task actually needs it.
Agent Skills were introduced by Anthropic in October 2025, and the format is open. Consider a new employee joining our team. We do not retrain them from zero. We hand them our team handbook with separate chapters - one for filing an expense, one for releasing software, one for writing a report. They open one chapter only when they face that job. A Skill is exactly one such chapter.
Inside the folder, there is one required file called SKILL.md with a name, a description, and the instructions. The magic is progressive disclosure, which means revealing information in stages, only as much as is needed right now:
Level 1 -> name + description only (always loaded, tiny)
Level 2 -> the full SKILL.md instructions (loaded when it matches)
Level 3 -> extra files in the folder (loaded only if needed)
Level 1 costs only about a hundred tokens for each Skill. Level 2 is normally kept under five thousand tokens, and we pay that only for the one Skill we are actually using. So, fifty Skills at Level 1 cost about the same as the body of one single Skill. The agent behaves as if it knows fifty procedures in depth, while its context stays almost empty.
Let me tabulate the differences between Agent Skills and MCP for your better understanding.
| Point | Agent Skills | MCP |
|---|---|---|
| What it gives | Know-how, our steps and our format | Reach, a connection to outside systems |
| What it is made of | A folder of instructions, and sometimes scripts | A running program that offers tools and data |
| Question it answers | How should this be done? | What can the agent reach? |
| Needs a server running | No | Yes |
| Best for | Procedures, formats, style, checklists | Databases, apps, live data, actions |
In real work, we use both together. MCP connects the agent to our issue tracker, and a Skill tells it how our team writes an issue. There is also a third piece built almost exactly like a Skill: OKF (Open Knowledge Format), which writes down the knowledge about our data as a folder of plain markdown files, so any agent knows what our tables actually mean.
Many times, we do not want free-form text from the LLM. We want a clean JSON object that we can use directly in our code.
Example: We are building a resume parser. We want the LLM to read a resume and return:
{
"name": "Amit Shekhar",
"email": "[email protected]",
"skills": ["Python", "Go", "Kotlin"],
"experience_years": 10
}
If we just ask the LLM "give me JSON", sometimes it returns proper JSON, sometimes it adds extra text like "Sure, here is the JSON:", and sometimes it returns broken JSON.
To fix this, modern APIs support structured output (also called JSON mode or constrained decoding):
Examples: OpenAI's response_format with JSON Schema, Anthropic's tool-use mode, vLLM's guided decoding, Outlines library.
The problem is solved. Now, the next step in our pipeline can trust the JSON without writing fragile parsers.
Structured output is critical when the LLM output goes into the next step of a pipeline. Without it, we have to handle a lot of edge cases.
An LLM by itself is stateless. Every time we call it, it is like talking to someone who has never met us before. AI Agent Memory is everything we build around the LLM to give it the feeling of remembering. The LLM does not actually remember. The agent remembers on its behalf.
Put plainly, Memory is the system that lets a stateless LLM act like it remembers across turns, sessions, and users.
AI Agent Memory is not a single thing. It is a stack of layers, each with a different lifespan and purpose.
+--------------------------------------------+
| AI Agent Memory |
| |
| Layer 1: Context Window (in the prompt) |
| - In the current prompt |
| - Lost when context overflows |
| |
| Layer 2: Short-Term Memory (session) |
| - Scratchpad, current task state |
| - Lost when the session ends |
| |
| Layer 3: Long-Term Memory (persistent) |
| - Facts about the user |
| - Past conversations |
| - Learned preferences |
| |
| Layer 4: External Knowledge (tools/RAG) |
| - Documents, databases, APIs |
| - Queried, not maintained by agent |
+--------------------------------------------+
Layer 1 - Context Window. This is the text the LLM sees in a single call. It is the fastest memory because the LLM reads it directly. But it is also the smallest. Once it is full, older messages have to be dropped or summarized.
Layer 2 - Short-Term Memory. This is the scratchpad the agent uses during one task or session. It holds the plan, the current step, and the intermediate results. When the session ends, this memory is usually gone.
Layer 3 - Long-Term Memory. This is the persistent memory that survives across conversations and sessions. It stores user facts, preferences, past conversation summaries, and learned behaviors. This is what makes the agent feel like it "knows" the user. Within Long-Term Memory, there are further sub-types - episodic (past experiences), semantic (facts and concepts), and procedural (how to do things).
Layer 4 - External Knowledge. This is everything the agent can query but does not maintain - documents, databases, APIs, the web. The agent reads from these sources but does not write back to them as its own memory.
All four layers work together. The context window is the LLM's desk. Short-term memory is the notepad next to the desk. Long-term memory is the personal diary in the drawer. External knowledge is the library across the street. We want to keep our desk clean, our notepad organized, our diary trustworthy, and the library indexed.
No matter how many layers we have, every memory system does only four things:
1. Write. Save new information. After the user says "I prefer code in Go", we write "preferred language: Go" to long-term memory.
2. Read. Pull relevant information back when needed. This is usually the hardest part. We do not want everything, only what matters right now.
3. Update. Modify existing memories when new information contradicts them. If the user's favorite language was Python last month and is now Go, the memory must be updated.
4. Forget. Remove memories that are stale, wrong, or no longer relevant. Without forgetting, the memory store grows forever and retrieval gets worse.
Memory System = Write + Read + Update + Forget.
If any one of these is broken, the whole memory system breaks.
User Message
|
v
+----------------------------+
| READ Memory |
| - Long-Term (user facts, |
| preferences, summaries) |
| - Short-Term (plan, |
| recent steps) |
+-------------+--------------+
|
v
+----------------------------+
| Build Prompt |
| (system prompt + |
| retrieved memories + |
| user message) |
+-------------+--------------+
|
v
+----------------------------+ +---------------------+
| LLM |<-->| External Knowledge |
| (reasons, replies) | | (tools, RAG, APIs) |
+-------------+--------------+ +---------------------+
|
v
+----------------------------+
| WRITE Memory |
| - Short-Term (new step) |
| - Long-Term (if worth |
| keeping) |
+-------------+--------------+
|
v
Response
The same flow in plain words: on every turn, the agent retrieves relevant memories, builds the prompt, calls the LLM (which may recommend a tool that the loop then runs), writes new memories, and responds. **Read -> Build prompt -> Respond -> Write, on