pluck('id')->toArray(); if (empty($knowledgeBaseIds)) { return [ 'context' => '', 'sources' => [], ]; } try { // 使用 Scout/Meilisearch 搜索并获取排名分数 $rawResults = Document::search($query, function ($meilisearch, $query, $options) use ($knowledgeBaseIds) { $options['showRankingScore'] = true; $filter = collect($knowledgeBaseIds) ->map(fn($id) => "knowledge_base_id = {$id}") ->implode(' OR '); $options['filter'] = $filter; $options['limit'] = self::TOP_K; return $meilisearch->search($query, $options); })->raw(); $hits = $rawResults['hits'] ?? []; } catch (\Exception $e) { Log::warning('Knowledge search failed', [ 'query' => $query, 'error' => $e->getMessage(), ]); return [ 'context' => '', 'sources' => [], ]; } if (empty($hits)) { return [ 'context' => '', 'sources' => [], ]; } // 取出文档 ID 并加载 Eloquent 模型 $hitIds = collect($hits)->pluck('id')->toArray(); $documents = Document::whereIn('id', $hitIds)->get()->keyBy('id'); // 构建排名分数映射 $rankingScores = collect($hits)->pluck('_rankingScore', 'id'); $context = ''; $sources = []; foreach ($hits as $hit) { $document = $documents->get($hit['id']); if (!$document) { continue; } $snippet = $this->extractSnippet($document); if (mb_strlen($context) + mb_strlen($snippet) > self::MAX_CONTEXT_LENGTH) { break; } $context .= $snippet . "\n\n"; $sources[] = [ 'title' => $document->title, 'id' => 'kb-doc-' . str_pad($document->id, 3, '0', STR_PAD_LEFT), 'relevance' => round($rankingScores->get($document->id, 0), 2), ]; } return [ 'context' => trim($context), 'sources' => $sources, ]; } /** * 从文档中提取摘要片段 */ private function extractSnippet($document): string { $content = $document->markdown_preview ?? $document->description ?? ''; if (mb_strlen($content) <= 500) { return "【{$document->title}】\n{$content}"; } return "【{$document->title}】\n" . mb_substr($content, 0, 500) . '...'; } }