RAG چیست و چطور با API هوش مصنوعی بسازیم؟
۱ آبان ۱۴۰۵ · نویسنده: کایا ای آی

RAG مخفف Retrieval-Augmented Generation است؛ یعنی «تولید پاسخ با کمک بازیابی اطلاعات». ایده ساده است: بهجای اینکه انتظار داشته باشیم مدل زبانی همهچیز را از حفظ بداند، اول اطلاعات مرتبط را از منبع خودمان پیدا میکنیم، بعد آن را در کنار پرسش کاربر به مدل میدهیم و از مدل میخواهیم فقط بر اساس همان اطلاعات پاسخ بدهد. نتیجه، پاسخی است که به دادههای واقعی شما متصل است، نه به حافظهی مبهم مدل.
کایا ای آی درگاه API هوش مصنوعی است و خودش مدل آموزش نمیدهد؛ اما هر دو قطعهی لازم برای ساخت RAG را در یک API سازگار با OpenAI در اختیار شما میگذارد: اندپوینت امبدینگ برای ساختن بردار معنایی و اندپوینت chat completions برای تولید پاسخ. کافی است base_url را روی https://kayaai.ir/api/v1 بگذارید.
RAG چیست؟ با یک مثال ملموس
فرض کنید یک شرکت ایرانی پشتیبانی مشتریان دارد و میخواهد یک دستیار بسازد که به سؤال «شرایط بازگشت کالای خرابیده چیست؟» جواب درست بدهد. این پاسخ در یک فایل PDF آییننامهی داخلی نوشته شده و هیچ مدل عمومیای آن را ندیده است.
دو مسیر داریم. مسیر اول این است که مدل را با آن سند آموزش بدهیم (فاینتیونینگ). مسیر دوم این است که سند را قطعهقطعه کنیم، برای هر قطعه یک بردار معنایی بسازیم، این بردارها را در یک وکتوراستور ذخیره کنیم و هنگام پرسش، نزدیکترین قطعهها را پیدا کرده و به مدل بدهیم. مسیر دوم همان RAG است.
در RAG مدل هیچچیز جدیدی «یاد نمیگیرد». مدل همان مدل قبلی است؛ فقط ورودیاش هوشمندانهتر انتخاب میشود. مزیت بزرگ این است که بهروزرسانی دانش، فقط با افزودن یا حذف قطعهها انجام میشود و نیازی به آموزش مجدد نیست.
RAG یا فاینتیونینگ؟ کدام را انتخاب کنیم
این دو رقیب هم نیستند؛ ابزارهای متفاوتی برای مسائل متفاوتاند.
| معیار | RAG | فاینتیونینگ |
|---|---|---|
| هدف اصلی | دسترسی به دانش تازه و اختصاصی | تغییر لحن، قالب و سبک رفتار مدل |
| بهروزرسانی دانش | فقط جایگزینی قطعهها در وکتوراستور | نیاز به دیتاست جدید و آموزش مجدد |
| شفافیت منبع | بالا؛ میتوانید بگویید پاسخ از کدام قطعه آمده | پایین؛ منبع پاسخ قابل ردیابی نیست |
| هزینهی نگهداری | عمدتاً هزینهی امبدینگ و توکن ورودی | هزینهی آموزش و نگهداری نسخهها |
قاعدهی سرانگشتی: اگر مسئله «مدل نمیداند» است، RAG. اگر مسئله «مدل میداند ولی بد جواب میدهد» است، فاینتیونینگ. ترکیب هر دو هم ممکن است، اما برای شروع، RAG سریعتر به نتیجه میرسد و برگشتپذیرتر است.
معماری یک سیستم RAG
هر سیستم RAG، ساده یا پیچیده، از چهار مرحلهی ثابت تشکیل میشود.
۱) قطعهبندی (Chunking)
سند را به قطعههای کوچک تقسیم میکنیم؛ چون هم مدل محدودیت طول ورودی دارد و هم بازیابی دقیقتر میشود. دو استراتژی رایج:
- قطعهبندی ثابت: هر N کاراکتر یا توکن، با مقداری همپوشانی (overlap) بین قطعههای متوالی.
- قطعهبندی ساختاری: تقسیم بر اساس سرتیتر، پاراگراف یا بندهای سند، تا هر قطعه یک واحد معنایی کامل باشد.
۲) تولید امبدینگ
برای هر قطعه، مدل امبدینگ را صدا میزنیم و یک بردار عددی میگیریم. متنهایی که معنای نزدیک دارند، بردارهای نزدیک دارند. این بردارها را ذخیره میکنیم تا در زمان پرسش، جستوجو ارزان و سریع باشد.
۳) ذخیره در وکتوراستور
وکتوراستور میتواند یک آرایهی NumPy در حافظه باشد (برای شروع و برای چند هزار قطعه کافی است) یا یک پایگاهدادهی برداری اختصاصی. نکتهی مهم این است که برای هر قطعه، هم بردار و هم متن اصلی و هم فراداده (نام فایل، شماره صفحه) را نگه دارید.
۴) بازیابی و تزریق در prompt
پرسش کاربر را هم امبدینگ میکنیم، نزدیکترین قطعهها را بر اساس شباهت کسینوسی برمیگردانیم و آنها را داخل prompt قرار میدهیم. یک الگوی ساده:
1به کمک «زمینه» زیر پاسخ بده. اگر پاسخ در زمینه نبود، صریحاً بگو «در اسناد موجود نیست».
2هیچ اطلاعاتی از خودت اضافه نکن.
3
4زمینه:
5{context}
6
7پرسش: {question}پیادهسازی گامبهگام با API کایا ای آی
نصب و پیکربندی
pip install openai numpy pypdf
export KAYA_API_KEY="کلید-شما"کلید را از پنل حساب خود بردارید. ثبتنام با شماره موبایل و رمز یکبارمصرف انجام میشود و هنگام ثبتنام ۱۰۰ اعتبار رایگان میگیرید. چون API سازگار با OpenAI است، از همان SDK رسمی استفاده میکنیم و فقط base_url را عوض میکنیم.
1import os
2from openai import OpenAI
3
4client = OpenAI(
5 base_url="https://kayaai.ir/api/v1",
6 api_key=os.environ["KAYA_API_KEY"],
7)
8
9# شناسهی دقیق مدلها را در https://kayaai.ir/models ببینید
10EMBED_MODEL = "text-embedding-3-small"
11CHAT_MODEL = "gpt-4o-mini"انتخاب مدل را جدی بگیرید: برای امبدینگ تقریباً همیشه یک مدل کوچک و ارزان کافی است، اما برای تولید پاسخ بسته به پیچیدگی سؤال ممکن است به مدل قویتری نیاز داشته باشید. فهرست کامل مدلهای در دسترس در /models آمده است.
قطعهبندی و امبدینگ
1import numpy as np
2
3def chunk_text(text: str, size: int = 700, overlap: int = 120) -> list[str]:
4 text = " ".join(text.split())
5 chunks, start = [], 0
6 while start < len(text):
7 end = start + size
8 chunks.append(text[start:end])
9 if end >= len(text):
10 break
11 start = end - overlap
12 return [c for c in chunks if c.strip()]
13
14def embed(texts: list[str]) -> np.ndarray:
15 """بردارها را بهصورت دستهای (batch) میگیرد تا تعداد درخواستها کم شود."""
16 resp = client.embeddings.create(model=EMBED_MODEL, input=texts)
17 return np.array([d.embedding for d in resp.data], dtype=np.float32)وکتوراستور ساده و جستوجو
1class VectorStore:
2 def __init__(self):
3 self.chunks: list[str] = []
4 self.matrix: np.ndarray | None = None
5
6 def add(self, chunks: list[str], vectors: np.ndarray) -> None:
7 self.chunks.extend(chunks)
8 self.matrix = vectors if self.matrix is None else np.vstack([self.matrix, vectors])
9
10 def search(self, query: str, k: int = 4):
11 q = embed([query])[0]
12 q = q / (np.linalg.norm(q) + 1e-10)
13 m = self.matrix / (np.linalg.norm(self.matrix, axis=1, keepdims=True) + 1e-10)
14 scores = m @ q
15 idx = np.argsort(-scores)[:k]
16 return [(self.chunks[i], float(scores[i])) for i in idx]ساخت پاسخ با اندپوینت چت
1SYSTEM = (
2 "تو یک دستیار پاسخگو هستی. فقط بر اساس «زمینه» پاسخ بده. "
3 "اگر پاسخ در زمینه نبود، بگو در اسناد موجود نیست. "
4 "در پایان پاسخ، شمارهی قطعههای استفادهشده را ذکر کن."
5)
6
7def ask(store: VectorStore, question: str, k: int = 4) -> str:
8 hits = store.search(question, k=k)
9 context = "\n\n---\n\n".join(f"[قطعه {i+1}] {text}" for i, (text, _) in enumerate(hits))
10
11 resp = client.chat.completions.create(
12 model=CHAT_MODEL,
13 temperature=0.2,
14 messages=[
15 {"role": "system", "content": SYSTEM},
16 {"role": "user", "content": f"زمینه:\n{context}\n\nپرسش: {question}"},
17 ],
18 )
19 return resp.choices[0].message.contentاگر پاسخها طولانیاند و میخواهید تجربهی کاربر سریعتر شود، همان درخواست را با stream=True بفرستید و توکنها را تدریجی نمایش دهید. جزئیات پارامترها در /docs/api آمده است.
نکات عملی برای متن فارسی
متن فارسی چند ویژگی دارد که اگر نادیده گرفته شوند، کیفیت بازیابی بهسرعت افت میکند.
- نرمالسازی اجباری است. «ي» عربی و «ی» فارسی، «ك» عربی و «ک» فارسی، اعداد عربی و فارسی، و نیمفاصله باعث میشوند دو رشتهی ظاهراً یکسان، دو بردار متفاوت بگیرند. همان تابع نرمالسازی را هم روی سند و هم روی پرسش کاربر اعمال کنید.
- همپوشانی را زیاد کنید. در فارسی، جملهها به هم وابستهاند و بریدن وسط جمله معنای قطعه را ناقص میکند. در chunking، همپوشانی ۱۰۰ تا ۱۵۰ کاراکتر منطقی است.
- قطعهبندی ساختاری را ترجیح دهید. اگر سند بند و سرتیتر دارد، هر بند را یک قطعه بگیرید؛ نتیجه از بریدن کورکورانهی متن بهتر است.
1import re
2
3AR_TO_FA = {"ي": "ی", "ك": "ک", "ة": "ه", "ۀ": "ه", "ؤ": "و", "إ": "ا", "أ": "ا"}
4
5def normalize_fa(text: str) -> str:
6 for a, p in AR_TO_FA.items():
7 text = text.replace(a, p)
8 text = re.sub(r"[\u064B-\u0652]", "", text) # حذف اعراب
9 text = text.replace("\u200c", "") # حذف نیمفاصله (یکدستسازی)
10 text = re.sub(r"[ـ]{2,}", "", text) # حذف کشیده
11 text = re.sub(r"\s+", " ", text)
12 return text.strip()نکتهی مهم: وقتی نیمفاصله را حذف میکنید، همین کار را روی پرسش کاربر هم انجام دهید؛ در غیر این صورت «میرود» و «میرود» دو چیز متفاوت محسوب میشوند.
مثال کامل: پرسشوپاسخ روی یک PDF
1from pypdf import PdfReader
2
3def load_pdf(path: str) -> str:
4 reader = PdfReader(path)
5 return "\n".join((page.extract_text() or "") for page in reader.pages)
6
7# ۱) بارگذاری و نرمالسازی
8raw = normalize_fa(load_pdf("employee_handbook.pdf"))
9
10# ۲) قطعهبندی
11chunks = chunk_text(raw, size=700, overlap=120)
12print(f"تعداد قطعهها: {len(chunks)}")
13
14# ۳) امبدینگ (بهصورت دستهای)
15store = VectorStore()
16BATCH = 64
17for i in range(0, len(chunks), BATCH):
18 batch = chunks[i:i + BATCH]
19 store.add(batch, embed(batch))
20
21# ۴) پرسشوپاسخ
22q = normalize_fa("مرخصی استحقاقی سالانه چند روز است؟")
23print(ask(store, q))این اسکریپت را میتوانید در چند دقیقه روی یک فایل واقعی اجرا کنید. برای سندهای بزرگتر، مرحلهی امبدینگ را یکبار انجام دهید و بردارها را روی دیسک ذخیره کنید تا هر بار هزینهی مجدد نپردازید.
خطاهای رایج در پیادهسازی RAG
- قطعههای خیلی بزرگ: بازیابی بیدقت میشود و هر درخواست، توکن ورودی زیادی مصرف میکند.
- قطعههای خیلی کوچک: معنا از هم میپاشد و مدل جملهی ناقص میگیرد.
- نادیدهگرفتن هزینهی امبدینگ: اگر با هر بار اجرای برنامه، کل سند را دوباره امبدینگ کنید، هزینهی بیدلیل میسازید. بردارها را کش کنید.
- نبود ارزیابی: بدون یک مجموعهی آزمون کوچک (۲۰ تا ۵۰ پرسش با پاسخ درست)، نمیفهمید تغییراتتان کیفیت را بهتر کرده یا بدتر.
- نرمالسازی ناهمگون: پرسش نرمالنشده در برابر سند نرمالشده، بازیابی را خراب میکند.
top_kبزرگ: دادن ده قطعه به مدل، هم پرهزینه است و هم احتمال پرتشدن حواس مدل را بالا میبرد. با ۳ تا ۵ شروع کنید.- نبود ارجاع به منبع: اگر مدل نگوید پاسخ از کجا آمده، کاربر نمیتواند صحتش را بسنجد.
هزینه و بهینهسازی
مدل مصرف در کایا ای آی Pay-as-you-go است؛ یعنی فقط بهازای توکن مصرفی واقعی هزینه میپردازید، بههمراه ۲٪ کارمزد سرویس. پرداخت ریالی و از طریق درگاه زیبال انجام میشود؛ نیازی به کارت ارزی یا حساب خارجی ندارید. نرخهای دقیق را در /pricing ببینید.
برای کاهش هزینهی RAG، این کارها بیشترین اثر را دارند:
- امبدینگ را یکبار بسازید. هزینهی امبدینگ در ساخت ایندکس است، نه در هر پرسش. پرسش کاربر فقط یک امبدینگ کوتاه مصرف میکند.
- مدل امبدینگ کوچک انتخاب کنید. برای جستوجوی متنی، تفاوت مدلهای بزرگ و کوچک امبدینگ اغلب کمتر از تفاوت هزینهشان است.
- طول زمینه را کنترل کنید. بهجای پر کردن پنجرهی زمینه، ۳ تا ۵ قطعهی مرتبط بفرستید.
- مدل پاسخ را با نیاز تطبیق دهید. برای پرسشهای ساده روی اسناد کوتاه، مدل کوچکتر کافی است؛ مدل بزرگ را برای پرسشهای استدلالی نگه دارید.
- خروجی ساختاریافته بگیرید. اگر پاسخ را در قالب JSON با فیلدهای مشخص میخواهید، با output ساختاریافته درخواست کنید تا نیاز به پسپردازش و درخواست دوباره نداشته باشید.
برای شروع، یک سند واقعی از کسبوکار خودتان را بردارید، با /auth ثبتنام کنید، اعتبار رایگان اولیه را مصرف کنید و یک خط لولهی ساده مثل کدهای بالا بسازید. بعد از اینکه پاسخهای درست گرفتید، سراغ بهبودهایی مثل قطعهبندی ساختاری، بازیابی ترکیبی (برداری + کلیدواژه) و ارزیابی سیستماتیک بروید. نمونههای بیشتر در /blog منتشر میشود.