2020use Qdrant \Qdrant ;
2121use Symfony \Component \HttpClient \Psr18Client ;
2222
23- /**
24- * Правильная реализация RAG-сервиса с тремя четкими этапами:
25- * 1. Query Processing - обработка и оптимизация запроса
26- * 2. Retrieval - поиск релевантных документов
27- * 3. Generation - генерация ответа на основе найденных документов
28- */
2923class ImprovedRAGService implements RAGServiceInterface
3024{
3125 private const COLLECTION_NAME = 'products ' ;
@@ -41,47 +35,30 @@ public function __construct(
4135 ) {
4236 }
4337
44- /**
45- * Главный метод RAG pipeline.
46- */
4738 public function search (string $ userQuery ): RAGSearchResult
4839 {
4940 return $ this ->searchWithContext ($ userQuery , 'default_session ' );
5041 }
5142
52- /**
53- * RAG поиск с учетом контекста сессии.
54- */
5543 public function searchWithContext (string $ userQuery , string $ sessionId ): RAGSearchResult
5644 {
5745 $ this ->ensureInitialized ();
5846
59- // Получаем контекст предыдущих запросов
6047 $ context = $ this ->contextService ->getSearchContext ($ sessionId );
61-
62- // Этап 1: Query Processing с контекстом
6348 $ optimizedQuery = $ this ->processQuery ($ userQuery , $ context );
64-
65- // Попытаемся определить категорию для фильтрации
6649 $ categoryFilter = $ context ?: $ this ->contextService ->inferCategoryFromQuery ($ userQuery );
67-
68- // Этап 2: Retrieval с возможной фильтрацией
6950 $ documents = $ this ->retrieveDocuments ($ optimizedQuery , $ categoryFilter );
7051
71- // Если с фильтром ничего не найдено, попробуем без фильтра
7252 if (empty ($ documents ) && $ categoryFilter ) {
7353 $ documents = $ this ->retrieveDocuments ($ optimizedQuery , null );
7454 }
7555
76- // Сохраняем контекст для следующих запросов
7756 if (!empty ($ documents )) {
7857 $ category = $ this ->contextService ->extractCategoryFromResults ($ documents );
7958 if ($ category ) {
8059 $ this ->contextService ->setSearchContext ($ sessionId , $ category , $ userQuery );
8160 }
8261 }
83-
84- // Этап 3: Generation
8562 $ aiResponse = $ this ->generateResponse ($ documents , $ userQuery );
8663
8764 return new RAGSearchResult (
@@ -92,53 +69,41 @@ public function searchWithContext(string $userQuery, string $sessionId): RAGSear
9269 );
9370 }
9471
95- /**
96- * ЭТАП 1: Query Processing
97- * Анализирует запрос через LLM с учетом контекста и создает оптимизированный поисковый термин.
98- */
9972 private function processQuery (string $ userQuery , ?string $ context = null ): string
10073 {
10174 try {
10275 return $ this ->llamaService ->analyzeSearchQuery ($ userQuery , $ context );
10376 } catch (\Exception $ e ) {
104- // Fallback: если LLM недоступен, используем исходный запрос
10577 return $ userQuery ;
10678 }
10779 }
10880
10981 /**
110- * ЭТАП 2: Retrieval
111- * Выполняет векторный поиск в Qdrant с возможной фильтрацией по категории.
112- *
11382 * @return array<int, array<string, mixed>>
11483 */
11584 private function retrieveDocuments (string $ optimizedQuery , ?string $ categoryFilter = null ): array
11685 {
117- // Векторизуем оптимизированный запрос
11886 if (null === $ this ->embedder ) {
11987 throw RAGException::serviceUnavailable ('Embedder not initialized ' );
12088 }
12189
12290 $ embedding = ($ this ->embedder )($ optimizedQuery , pooling: 'mean ' , normalize: true );
12391 $ queryVector = is_array ($ embedding ) ? $ embedding [0 ] : ($ embedding instanceof \Codewithkyrian \Transformers \Tensor \Tensor ? $ embedding [0 ] : []);
12492
125- // Создаем поисковый запрос для Qdrant
12693 $ searchVector = new VectorStruct ($ queryVector , 'default ' );
12794 $ searchRequest = new SearchRequest ($ searchVector );
12895 $ searchRequest
12996 ->setLimit (self ::DEFAULT_LIMIT )
13097 ->setWithPayload (true )
13198 ->setScoreThreshold (self ::DEFAULT_THRESHOLD );
13299
133- // Добавляем фильтр по категории, если указана
134100 if ($ categoryFilter ) {
135101 $ filter = new Filter ();
136102 $ condition = new MatchString ('category ' , $ categoryFilter );
137103 $ filter ->addMust ($ condition );
138104 $ searchRequest ->setFilter ($ filter );
139105 }
140106
141- // Выполняем поиск
142107 try {
143108 $ response = $ this ->qdrantClient
144109 ?->collections(self ::COLLECTION_NAME )
@@ -152,9 +117,6 @@ private function retrieveDocuments(string $optimizedQuery, ?string $categoryFilt
152117 }
153118
154119 /**
155- * ЭТАП 3: Generation
156- * Генерирует персонализированный ответ на основе найденных документов.
157- *
158120 * @param array<int, array<string, mixed>> $documents
159121 */
160122 private function generateResponse (array $ documents , string $ originalQuery ): string
@@ -164,47 +126,37 @@ private function generateResponse(array $documents, string $originalQuery): stri
164126 }
165127
166128 try {
167- // Используем constrained generation для точного ответа
168129 return $ this ->llamaService ->generateConstrainedResponse ($ documents , $ originalQuery );
169130 } catch (\Exception $ e ) {
170- // Fallback: базовый ответ без LLM
171131 $ count = count ($ documents );
172132 $ topProduct = $ documents [0 ]['payload ' ]['name ' ] ?? 'товар ' ;
173133
174134 return "Найдено $ count товар(ов). Рекомендуем: $ topProduct " ;
175135 }
176136 }
177137
178- /**
179- * Инициализация всех необходимых сервисов.
180- */
181138 private function ensureInitialized (): void
182139 {
183140 if (null !== $ this ->qdrantClient && null !== $ this ->embedder ) {
184141 return ;
185142 }
186143
187- // Настройка окружения для избежания конфликтов OpenMP
188144 if (!getenv ('KMP_DUPLICATE_LIB_OK ' )) {
189145 putenv ('KMP_DUPLICATE_LIB_OK=TRUE ' );
190146 }
191147
192- // Инициализация Qdrant клиента
193148 if (null === $ this ->qdrantClient ) {
194149 $ config = new Config ('http://localhost ' , 6333 );
195150 $ transport = new Transport (new Psr18Client (), $ config );
196151 $ this ->qdrantClient = new Qdrant ($ transport );
197152 }
198153
199- // Инициализация embedder модели
200154 if (null === $ this ->embedder ) {
201155 $ this ->embedder = pipeline (Task::Embeddings, 'onnx-community/Qwen3-Embedding-0.6B-ONNX ' );
202156 }
203157 }
204158
205159 /**
206- * Проверка готовности всех компонентов RAG системы.
207- *
208160 * @return array<string, mixed>
209161 */
210162 public function healthCheck (): array
@@ -217,35 +169,27 @@ public function healthCheck(): array
217169 ];
218170
219171 try {
220- // Проверка LlamaService
221172 $ health ['llama_service ' ] = $ this ->llamaService ->isAvailable ();
222173 } catch (\Exception ) {
223174 $ health ['llama_service ' ] = false ;
224175 }
225176
226177 try {
227- // Инициализируем если нужно
228178 $ this ->ensureInitialized ();
229179
230- // Проверка Qdrant
231180 $ this ->qdrantClient ?->collections(self ::COLLECTION_NAME )->info ();
232181 $ health ['qdrant ' ] = true ;
233182
234- // Проверка embedder (если инициализирован без ошибок, то работает)
235183 $ health ['embedder ' ] = null !== $ this ->embedder ;
236184 } catch (\Exception ) {
237- // Qdrant или embedder недоступны
238185 }
239186
240- // Общая готовность - можем работать даже без LLM, но нужны Qdrant + embedder
241187 $ health ['overall ' ] = $ health ['qdrant ' ] && $ health ['embedder ' ];
242188
243189 return $ health ;
244190 }
245191
246192 /**
247- * Получить статистику коллекции.
248- *
249193 * @return array<string, mixed>
250194 */
251195 public function getCollectionStats (): array
0 commit comments