Практика: документ-поиск
Соберём RAG-агента для поиска по документам.
Шаг 1: Миграция
Schema::ensureVectorExtensionExists();
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->vector('embedding', dimensions: 1536)->index();
$table->timestamps();
});
Шаг 2: Модель
class Document extends Model
{
protected $fillable = ['title', 'content', 'embedding'];
protected function casts(): array
{
return [
'embedding' => 'array',
];
}
}
Шаг 3: Job для embedding
class ProcessDocument implements ShouldQueue
{
use Dispatchable;
public function __construct(public Document $document) {}
public function handle(): void
{
$embedding = Str::of($this->document->content)->toEmbeddings();
$this->document->update(['embedding' => $embedding]);
}
}
Шаг 4: Агент
use Laravel\Ai\Tools\SimilaritySearch;
use Laravel\Ai\Concerns\RemembersConversations;
class DocumentSearch implements Agent, HasTools
{
use Promptable, RemembersConversations;
public function instructions(): string
{
return 'Ты — ассистент по документам. Отвечай на основе найденных фрагментов.';
}
public function tools(): iterable
{
return [
SimilaritySearch::usingModel(Document::class, 'embedding')
->minSimilarity(0.4),
];
}
}
Шаг 5: Загрузка
Route::post('/documents', function (Request $request) {
$document = Document::create([
'title' => $request->title,
'content' => $request->content,
]);
ProcessDocument::dispatch($document);
return response()->json(['status' => 'processing']);
});
Шаг 6: Поиск
Route::post('/search', function (Request $request) {
$response = (new DocumentSearch)->prompt($request->query);
return response()->json(['answer' => (string) $response]);
});
Streaming вариант
Route::get('/search/stream', function (Request $request) {
return (new DocumentSearch)
->stream($request->query)
->usingVercelDataProtocol();
});
Итоги
- Миграция с vector
- Job для embedding
- Агент с SimilaritySearch
- Route для загрузки и поиска
- Streaming для UX