بناء روبوت محادثة RAG باستخدام Supabase pgvector و Next.js

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

نماذج اللغة الكبيرة مثيرة للإعجاب، لكن لديها قيد حاسم: إنها تعرف فقط ما تم تدريبها عليه. ماذا لو كنت تريد مساعداً ذكياً يفهم وثائقك، منتجاتك، أو معرفة شركتك؟

هنا يأتي دور RAG (التوليد المعزز بالاسترجاع). في هذا الدليل، ستبني روبوت محادثة يمكنه الإجابة على الأسئلة باستخدام بياناتك الخاصة من خلال الجمع بين امتداد pgvector من Supabase وواجهات برمجة تطبيقات OpenAI.

ما ستبنيه

بنهاية هذا الدليل، سيكون لديك:

  • تطبيق Next.js مع واجهة محادثة
  • قاعدة بيانات Supabase تخزن مستنداتك كتضمينات متجهية
  • بحث دلالي يجد المحتوى ذي الصلة بناءً على المعنى
  • روبوت محادثة مدعوم بـ RAG يجيب على الأسئلة باستخدام بياناتك

المتطلبات الأساسية

قبل البدء، تأكد من توفر:

  • Node.js 18+ مثبت
  • حساب Supabase (الطبقة المجانية تعمل)
  • مفتاح OpenAI API مع وصول إلى التضمينات ونماذج المحادثة
  • معرفة أساسية بـ React و TypeScript
  • إلمام بـ Next.js App Router

فهم البنية

قبل الغوص في الكود، دعنا نفهم كيف يعمل RAG:

User Question → Embed Question → Search Similar Docs → Augment Prompt → LLM Response
     ↓               ↓                   ↓                  ↓              ↓
  "What is X?"   [0.1, 0.2...]    Find top 5 docs     Add context      Answer!
  1. المستخدم يطرح سؤالاً باللغة الطبيعية
  2. تضمين السؤال في متجه (مصفوفة من الأرقام)
  3. البحث الدلالي يجد المستندات ذات المتجهات المشابهة
  4. تعزيز الموجه بإضافة المستندات المسترجعة كسياق
  5. LLM يولد إجابة بناءً على السياق

السحر في الخطوة 3: بدلاً من مطابقة الكلمات المفتاحية، نجد المستندات المشابهة دلالياً - حتى لو لم تشترك في نفس الكلمات.

الخطوة 1: إعداد المشروع

أنشئ مشروع Next.js جديد مع TypeScript:

npx create-next-app@latest rag-chatbot --typescript --tailwind --app --src-dir
cd rag-chatbot

ثبّت التبعيات المطلوبة:

npm install @supabase/supabase-js openai ai

أنشئ ملف .env.local مع بيانات اعتمادك:

NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
OPENAI_API_KEY=your-openai-api-key

الخطوة 2: تهيئة Supabase مع pgvector

اذهب إلى لوحة تحكم Supabase وافتح SQL Editor. شغّل ما يلي لتمكين امتداد pgvector:

-- Enable the pgvector extension
create extension if not exists vector with schema extensions;

الآن أنشئ جدولاً لتخزين مستنداتك مع تضميناتها:

-- Create the documents table
create table documents (
  id bigint primary key generated always as identity,
  content text not null,
  metadata jsonb,
  embedding extensions.vector(1536)  -- OpenAI text-embedding-3-small dimension
);
 
-- Enable Row Level Security
alter table documents enable row level security;
 
-- Create policy for reading (adjust as needed)
create policy "Allow public read access"
  on documents for select
  using (true);
 
-- Create an index for faster similarity search
create index on documents
using hnsw (embedding vector_cosine_ops);

يتطابق vector(1536) مع أبعاد إخراج نموذج text-embedding-3-small من OpenAI. فهرس HNSW يمكّن البحث السريع عن أقرب الجيران التقريبي.

الخطوة 3: إنشاء دالة التضمين

أنشئ ملفاً جديداً src/lib/embeddings.ts:

import OpenAI from 'openai';
 
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});
 
export async function generateEmbedding(text: string): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  });
 
  return response.data[0].embedding;
}
 
export async function generateEmbeddings(texts: string[]): Promise<number[][]> {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: texts,
  });
 
  return response.data.map((item) => item.embedding);
}

الخطوة 4: بناء API لاستيعاب المستندات

أنشئ src/app/api/ingest/route.ts لإضافة مستندات إلى قاعدة معارفك:

import { createClient } from '@supabase/supabase-js';
import { generateEmbeddings } from '@/lib/embeddings';
import { NextResponse } from 'next/server';
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);
 
interface Document {
  content: string;
  metadata?: Record<string, unknown>;
}
 
export async function POST(request: Request) {
  try {
    const { documents }: { documents: Document[] } = await request.json();
 
    if (!documents || documents.length === 0) {
      return NextResponse.json(
        { error: 'No documents provided' },
        { status: 400 }
      );
    }
 
    // Generate embeddings for all documents
    const contents = documents.map((doc) => doc.content);
    const embeddings = await generateEmbeddings(contents);
 
    // Prepare data for insertion
    const rows = documents.map((doc, index) => ({
      content: doc.content,
      metadata: doc.metadata || {},
      embedding: embeddings[index],
    }));
 
    // Insert into Supabase
    const { data, error } = await supabase
      .from('documents')
      .insert(rows)
      .select('id');
 
    if (error) {
      throw error;
    }
 
    return NextResponse.json({
      success: true,
      inserted: data.length,
    });
  } catch (error) {
    console.error('Ingestion error:', error);
    return NextResponse.json(
      { error: 'Failed to ingest documents' },
      { status: 500 }
    );
  }
}

الخطوة 5: إنشاء دالة البحث الدلالي

أنشئ src/lib/search.ts لإيجاد المستندات ذات الصلة:

import { createClient } from '@supabase/supabase-js';
import { generateEmbedding } from './embeddings';
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);
 
export interface SearchResult {
  id: number;
  content: string;
  metadata: Record<string, unknown>;
  similarity: number;
}
 
export async function semanticSearch(
  query: string,
  topK: number = 5,
  threshold: number = 0.5
): Promise<SearchResult[]> {
  // Generate embedding for the query
  const queryEmbedding = await generateEmbedding(query);
 
  // Call the similarity search RPC function
  const { data, error } = await supabase.rpc('match_documents', {
    query_embedding: queryEmbedding,
    match_threshold: threshold,
    match_count: topK,
  });
 
  if (error) {
    throw error;
  }
 
  return data;
}

الآن أضف دالة المطابقة في SQL Editor الخاص بـ Supabase:

-- Create the similarity search function
create or replace function match_documents(
  query_embedding vector(1536),
  match_threshold float,
  match_count int
)
returns table (
  id bigint,
  content text,
  metadata jsonb,
  similarity float
)
language sql stable
as $$
  select
    documents.id,
    documents.content,
    documents.metadata,
    1 - (documents.embedding <=> query_embedding) as similarity
  from documents
  where 1 - (documents.embedding <=> query_embedding) > match_threshold
  order by documents.embedding <=> query_embedding
  limit match_count;
$$;

المعامل <=> يحسب مسافة جيب التمام. نحوله إلى تشابه بطرحه من 1.

الخطوة 6: بناء API للمحادثة RAG

أنشئ src/app/api/chat/route.ts:

import OpenAI from 'openai';
import { semanticSearch } from '@/lib/search';
import { NextResponse } from 'next/server';
 
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});
 
export async function POST(request: Request) {
  try {
    const { message, conversationHistory = [] } = await request.json();
 
    if (!message) {
      return NextResponse.json(
        { error: 'No message provided' },
        { status: 400 }
      );
    }
 
    // Step 1: Search for relevant documents
    const relevantDocs = await semanticSearch(message, 5, 0.5);
 
    // Step 2: Build context from retrieved documents
    const context = relevantDocs
      .map((doc, i) => `[Document ${i + 1}]\n${doc.content}`)
      .join('\n\n');
 
    // Step 3: Create the augmented prompt
    const systemPrompt = `You are a helpful assistant that answers questions based on the provided context.
 
CONTEXT:
${context || 'No relevant documents found.'}
 
INSTRUCTIONS:
- Answer the user's question based on the context above
- If the context doesn't contain relevant information, say so
- Be concise but thorough
- Cite which document(s) you're referencing when applicable`;
 
    // Step 4: Generate response with GPT-4
    const completion = await openai.chat.completions.create({
      model: 'gpt-4-turbo-preview',
      messages: [
        { role: 'system', content: systemPrompt },
        ...conversationHistory,
        { role: 'user', content: message },
      ],
      temperature: 0.7,
      max_tokens: 1000,
    });
 
    const assistantMessage = completion.choices[0].message.content;
 
    return NextResponse.json({
      response: assistantMessage,
      sources: relevantDocs.map((doc) => ({
        id: doc.id,
        preview: doc.content.slice(0, 200) + '...',
        similarity: doc.similarity,
      })),
    });
  } catch (error) {
    console.error('Chat error:', error);
    return NextResponse.json(
      { error: 'Failed to process chat' },
      { status: 500 }
    );
  }
}

اختبار التنفيذ

شغّل خادم التطوير:

npm run dev

افتح http://localhost:3000 وجرب طرح أسئلة مثل:

  • "ما هو RAG؟"
  • "كيف تعمل تضمينات المتجهات؟"
  • "لماذا يُستخدم pgvector؟"

يجب أن يستجيب روبوت المحادثة بمعلومات دقيقة بناءً على المستندات المضافة، موضحاً المصادر التي استخدمها.

الخلاصة

لقد بنيت روبوت محادثة مدعوماً بـ RAG يمكنه الإجابة على الأسئلة باستخدام بياناتك الخاصة. يوفر الجمع بين امتداد pgvector من Supabase وواجهات برمجة تطبيقات OpenAI أساساً قوياً وقابلاً للتوسع لتطبيقات الذكاء الاصطناعي.

النقاط الرئيسية:

  • تضمينات المتجهات تمكّن البحث الدلالي بما يتجاوز مطابقة الكلمات المفتاحية
  • pgvector يجلب إمكانيات البحث المتجهي إلى PostgreSQL
  • RAG يرسخ استجابات LLM في بياناتك الفعلية
  • Supabase يوفر خلفية كاملة بأقل إعداد

تتوسع هذه البنية بشكل جيد - أضف المزيد من المستندات، وحسّن استراتيجيات التقسيم، وضبط معلمات الاسترجاع مع نمو قاعدة معارفك.


الموارد:


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على دليل تكامل Twilio.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة