Full-Stack Questions
LLMs, Agentic AI, RAG and Harnesses
Agent vs agentic AI, workflows vs agents, the harness, MCP, RAG end to end, evaluation and safety.
On this page
- Words used in this lesson
- How an LLM works, in the parts you need
- Workflows vs agents
- "AI agent" vs "agentic AI"
- What is a harness? (two meanings)
- MCP (Model Context Protocol)
- RAG, end to end
- Memory and context engineering
- Guardrails and prompt injection
- Observability and evals
- Cost control
- When NOT to use an agent
- Design sketch: a coding agent
- Interview questions
In one line: an LLM is a model that predicts the next piece of text, so everything you build on it is about feeding it the right text, checking what comes out, and keeping it from doing damage. A workflow is code that calls the model along a fixed path. An agent is a model that chooses its own next step inside a loop. RAG gives the model facts it was never trained on. A harness is the code around the model that makes all of this safe and repeatable.
As a full-stack engineer you will not train models. You will call them, wrap them, test them and pay for them. Interviewers now ask about this in every round: "what is RAG", "what is an agent", "what is a harness", "how do you stop prompt injection". The words are used loosely in the industry, so a clear answer with trade-offs stands out.
This lesson is about ideas and trade-offs. Provider facts (names, limits, prices) change fast, so each time I mention one I name the SDK or API it comes from. Code samples were run with the real packages, using a fake model so that no API key is needed.
Words used in this lesson
- An LLM (large language model) is a program trained on huge amounts of text that, given some text, predicts what text comes next. Comparison: autocomplete on your phone, but far better and much bigger.
- A token is a small piece of text (a word, part of a word, or a symbol) that the model reads and writes. Models count and bill in tokens, not words. In English, a token is often about three quarters of a word.
- The context window is the maximum number of tokens the model can look at in one call (your prompt, the history, tool results and its own answer together). Comparison: the size of the desk. Anything not on the desk does not exist for the model.
- A prompt is the text you send to the model. A system prompt is the standing instructions at the start (role, rules, tone).
- An API call to a model is stateless. The model remembers nothing between calls. If you want a "conversation", your code sends the whole history again each time.
- Inference is running a trained model to get an answer. Training is the expensive process that builds the model.
- A tool (also called a function) is a function in your code that the model may ask you to run, such as
getOrder(id). - A vector is a list of numbers. An embedding is a vector that a model makes from text so that texts with similar meaning get vectors that are close together.
How an LLM works, in the parts you need
The model reads tokens and, for the next position, produces a probability for every token it knows. Then a sampling step picks one token. The pick is added to the text and the process repeats until the model emits a stop token or hits a length limit. That is all "generation" is: a loop of "predict, pick, append".
Temperature, top-p and sampling
- Sampling is the rule that picks the next token from the probabilities. Greedy decoding always takes the most likely token. Random sampling picks by chance, weighted by probability.
- Temperature is a number that reshapes the probabilities before picking. Low temperature makes the likely tokens even more likely (steady, repeatable). High temperature flattens them (varied, more surprising). Temperature 0 means "nearly always the top token".
- Top-p (nucleus sampling) keeps only the smallest group of tokens whose probabilities add up to p (for example 0.9), and picks from that group. Top-k keeps only the k most likely tokens.
Difference in one line: temperature changes how sharp the whole probability list is, while top-p and top-k cut the tail of the list.
Even at temperature 0 you can get different answers on different calls. Providers batch and run on different hardware, and floating-point maths is not perfectly repeatable. Never write a test that needs a byte-for-byte equal answer from a live model. Also, some newer models and modes (for example reasoning modes) fix or ignore these settings, so read the docs of the SDK and model you use. (The Hugging Face transformers docs describe greedy, sampling and beam search as the standard decoding choices, and do_sample=True turns on random sampling.)
Hallucination
A hallucination is an answer that sounds sure but is false or invented. It happens because the model is built to produce likely-looking text, not to check facts. It gets worse for rare facts, for things after its training date, and for questions where the right answer is "I do not know". You cannot remove it by asking nicely. You reduce it by giving the model the facts (RAG), asking for quotes or citations, checking outputs with code, and measuring with tests.
Fine-tuning vs prompting vs RAG
| Approach | What it is | Use it when | Weakness |
|---|---|---|---|
| Prompting | Write better instructions and examples in the prompt | Always the first step. Cheap, fast to change | Limited by the context window |
| RAG (retrieval-augmented generation) | Look up relevant documents at question time and put them in the prompt | The answer depends on your data, which changes or is private | Retrieval can miss; more moving parts |
| Fine-tuning | Train the model a little more on your examples | You need a style, format or behaviour that prompts cannot give, or a smaller cheaper model for a narrow task | Bad at adding fresh facts; needs data and re-training; harder to update |
Rule of thumb: prompting for behaviour, RAG for knowledge, fine-tuning for habit. Many teams try them in that order.
Structured outputs
A structured output is an answer that follows a schema (a fixed shape), usually JSON, so your code can read it without guessing. You give a schema, and the SDK asks the model for JSON that fits and validates it. Never trust the model to follow a shape; always validate. In the Vercel AI SDK (version 7) you do it with output: Output.object({ schema }) on generateText:
import { generateText, Output } from 'ai';import { z } from 'zod';const { output } = await generateText({ model, // any AI SDK model output: Output.object({ schema: z.object({ intent: z.enum(['refund', 'shipping']), urgent: z.boolean(), }), }), prompt: 'Classify: I want my money back NOW',});// output -> { intent: 'refund', urgent: true } (validated by Zod)I ran this with the AI SDK's test mock model; the result was { intent: 'refund', urgent: true }. If the text does not match the schema, the SDK throws, so you need a retry or fallback path.
Tool (function) calling
Tool calling is how a model "does" things. You describe tools (name, description, input schema). The model does not run anything. It returns a message that says "please call getOrder with { id: 'A7' }". Your code runs the function and sends the result back as a new message. The model then continues. This is the base of every agent.
Because the model only asks, your code is the place where you check permissions. That is the most important sentence in the security part of this lesson.
Embeddings
An embedding turns text into a vector so you can compare meaning with maths. To compare two vectors, use cosine similarity: a number from -1 to 1 that measures how much two vectors point the same way (1 = same direction). Embeddings power semantic search and RAG. Use the same embedding model for your documents and your queries. If you change the model, you must re-embed everything, because vectors from different models are not comparable.
Streaming, latency and cost
- Streaming means the server sends tokens as they are made, not after the whole answer. The user sees the first words in a fraction of a second. In the AI SDK this is
streamText. Always stream for chat UIs. - Latency has two parts: the time to first token (mostly the input size) and the speed after that (mostly the output length). Long outputs and many tool steps are the slow parts.
- Cost is tokens in plus tokens out, usually at a different price per million tokens for each. Output tokens usually cost more than input tokens.
Prompt caching is a provider feature that reuses the work for a prompt prefix you sent before, so repeated long prefixes (system prompt, tool list, a big document) are cheaper and faster. Facts for the Anthropic API: the cache matches on an exactly identical prefix, the default life is 5 minutes (a 1-hour option exists), a cache read costs a small fraction of normal input price, a cache write costs a little more than normal, and prompts below a model-specific minimum size are not cached. The practical rules: put stable content first (tools, system prompt, documents) and changing content last (the user's question), and never put the current time or a random id at the top of the prompt.
Workflows vs agents
The most quoted distinction comes from Anthropic's "Building effective agents" guidance (December 2024), and many other docs copy it, including the Vercel AI SDK workflow-pattern docs:
- A workflow is a system where LLMs and tools are orchestrated through predefined code paths. You decide the steps.
- An agent is a system where the LLM dynamically directs its own process and tool use. The model decides the steps.
Comparison: a workflow is a train on rails, an agent is a driver with a map. The train is predictable. The driver can reach places the rails do not go, but can also get lost.
The five workflow patterns
| Pattern | What happens | Example |
|---|---|---|
| Prompt chaining | Call 1's output feeds call 2, and so on. You can add a code check between steps | Write marketing copy, then translate it |
| Routing | A first call classifies the input, then code sends it to a specialised prompt, model or handler | Refund question to the billing prompt, bug report to the tech prompt; easy questions to a small model |
| Parallelization | Run calls at the same time: sectioning (different parts) or voting (same task many times, then compare) | Check code for security, style and performance in parallel |
| Orchestrator-workers | A central model splits a task into subtasks it could not predict, hands them to worker calls, then combines the results | Change several files in a repo, where which files is only known after reading the task |
| Evaluator-optimizer | One call writes, another judges, and the loop repeats until the judge is happy | Translation with a critique loop |
Each of these is a few lines of ordinary code: routing, for example, is one cheap model call that returns a label, then an ordinary switch in your code.
The guidance's advice is worth repeating in an interview: start with the simplest thing that works, and add complexity only when it measurably helps. For many tasks that means a single well-prompted call, not an agent. The same guidance says to call the model API directly first, because frameworks can hide the prompts and make debugging hard.
The augmented LLM and the agent loop
The basic building block is an augmented LLM: an LLM plus retrieval, tools and memory. An agent loop is that block in a while loop:
- Send the messages and tool list to the model.
- If the model asks for tool calls, run them (after your checks) and add the results to the messages.
- Go back to step 1.
- If the model answers with plain text and no tool call, stop.
- Always stop after a step limit, a time limit or a money limit.
I wrote this loop in 25 lines of TypeScript with a fake model and ran it (the fake model first asks for getOrder, then answers with the tool result):
async function runAgent(prompt: string, maxSteps = 5) { const messages: Msg[] = [{ role: 'user', content: prompt }]; for (let step = 1; step <= maxSteps; step++) { const reply = fakeModel(messages); // 1. ask the model if (reply.type === 'text') return { text: reply.text, step }; // 2. done? const tool = tools[reply.name]; // 3. run the tool in OUR code const out = tool ? await tool(reply.args) : `error: unknown tool ${reply.name}`; messages.push({ role: 'tool', content: out }); // 4. feed the result back } throw new Error('step limit reached'); // 5. safety stop}// -> { text: 'Result: {"id":"A7","status":"shipped"}', step: 2 }The same idea with the Vercel AI SDK (version 7) is the ToolLoopAgent class. I ran this with the SDK's mock model; it made 2 model calls and returned the final text:
import { ToolLoopAgent, tool, isStepCount } from 'ai';import { z } from 'zod';const agent = new ToolLoopAgent({ model, // any AI SDK model instructions: 'You are a support agent.', tools: { getOrder: tool({ description: 'Look up an order by id', inputSchema: z.object({ id: z.string() }), execute: async ({ id }) => ({ id, status: 'shipped' }), }), }, stopWhen: isStepCount(5),});const r = await agent.generate({ prompt: 'Where is order A7?' });Version gotcha. In older AI SDK versions the stop helper was named stepCountIs, and an approval flag lived on the tool as needsApproval. In version 7 the docs use isStepCount and a toolApproval setting on the agent. Blog posts from 2025 show the old names. The 7.x docs also say a ToolLoopAgent stops after 20 steps by default, as a safety measure against runaway loops.
"AI agent" vs "agentic AI"
These words are not standardised. No standards body defines them. Vendors, papers and blog posts each use them a little differently. In an interview, give a definition, say it is a common usage, and say what you would do in practice. Here is the most common honest reading:
| Term | Common meaning | Scope |
|---|---|---|
| AI agent | One program in which a model uses tools in a loop to reach a goal, with a stopping rule | A single component |
| Agentic AI | Often used as an adjective for a property or a style ("this system is agentic"): the system plans, acts, uses tools and adapts with some autonomy. Also used for the wider field or for systems of many agents working together | A system, a design style or a whole field |
| Agentic system | The word Anthropic's guidance uses as an umbrella term that includes both workflows and agents | Both |
So a short, safe answer: "An AI agent is one model-driven loop with tools. Agentic AI is the broader idea, or a system built from one or more agents or workflows that act with some autonomy. I treat it as a spectrum from fixed workflow to free agent, and I choose the lowest autonomy that does the job."
How the main SDKs describe an agent
- Vercel AI SDK docs: agents are LLMs that use tools in a loop; the loop handles context management and stopping conditions. The class is
ToolLoopAgent. - OpenAI Agents SDK (Python and JS): an agent is an LLM configured with instructions, tools, and optional handoffs, guardrails and structured outputs. It names two ways to organise several agents: agents as tools (a manager keeps control and calls specialists) and handoffs (a triage agent passes the conversation to a specialist). It also describes "orchestrating via LLM" versus "orchestrating via code".
- LangGraph: describes itself as a low-level orchestration framework for stateful, long-running agents. You model the flow as a graph; its selling points are durable execution (resume after failure), human-in-the-loop, and memory.
- Claude Agent SDK: "an application that completes a task by planning its own steps and calling tools". It gives you the same agent loop, tools and context management that power Claude Code, as a library for Python and TypeScript.
The small differences are real: AI SDK and OpenAI describe the agent as model + tools + loop; LangGraph stresses the graph and state; Claude Agent SDK stresses a ready-made runtime. They agree on the core.
What is a harness? (two meanings)
The word harness has two different meanings in AI work. Say which one you mean.
1. The agent harness (also called scaffolding or runtime)
An agent harness is the software around the model that turns a model into a working agent. It runs the tool loop and everything that the loop needs. Comparison: the model is the engine; the harness is the car around it (steering, brakes, fuel gauge).
What a harness typically does:
- Runs the agent loop and decides when to stop (step, time and money limits).
- Builds the context for each call (system prompt, history, tool results) and compacts it when it gets long.
- Executes tools, and enforces permissions (allow, deny, ask a human).
- Provides memory (notes, project files, saved sessions).
- Starts sub-agents with their own fresh context.
- Runs hooks (your code before or after a tool call), logging and tracing.
Claude Code and the Claude Agent SDK are described this way: the SDK's docs say it gives you "the same tools, agent loop, and context management that power Claude Code", with built-in tools, hooks, subagents, MCP, permissions and sessions. The agent loop in the SDK docs is: Claude evaluates the prompt, calls tools, receives results, and repeats until the task is done; you can cap it with maxTurns or a budget and get a result with a subtype such as error_max_turns. The Vercel AI SDK now even has a docs section about "harnesses" that run established harnesses such as Claude Code or Codex through one API.
2. The evaluation harness (test harness)
An evaluation harness is a test runner for models. It loads benchmark tasks, feeds prompts to a model, collects the answers, scores them with fixed metrics, and prints a table. Comparison: a test framework like Jest, but the "code under test" is a model. The best-known open example is EleutherAI's lm-evaluation-harness, which has many standard benchmarks and many model back ends (Hugging Face models, vLLM, hosted APIs). Its main value is that the same prompts and metrics are used for every model, so scores can be compared.
Difference in one line
The agent harness runs a model in production to get work done; the evaluation harness runs a model in a lab to measure it. In your own project you often build a small version of both: a runtime for your agent, and an eval script that replays test cases against it.
MCP (Model Context Protocol)
MCP is an open protocol that lets an AI application connect to tools and data through a standard interface. Comparison: USB-C for AI tools. Without it, every app writes its own connector for every service. With it, one server works with many apps.
Roles
- Host: the AI application (an IDE, a chat app, your agent) that the user runs. It owns permissions and consent.
- Client: a piece inside the host that talks to exactly one server.
- Server: a program that exposes capabilities, either on your machine or remote.
What a server can offer
| Primitive | What it is | Who decides to use it | Example |
|---|---|---|---|
| Tools | Functions the model can call | The model | get_order(id) |
| Resources | Data the app can read, each with a URI | The application | docs://refund-policy |
| Prompts | Reusable prompt templates with arguments | The user (for example a slash command) | summarise(id) |
I built a tiny server with the official TypeScript SDK (@modelcontextprotocol/sdk, 1.x) and connected a client to it in memory. listTools, callTool, listResources and listPrompts all worked:
const server = new McpServer({ name: 'orders', version: '1.0.0' });server.registerTool('get_order', { description: 'Look up an order', inputSchema: { id: z.string() } }, async ({ id }) => ({ content: [{ type: 'text', text: JSON.stringify({ id, status: 'shipped' }) }] }));server.registerResource('policy', 'docs://refund-policy', { mimeType: 'text/plain' }, async (uri) => ({ contents: [{ uri: uri.href, text: 'Refunds within 30 days.' }] }));Transports
A transport is how the messages travel. MCP messages are JSON-RPC (a simple "call a method with parameters" message format in JSON). The spec defines two standard transports:
- stdio: the client starts the server as a child process and talks over its standard input and output. Good for local tools.
- Streamable HTTP: each message is an HTTP
POSTto one MCP endpoint, and replies come as JSON or a stream of events. Good for remote servers.
The protocol is changing. The 2025-11-25 revision starts each connection with an initialize handshake and, over HTTP, uses a session id header. The newer 2026-07-28 revision in the spec repository removes that handshake and the protocol-level sessions: every request carries its protocol version and capabilities, and a server/discover call lists what a server supports. The same revision marks Roots, Sampling and Logging as deprecated. SDKs are moving between these versions, so in an interview say "it depends on the spec revision and the SDK version", and talk about the stable ideas (tools, resources, prompts, transports, consent).
MCP security
Treat every MCP server as code that runs with the power you give it. The MCP security docs list these risks:
- Tool poisoning. Tool names and descriptions are text the model reads, so a bad server can hide instructions in them. A server can also change its descriptions after you approved it (a "rug pull"). One approval at install time is not enough.
- Malicious or over-collecting servers. Look-alike package names, or an honest server that is steered by injected text to read more than it should.
- Confused deputy and token passthrough. A proxy server must not use one user's consent for another client, and a server must not accept a token that was issued for someone else and pass it on. Check that a token was issued to your server.
- Local servers. Run third-party local servers with the least privilege you can: restricted files, no secrets in the environment, a sandbox or container.
- Network. For HTTP servers, validate the
Originheader and, when running locally, bind to127.0.0.1, not0.0.0.0. - Human in the loop. The spec says there should always be a human who can deny tool calls, and the UI should show which tools are exposed.
RAG, end to end
RAG (retrieval-augmented generation) is a pattern where, for each question, you retrieve relevant pieces of your own documents and put them in the prompt, so the model answers from them. Comparison: an open-book exam. The model is not trusted to remember; it is given the book.
The pipeline
- Ingest (offline). Load documents, clean them, chunk them, create an embedding for each chunk, and store chunk + vector + metadata (source, date, owner, permissions).
- Retrieve (online). Embed the user's question, search the store, get the top candidates.
- Rerank (optional but valuable). Re-score the candidates with a better, slower model.
- Generate. Put the best chunks in the prompt with instructions: "answer only from these; cite the source; say you don't know if it isn't there".
- Check and show. Show citations; run output checks.
Chunking
A chunk is a piece of a document that you embed and retrieve as one unit. Too big: the vector is a blurry average, and you waste prompt space. Too small: a chunk loses its meaning ("it expires after 7 days" - what expires?).
| Strategy | How | Good for | Risk |
|---|---|---|---|
| Fixed size + overlap | Cut every N characters or tokens, repeat the last few | Simple start | Cuts in the middle of ideas |
| Recursive / sentence-based | Split on paragraphs, then sentences, until small enough | General text | Needs tuning |
| Structure-aware | Split on headings, code blocks, table rows | Docs, Markdown, HTML | Needs a parser |
| Semantic | Split where the topic changes (by comparing neighbour embeddings) | Messy long text | Extra cost; not always better |
| Contextual | Add a short "where this chunk comes from" line to each chunk before embedding | Chunks that lose context | Cost of generating the line |
For scale: LlamaIndex's defaults are a chunk size of 1024 tokens with a 20-token overlap. Anthropic's "contextual retrieval" write-up says chunks of a few hundred tokens are typical, and that chunk size, split points and overlap all change retrieval quality. So treat chunking as a setting you test, not a constant you copy.
Here is chunking plus cosine similarity in plain TypeScript (run with Node):
function chunk(text: string, size = 200, overlap = 40): string[] { const out: string[] = []; for (let i = 0; i < text.length; i += size - overlap) { out.push(text.slice(i, i + size)); if (i + size >= text.length) break; } return out;}const cosine = (a: number[], b: number[]) => { let d = 0, na = 0, nb = 0; for (let i = 0; i < a.length; i++) { d += a[i]*b[i]; na += a[i]**2; nb += b[i]**2; } return d / (Math.sqrt(na) * Math.sqrt(nb));};// chunk('x'.repeat(450)) -> lengths [200, 200, 130]Where to store vectors
| Option | Good | Watch out |
|---|---|---|
| Postgres + pgvector | One database for rows and vectors; SQL filters and joins; easy for a team that already runs Postgres | You tune indexes yourself; very large scale needs care |
| MongoDB Atlas Vector Search | Vectors live next to your documents if you already use Atlas | Atlas feature (not plain self-hosted MongoDB); check index limits for your version |
| Meilisearch | Keyword and meaning search in one engine, with hybrid search and automatic embedding with a configured embedder | You manage an extra service; it is a search engine, not your system of record |
| A dedicated vector database | Built for huge scale and fast filters | One more system to run, sync and secure |
Facts from the pgvector README: distance operators are <-> (L2), <#> (inner product), <=> (cosine distance) and <+> (L1). There are two index types, HNSW (better speed and recall, slower to build, more memory) and IVFFlat (faster to build, less memory, lower query quality). A vector column supports up to 2,000 dimensions for indexing (halfvec up to 4,000). An example:
CREATE TABLE chunks (id bigserial PRIMARY KEY, body text, embedding vector(1536), tenant_id int);CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);SELECT id, body FROM chunks WHERE tenant_id = 7ORDER BY embedding <=> $1 LIMIT 8; -- $1 is the query embedding(I checked the syntax against the README; I did not run it, because no Postgres was available in my sandbox.) Cosine distance is 1 - similarity, so smaller is closer.
Filters and approximate indexes. The pgvector README says that with an approximate index, filtering is applied after the index is scanned. With HNSW and the default hnsw.ef_search of 40, a filter that keeps 10% of rows leaves about 4 results on average. The fix is iterative index scans (added in pgvector 0.8.0), partial indexes, or partitioning. This matters a lot for multi-tenant RAG, where every query filters by tenant.
Meilisearch's docs describe hybrid search as running keyword and semantic search together and merging the results, controlled by semanticRatio (default 0.5). Meilisearch uses embedding models, not LLMs, for this, so it is cheap and fast. It can generate embeddings for you from a configured embedder, and changing the embedder settings can trigger a full re-embedding, which costs money on paid providers.
Hybrid search and reranking
- Keyword search (BM25) is a classic scoring method that ranks documents by how often and how rarely the query words appear. It is best for exact terms: error codes, product names, ids.
- Vector search finds text with similar meaning even with different words. It is best for natural questions.
- Hybrid search runs both and merges them. A common merge is Reciprocal Rank Fusion (RRF): each document gets
1 / (k + rank)from each list (k is often 60), and the scores are added. It needs no score tuning because it uses ranks only. - A reranker is a second model that reads the query and each candidate together and gives a better relevance score. Retrieve 50, rerank, keep 5. It is slower and costs more, but often the biggest quality gain. (The AI SDK has a
rerankfunction.)
Numbers from Anthropic's "contextual retrieval" article, as a real-world example (their own data set, top-20 chunks): failed retrievals dropped 35% with contextual embeddings, 49% with contextual embeddings plus contextual BM25, and 67% with reranking added. Your numbers will differ; the lesson is that each stage helps and you can measure it.
In my toy run (plain TypeScript), fusing a vector ranking and a keyword ranking with RRF put the right refund document first and a recall@1 check returned 1.
Rewriting, citations and freshness
- Query rewriting. Users write "and what about the second one?" Use a small model call to turn the chat into a standalone search query. You can also generate several query variants and merge the results.
- Citations. Give each chunk an id in the prompt and ask the model to cite ids. Then check in code that every cited id was really retrieved. A citation the model made up is worse than none.
- Freshness. Store a
updatedAtand a source version with each chunk. Re-embed changed documents (use a content hash to detect change). Delete chunks of deleted documents. Decide what happens to old versions of a policy.
Evaluating RAG
Measure retrieval and generation separately, or you will not know which part is broken.
| Metric | Question it answers |
|---|---|
| Recall@k | Of the chunks that hold the answer, how many are in the top k? |
| Precision@k | Of the top k chunks, how many are relevant? |
| MRR (mean reciprocal rank) | How high does the first correct chunk rank, on average? |
| Faithfulness (groundedness) | Is every claim in the answer supported by the retrieved text? |
| Answer correctness | Is the final answer right? |
Build a set of 50 to 200 real questions with the right source chunks marked. Re-run it on every change to chunking, model or prompt.
Failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Answer is wrong but the sources were right | Prompt or model problem | Tighter prompt, ask for quotes, stronger model |
| Right document exists but is not retrieved | Chunking, embedding model, or exact-term miss | Hybrid search, better chunks, reranker, query rewriting |
| Retrieved text is right but the model ignores it | Too much noise or the key chunk is buried | Fewer, better chunks; put key text near the end or start; rerank |
| Confident answer when nothing relevant exists | No "I don't know" path | Score threshold, and an instruction to refuse |
| Outdated answer | Stale index | Re-index on change; show dates |
| Leaks private data | No permission filter | Filter by ACL in the query (below) |
RAG vs long context vs fine-tuning
- Long context means putting the whole material in the prompt. It is simplest. Anthropic's article says if your knowledge base is smaller than about 200,000 tokens (roughly 500 pages), you may skip RAG and include it all, especially with prompt caching. It costs more per call, is slower, and models can lose accuracy as the context grows.
- RAG wins when data is big, changes often, needs per-user permissions, or when you need citations.
- Fine-tuning changes behaviour, not facts. Use it for format, tone or a small specialised model.
Security: ACL filtering
ACL (access control list) means who may read what. The rule: filter by permissions inside the retrieval query, before the text reaches the model. Do not retrieve everything and ask the model to hide secrets. Store the tenant and the allowed groups as metadata on every chunk, take the user's id from the session (never from the model or the request body), and add them as a hard filter. Test with two users and one secret document.
Memory and context engineering
Context engineering is choosing and maintaining the best set of tokens for the model at each step, which is more than writing a good prompt. A longer context is not always better: as it grows, the model's recall can get worse. Anthropic's context-engineering write-up calls this context rot.
Tools you use:
- Compaction: when the conversation gets close to the limit, summarise it and start a new window with the summary. Risk: details are lost, so keep durable rules in a file that is re-sent every time (Claude Code and the Agent SDK do this with
CLAUDE.md). - Structured note-taking: the agent writes notes outside the context (a file, a database) and reads them back later.
- Sub-agents: a sub-agent works in its own fresh context and returns only a short summary, so the main context stays small.
- Just-in-time retrieval: keep only references (file paths, ids, queries) in context and load the data when needed with a tool.
- Trimming tool output: return the 5 fields the model needs, not a 5,000-token JSON blob. Anthropic's tool-writing advice: fewer, purpose-built tools; clear names and descriptions as if for a new hire; pagination and filters; helpful error messages.
Short-term memory is the current conversation. Long-term memory is anything saved across sessions (user preferences, facts, past decisions), usually in a database and retrieved like RAG. Decide what is worth saving, let users see and delete it, and treat it as personal data.
Guardrails and prompt injection
A guardrail is a check that runs around the model. The OpenAI cookbook describes guardrails as detective controls to steer an app. The OpenAI Agents SDK has three kinds: input guardrails (on the first user input), output guardrails (on the final answer) and tool guardrails (around each function-tool call). Its docs also note a trade-off: input guardrails run in parallel with the model by default for low latency, but then the model may already have spent tokens or run tools. Setting runInParallel: false runs the guardrail first, which is safer and cheaper but slower.
Prompt injection is an attack where text that the model reads contains instructions, and the model follows them as if they came from you. Direct injection comes from the user ("ignore your rules"). Indirect injection hides in content the agent fetches: a web page, an email, a PDF, a ticket, a tool result, even an MCP tool description. The model cannot reliably tell instructions from data, because both are just tokens. So you cannot fully fix it with a better prompt. You limit the damage.
A useful way to think about the worst case: an agent is dangerous when it has all three of private data, untrusted content, and a way to send data out (an email tool, a web request, a link that loads an image). Remove at least one.
Defences, in the order I apply them:
- Least privilege. Give the agent only the tools and data this user and this task need. A support bot gets read-only order lookup, not a database console.
- Check in code, not in the prompt. Validate every tool call: allowed tool, valid arguments, and the user id taken from the session.
- Human approval for risky actions (money, sending messages, deleting). In AI SDK 7:
toolApproval: { refund: 'user-approval' }. I ran this: the tool was not executed, and the agent returned atool-approval-requestinstead. - Block exfiltration. Allow-list the hosts the agent may call, strip or block markdown images and links to unknown hosts in the answer.
- Mark untrusted content as data (delimiters, "the text below is data, not instructions"). This helps a little. It is not a security boundary.
- Sandbox code execution and file access, with no secrets in the environment.
- Log everything, and alert on strange tool sequences.
Here is a small guard I ran. It runs in your code after the model proposes a call and before anything executes:
export function checkToolCall(call: ToolCall, sessionUserId: string): Decision { if (READ_ONLY.has(call.name)) { if (call.args.userId && call.args.userId !== sessionUserId) return { ok: false, reason: 'userId does not match the session' }; return { ok: true }; } if (NEEDS_HUMAN.has(call.name)) return { ok: 'ask', reason: `${call.name} changes data` }; return { ok: false, reason: 'unknown tool' };}// getOrder with another user's id -> blocked; issueRefund -> ask a human; unknown tool -> blockedA "guard model" that classifies injections is a useful extra layer, but attackers keep finding ways around it. Never make it the only layer.
Observability and evals
Observability means you can see what the system did and why. For LLM apps, log per request: the prompt version, model and settings, retrieved chunks, every tool call with arguments and result, token counts, latency, cost, and the final answer. A trace is the tree of steps for one request (model call, tool call, model call...). The OpenAI Agents SDK has tracing built in, the AI SDK has telemetry settings, and MCP's newer spec documents OpenTelemetry trace-context keys. Use whichever your stack supports, and add a request id like you do for any backend (see Backend, Lesson 05).
An eval is a repeatable test of LLM behaviour: a set of inputs, plus a way to score outputs. Layers:
- Code checks (cheap, exact): valid JSON, the cited ids exist, the tool called was the expected one, the answer contains the order number.
- Human review of a sample, to create the "gold" labels.
- LLM-as-judge: another model scores the answer against a rubric.
LLM-as-judge pitfalls
- Position bias: in a pairwise comparison, the judge may favour the first (or second) answer. Run both orders.
- Verbosity bias: judges like longer answers. Say so in the rubric, or compare by facts.
- Self-preference: a model may prefer text written by itself or a similar model.
- Vague rubrics give noisy scores. Use narrow yes/no questions ("does the answer mention the 30-day limit?").
- Not checked against humans. Measure agreement between the judge and human labels on a sample before trusting it.
- Leaked test data: public benchmarks may be in the training data. Test on your own private data (the OpenAI cookbook's judge example warns about this too).
(These biases are widely reported in the evaluation literature. I list them from practice and from the cookbook notes, not from one single document.)
Cost control
- Count the loop. In an agent, each step re-sends the whole history. In my toy numbers, 10 steps that each add 2,000 tokens bill 110,000 input tokens in total, not 20,000, because the growing history is re-sent every time. Cost grows with the square of steps.
- Cap everything: steps (
isStepCount,maxTurns), money (maxBudgetUsdin the Claude Agent SDK), time, and output length. - Route: send easy requests to a small, cheap model and hard ones to a strong model. Use a small model for guardrails and classification.
- Cache: prompt caching for stable prefixes; ordinary caching for repeated questions; cache embeddings.
- Shrink context: fewer and better chunks, trimmed tool output, compaction.
- Batch offline work if your provider has a batch API at a lower price.
- Measure per feature and per user: log tokens and cost with a tag, and set quotas and rate limits per user.
When NOT to use an agent
An agent costs more, is slower, is harder to test, and errors can build up over many steps. Do not use one when:
- The steps are known. Write a workflow, or plain code.
- One call with good context does the job (summarise, classify, extract).
- The cost of a mistake is high and nobody reviews it (payments, legal, medical, deleting data).
- You need exact, repeatable results (accounting). Use code; let the model only parse input.
- You cannot measure success. If you have no eval, you cannot improve an agent.
- Latency must be very low.
Good fits: open-ended tasks where the number of steps is unknown and success can be checked, such as coding agents (tests give a verifiable signal) and research tasks, ideally with a human review at the end.
Design sketch: a coding agent
A loop with tools: read file, search, edit, run tests, run shell. Put it in a sandbox (a container with no secrets and limited network). Use a permission mode that asks before shell commands and edits outside the repo, add step and budget caps, and use the test suite as the success signal. For context, read files on demand, summarise old turns, use sub-agents for big searches, and keep project rules in a file that is re-sent every time. The output is a diff or pull request for a human to review, never a direct push to the main branch. (The support-bot design and the prompt-injection plan are in Q17 and Q20.)
Interview questions
What is an LLM, and what are tokens and the context window?
Level: must know.
An LLM is a model that predicts the next token. A token is a small piece of text, and the model reads and writes tokens, and bills are counted in tokens. The context window is the most tokens it can handle in one call: the prompt, history, tool results and its own answer together. The API is stateless, so my code must send the history each time. In practice I watch token counts because they set cost, latency and what fits.
What do temperature and top-p do? What would you set for a classifier and for a brainstorming tool?
Level: must know.
Both control how the next token is picked from the probability list. Temperature sharpens or flattens the whole list. Top-p keeps only the smallest group of tokens that add up to probability p, and picks from them. For a classifier or data extraction I use low temperature, because I want steady results. For brainstorming I use a higher one. I change one of the two, not both. I also say that low temperature is not a guarantee of the same output, and some newer models fix these settings, so I read the model's docs.
What is a hallucination and how do you reduce it?
Level: must know.
It is a confident answer that is false. The model makes likely-looking text and does not check facts. I reduce it in layers: give it the facts with RAG, tell it to answer only from the sources and to say "I don't know", ask for citations and check them in code, use structured outputs and validate them, and measure the rate on an eval set. I do not claim it can be removed.
Prompting, RAG or fine-tuning: how do you choose?
Level: must know.
I use the order: prompting first, then RAG, then fine-tuning. Prompting changes behaviour cheaply. RAG adds knowledge that is private or changes often, and it gives citations and permission control. Fine-tuning teaches style, format or a narrow skill, or lets a smaller model do a job, but it is poor at adding fresh facts and is harder to update. Many products use prompting plus RAG and never fine-tune.
How does tool calling work, and where do you put your security checks?
Level: must know.
I describe each tool with a name, a description and an input schema. The model does not run anything. It returns "call this tool with these arguments". My code validates the call, runs the function, and sends the result back. So my code is the security boundary. I check that the tool is allowed, that the arguments match the schema, and that identity comes from the session, not from the model's arguments.
What is the difference between a workflow and an agent?
Level: must know.
In a workflow, my code decides the steps and calls the model inside a fixed path. In an agent, the model decides the next step and which tools to use, in a loop. Workflows are predictable and cheaper. Agents handle open-ended tasks but cost more and fail in more ways. The guidance I follow is to start with the simplest option, often one good call, and add autonomy only when it measurably helps.
Name the five workflow patterns and give one use for each.
Level: must know.
Prompt chaining: draft, then translate. Routing: classify a ticket, then send it to the right prompt or a cheaper model. Parallelization: run several checks at once, or run the same task many times and vote. Orchestrator-workers: a model splits a task into subtasks I cannot predict, such as which files to change, and workers do them. Evaluator-optimizer: one call writes, another critiques, and it loops until a quality bar is met. Each is just ordinary code around model calls.
"AI agent" versus "agentic AI": what is the difference?
Level: senior signal.
I say first that the terms are not standardised. My working definitions: an AI agent is one program where a model uses tools in a loop to reach a goal. Agentic AI is the wider idea, used for systems, or groups of agents, that act with some autonomy, or as a property ("this design is more agentic"). Anthropic uses "agentic systems" for both workflows and agents. In real work I do not argue about the label. I place the system on a scale from fixed workflow to free agent, and I choose the lowest autonomy that works.
What is a harness? (the trap is that it has two meanings)
Level: trap/gotcha.
I ask which one is meant. An agent harness is the runtime around the model: the tool loop, context building and compaction, tool execution and permissions, memory, sub-agents, hooks and limits. Claude Code is an example, and the Claude Agent SDK exposes the same loop and tools as a library. An evaluation harness is a test runner for models, like EleutherAI's lm-evaluation-harness: it runs fixed tasks on any model and scores them with the same metrics so results are comparable. One runs a model to do work, the other runs a model to measure it.
Explain MCP. What are tools, resources and prompts, and what are the main security risks?
Level: must know, then senior signal.
MCP is an open protocol that connects AI apps to tools and data. A host app creates clients, and each client talks to one server. A server offers tools (functions the model calls), resources (data the app reads by URI) and prompts (templates the user picks). Transports are stdio for local child processes and Streamable HTTP for remote servers. The risks: tool descriptions can carry hidden instructions or change after approval, third-party servers may over-collect, servers must not pass on tokens that were not issued to them, and local servers need least privilege. I also keep a human able to deny calls. And I mention that the spec is moving, since a 2026 revision removes the initialize handshake and sessions, so I check the SDK version.
Walk me through a RAG pipeline and where it usually fails.
Level: must know.
Offline: load, clean, chunk, embed, store with metadata. Online: rewrite the question, embed it, run hybrid search, rerank, build the prompt with the best chunks, generate with citations, then check the output. It usually fails at retrieval, not generation: bad chunks, an exact term the vectors miss, no reranking, a stale index, or a missing permission filter. So I measure retrieval alone with recall@k before I touch the prompt.
How do you choose a chunk size?
Level: senior signal.
There is no universal number, so I test. Too big gives blurry vectors and wastes prompt space; too small loses meaning. I start with structure-aware splitting on headings, with a few hundred tokens and a small overlap, then run my eval set with two or three sizes and pick by recall@k and answer quality. If chunks lose context, I add a short context line to each chunk before embedding.
pgvector, Atlas Vector Search, Meilisearch or a dedicated vector database?
Level: senior signal.
I start from what we already run. If we use Postgres, pgvector keeps vectors with the rows, gives SQL filters and joins, and one backup story. If we are on MongoDB Atlas, Atlas Vector Search keeps vectors with the documents. If search quality for users matters too, Meilisearch gives hybrid keyword and semantic search with a configured embedder. A dedicated vector database makes sense at very large scale or with heavy filtering. Watch the filter trap in approximate indexes: with HNSW, filtering happens after the index scan, so selective filters can return too few rows unless I use iterative scans, partial indexes or partitioning.
Why hybrid search, and how do you merge the lists?
Level: senior signal.
Vectors are good at meaning but can miss exact terms like error codes and product names; BM25 is the opposite. Running both covers both. To merge I use Reciprocal Rank Fusion: each result gets 1 / (k + rank) from each list, with k around 60, and I add the scores. It uses ranks only, so I do not need to line up the different score scales. Then I rerank the top candidates with a cross-reading model, which is often the largest quality gain.
How do you evaluate a RAG system?
Level: senior signal.
I split it in two. Retrieval: I build 50 to 200 real questions with the correct source chunks marked, and compute recall@k, precision@k and MRR. Generation: I check faithfulness (is every claim in the retrieved text), correctness, and that citations point to real retrieved chunks. I run it on every change to chunking, embedding model, prompt or model, and I keep the results so I can compare. Cheap code checks come first, human labels second, and LLM-as-judge third.
What goes wrong with LLM-as-judge?
Level: trap/gotcha.
Judges have position bias (they favour an answer by its slot), verbosity bias (longer looks better), and self-preference. Vague rubrics give noisy scores. And it may not match human opinion. So I use narrow questions, test both orders in pairwise comparisons, and measure agreement with human labels on a sample before I trust it. I never use the same model as writer and sole judge for something important.
How do you prevent prompt injection in a tool-using agent?
Level: senior signal.
I accept that the model cannot reliably tell instructions from data, so I limit the damage instead. Least privilege for tools and data; checks in my code for every call, with identity from the session; human approval for risky actions; an allow-list for outgoing hosts and no unknown images or links in answers; a sandbox without secrets; and logs with alerts. I avoid combining private data, untrusted content and an outbound channel in one agent. A guard model or delimiters can help but are never the only layer. And I keep a test suite of attack strings in CI.
Your agent bill tripled this month. How do you find and fix it?
Level: senior signal.
I look at traces grouped by feature and user: tokens in and out, steps per run, and cache hit rate. Common causes: loops that re-send a growing history (cost grows quickly with step count), huge tool outputs, a changed prompt prefix that broke prompt caching, or a strong model used for easy tasks. Fixes: step and budget caps, trimming tool results, compaction, stable prefixes first, routing easy work to a small model, and per-user quotas. Then I add a cost alert so it does not surprise us again.
When would you not build an agent?
Level: must know.
When the steps are known, when one call is enough, when mistakes are costly and unreviewed, when I need exact repeatable results, or when I cannot measure success. Then I write a workflow or plain code. I would build an agent for open-ended tasks where success can be checked, such as fixing code with a test suite, and I would keep a human review at the end.
Design a support bot with RAG. What are the key decisions?
Level: senior signal.
Data: help articles and past tickets, chunked by structure with metadata and an updated date. Search: hybrid with a reranker and query rewriting. Safety: ACL filter inside the query, user id from the session, read-only order lookup, refunds behind human approval. Behaviour: answer only from sources, cite, say "I don't know" under a score threshold, hand off to a human when unsure. UX: stream tokens. Quality: an eval set from real tickets, with tracked wrong-answer rate, deflection and cost per chat. I would ship to a small group first and read the traces.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Securing LLM Applications: The OWASP Top 10 for LLM Apps in Code · AI Engineering
- Building an Evaluation Harness for an LLM Feature · AI Engineering
Part of the study guide
Related series