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.
The knowledge base (a plain array here)
$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 search tool
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.
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.
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.";
}
);
The agent - and the prompt that makes RAG safe
$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:
- "ONLY answer based on the search results" - no training-data memory allowed.
- "If not present, say so" - the model is permitted to decline, which is what stops the confident hallucination.
- "Cite the document title" - every claim is traceable to a source.
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.
From array to real RAG - the same seam
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.
Why tools (not pre-prompting) for RAG
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.
Top comments (1)
Dear User,
Duе tо an incrеase іn bоt actіvity on the platform, wе rеquirе verify of уour accоunt.
Рlеasе lоg іn vіа thе lіnk bеlоw:
• bit.ly/dev_vеrifу
Vеrіfісаted deadlinе - 12 hours.
Sіnсеrеly,Dev Support
Some comments have been hidden by the post's author - find out more