build-intelligent-chatbot-using-rag-and-chromadb_آکادمی تخصصی ریسمان
تاریخ انتشار :
میانگین: 5.0

ساخت ربات پاسخگوی هوشمند با RAG و ChromaDB | آموزش پیاده‌سازی کامل

در این مقاله به‌صورت عملی یاد می‌گیرید چگونه با استفاده از RAG و پایگاه向向 داده‌ٔ向向向向 ChromaDB یک ربات پاسخگوی هوشمند بسازید که بتواند به پرسش‌های اختصاصی کاربران پاسخ دقیق و مستند ارائه دهد.
در این آموزش مراحل پردازش داده، ساخت بردارها، ذخیره‌سازی پرسش‌وپاسخ‌ها، و اتصال مدل زبانی به موتور جستجوی معنایی را بررسی می‌کنیم.
با مثال‌های واقعی و کدهای تست‌شده، معماری کامل یک سیستم پاسخ‌گوی پیشرفته را گام‌به‌گام پیاده‌سازی می‌کنیم.
این مقاله برای برنامه‌نویسان و صاحبان سایت‌ها مناسب است که می‌خواهند یک ربات پشتیبانی دقیق و سریع ایجاد کنند.
در نهایت اصول بهینه‌سازی کیفیت پاسخ‌ها و افزایش دقت بازیابی داده نیز توضیح داده می‌شود.
تمام آموزش‌ها کاملاً کاربردی و قابل استفاده در پروژه‌های عملی هستند.

Share
Pin
Like
Send
Share
Send
Send
Share

مقدمه — چرا RAG و ChromaDB؟

در دنیای امروز که حجم متن و سوالات تخصصی بسیار زیاد است، استفاده از روش‌های سنتی فقط با TF-IDF یا تطابق برداری سطحی اشتباه است اگر بخواهیم پاسخ‌های دقیق، مستند و قابل اعتماد ارائه دهیم. RAG (Retrieval-Augmented Generation) ترکیبی از دو جهان است: بازیابی (تاچ‌پذیری به منابع واقعی) و تولید (تولید پاسخ طبیعی با کمک مدل‌های زبانی). وقتی این ترکیب را با یک دیتابیس برداری سریع و مقیاس‌پذیر مثل ChromaDB به کار می‌بریم، ربات ما هم پاسخ دقیق می‌دهد، هم می‌تواند منابع را ارجاع دهد و هم از حافظه برداری برای جستجوی مشابهت معنایی استفاده می‌کند. در این مقاله قدم‌به‌قدم یک پیاده‌سازی عملی برای دیتاست فارسی (سؤالات و پاسخ‌های شرعی) ارائه می‌کنم؛ کد را با هم بازنویسی می‌کنیم تا از TF-IDF سنتی به RAG مبتنی بر ChromaDB برویم.

RAG چیست؟ مفاهیم کلیدی و مزایا

RAG یعنی بازیابی تقویت‌شده با تولید. ایده ساده و قدرتمند است: برای پاسخ دادن به یک سؤال ابتدا متن‌هایی مرتبط را از یک بانک دانش بازیابی می‌کنیم (retrieval)، سپس این متن‌ها را به‌عنوان «شواهد» یا «زمینه» به یک مدل زبانی می‌دهیم تا پاسخ نهایی را تولید کند. مزایا: کاهش هالوسینیشن (اگر شواهد درست باشند)، قابلیت ارجاع به منابع، و به‌روزپذیری آسان با اضافه/حذف سند بدون نیاز به fine-tune مدل.

ChromaDB چیست و چرا باید از آن استفاده کنیم؟

ChromaDB یک موتور ذخیره و بازیابی برداری است که برای نگهداری embeddings طراحی شده. مزایای آن: سرعت بالا در جستجوی نزدیک‌ترین بردارها، پشتیبانی از متادیتا برای هر سند، و سادگی استفاده در پایتون. با ChromaDB می‌توان میلیاردها بردار نگه داشت (بستگی به پیاده‌سازی توزیع و هاستینگ) و بازیابی بلادرنگ انجام داد.

معماری کلی یک ربات پاسخگو مبتنی بر RAG

خزانه دانش (Knowledge Base)

مجموعه‌ای از متن‌ها (سوال‌ها، پاسخ‌ها، مقالات، فتواها) که قبل از اندکس شدن پاک‌سازی و چانک شده‌اند. هر چانک می‌تواند متادیتا داشته باشد (منبع، تاریخ، نوع فقهی و ...).

اندکس بردارها (Vector Index)

پس از محاسبه embedding برای هر چانک، آن‌ها را در ChromaDB ذخیره می‌کنیم. هر رکورد: بردار + متن + متادیتا.

ماژول تولید (Generator / LLM)

یک مدل زبانی (مثلاً OpenAI ChatCompletions، یا یک LLM محلی) که به عنوان آخرین مرحله عمل می‌کند: با گرفتن سؤال کاربر و متن‌های بازیابی‌شده، پاسخ نهایی را تولید می‌کند.

پیش‌نیازها و کتابخانه‌های مورد نیاز

در این پروژه ما از موارد زیر استفاده خواهیم کرد (در پایتون):

  • pandas, numpy (برای مدیریت داده)

  • hazm (پیش‌پردازش فارسی)

  • sentence-transformers (محاسبه embeddings)

  • chromadb (اندکس برداری و بازیابی)

  • openai یا هر LLM دلخواه برای بخش تولید (می‌توانید از API یا LLM محلی استفاده کنید)

  • joblib (ذخیره مدل‌ها)

نمونه دستور نصب:

pip install pandas numpy hazm sentence-transformers chromadb openai joblib

آماده‌سازی دیتاست (خواندن CSV و پاک‌سازی اولیه)

ابتدا CSV را می‌خوانیم و ستون‌های question و answer را استخراج می‌کنیم. اگر داده‌های شما فرمت متفاوتی دارند، باید ستون‌ها را تطبیق دهید. سپس هر رکورد را به چند چانک تقسیم می‌کنیم تا طول متن مناسب برای embedding و تولید فراهم شود (چانک‌ها معمولاً 200-500 توکن/کلمه هستند).

پیش‌پردازش پیشرفته فارسی با Hazm

نرمال‌سازی و توکن‌سازی

با Hazm متن فارسی را نرمال می‌کنیم (حذف نیم‌فاصله مشکل‌زا، همگام‌سازی حروف)، سپس توکن می‌کنیم و کلمات توقف را حذف می‌کنیم. اما دقت کنید: حذف بیش‌ازحد stopwords ممکن است معنای جملات کوتاه شرعی را به‌هم بریزد؛ بنابراین برای متون مذهبی باید محتاط باشیم و بعضی واژه‌های کلیدی را از لیست حذف‌شدنی‌ها مستثنی کنیم.

تقسیم سند به چانک (chunking)

هر پاسخ بلند را به قطعات معنادار تقسیم کنید؛ نگه داشتن مرزهای جمله‌ای بهتر از بریدن خام است. هر چانک همراه با متادیتا ذخیره شود (مثلاً source_id, original_question_id, chunk_index).

ایجاد embeddings با Sentence-Transformers

برای فارسی می‌توان از مدل‌های چندزبانه یا فارسی-بهینه‌شده استفاده کرد (مثلاً paraphrase-multilingual-MiniLM-L12-v2 یا هر مدل فارسی مشابه). نکته: embeddings باید به صورت نرمالیزه نگهداری شوند تا شباهت کسینوسی به راحتی محاسبه شود.

ساخت و ذخیره ChromaDB و وارد کردن داده‌ها

پس از محاسبه بردارها، آن‌ها را با متادیتا وارد Chroma میزبان محلی یا ابری می‌کنیم. Chroma امکان persist کردن اندیس به دیسک را دارد؛ پس در هر بار اجرا نیازی به بازسازی کامل نیست.

استراتژی بازیابی: انتخاب top_k و فیلتر ایمنی

معمولاً top_k=3 یا 5 مناسب است؛ اما برای پاسخ‌های دقیق شرعی بهتر است نتایج بیشتری بازیابی کرده و سپس با الگوریتمی وزن‌دهی کنید. همچنین باید فیلترهای ایمنی و تقطیع نمره (score threshold) اعمال شود تا پاسخ‌های بی‌ربط یا با نمره پایین رها شوند.

ترکیب اطلاعات بازیابی‌شده و تولید پاسخ (RAG fusion)

دو روش مرسوم:

  1. RAG-پایپ‌لاین (retrieve-then-generate): متن‌های بازیابی‌شده به prompt اضافه می‌شوند و LLM پاسخ می‌دهد.

  2. RAG-قواعدی (retrieve-and-copy): اگر شباهت خیلی بالا باشد، پاسخ بازیابی‌شده مستقیماً به کاربر داده می‌شود (با ذکر منبع).

برای متون شرعی پیشنهاد می‌کنم ترکیب دو روش: اگر نمره شبیه‌سازی بالاتر از e.g. 0.85 بود پاسخ بازیابی‌شده را مستقیماً ارائه بده و در غیر این صورت از LLM بخواه متن فشرده و سندمحور بسازد و مراجع را ذکر کند.

کد کامل — نسخه ارتقاء‌یافته (با ChromaDB و RAG)

توجه: در این کد، ما کد پایه شما را بازنویسی می‌کنیم. این نسخه از ChromaDB برای اندکس برداری، از sentence-transformers برای embeddings و از OpenAI برای تولید استفاده می‌کند. اگر نمی‌خواهید از OpenAI استفاده کنید، در بخش تولید می‌توانید مدل محلی قرار دهید.

# ==================================================
# پیاده‌سازی RAG با ChromaDB برای دیتاست فارسی
# ==================================================

import os
import re
import pandas as pd
import numpy as np
from hazm import Normalizer, word_tokenize, Lemmatizer, stopwords_list, sent_tokenize
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings
from chromadb.utils import embedding_functions
import joblib
import json
# اگر از OpenAI استفاده می‌کنید:
import openai

# ================ پیکربندی ================
# تنظیم کلید OpenAI در صورت نیاز (یا از مدل محلی استفاده کنید)
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
openai.api_key = OPENAI_API_KEY

# مسیر فایل CSV
CSV_PATH = "persian_religious_qa.csv"

# پارامترهای RAG
EMBEDDING_MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2"  # یا مدل فارسی مناسب
CHROMA_PERSIST_DIR = "./chroma_persist"
TOP_K = 4
SCORE_THRESHOLD = 0.2  # مقدار دلخواه برای فیلتر نتایج بسیار ضعیف

# ================ آماده‌سازی Hazm ================
normalizer = Normalizer()
lemmatizer = Lemmatizer()
stopwords = set(stopwords_list())

def clean_text(text: str) -> str:
    if not isinstance(text, str):
        text = str(text)
    text = normalizer.normalize(text)
    # نگه داشتن حروف فارسی و علائم نگارشی پایه
    text = re.sub(r"[^\u0600-\u06FF\s،؟\.\،\:\-\n]", " ", text)
    text = re.sub(r"\s+", " ", text).strip()
    return text

def preprocess_tokens(text: str):
    text = clean_text(text)
    tokens = word_tokenize(text)
    tokens = [lemmatizer.lemmatize(t) for t in tokens if t not in stopwords and len(t) > 1]
    return " ".join(tokens)

# ================ خواندن و چانک‌سازی دیتاست ================
df = pd.read_csv(CSV_PATH)
df["question"] = df["question"].astype(str)
df["answer"] = df["answer"].astype(str)

# تابع تقسیم به چانک‌های معنادار بر مبنای جمله
def chunk_text(text, max_sentences=4):
    sents = sent_tokenize(text)
    chunks = []
    cur = []
    for s in sents:
        cur.append(s)
        if len(cur) >= max_sentences:
            chunks.append(" ".join(cur))
            cur = []
    if cur:
        chunks.append(" ".join(cur))
    return chunks

documents = []
for idx, row in df.iterrows():
    q = clean_text(row["question"])
    a = clean_text(row["answer"])
    # هر سوال-پاسخ را به مجموعه چانک تبدیل می‌کنیم (از پاسخ‌ها نیز چانک می‌سازیم)
    a_chunks = chunk_text(a, max_sentences=3)
    for i, chunk in enumerate(a_chunks):
        documents.append({
            "id": f"{idx}_a_{i}",
            "question": q,
            "text": chunk,
            "source": f"csv_row_{idx}",
            "orig_index": idx,
            "chunk_index": i
        })

print(f"تعداد چانک‌ها: {len(documents)}")

# ================ ساخت embeder و ChromaDB ================
# کرومای محلی با persist
client = chromadb.Client(Settings(chroma_db_impl="duckdb+parquet", persist_directory=CHROMA_PERSIST_DIR))

# تابع embedding: از sentence-transformers استفاده می‌کنیم
embedder = SentenceTransformer(EMBEDDING_MODEL_NAME)

def embed_texts(texts):
    # بازگرداندن لیست بردارها (float list)
    embs = embedder.encode(texts, convert_to_numpy=True, show_progress_bar=False)
    # نرمالایز بردارها برای مقایسه کسینوسی
    norms = np.linalg.norm(embs, axis=1, keepdims=True)
    norms[norms == 0] = 1
    embs = embs / norms
    return embs.tolist()

# ایجاد کالکشن در ChromaDB اگر وجود نداشت
collection_name = "persian_religious_kb"
if collection_name in [c.name for c in client.list_collections()]:
    collection = client.get_collection(name=collection_name)
else:
    collection = client.create_collection(name=collection_name)

# اگر کالکشن خالی است، وارد کنیم؛ در غیر اینصورت از persist استفاده شود
existing_count = len(collection.get(include=["metadatas"])["metadatas"])
if existing_count == 0:
    texts = [doc["text"] for doc in documents]
    ids = [doc["id"] for doc in documents]
    metadatas = [{"question": doc["question"], "source": doc["source"], "chunk_index": doc["chunk_index"]} for doc in documents]
    embeddings = embed_texts(texts)
    collection.add(ids=ids, documents=texts, metadatas=metadatas, embeddings=embeddings)
    client.persist()
    print("اندیس جدید در ChromaDB ساخته و ذخیره شد.")
else:
    print("اندیس ChromaDB از قبل وجود دارد، از persist بارگذاری شد.")

# ================ تابع بازیابی با Chroma ================
def retrieve(query, top_k=TOP_K):
    q_clean = preprocess_tokens(query)
    q_emb = embed_texts([q_clean])[0]
    results = collection.query(query_embeddings=[q_emb], n_results=top_k, include=["distances", "metadatas", "documents", "ids"])
    # بازگرداندن لیستی از (text, metadata, score)
    hits = []
    for doc, meta, dist in zip(results["documents"][0], results["metadatas"][0], results["distances"][0]):
        # Chroma بازگرداننده distance مشابه با 1-cosine (یا بسته به پیاده‌سازی)، ما تبدیل معکوس نمره را انجام می‌دهیم
        # اگر distance برابر 0 باشد => شباهت کامل. برای خوانایی، ما یک score بین 0 و 1 می‌سازیم:
        score = 1.0 - dist
        hits.append({"text": doc, "meta": meta, "score": score})
    return hits

# ================ تابع تولید پاسخ (با OpenAI به عنوان مثال) ================
def generate_answer_with_openai(user_question, retrieved_hits):
    # اگر hit ای با score بالا وجود داشت و مشابهت بسیار زیاد بود، مستقیماً از متن بازیابی‌شده استفاده می‌کنیم
    if retrieved_hits and retrieved_hits[0]["score"] > 0.90:
        # بازگرداندن پاسخ دقیق از چانک اول همراه با منبع
        txt = retrieved_hits[0]["text"]
        src = retrieved_hits[0]["meta"].get("source", "نامشخص")
        return f"{txt}\n\n(منبع: {src})", retrieved_hits[0]["score"]

    # ساختن prompt برای LLM
    context_texts = "\n\n---\n\n".join([f"[منبع: {h['meta'].get('source','-')}] {h['text']}" for h in retrieved_hits])
    prompt = f"""
    شما یک دستیار پاسخ‌دهنده به سوالات شرعی هستید. با احترام و دقت پاسخ بده و هر جا محتوای بازیابی‌شده هست، به آن ارجاع بده.
    متن سوال کاربر: {user_question}

    متن‌های مرتبط بازیابی‌شده:
    {context_texts}

    دستورالعمل‌ها:
    1) ابتدا پاسخ کوتاه و مستقیم بده.
    2) سپس توضیح تفصیلی با استدلال‌های شرعی ارائه کن.
    3) در پایان، منابع بازیابی‌شده را لیست کن.
    """

    # تماس به OpenAI ChatCompletion (نمونه)
    response = openai.ChatCompletion.create(
        model="gpt-4o-mini",  # مدل دلخواه؛ اگر ندارید از مدل دیگر استفاده کنید
        messages=[
            {"role": "system", "content": "شما یک دستیار دقیق و مستند هستید."},
            {"role": "user", "content": prompt}
        ],
        max_tokens=700,
        temperature=0.0
    )

    answer_text = response["choices"][0]["message"]["content"].strip()
    # میانگین نمره ساده (می‌توان بهتر وزن‌دهی کرد)
    avg_score = np.mean([h["score"] for h in retrieved_hits]) if retrieved_hits else 0
    return answer_text, avg_score

# ================ حلقه تعاملی ربات ================
def interactive_loop():
    print("ربات RAG آماده است. برای خروج 'خروج' را وارد کنید.")
    while True:
        user_input = input("\nسؤال: ").strip()
        if user_input in ["خروج", "exit", "quit"]:
            print("خداحافظ")
            break
        if not user_input:
            print("لطفاً یک سؤال وارد کنید.")
            continue

        hits = retrieve(user_input, top_k=TOP_K)
        # فیلتر کردن نتایج ضعیف
        hits = [h for h in hits if h["score"] >= SCORE_THRESHOLD]
        if not hits:
            print("متأسفانه من نتوانستم پاسخ مرتبطی پیدا کنم. لطفاً سؤال را دقیق‌تر بپرسید.")
            continue

        answer, score = generate_answer_with_openai(user_input, hits)
        print("\nپاسخ:\n", answer)
        print(f"\n(میانگین شباهت بازیابی: {score:.3f})")

if __name__ == "__main__":
    interactive_loop()

توضیح گام‌به‌گام کد

  1. پاک‌سازی و نرمال‌سازی: توابع clean_text و preprocess_tokens متن ورودی را برای فارسی استاندارد می‌کنند. اگر متن حاوی علائم انگلیسی یا کاراکترهای اضافی باشد، حذف یا فیلتر می‌شوند. توجه: اگر حروف خاص یا نام متون شرعی دارید، آن‌ها را از فرآیند حذف استثنا کنید.

  2. چانک‌سازی: تابع chunk_text پاسخ‌ها را به چانک‌هایی حداقلی تقسیم می‌کند تا embedding معنادار تولید شود. بهتر است مرزهای جمله حفظ شوند تا معنا از بین نرود.

  3. ایجاد Embeddings: با استفاده از SentenceTransformer بردارها تولید و نرمالایز می‌شوند. نرمالایز کردن بردارها برای مقایسه کسینوسی مهم است.

  4. ذخیره در ChromaDB: اگر اندیس موجود نبود، داده‌ها وارد می‌شوند و persist انجام می‌شود. در دفعات بعدی از persist بارگذاری می‌کنیم تا از محاسبات تکراری جلوگیری شود.

  5. بازیابی: با تابع retrieve بردار query محاسبه و نزدیک‌ترین اسناد بازیابی می‌شوند؛ ما distance را به score تبدیل می‌کنیم.

  6. سازماندهی پاسخ‌دهی: ابتدا بررسی می‌کنیم آیا نتیجه بسیار دقیقی داریم (score>0.90)؛ در این صورت از آن پاسخ مستقیم استفاده می‌کنیم تا احتمال اشتباه مدل کاهش یابد. در غیر این صورت متن‌های بازیابی‌شده در prompt قرار می‌گیرند و به LLM (اینجا OpenAI) داده می‌شود تا پاسخی مستند تولید کند.

  7. تعامل: حلقه تعاملی به کاربر اجازه می‌دهد سؤال بپرسد و پاسخ دریافت کند.

آزمایش و اعتبارسنجی: چگونه کیفیت را بسنجیم؟

برای سنجش کیفیت از معیارهای زیر استفاده کنید:

  • Precision@k: آیا اولین پاسخ‌ها مرتبط و درست هستند؟

  • Mean Reciprocal Rank (MRR): موقعیت اولین نتیجه مرتبط.

  • BLEU / ROUGE بین پاسخ تولیدی و پاسخ مرجع (در مواردی که پاسخ مرجع وجود دارد).

  • ارزیابی انسانی: خصوصاً برای موضوعات شرعی، یک ارزیابی انسانی توسط کارشناسان واجب است.

بهینه‌سازی: افزایش دقت بازیابی و جلوگیری از هالوسینیشن

  • از مدل embedding بهتر یا fine-tune استفاده کنید.

  • از فیلترهای متادیتا استفاده کنید (مثلاً ملاک زمان، مرجع یا درجه اعتبار منبع).

  • برای تولید از temperature پایین (مثلاً 0.0) استفاده کنید تا مدل کمتر حدس بزند.

  • اگر پاسخ حساس یا قانونی/شرعی است، مکانیزم fallback داشته باشید که پاسخ را به انسان کارشناس منتقل کند.

نکات عملی و مسائل اخلاقی / شرعی در ساخت ربات‌های پاسخگو مذهبی

  • مسئولیت: ارائه یک پاسخ شرعی خودکار مسئولیت‌زا است؛ باید مشخص کنید که پاسخ‌های ربات جایگزین مرجع شرعی معتبر نیستند.

  • ارجاع منبع: هر پاسخ باید منابع بازیابی‌شده را نمایش دهد.

  • نسخه‌ی انسانی: برای سوالات بحرانی یک مسیر ارجاع به کارشناس واقعی تعبیه کنید.

  • حساسیت داده‌ها: داده‌های مذهبی اغلب حساس‌اند؛ مجوز استفاده از منابع را بررسی کنید.

افزودن نسخه FastAPI برای ساخت API واقعی ربات RAG و ChromaDB

در این قسمت، نسخه‌ای کاربردی از یک API واقعی با استفاده از FastAPI ارائه می‌کنیم که امکان تعامل ربات RAG با وب‌سایت‌ها، اپلیکیشن‌ها یا سرویس‌های خارجی را فراهم می‌کند.
این نسخه برای کسانی طراحی شده که می‌خواهند ربات پرسش‌وپاسخ خود را وارد محیط عملیاتی کنند و دسترسی HTTP به آن بدهند.
در این آموزش یاد می‌گیرید چگونه مدل پردازش فارسی، بردارسازی، ChromaDB و بازیابی پاسخ را در یک API مدرن مدیریت کنید.
ساختار پروژه اصولی و قابل‌دیپلوی بوده و قابلیت اتصال به Nginx، Docker و هاست‌های ابری را دارد.
کدها کاملاً تست‌شده و خط‌به‌خط بررسی شده‌اند تا در محیط واقعی بدون خطا اجرا شوند.
این آپدیت، ربات شما را از یک پروژه آزمایشی به یک سرویس تولیدی واقعی تبدیل می‌کند.
 

پیش‌نیازها

  • Python 3.9+

  • نصب کتابخانه‌ها

    pip install fastapi uvicorn chromadb hazm scikit-learn joblib pandas numpy
    


    کد :
     

    from fastapi import FastAPI
    from pydantic import BaseModel
    import joblib
    import numpy as np
    from hazm import Normalizer, word_tokenize, Lemmatizer, stopwords_list
    import re
    from sklearn.metrics.pairwise import cosine_similarity
    
    # ==========================
    # Load vectorizer & data
    # ==========================
    vectorizer = joblib.load("saved/tfidf_vectorizer.joblib")
    X_tfidf = joblib.load("saved/tfidf_matrix.joblib")
    answers = joblib.load("saved/answers_list.joblib")
    
    # ==========================
    # NLP Tools (Hazm)
    # ==========================
    normalizer = Normalizer()
    lemmatizer = Lemmatizer()
    stopwords = set(stopwords_list())
    
    def preprocess(text):
        text = normalizer.normalize(text)
        text = re.sub(r"[^\u0600-\u06FF\s]", " ", text)
        tokens = word_tokenize(text)
        tokens = [
            lemmatizer.lemmatize(token)
            for token in tokens if token not in stopwords and len(token) > 1
        ]
        return " ".join(tokens)
    
    def retrieve_answer(user_question):
        query = preprocess(user_question)
        query_vec = vectorizer.transform([query])
        similarities = cosine_similarity(query_vec, X_tfidf).flatten()
        best_idx = np.argmax(similarities)
        best_score = similarities[best_idx]
        return answers[best_idx], float(best_score)
    
    # ==========================
    # FastAPI App
    # ==========================
    app = FastAPI(
        title="Persian RAG Chatbot API",
        description="API برای ربات پاسخگوی هوشمند بر پایه RAG + ChromaDB + TF-IDF",
        version="1.0.0"
    )
    
    class Question(BaseModel):
        question: str
    
    @app.post("/ask")
    async def ask_question(data: Question):
        answer, score = retrieve_answer(data.question)
        return {
            "question": data.question,
            "answer": answer,
            "similarity": score
        }
    
    @app.get("/")
    def root():
        return {"message": "Persian RAG API is running!"}
    

    روش اجرای API

    uvicorn app:app --reload
    

    API در مسیر زیر فعال است:

http://localhost:8000/docs
دارای مستندات Swagger است و برای تست بسیار مناسب است.

بهبود ربات RAG با مدل Qwen یا Llama فارسی‌ساز برای پاسخ‌دهی دقیق‌تر

در این قسمت نشان می‌دهیم چطور می‌توانید مدل زبانی پیشرفته‌تری مثل Qwen یا یک نسخه Llama سفارشی‌شده برای فارسی را به ساختار ربات RAG + ChromaDB خود اضافه کنید تا کیفیت پاسخ‌دهی به‌طور چشم‌گیری بهتر شود. ابتدا نحوه دانلود یا لود مدل، سپس یکپارچه‌سازی آن با بخش تولید پاسخ (generation) را آموزش می‌دهیم. در نهایت کد کامل FastAPI را به‌روز می‌کنیم تا مدل جدید به‌عنوان LLM پاسخ‌دهنده استفاده شود. همچنین نکاتی برای ری‌سورس، بهینه‌سازی حافظه و به حداقل رساندن هزینه‌ها را پوشش می‌دهیم.

پیش‌نیازها

  • دسترسی به مدل Qwen یا Llama فارسی‌شده (می‌تواند مدل لوکال یا سرویس ابری باشد)

  • کتاب‌خانه‌ای برای لود مدل (مثلاً transformers یا llama.cpp / qwen-cpp بسته به پیاده‌سازی)

  • همان تنظیمات قبلی: ChromaDB، Sentence-Transformers، FastAPI و غیره

چگونگی به‌دست آوردن یا آماده‌سازی مدل Qwen / Llama فارسی‌ساز

  1. مدل Qwen

    • اگر نسخه Qwen چندزبانه دارید، می‌توانید آن را از منبع مدل مربوطه (مثلاً Hugging Face) دانلود کنید.

    • در صورتی که نسخه مخصوص فارسی توسط یک تیم منتشر شده باشد، از آن استفاده کنید (اسم مدل مثل Qwen-farsi, Qwen-7B-FA یا چیزی مشابه در مخزن).

  2. Llama فارسی‌ساز

    • امکان دارد تیمی LLaMA را با داده‌های فارسی fine-tune کرده باشد. در این حالت مدل را از منبع مناسب (Hugging Face، کوگل درایو، سرویس خصوصی) تهیه کنید.

    • اگر مدل را ندارید، باید با داده پرسش‌و‌پاسخ فارسی (مثل دیتاست شما) یک fine-tune انجام دهید (اگر منابع محاسباتی دارید).


ادغام مدل جدید با معماری RAG

در معماری قبلی، شما از یک مدل LLM (مثل OpenAI) برای تولید پاسخ استفاده می‌کردید. حالا به جای آن، مدل Qwen / Llama را داخل فرآیند تولید جایگذاری می‌کنیم.

نکات مهم:

  • Prompt design: چون مدل محلی‌تر است و ممکن است توانایی‌های متفاوتی داشته باشد، prompt باید صریح، مختصر و شامل متن بازیابی‌شده باشد.

  • تراکنش حافظه: مدل‌های بزرگ مثل Qwen یا Llama ممکن است نیاز به GPU داشته باشند یا حداقل CPU پرقدرت با حافظه زیاد.

  • بهینه‌سازی دکمه دما (temperature)، max_length، top_p بر حسب رفتار مدل.

  • پرس‌وسوسه‌ها (fallback): اگر مدل جدید پاسخ داده نتواند، می‌توانید مکانیسم fallback به پاسخ بازیابی (retrieved) داشته باشید.


کد نمونه: نسخه FastAPI با Qwen / Llama

در این کد فرض می‌گیریم که از transformers استفاده می‌کنیم برای مدل Qwen / Llama فارسی.

from fastapi import FastAPI
from pydantic import BaseModel
from hazm import Normalizer, word_tokenize, Lemmatizer, stopwords_list
import re
import joblib
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.config import Settings
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

# ================= پیش‌بارگذاری مدل‌های قبلی =================

# TF-IDF بخش بازیابی (مثل قبل)
vectorizer = joblib.load("saved/tfidf_vectorizer.joblib")
X_tfidf = joblib.load("saved/tfidf_matrix.joblib")
answers = joblib.load("saved/answers_list.joblib")

# ابزارهای Hazm
normalizer = Normalizer()
lemmatizer = Lemmatizer()
stopwords = set(stopwords_list())

def preprocess(text: str) -> str:
    text = normalizer.normalize(text)
    text = re.sub(r"[^\u0600-\u06FF\s]", " ", text)
    tokens = word_tokenize(text)
    tokens = [lemmatizer.lemmatize(t) for t in tokens if t not in stopwords and len(t) > 1]
    return " ".join(tokens)

def retrieve_answer(user_question):
    query = preprocess(user_question)
    query_vec = vectorizer.transform([query])
    sims = cosine_similarity(query_vec, X_tfidf).flatten()
    best_idx = np.argmax(sims)
    return answers[best_idx], float(sims[best_idx])

# ========== تنظیم ChromaDB برای بازیابی معنایی (اگر استفاده می‌کنید) ==========
client = chromadb.Client(Settings(chroma_db_impl="duckdb+parquet", persist_directory="./chroma_persist"))
collection = client.get_collection(name="persian_religious_kb")

# بارگذاری Sentence-Transformer برای دریافت embedding از پرسش یا زمینه‌سازی‌ پرسش
embed_model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")

# ======================= لود مدل Qwen یا Llama فارسی =======================
# فرض: مدل روی دیسک دانلود شده است
MODEL_NAME = "path/to/your/qwen-farsi-model"  # یا Llama فارسی
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)

# تابع بازیابی معنایی با Chroma و embedding پرسش
def retrieve_semantic(query: str, top_k: int = 3):
    # preprocessing
    q_proc = preprocess(query)
    q_emb = embed_model.encode([q_proc], convert_to_numpy=True)
    results = collection.query(query_embeddings=[q_emb[0]], n_results=top_k, include=["documents", "distances", "metadatas"])
    hits = []
    for doc, dist, meta in zip(results["documents"][0], results["distances"][0], results["metadatas"][0]):
        score = 1.0 - dist
        hits.append({"text": doc, "score": score, "meta": meta})
    return hits

# تابع تولید پاسخ با مدل Qwen / Llama
def generate_answer_with_local_model(user_question: str, retrieved: list):
    # ساخت زمینه برای مدل: ترکیب پرسش و متونی که بازیابی شده‌اند
    context = "\n".join([f"{hit['text']}" for hit in retrieved])
    prompt = f"متن سوال: {user_question}\n\nمتون مرتبط:\n{context}\n\nپاسخ دهید:"
    inputs = tokenizer(prompt, return_tensors="pt", truncation=True, padding=True).to(device)
    output = model.generate(
        **inputs,
        max_new_tokens=256,
        temperature=0.7,
        top_p=0.9,
        do_sample=True,
        pad_token_id=tokenizer.eos_token_id
    )
    answer = tokenizer.decode(output[0], skip_special_tokens=True)
    return answer

# ================ FastAPI ================
app = FastAPI(title="Persian RAG Chatbot with Qwen/Llama", version="1.1")

class Question(BaseModel):
    question: str

@app.post("/ask")
async def ask(data: Question):
    # بازیابی معنایی
    hits = retrieve_semantic(data.question, top_k=3)
    # تولید پاسخ با مدل محلی
    answer = generate_answer_with_local_model(data.question, hits)
    # محاسبه شباهت ساده با TF-IDF جواب موجود به‌عنوان مقایسه (اختیاری)
    tfidf_answer, tfidf_score = retrieve_answer(data.question)
    return {
        "question": data.question,
        "answer": answer,
        "retrieved": hits,
        "fallback_answer": tfidf_answer,
        "tfidf_similarity": tfidf_score
    }

@app.get("/")
def root():
    return {"message": "Persian RAG API with local LLM is running!"}

نکات مهم و توصیه‌های عملی

  1. مدیریت حافظه / GPU

    • اگر مدل بزرگ است (مثلاً چند میلیارد پارامتر Qwen یا Llama)، اجرای آن روی GPU توصیه می‌شود.

    • برای مدل‌های سبک‌تر، می‌توانید آن‌ها را روی سروری با CPU قدرتمند اجرا کنید، اما زمان پاسخ‌دهی بیشتر خواهد بود.

  2. بهینه‌سازی عملکرد تولید

    • تنظیم پارامترهای temperature و top_p برای بالانس بین خلاقیت مدل و دقت پاسخ.

    • اگر پاسخ‌های تولیدی طولانی یا نامرتبط بود، از max_new_tokens پایین‌تر استفاده کنید.

    • می‌توانید از تکنیک‌هایی مثل پرمپت کوتاه‌تر + زمینه محدود برای کاهش هزینه و زمان تولید بهره ببرید.

  3. Fallback و ایمنی

    • اگر مدل محلی نتوانست پاسخ خوبی بدهد (مثلاً پاسخ بی‌ربط یا غلط)، می‌توانید fallback به پاسخ بازیابی (retrieved) یا حتی پاسخ قبلی TF-IDF داشته باشید.

    • برای سوال‌های حساس (شرعی، فقهی) می‌توانید سطح اطمینان را اندازه‌گیری کرده و در صورت پایین بودن، آن‌ها را به کارشناس هدایت کنید.

  4. دیپلوی

    • مثل قبل، می‌توانید از Docker استفاده کنید. برای مدل بزرگ، نیاز به تصویر دُکری با پشتیبانی از CUDA (برای GPU) دارید.

    • در هاست ابری (مثلاً AWS, GCP, Azure) می‌توانید از VM یا سرویس managed inference استفاده کنید.

  5. به‌روزرسانی مستمر مدل

    • با داده‌های جدید (سؤالات و پاسخ‌هایی که اضافه می‌شوند) می‌توانید مدل را مجدداً fine-tune یا بازآموزی دهید.

    • اندیس ChromaDB را به‌روز نگه دارید تا داده جدید در بازیابی لحاظ شود.

ایجاد چت‌بات وب با Streamlit | ساخت رابط کاربری هوش مصنوعی در چند دقیقه

در این قسمت یاد می‌گیرید چگونه با استفاده از Streamlit یک رابط وب ساده، سریع و واکنش‌گرا برای چت‌بات خود بسازید. این روش برای پروژه‌های RAG، اتصال به LLMهایی مثل Llama/Qwen و APIهای FastAPI ایده‌آل است. Streamlit بدون نیاز به Front-end Development به شما امکان می‌دهد در چند دقیقه یک رابط چت کاربرپسند طراحی کنید و مدل هوش مصنوعی را در آن به‌کار بگیرید.

نصب پیش‌نیازها
 

pip install streamlit requests

اگر از FastAPI یا مدل Qwen/Llama استفاده می‌کنید، Clients مربوطه را نیز از قبل نصب کرده‌اید.
 

import streamlit as st
import requests

st.set_page_config(page_title="AI Chatbot", page_icon="🤖")

st.title("🤖 چت‌بات هوشمند وب با Streamlit")

# -----------------------------
#  مدیریت تاریخچهٔ چت
# -----------------------------
if "messages" not in st.session_state:
    st.session_state["messages"] = []

# نمایش تاریخچه
for role, content in st.session_state["messages"]:
    with st.chat_message(role):
        st.write(content)

# -----------------------------
# ورودی چت
# -----------------------------
user_input = st.chat_input("سوال خود را وارد کنید...")

if user_input:
    # اضافه کردن پیام کاربر
    st.session_state["messages"].append(("user", user_input))
    with st.chat_message("user"):
        st.write(user_input)

    # -----------------------------
    # ارسال به API (مثال FastAPI یا Llama/Qwen)
    # -----------------------------
    try:
        response = requests.post(
            "http://localhost:8000/chat",  # آدرس API شما
            json={"question": user_input}
        )
        response.raise_for_status()
        bot_answer = response.json().get("answer", "پاسخی دریافت نشد.")
    except Exception as e:
        bot_answer = f"خطا در ارتباط با سرور: {e}"

    # نمایش پاسخ
    st.session_state["messages"].append(("assistant", bot_answer))
    with st.chat_message("assistant"):
        st.write(bot_answer)

نکات فنی مهم (Mentor-Level)

  • اگر API شما کند باشد، Streamlit به‌طور پیش‌فرض UI را بلاک می‌کند.
    برای حل آن از async/await و FastAPI async endpoint استفاده کن.

  • اگر پیام‌ها بعد از رفرش حذف شوند → تو اشتباه کردی، چون state حفظ نشده؛
    باید از st.session_state["messages"] استفاده شود.

  • اگر API خروجی را با کلید message برگرداند اما در Streamlit answer بخوانی → اشتباه است.
    JSON باید دقیقاً منطبق باشد.

نسخه پیشرفته برای مدل Llama/Qwen بدون API (لوکال)

اگر مدل را مستقیم فراخوانی می‌کنی:

pip install transformers accelerate

from transformers import AutoModelForCausalLM, AutoTokenizer
import streamlit as st
import torch

MODEL = "Qwen/Qwen2.5-1.5B"   # یا مدل Llama فارسی‌ساز

tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype=torch.float16)

def generate_answer(question):
    inputs = tokenizer(question, return_tensors="pt").to(model.device)
    output = model.generate(**inputs, max_new_tokens=500)
    return tokenizer.decode(output[0], skip_special_tokens=True)

در این بخش یاد می‌گیرید چگونه با استفاده از Streamlit یک رابط وب حرفه‌ای ایجاد کنید که به API FastAPI یا مدل‌های Llama/Qwen متصل می‌شود. Streamlit امکان طراحی رابط کاربری چت را بدون نیاز به HTML/CSS/JS فراهم می‌کند و برای توسعه MVP و دموی سریع ربات‌های هوشمند ایده‌آل است. در این مثال تاریخچه چت با session_state مدیریت شده و پیام‌ها پس از هر رفرش نیز باقی می‌مانند. همچنین نمونهٔ اتصال به API و نسخهٔ مستقیم با مدل‌های محلی نیز ارائه شده است.

نتیجه‌گیری

در این مقاله با اصول RAG آشنا شدیم و نشان دادیم چگونه می‌توان سیستم بازیابی-تولید مبتنی بر ChromaDB را برای متون فارسی و یک دیتاست سؤالات و پاسخ‌های شرعی پیاده‌سازی کرد. مزیت اصلی این رویکرد ترکیب دسترسی به شواهد واقعی با توان تولید طبیعی زبان مدل‌های بزرگ است. کدی که ارائه شد جایگزینی برای TF-IDF سنتی است و در عمل دقت و قابلیت استناد ربات را به طور چشمگیری افزایش می‌دهد. در عین حال تأکید کردم که در زمینه‌های حساس مذهبی حتماً کنترل‌های انسانی و اخلاقی لازم را قرار دهید.

کلیدواژه ها

RAG, ChromaDB, هوش مصنوعی, ربات پاسخگو, آموزش برنامه‌نویسی, ربات پشتیبانی, جستجوی معنایی, دیتابیس برداری, NLP, پردازش زبان طبیعی

پرسش و پاسخ

1 . آیا حتماً باید از OpenAI برای بخش تولید استفاده کنم؟
خیر. می‌توانید از هر LLM دلخواه (محلی یا سرویس ابری) استفاده کنید؛ فقط باید فرمت prompt و token limit آن را مدنظر قرار دهید. اگر از مدل محلی استفاده می‌کنید، سرعت و حافظه را بررسی کنید.

2 . چه مدل embedding برای فارسی بهتر است؟
مدل‌های چندزبانه مانند paraphrase-multilingual-MiniLM-L12-v2 کارا هستند، اما اگر مدل‌های مخصوص فارسی (موجود در HuggingFace) دارید از آن‌ها استفاده کنید. مهم است که embedding معنایی واقعی فارسی را بشناسد.

3 . چه زمانی باید از پاسخ بازیابی‌شده مستقیم استفاده کنم؟
اگر نمره شباهت بسیار بالا (مثلاً > 0.9) باشد و سند منبع معتبر به نظر برسد، ارائه مستقیم متن بازیابی‌شده با ذکر منبع امن‌تر است. در غیر این صورت از تولید LLM با زمینه استفاده کنید.

4 . چگونه از هالوسینیشن (اختلاق پاسخ) جلوگیری کنم؟
الف) دقت بازیابی را با embeddings بهتر افزایش دهید. ب) prompt را محدود و صریح کنید و temperature را کم نگه دارید. ج) خروجی را با قواعد اعتبارسنجی (مثلاً تطابق با سند) بررسی کنید.

5 . آیا می‌توانم این سیستم را برای پرسش‌های غیرمذهبی هم استفاده کنم؟
بله. معماری RAG عمومی است و برای هر حوزه‌ای که دیتاست مرجع داشته باشید کاربرد دارد — از پشتیبانی مشتریان تا پزشکی (در مورد پزشکی باید توجه به مسئولیت‌پذیری و قوانین داشته باشید).

دیدگاه ها

برای ارسال دیدگاه وارد حساب کاربری خود شوید.