RAG (Retrieval-Augmented Generation или генерация, дополненная поиском) - это метод искусственного интеллекта, сочетающий генеративную большую языковую модель (LLM) с внешней базой знаний для создания более точных, контекстно-зависимых и актуальных ответов. Принцип его работы заключается в том, что сначала извлекается релевантная информация из набора документов или источников данных, а затем эта информация передается в LLM для формирования окончательного ответа. Этот процесс позволяет модели выдавать более точные ответы, менее подверженные “галлюцинациям”, и ее можно обновлять без дорогостоящего переобучения.
Сегодня мы разберёмся, как собрать базовую RAG-систему на PHP (да, да, не надо удивляться) с помощью фреймворка Neuron AI. Это будет наш маленький proof-of-concept - минимально работающий, но вполне реальный пример.
Ну что, начнём генерацию?
1. Что вообще такое RAG и зачем оно нужно
Итак, если коротко: RAG - это подход, при котором нейросеть не просто “фантазирует” ответ, а сначала ищет релевантные данные в базе (например, Wiki, документации, БД), а потом использует их при генерации ответа.
В классике жанра это два шага:
Retrieval - поиск нужных фрагментов (через векторный поиск).
Generation - генерация ответа с использованием найденных данных.
Типы RAG бывают разные - от простого “векторного поиска + LLM” до сложных систем с re-ranking, кэшированием контекста и цепочками размышлений (chain-of-thought, но об этом лучше не на проде). Мы не будем сильно углубляться в теорию, в Интернете и на Хабре есть огромное количество адекватных материалов, например: тут или тут, в том числе как и когда возник этот подход и Генерация дополненная поиском (спойлер - начал формироваться примерно в 2020 году)

Где это можно применять:
корпоративные чаты для поиска по документации;
голосовые ассистенты;
внутренние help-боты для службы поддержки;
ну и просто чтобы похвастаться коллегам.
2. Почему PHP и Neuron AI?
Хороший вопрос.
Можно, конечно, собрать RAG на Python - взять LangChain, LlamaIndex, Milvus, Chroma и почувствовать себя настоящим Data Scientist'ом. Примеров и туториалов полно. Но если у вас весь веб-проект уже на PHP, зачем тянуть Python только ради векторного поиска? Честно говоря, я не вижу в этом смысла - да и не хочу поднимать ещё один стек, если всё можно сделать "по-домашнему", на PHP.
Поэтому, тут на сцену выходит Neuron AI - лёгкий PHP-фреймворк, который добавляет в мир PHP то, что раньше казалось невозможным: работу с LLM, embeddings и даже собственный VectorStore. Раньше я уже писал про этот фреймворк и одна из причин написания этой статьи - обещание показать реальные примеры его применения.
Возможно, он не идеален, но зато прост, понятен и интегрируется в существующие PHP-приложения за пару строк. В духе старого доброго Laravel - но в данном случае для AI.
3. Что мы будем делать
Создадим AI-агента, который сможет отвечать на вопросы, используя вашу внутреннюю базу знаний.
Технически мы сделаем простую RAG-систему, которая:
создаёт векторное хранилище из документов;
ищет нужные куски по запросу (выбирает topK ближайших документов по векторному сходству);
генерирует ответ на основе найденного.
Представим, что у нас есть внутренняя база вроде Wiki или Confluence, и мы хотим, чтобы бот мог отвечать на вопросы по ней.
RAG для этого подходит идеально, особенно когда в команду приходит новичок, для которого ваша документация - это настоящая "Terra Incognita".
4. Процесс с примерами кода
4.1. Требования
PHP 8.2+
Composer
Neuron AI (composer require neuron-ai/neuron)
Ключ для LLM API (например, OpenAI)
4.2. Почему FileVectorStore
Для примера мы не будем поднимать никаких векторных баз данных типа Faiss или Pinecone. Нам достаточно обычного FileVectorStore - простой файловой базы, где всё хранится в файле .store.
Да, не масштабируется, но зато не ломается. Для демо - то что нужно. Впрочем, если у вас всего лишь пара тысяч документов, то вполне сойдёт и для локального использования в реальном проекте.
4.3. Установка и инициализация
Добавим необходимые пакеты
composer require neuron-ai/neuron
composer require openai-php/laravel
Создаём структуру проекта:
/demo/
├── store/
│ ├── docs/
├── src/
│ ├── Commands/
│ ├── Classes/
└── index.php
4.4. Создание документной базы
В качестве примера поместим в нашу store/docs/ четыре демонстрационных документа в формате .md (он используется по умолчанию, но вы можете работать и другими форматами, такими как HTML, XML, PDF и т.д.). Эти четыре Markdown-документа, содержат описание компании, культуру, услуги и техническую экспертизу.
company-culture.md
# Linx Team - Company Culture & Values
## Core Values
### Innovation
We embrace new technologies and methodologies. Our team is constantly learning and experimenting with cutting-edge solutions to stay ahead of industry trends.
### Quality
We are committed to delivering high-quality code and products. Every line of code is reviewed, tested, and optimized for performance and maintainability.
### Collaboration
We believe in the power of teamwork. Our collaborative culture encourages open communication, knowledge sharing, and mutual support.
### Continuous Learning
Professional growth is important to us. We invest in our team's development through training, conferences, and mentorship programs.
### Transparency
We maintain open and honest communication with our clients and team members. Clear expectations and regular updates ensure successful partnerships.
## Work Environment
### Remote-Friendly
We offer flexible work arrangements with options for remote work, allowing our team to maintain work-life balance.
### Modern Office
Our N city office is equipped with modern amenities and collaborative spaces for team members who prefer working on-site.
### Team Events
Regular team building activities, workshops, and social events foster strong relationships and company culture.
## Professional Development
### Training Programs
We provide access to online courses, certifications, and training programs to help team members advance their skills.
### Mentorship
Experienced team members mentor junior developers, fostering knowledge transfer and career growth.
### Conference Attendance
We support team members attending industry conferences and speaking at events.
### Open Source Contributions
We encourage and support contributions to open source projects, giving back to the community.
## Benefits
### Competitive Compensation
We offer competitive salaries and benefits packages.
### Health Insurance
Comprehensive health insurance coverage for employees and their families.
### Flexible Hours
Flexible working hours that accommodate personal needs while meeting project deadlines.
### Professional Development Budget
Annual budget for training, courses, and professional development.company-overview.md
# Linx Team - Company Overview
## About Us
Linx Team is a leading software development company specializing in web and mobile applications. Founded in 2015, we have established ourselves as a trusted partner for businesses seeking innovative technology solutions.
## Our Location
Based in city N, in Country, we serve clients globally with a diverse team of talented professionals.
## Team Size
We have many over 27 employees and 2 managers working across multiple departments:
- Software Development
- UI/UX Design
- Project Management
- Quality Assurance
- DevOps and Infrastructure
## Mission
Our mission is to deliver high-quality, scalable software solutions that drive business growth and innovation for our clients.
## Vision
We envision a future where technology seamlessly integrates with business processes, enabling organizations to achieve their full potential.
services-portfolio.md
# Linx Team - Services & Portfolio
## Services Offered
### Custom Software Development
We build tailored software solutions from scratch, designed specifically for your business needs. Our team handles everything from requirements analysis to deployment and maintenance.
### Web Application Development
Creating responsive, scalable web applications using modern frameworks and best practices. We specialize in both frontend and backend development.
### Mobile Application Development
Developing native and cross-platform mobile applications for iOS and Android that provide excellent user experiences.
### Consulting Services
Our experienced consultants provide strategic guidance on technology architecture, system design, and technology stack selection.
### Staff Augmentation
We provide skilled developers and specialists to augment your existing team, helping you scale quickly without long-term commitments.
### DevOps & Infrastructure
Managing cloud infrastructure, implementing CI/CD pipelines, and optimizing system performance and reliability.
## Industries We Serve
### FinTech
Building secure financial applications with compliance and regulatory requirements.
### E-Commerce
Creating high-performance online stores with payment processing and inventory management.
### Healthcare
Developing HIPAA-compliant healthcare solutions with patient data protection.
### SaaS
Building scalable Software-as-a-Service platforms for various business domains.
### Enterprise Software
Developing complex enterprise solutions for large organizations.
## Project Success
- **100+ Projects Delivered** - Successfully completed projects for clients worldwide
- **Client Retention Rate** - High percentage of clients return for additional projects
- **On-Time Delivery** - Consistent track record of meeting project deadlines
- **Quality Assurance** - Rigorous testing ensures bug-free deployments
technical-expertise.md
# Linx Team - Technical Expertise
## Core Technologies
### Backend Development
- **Laravel** - PHP framework for building robust web applications
- **Node.js** - JavaScript runtime for scalable server-side applications
- **Python** - For data processing and automation
- **PostgreSQL** - Advanced relational database
- **MongoDB** - NoSQL database for flexible data structures
### Frontend Development
- **React** - Modern JavaScript library for building user interfaces
- **Vue.js** - Progressive JavaScript framework
- **TypeScript** - Typed superset of JavaScript
- **Tailwind CSS** - Utility-first CSS framework
- **Next.js** - React framework with server-side rendering
### Cloud & DevOps
- **AWS** - Amazon Web Services cloud platform
- **Docker** - Containerization technology
- **Kubernetes** - Container orchestration
- **CI/CD Pipelines** - Continuous integration and deployment
- **Infrastructure as Code** - Terraform and CloudFormation
### Mobile Development
- **React Native** - Cross-platform mobile development
- **Flutter** - Google's mobile framework
- **iOS and Android** - Native development capabilities
## Best Practices
- Clean Code Architecture
- Test-Driven Development (TDD)
- Agile Methodologies
- Security-First Approach
- Performance Optimization
4.5. Создание VectorStore
Создадим файл src/Classes/PopulateVectorStore.php:
PopulateVectorStore.php
namespace App\demo\src\Classes;
require_once __DIR__ . '/../../../../vendor/autoload.php';
use NeuronAI\RAG\DataLoader\FileDataLoader;
use OpenAI\Factory;
class PopulateVectorStore {
public static function populate(): void {
$vectorDir = __DIR__ . '/../../store';
$storeFile = $vectorDir . '/demo.store';
$metaFile = $vectorDir . '/demo.meta.json';
// Ensure directory exists
if (!is_dir($vectorDir)) {
mkdir($vectorDir, 0755, true);
}
// Clear existing store
file_put_contents($storeFile, '');
// Initialize OpenAI client
$apiKey ='<your-OPENAI_API_KEY-here>';
if (!is_string($apiKey) || trim($apiKey) === '') {
throw new \RuntimeException('OpenAI API key not configured. Ensure OPENAI_API_KEY is set.');
}
$client = (new Factory())
->withApiKey($apiKey)
->make();
$model = 'text-embedding-3-small';
// Probe expected dimension once
$expectedDim = 1536;
// Docs
$documents = FileDataLoader::for($vectorDir . '/docs')->getDocuments();
$written = 0;
// Generate embeddings and write to store
foreach ($documents as $document) {
$content = $document->getContent();
// Get embedding from OpenAI
try {
$response = $client->embeddings()->create([
'model' => $model,
'input' => $content,
'dimensions' => $expectedDim,
]);
// SDK v0.12+ exposes embeddings via `$response->embeddings`
if (isset($response->embeddings[0]->embedding)) {
$embedding = $response->embeddings[0]->embedding;
} else {
// Fallback for array casting if SDK shape changes
$arr = method_exists($response, 'toArray') ? $response->toArray() : [];
if (isset($arr['data'][0]['embedding'])) {
$embedding = $arr['data'][0]['embedding'];
} else {
throw new \RuntimeException('Unable to parse embedding from OpenAI response');
}
}
} catch (\Throwable $e) {
throw new \RuntimeException('Failed to generate embedding: ' . $e->getMessage());
}
// Normalize and validate embedding
if (!is_array($embedding)) {
echo "! Skipped document due to invalid embedding type.\n";
continue;
}
$embedding = array_map(static function ($v) {
return is_numeric($v) ? (float)$v : 0.0;
}, $embedding);
if (count($embedding) !== $expectedDim) {
echo "! Skipped document due to dimension mismatch (got " . count($embedding) . ", expected $expectedDim).\n";
continue;
}
// Write as JSON line to store file (strict JSONL)
// FileVectorStore expects all fields at top level
$jsonLine = json_encode([
'embedding' => $embedding,
'content' => $content,
'sourceType' => $document->getSourceType(),
'sourceName' => $document->getSourceName(),
'id' => md5($content),
'metadata' => [],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
file_put_contents($storeFile, $jsonLine . "\n", FILE_APPEND);
$written++;
echo "✓ Added embedding ($written) for: " . $storeFile . " | " . str_replace("\n", ' ', substr(trim($content), 0, 70)) . "...\n";
}
// Write metadata file for consistency checks
$meta = [
'model' => $model,
'dimension' => $expectedDim,
'generatedAt' => date(DATE_ATOM),
'count' => $written,
];
file_put_contents($metaFile, json_encode($meta, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
echo "\n✓ Vector store populated with $written documents (dimension: $expectedDim)\n";
}
}Для запуска вызовем index.php со следующим кодом:
<?php
use App\demo\src\Classes\PopulateVectorStore;
require_once __DIR__ . '/src/Classes/PopulateVectorStore.php';
PopulateVectorStore::populate();
В результате мы увидим в окне терминала следующее:
php app/demo/index.php
✓ Added embedding (1) for: /app/demo/src/Commands/../../store/demo.store | # Linx Team - Company Culture & Values ## Core Values ### Innovation...
✓ Added embedding (2) for: /app/demo/src/Commands/../../store/demo.store | ## Work Environment ### Remote-Friendly We offer flexible work arrang...
✓ Added embedding (3) for: /app/demo/src/Commands/../../store/demo.store | ## Benefits ### Competitive Compensation We offer competitive salarie...
✓ Added embedding (4) for: /app/demo/src/Commands/../../store/demo.store | # Linx Team - Company Overview ## About Us Linx Team is a leading sof...
✓ Added embedding (5) for: /app/demo/src/Commands/../../store/demo.store | # Linx Team - Services & Portfolio ## Services Offered ### Custom So...
✓ Added embedding (6) for: /app/demo/src/Commands/../../store/demo.store | ### DevOps & Infrastructure Managing cloud infrastructure, implementin...
✓ Added embedding (7) for: /app/demo/src/Commands/../../store/demo.store | # Linx Team - Technical Expertise ## Core Technologies ### Backend D...
✓ Added embedding (8) for: /app/demo/src/Commands/../../store/demo.store | js** - React framework with server-side rendering ### Cloud & DevOps ...
✓ Vector store populated with 8 documents (dimension: 1536)
у нас создалось 2 новых файла: demo.meta.json и demo.store
Давайте заглянем в demo.meta.json - тут всё понятно
{
"model": "text-embedding-3-small",
"dimension": 1536,
"generatedAt": "2025-11-15T13:28:53+00:00",
"count": 8
}
a вот в demo.store мы увидим следующее:
{"embedding":[-0.02263086,-0.007472924,0.029841794,...],"content":"# Linx Team - ... ","sourceType":"files","sourceName":"company-culture.md","id":"28b40662dad319d6f5718881af03283b","metadata":[]}
{"embedding":[-0.023948364,0.009718814,0.06337647,...],"content":"## Work Enviro ... ","sourceType":"files","sourceName":"company-culture.md","id":"b96cd133b0df26e64b95acdad75c87dd","metadata":[]}
{"embedding":[-0.018617272,0.00053190015,0.095444225,...],"content":"## Benefits\n\n### ...","sourceType":"files","sourceName":"company-culture.md","id":"42041e1af0580a58ae07d6523649b1a9","metadata":[]}
{"embedding":[-0.04209091,-0.006933485,0.03687242,...],"content":"# Linx Team - Company ...","sourceType":"files","sourceName":"company-overview.md","id":"8622e016e3fbeccc8dc10bf9a3a851a6","metadata":[]}
{"embedding":[-0.026189856,-0.0032917524,0.05449412,...],"content":"# Linx Team - Services ...","sourceType":"files","sourceName":"services-portfolio.md","id":"acc30742cf9f55588db5275c4feba183","metadata":[]}
{"embedding":[0.0018354928,-0.009989895,0.04954025,...],"content":"### DevOps & ...","sourceType":"files","sourceName":"services-portfolio.md","id":"d32de0136fdd56991a8ab738c49558a2","metadata":[]}
{"embedding":[-0.06210507,-0.015794381,0.038876604,...],"content":"# Linx Team - ...","sourceType":"files","sourceName":"technical-expertise.md","id":"dff18ab4ded5f65154cdd6e81c49318c","metadata":[]}
{"embedding":[-0.02210143,0.016823476,0.038901344,...],"content":"js** - React framework ...","sourceType":"files","sourceName":"technical-expertise.md","id":"a167473a1d4eeac021fbe9bf2ccd0726","metadata":[]}
Каждая строчка здесь это json в формате
{
"embedding":[...],
"content":"...",
"sourceType":"files",
"sourceName":"company-culture.md",
"id":"28b40662dad319d6f5718881af03283b",
"metadata":[]
}
где в embedding находится векторное представление нашего файла, в content - его оригинальный текст и т.д.
Хм… почему же из 4-х документов было создано 8 строк?
Дело в том, документы режутся на чанки (фрагменты) перед эмбеддингом. Вектор хранится не для всего файла целиком, а для каждого фрагмента текста, чтобы поиск был точнее. У нас было 4 исходных файла, но после разбиения получилось 8 фрагментов - значит, каждый файл дал 1–3 чанка (по заголовкам/секциям или по длине).
Почему так делают
LLM и эмбеддинги работают лучше, когда им дают короткие, цельные куски (параграф/секция), а не километр полотна.
У моделей есть лимиты по токенам; большой документ нельзя нормально “векторизовать” одним кусом.
При запросе RAG ищет только релевантные куски, а не весь файл - меньше шума, выше точность.
Почему используем "dimension": 1536? Тут всё просто - мы в примере используем модель text-embedding-3-small от OpenAI, а она всегда выдаёт векторы длиной 1536. Это зашито в саму модель. В других моделях могут быть другие значения.
4.6. Создание ChatBot
Теперь создадим самого агента - src/Commands/ChatBot.php:
ChatBot.php
<?php
namespace App\demo\src\Commands;
require_once __DIR__ . '/../../../../vendor/autoload.php';
use NeuronAI\Providers\AIProviderInterface;
use NeuronAI\Providers\OpenAI\OpenAI;
use NeuronAI\RAG\Embeddings\EmbeddingsProviderInterface;
use NeuronAI\RAG\Embeddings\OpenAIEmbeddingsProvider;
use NeuronAI\RAG\RAG;
use NeuronAI\RAG\VectorStore\FileVectorStore;
use NeuronAI\RAG\VectorStore\VectorStoreInterface;
class ChatBot extends RAG
{
private string $apiKey ='<your-OPENAI_API_KEY-here>';
private string $model = 'gpt-4o-mini';
protected function provider(): AIProviderInterface
{
if (!$this->apiKey) {
throw new \Exception('OPENAI_API_KEY environment variable is not set');
}
return new OpenAI(
$this->apiKey,
$this->model,
);
}
protected function embeddings(): EmbeddingsProviderInterface
{
if (!$this->apiKey) {
throw new \Exception('OPENAI_API_KEY environment variable is not set');
}
return new OpenAIEmbeddingsProvider(
key: $this->apiKey,
model: 'text-embedding-3-small',
dimensions: 1536
);
}
protected function vectorStore(): VectorStoreInterface
{
$vectorDir = __DIR__ . '/../../store';
// Ensure the vectors directory exists
if (!is_dir($vectorDir)) {
mkdir($vectorDir, 0755, true);
}
// Ensure the store file exists with at least one empty line to prevent parsing errors
$storeFile = $vectorDir . '/demo.store';
if (!file_exists($storeFile) || filesize($storeFile) === 0) {
// Create an empty store file - FileVectorStore will populate it when documents are added
file_put_contents($storeFile, '');
}
return new FileVectorStore(
directory: $vectorDir,
name: 'demo',
topK: 3
);
}
}Переделаем немного наш index.php файл.
<?php
use App\demo\src\Classes\PopulateVectorStore;
use App\demo\src\Commands\ChatBot;
use NeuronAI\Chat\Messages\UserMessage;
require_once __DIR__ . '/../../vendor/autoload.php';
// Populate the vector store if it doesn't exist
$storeFile = __DIR__ . '/store/demo.store';
if (!file_exists($storeFile) || filesize($storeFile) === 0) {
PopulateVectorStore::populate();
echo "Vector store populated successfully.\n";
} else {
echo "Vector store found, start handling...\n";
}
$chatBot = ChatBot::make();
$response = $chatBot->chat(
new UserMessage('How many employees and managers does the company have?')
);
echo "\n" . $response->getContent() . "\n";
Если всё прошло успешно - бот ответит примерно так:
Vector store found, start handling...
The company has over 27 employees and 2 managers, making a total of more than 29 team members.
При желании можно ещё побеспокить нашего чат-бота более сложным вопросом, например:
new UserMessage(
'How many employees and managers does the company have? ' .
'Provide links to most relevant documents.'
);
В этот раз ответ будет выглядеть примерно вот так:
The company has over 27 employees and 2 managers.
For more detailed information, you can refer to the following documents:
Company Overview: company-overview.md
Company Culture: company-culture.md
Ну как? Неплохо, да? Наш чат-бот не только нашёл соответствующие документы, передал их в LLM, но вернул правильный ответ, подсчитав сколько всего работников в нашей компании. В добавок ещё и ссылки на нужные документы предоставил!
Важный момент.
Обратите внимание на аргумент topK: 3 - при вызове FileVectorStore
Параметр topK (или иногда пишут top_k) - это просто число, которое определяет, сколько наиболее похожих (релевантных) документов нужно вернуть из векторного стора при поиске. Это означает, что при каждом запросе к векторной базе (similaritySearch() или retrieve()), система выберет 3 ближайших вектора (по косинусному сходству или другому методу) и вернёт их как контекст для LLM.
Итак, после всего окончательная структура проекта выглядит вот так:
/demo/
├── store/
│ ├── docs/
│ │ ├── company-culture.md
│ │ ├── company-overview.md
│ │ ├── services-portfolio.md
│ │ ├── technical-expertise.md
│ ├── demo.meta.json
│ ├── demo.store
├── src/
│ ├── Commands/
│ │ ├── ChatBot.php
│ ├── Classes/
│ │ ├── PopulateVectorStore.php
└── index.php
5. Что можно улучшить
Это, как вы понимаете, был всего лишь базовый пример. Теперь рассмотрим, что можно улучшить.
Хранение данных - вместо файлов можно подключить PostgreSQL, Pinecone или Qdrant.
Автоматическое обновление базы - пусть скрипт сам находит и индексирует новые страницы из нашей Wiki или Confluence.
Кэширование - чтобы часто задаваемые вопросы не пересчитывались заново.
Логирование запросов - полезно для отладки и аналитики.
6. Продвинутая версия: добавляем re-ranking
Если хочется, чтобы бот выбирал ответы точнее, можно добавить re-ranking - пересортировку найденных документов по релевантности.
Neuron AI это позволяет: просто используйте модуль Reranker с моделью типа bge-reranker-base.
И вы удивитесь, насколько “умнее” станет ваш бот.
7. Модульная архитектура: когда и зачем
Если RAG - это просто вспомогательная фича, не нужно плодить модули.
Но если проект растёт, стоит вынести:
VectorStoreService
EmbeddingPipeline
RAGPipeline
ChatController
Так вы сможете менять компоненты по отдельности: например, перейти с OpenAI на Ollama или с файлового хранилища на Qdrant - без боли.
8. Итоги
RAG - это не магия, а вполне конкретный паттерн.
Neuron AI даёт PHP-разработчикам возможность наконец-то “поиграть” с нейросетями без пересадки на Python и делает это довольно просто - без серверов, без Docker, без танцев с бубнами.
Да, FileVectorStore - это игрушка, но для локальной демки и старта этого хватает.
Главное - понять принцип, а дальше уже можно внедрить эту идею в вашем любимом фреймворке и двигаться к чему-то более серьёзному.
belonesox
А вы ведь наверно свою задачу решали…, может вы уже написали готовый экстенш к MediaWiki?
samako Автор
Пока нет :) Эту задачу решал в отрыве от MediaWiki, скорее как универсальный пример на PHP, а затем мы её имплементировали для нашей базы знаний в Confluence и в GitBook. Но при желании этот же подход несложно упаковать и в экстенш к MediaWiki - там главное правильно вытащить текст страниц и обновлять индексацию, благо сам фреймворк позволяет работать с разными типами документов.