# 09 - RAG in PHP - Answer From Your Own Docs, Not the Model's Memory

> Source: <https://dev.to/hammrouni/09-rag-in-php-answer-from-your-own-docs-not-the-models-memory-1md8>
> Published: 2026-09-27 20:47:33+00:00

The failure mode you hit the moment you point an LLM at internal questions: it **hallucinates policy** that sounds right but was never written. "Can I work from Hawaii for two months?" is exactly the kind of question where a confident wrong answer costs you real money.

Retrieval-Augmented Generation (RAG) fixes it: **retrieve the relevant doc first, then answer from that - and say so when there's no answer.** In NanoAgent, RAG is just a search *tool*. No vector database required to start.

``` php
$knowledgeBase = [
    'policy_wfh' => [
        'title' => 'Remote Work Policy 2024',
        'content' => 'Employees may work remotely up to 3 days a week. Full remote work '
                    . 'requires Director approval. Working from international locations is '
                    . 'limited to 30 days per year due to tax implications.'
    ],
    'policy_holiday' => [
        'title' => 'Holiday Schedule 2024',
        'content' => 'Closed on New Year\'s Day, Memorial Day, Independence Day, Labor Day, '
                    . 'Thanksgiving, and Christmas Day.'
    ],
    'it_support' => [
        'title' => 'IT Support Contacts',
        'content' => 'Urgent: ext 5555. Non-urgent: support@company.com. Password resets via portal.'
    ]
];
```

The tool takes one argument - free-text keywords - and describes itself as a policy search, so the model knows to reach for it on anything HR-shaped.

``` php
use NanoAgent\Agent;
use NanoAgent\Tools\FunctionTool;

$searchTool = new FunctionTool(
    name: 'search_knowledge_base',
    description: 'Searches the internal knowledge base for policy documents. Input keywords.',
    parameters: [
        'type' => 'object',
        'properties' => [
            'query' => ['type' => 'string', 'description' => 'Keywords to search for']
        ],
        'required' => ['query']
    ],
```

The `callable` does a plain case-insensitive substring match against title and content, and returns either the matching documents or an explicit "nothing found" - never a guess.

``` php
    callable: function (array $args) use ($knowledgeBase) {
        $q = strtolower($args['query']);
        $hits = [];
        foreach ($knowledgeBase as $doc) {
            if (str_contains(strtolower($doc['title']), $q)
                || str_contains(strtolower($doc['content']), $q)) {
                $hits[] = "Title: {$doc['title']}\nContent: {$doc['content']}";
            }
        }
        return $hits
             ? implode("\n\n---\n\n", $hits)
             : "No relevant documents found.";
    }
);
php
$agent = new Agent(
    llm: $llmConfig,
    systemPrompt: "You are a professional HR assistant. You must ONLY answer based on the "
                . "search results. If the information is not present, politely say so. "
                . "Always cite the document title.",
    tools: [$searchTool]
);

$response = $agent->chat("Can I work from Hawaii for two months?");
echo $response;
```

That last prompt line is the whole safety model. Three rules do the heavy lifting:

For the Hawaii question, the tool retrieves `Remote Work Policy 2024` (it contains "international locations"), and the agent answers: *limited to 30 days/year, so two months isn't allowed - per Remote Work Policy 2024.* Grounded, cited, correct.

The only thing that changes as you scale is what the `callable` does:

| Stage | The `search_knowledge_base` callable does | 
|---|---|
| Demo | keyword scan of a PHP array | 
| Real (small) | query your SQL/Postgres docs table | 
| Real (large) | embed the query + docs, hit a **vector store** (Qdrant, pgvector) | 

**The agent code never changes.** You keep the same tool name and the same prompt; you just make retrieval smarter inside the function. Start with the dumb keyword search - it's enough to prove the pattern and to handle small corpora - and upgrade the retrieval when you need to.

You could stuff the whole doc into the prompt. That blows the context window and wastes tokens on irrelevant content. A *tool* lets the model decide **what to retrieve and when**, pulling only what's relevant. As your corpus grows, that's the difference between "works" and "doesn't fit."

*Part of the NanoAgent examples series. [Landing + demos](https://hammrouni.github.io/nanoagentphp/).*
