DEV Community

Cover image for Every AI Word You Keep Hearing, Explained With Laravel Code
Hafiz
Hafiz

Posted on Originally published at hafiz.dev

Every AI Word You Keep Hearing, Explained With Laravel Code

Originally published at hafiz.dev


Someone drops this into your team chat: "we'll put a guardrail on the sub-agent before its tool call hits the vector store." You know every single one of those words. The sentence still means nothing.

That was me for most of this year. The code was never the hard part. The vocabulary was, because almost every explainer is written in Python for people building models, not for people wiring a model into an app that already has a Stripe integration and a queue that has to stay up.

So this is the map I wanted. Thirty-one terms, grouped into five layers, each one anchored to code that runs in a Laravel app. No maths beyond what you already remember.

One thing holds the whole map together, and it's worth saying before the first term. All of it sits on next-token prediction plus plumbing you write yourself. Once you accept that, the fancy words stop being fancy.

Layer 1: what the model actually is

An LLM, or large language model, guesses what comes next. That's the entire job. You give it some text, it produces a probability distribution over what the next chunk of text might be, picks one, appends it, and does the whole thing again.

That chunk is a token. A token isn't a word. It's closer to a syllable-sized piece of a word, and the model has its own vocabulary of them. OpenAI's rule of thumb for English is that one token runs to roughly four characters, or about three quarters of a word, so 100 tokens is about 75 words. Short common words are one token. Long or unusual ones split into several.

You don't have to trust a rule of thumb, though. The Laravel AI SDK hands you the real count on every response:

$response = (new SalesCoach)->prompt('Summarise this transcript.');

$response->usage->inputTokens;
$response->usage->outputTokens;
$response->usage->totalTokens;
Enter fullscreen mode Exit fullscreen mode

Log those three numbers on your first real agent and the cost model stops being abstract. If your provider supports prompt caching, cacheReadInputTokens and cacheWriteInputTokens are there too, and they matter more than people expect once a system prompt gets long.

Next-token prediction is the part that's hardest to accept. Watching a model write a working migration, a test, and a passing implementation, it feels impossible that it's picking one token at a time. It is, though. And knowing that explains most of its failures better than any theory about reasoning does.

Hallucination is the name for the most common of those failures. The model produces something fluent and wrong, and gives you no signal that it's wrong, because nothing in next-token prediction separates true from likely. It isn't lying and it isn't broken. It's doing the only thing it does, with nothing to check the output against. That's the argument for tools and retrieval later in this post. Both exist to give the model something real to work from.

The thing that made this work at scale was the transformer, from a 2017 paper by Vaswani and seven colleagues called Attention Is All You Need, submitted on 12 June that year. Earlier architectures read text in order, one position at a time. The transformer looks at all positions at once and learns which ones should pay attention to which. That's the breakthrough, compressed into a sentence.

Temperature controls how adventurous the pick is. When the model has ten plausible next tokens, low temperature makes it take the most likely one nearly every time, and high temperature lets it wander. In the SDK it's an attribute on the agent class:

use Laravel\Ai\Attributes\Temperature;

#[Temperature(0.2)]
class InvoiceClassifier implements Agent
{
    use Promptable;
}
Enter fullscreen mode Exit fullscreen mode

Classification, extraction, anything you're going to parse: keep it low. Naming things and writing copy: raise it.

The context window is how much text the model can consider in one go. Everything counts against it. Your system prompt, the conversation so far, retrieved documents, tool definitions, tool results, all of it.

Here's where I'd push back on the usual advice, which is that bigger is better. Chroma's Context Rot report from July 2025 tested 18 models and found that performance degrades as input grows, well before the window is anywhere near full. NVIDIA's RULER benchmark found something similar and blunter, that plenty of models advertising 32k or more can't actually hold quality across 32k. Advertised window and usable window are different numbers. Give the model what it needs and stop there.

Last one for this layer. Open weights and open source are not synonyms, though they get used that way constantly. Open weights means you can download the parameters and run the model yourself. Open source, used strictly, would also mean the training data and code are available, which for most so-called open models they aren't. Llama and Mistral are open weights. Very little meets that second definition.

Layer 2: what turns a model into an agent

A chatbot answers. An agent acts. That's the whole distinction, and everything else in this layer is machinery for making acting safe and useful.

A tool is a function you write, described in a way the model can understand, that the model may choose to call. The model never runs your code. It emits a request to run it, your framework runs it, and the result goes back into the conversation. In the SDK a tool is a class with three methods:

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class LookupOrder implements Tool
{
    public function description(): Stringable|string
    {
        return 'Look up an order and its current status by order ID.';
    }

    public function handle(Request $request): Stringable|string
    {
        return Order::findOrFail($request['order_id'])->toJson();
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'order_id' => $schema->integer()->required(),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

The description is not a comment. It's the only thing the model reads when deciding whether to call this tool, so it does more work than the implementation does.

There's a second reason tools exist, beyond reaching the outside world. Models are bad at anything that has to be exact. Dates, arithmetic, counting, sorting. They're producing likely-looking tokens, not calculating, so "what date is 45 working days from today" is a guess. Give it a tool. Deterministic work belongs in PHP, where it's just code.

An agent bundles instructions and tools together:

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class SupportAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You help customers with order and billing questions.';
    }

    public function tools(): iterable
    {
        return [new LookupOrder, new IssueRefund];
    }
}
Enter fullscreen mode Exit fullscreen mode

The agent loop is what happens when you prompt that class. The model reads the conversation, decides whether to answer or call a tool, and if it calls one, your code runs and the result is appended. Then it starts again. It keeps going until it produces a final answer or hits the limit you set with #[MaxSteps(10)].

Each pass is a step, and the SDK exposes them on the response as $response->steps. Steps are where cost lives, because every step resends the whole thing: system prompt, tool definitions, and every previous call and result. Step five is much more expensive than step one. Not because the model got slower, but because the conversation got longer.

The diagram below is the part I wish someone had drawn for me on day one. The model only ever does one thing, which is decide. Everything else in the loop is code you wrote.

View the interactive component on hafiz.dev

ReAct is the name for making the model write its reasoning before it acts. Reasoning and Acting, shortened into one word. When you see a "thinking" panel in a chat interface, that's this. It works because tokens spent explaining the plan condition the tokens that come after, which makes the tool choice better.

Multi-agent systems put agents inside other agents. In the SDK, an agent becomes callable as a tool by implementing CanActAsTool and giving itself a name and description:

class RefundsAgent implements Agent, CanActAsTool, HasTools
{
    use Promptable;

    public function name(): string
    {
        return 'refunds_specialist';
    }

    public function description(): string
    {
        return 'Decide whether an order qualifies for a refund.';
    }
}
Enter fullscreen mode Exit fullscreen mode

Then a parent agent lists new RefundsAgent among its tools, and delegation is just a tool call. It's elegant. It's also the fastest way to spend money I know of, because every sub-agent carries its own context and its own steps. My honest advice is to reach for it only when one agent's tool list has grown incoherent, not because the architecture diagram looks better. I wrote up the sub-agent patterns in detail if you want the longer version.

Layer 3: what gives it knowledge it wasn't trained on

Models have no memory. None. Every API call starts from nothing, and the illusion of continuity in ChatGPT exists because the interface resends the conversation each time. The first time you call a model from your own code, this is the surprise.

Memory, then, means deciding what to resend. The SDK gives you conversation persistence out of the box:

use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Conversational;

class SupportAgent implements Agent, Conversational
{
    use Promptable, RemembersConversations;
}

$response = (new SupportAgent)->forUser($user)->prompt('Where is my order?');

$next = (new SupportAgent)
    ->continue($response->conversationId, as: $user)
    ->prompt('And the one before that?');
Enter fullscreen mode Exit fullscreen mode

That covers short conversations. Long ones need a strategy, because resending everything eventually collides with both the context window and your budget. Summarising older turns while keeping recent ones verbatim is the common answer, and choosing where to cut is still unsolved.

RAG stands for retrieval augmented generation, and it's a plain idea hidden behind an acronym. Before answering, go and fetch the relevant bits of your own data, put them in the prompt, and let the model answer from those. No retraining. Your support tickets and internal docs were never in the training data, and this is how they get in front of the model anyway.

Making it work is a pipeline. Split documents into chunks, because whole documents are too big and single sentences lose their meaning. Convert each chunk into an embedding, which is an array of numbers representing what the text means rather than what it says. Store them. At query time, embed the question, find the closest stored chunks, and put those in the prompt.

View the interactive component on hafiz.dev

Laravel does all of this natively now, which surprised me when I first went looking. Generating embeddings is one call:

use Illuminate\Support\Str;

$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings();
Enter fullscreen mode Exit fullscreen mode

Storing them is a column type, and an HNSW index keeps similarity search fast as the table grows:

Schema::ensureVectorExtensionExists();

Schema::create('documents', function (Blueprint $table) {
    $table->id();
    $table->text('content');
    $table->vector('embedding', dimensions: 1536)->index();
    $table->timestamps();
});
Enter fullscreen mode Exit fullscreen mode

A vector database is a database built to store those numeric arrays and find the nearest ones quickly. Pinecone and Qdrant are the well-known standalone ones. But you very likely don't need either, because PostgreSQL with pgvector, MariaDB 11.7 or later, and MongoDB all do this from inside your existing database, and Laravel ships the query method:

$documents = Document::query()
    ->where('team_id', $user->team_id)
    ->whereVectorSimilarTo('embedding', 'best wineries in Napa Valley', minSimilarity: 0.4)
    ->limit(10)
    ->get();
Enter fullscreen mode Exit fullscreen mode

Pass a string and Laravel embeds it for you. Note the ordinary where clause sitting next to it, which is the thing a separate vector service makes painful and your own database makes trivial.

Reranking is a cheap trick, and it has done more for my results than anything else in this layer. Retrieve a wide set fast, then have a model reorder the top candidates by actual relevance:

$articles = Article::query()
    ->whereFullText('body', $query)
    ->limit(50)
    ->get()
    ->rerank('body', $query, limit: 10);
Enter fullscreen mode Exit fullscreen mode

I've covered the retrieval side more thoroughly in Laravel search in 2026, including when plain full-text beats all of this.

Layer 4: how it reaches the rest of your system

Tools solve access for your own app. MCP, the Model Context Protocol, solves it for everyone else's.

Anthropic open-sourced it on 25 November 2024, and the official docs describe it as "a USB-C port for AI applications", which is a fair description. Before it, connecting M AI clients to N systems meant writing M times N bespoke integrations. A protocol turns that into M plus N.

An MCP server exposes tools, resources and prompts. An MCP client consumes them. Your Laravel app can be either, and there's a first-party package:

use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/support', SupportServer::class)
    ->middleware(['auth:sanctum', 'throttle:mcp']);
Enter fullscreen mode Exit fullscreen mode

That middleware line deserves more attention than it usually gets. An MCP server is a public API whose consumers are language models, so it needs the same authorisation you'd put on any other endpoint, applied per tool as well as per route. I wrote a whole post on locking one down after realising how many public ones ship wide open.

You'll also see A2A and various agent-to-agent protocols. Several appeared once MCP became popular. Adoption has gone almost entirely one way so far, and agents can already talk through MCP, so I'd wait.

Layer 5: how you stop it hurting you

Guardrails means checking input on the way in and output on the way out. Prompt injection is the reason for the first, where text from a user or a fetched document tries to overwrite your instructions. Reputation is the reason for the second, since a model trained on the internet will occasionally produce something you don't want appearing under your company's name.

In Laravel this is middleware, and it works on both directions in one class:

use Closure;
use Laravel\Ai\Prompts\AgentPrompt;
use Laravel\Ai\Responses\AgentResponse;

class ScreenContent
{
    public function handle(AgentPrompt $prompt, Closure $next)
    {
        abort_if($this->looksLikeInjection($prompt->prompt), 422);

        return $next($prompt)->then(function (AgentResponse $response) {
            Log::info('agent.responded', ['text' => $response->text]);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The agent picks that up by implementing HasMiddleware and returning the class from a middleware() method, the same shape as HTTP middleware.

Human in the loop means the agent stops and asks before doing something it can't undo. The SDK makes approval a property of the tool, so the dangerous ones pause and the harmless ones don't:

protected function needsApproval(Request $request): Approval|bool
{
    return $request['amount'] > 5000
        ? Approval::required('Refunds above 5,000 need a human.')
        : false;
}
Enter fullscreen mode Exit fullscreen mode

The agent then returns with hasPendingApprovals() true, you show a human the pending call and its arguments, and you resume with Decision::approve() or Decision::reject(). The full workflow is here, including what happens when generation fails mid-approval.

A sandbox is where you run code the model wrote. If your agent generates and executes anything, it runs in a container that can be thrown away, never on the machine holding your database credentials.

Evals are tests for non-deterministic output. You can't assert on an exact string, so you assert on properties instead: did it call the right tool, is the JSON shaped correctly, does a cheaper model grade the answer as acceptable. Skipping these is the most common mistake I see, because everything feels fine until a model version changes underneath you.

Then cost. Two attributes cover most of it, since not every task needs your best model:

#[UseCheapestModel]
class TagExtractor implements Agent {}

#[UseSmartestModel]
class MigrationPlanner implements Agent {}
Enter fullscreen mode Exit fullscreen mode

You'll see people quote splits like 60/30/10 for cheap, mid and premium models. Treat those as somebody's anecdote rather than a rule. Measure your own totalTokens per task type and route from that. Local models through Ollama are worth testing for the boring high-volume work, where the marginal cost is zero and the quality is often fine.

For watching it in production, the same tools you already use apply. Telescope locally, Pulse or Nightwatch in production, plus SDK middleware logging prompts and token counts per run.

What to build first

Reading about this does very little. The path that worked for me was small and boring, in this order.

  1. A tool that adds two numbers. Pointless in itself, and the fastest way to see that the model decides when to call it and your PHP does the work.
  2. A tool that hits your own database. Read-only. Now the model can answer questions about real data, and you'll immediately want to constrain what it can see.
  3. Turn on conversations. Add RemembersConversations and watch your token count per message climb as history accumulates. This teaches context cost better than any article.
  4. Add an approval gate. Give a tool a needsApproval that fires, and build the screen that shows a pending call.
  5. Then RAG. Embed a folder of markdown, store it with vector, query it with whereVectorSimilarTo. Doing it manually once is worth more than any framework tour.

Only after that should you look at sub-agents or MCP. Both are much easier to reason about once you've felt where the tokens go.

What I'd skip

Multi-agent architectures, for most applications. The demos look impressive and the bills are real. One agent with a well-described set of tools beats a hierarchy of specialists for the majority of what people actually build, and you can always split later.

Standalone vector databases, unless you've measured a reason. Your Postgres already does this, and keeping vectors next to the rows they belong to means you can filter by team, tenant or status in the same query.

Chasing new protocols. MCP took hold because it solved an actual integration problem and shipped SDKs. Most of what followed is positioning.

What I wouldn't skip is evals and token logging. They're boring, and they're the difference between an agent you can change with confidence and one nobody wants to touch.

FAQ

Do I need Python for any of this?

No. Everything in this post runs in PHP through the Laravel AI SDK, including embeddings, vector search, reranking and MCP servers. Python dominates model training and research. Application work, which is what most of us are doing, has first-party PHP support now.

Do I need a vector database like Pinecone or Qdrant?

Probably not. PostgreSQL with pgvector, MariaDB 11.7 or later, and MongoDB all store vectors and run similarity search, and Laravel's whereVectorSimilarTo works against them directly. Keeping vectors in your main database also means normal where clauses compose with similarity search, which is awkward when your vectors live in a separate service. Reach for a dedicated one when you've outgrown that, not before.

Is RAG still worth it now that context windows are so large?

Yes, for two reasons. Cost, because retrieving five relevant chunks is far cheaper than sending an entire knowledge base on every request. And quality, because Chroma's context rot research shows accuracy falling as input grows, even well inside the advertised window. Less relevant context beats more context.

What's the difference between open source and open weights?

Open weights means the parameters are downloadable, so you can run the model on your own hardware. Open source, taken literally, would also require the training data and pipeline, which almost no widely used model provides. Most models described as open are open weights. For running something locally the distinction rarely matters, but the words aren't interchangeable.

The thing worth remembering

Go back through the terms and you'll notice they fall into two buckets. Some describe next-token prediction and its consequences, which covers tokens, temperature, context windows and hallucination. The rest describe plumbing you write yourself: tools, memory, retrieval, approval gates, guardrails.

There's no third bucket. Nothing in the list is a machine that thinks. That's why an agent with no tools can't do anything, why it's bad at arithmetic until you hand it a calculator, and why the interesting engineering is almost entirely in the second bucket.

Which is good news for us, honestly. The second bucket is just software, and you already know how to write that.

Top comments (0)