فصل ۱ · درس ۵ از ۴۳

پروژه: تولد ذهن‌یار

وقت کد است. در این درس اسکلت محصولی را می‌سازیم که تا فصل ۱۰ همراهمان خواهد بود — و مهم‌ترین تصمیم معماری کل دوره را می‌گیریم.

چه می‌سازیم؟

ذهن‌یار یک دستیار پرسش‌وپاسخ فارسی است که به سؤال‌های شما دربارهٔ همین دوره جواب می‌دهد. منبع دانشش، درس‌نامه‌های همین پنل است. یعنی هرچه بیشتر جلو بروید، دستیارتان هم باهوش‌تر می‌شود.

در فصل ۱ فقط اسکلت را می‌سازیم: یک لایهٔ تمیز که با «مدل» حرف می‌زند. هنوز هیچ هوشی در کار نیست — و این عمدی است.

قدم ۱ — محیط را آماده کنید

پوشهٔ پروژه از قبل ساخته شده و یک محیط مجازی پایتون هم دارد. محیط مجازی یعنی یک جعبهٔ جدا که کتابخانه‌های این پروژه داخلش نصب می‌شوند و با بقیهٔ سیستم شما قاطی نمی‌شوند.

پاورشل را در پوشهٔ پروژه باز کنید و این را بزنید تا مطمئن شوید همه‌چیز سر جایش است:

PowerShellpowershell
.venv\Scripts\python.exe --version

باید چیزی شبیه Python 3.12.10 ببینید.

قدم ۲ — قرارداد مشترک را تعریف کنید

اول باید بگوییم «هر مدلی، هر که باشد، باید بتواند این کار را بکند». این همان قرارداد است. در پایتون به این کار کلاس پایه می‌گویند.

دو مفهوم در این فایل هست که احتمالاً برایتان تازه است:

  • @dataclass — یک میان‌بر پایتون برای ساختن کلاسی که فقط داده نگه می‌دارد. به‌جای نوشتن ده خط کد تکراری، فقط نام و نوع فیلدها را می‌نویسید.
  • ABC و @abstractmethod — یعنی «این کلاس فقط یک قالب است، نمی‌شود مستقیم ازش نمونه ساخت؛ هر کس ارثش را می‌برد باید این متد را بنویسد». این همان چیزی است که قرارداد را الزام‌آور می‌کند.
zehnyar/providers/base.pypython
"""قرارداد مشترک همهٔ ارائه‌دهنده‌های مدل."""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field


@dataclass
class Message:
    """یک پیام در گفت‌وگو. role یکی از system / user / assistant است."""
    role: str
    content: str


@dataclass
class Completion:
    """پاسخ مدل، به‌همراه سنجه‌هایی که یک مهندس به آن‌ها نیاز دارد."""
    text: str
    model: str
    provider: str
    prompt_tokens: int = 0
    completion_tokens: int = 0
    latency_ms: float = 0.0      # کل زمان پاسخ
    ttft_ms: float = 0.0         # زمان تا اولین توکن
    cost_usd: float = 0.0
    meta: dict = field(default_factory=dict)


class Provider(ABC):
    """هر ارائه‌دهنده باید بتواند یک گفت‌وگو را به یک پاسخ تبدیل کند."""

    name: str = "base"
    model: str = "unknown"

    @abstractmethod
    def complete(self, messages: list[Message], **params) -> Completion:
        """فهرست پیام‌ها را می‌گیرد و یک Completion برمی‌گرداند."""
        raise NotImplementedError

    def ping(self) -> bool:
        """آیا این ارائه‌دهنده همین حالا در دسترس است؟"""
        return True

قدم ۳ — ارائه‌دهندهٔ آفلاین

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

zehnyar/providers/mock.pypython
"""ارائه‌دهندهٔ آفلاین و رایگان — برای یادگیری و تست."""
import time
from .base import Provider, Message, Completion

# دانش اولیهٔ ذهن‌یار: کلیدواژه -> پاسخ
KNOWLEDGE = {
    ("مدل پایه", "foundation"):
        "مدل پایه یک مدل بزرگ و همه‌منظوره است که می‌توان آن را "
        "پایهٔ ساخت ده‌ها اپلیکیشن مختلف قرار داد.",
    ("توکن", "token"):
        "توکن واحد کار مدل است؛ چیزی بین حرف و کلمه. "
        "برای انگلیسی هر توکن حدوداً سه‌چهارم یک کلمه است.",
    ("خودنظارتی", "self-supervis"):
        "خودنظارتی یعنی برچسب‌ها از خود داده استخراج می‌شوند، "
        "پس نیازی به برچسب‌زنی دستی نیست.",
    ("تنظیم دقیق", "finetun"):
        "تنظیم دقیق یعنی ادامهٔ آموزش یک مدل آماده روی دادهٔ خودتان، "
        "که وزن‌های مدل را تغییر می‌دهد.",
    ("rag", "بازیابی"):
        "RAG یعنی مدل قبل از پاسخ دادن، متن مرتبط را از یک منبع "
        "بیرونی پیدا می‌کند و آن را به پرامپت اضافه می‌کند.",
}

FALLBACK = (
    "هنوز چیزی در این باره بلد نیستم. در فصل ۶ با RAG یاد می‌گیرم "
    "که جواب را از متن درس‌ها پیدا کنم."
)


def count_tokens(text: str) -> int:
    """تخمین خیلی ساده از تعداد توکن. در فصل ۲ درستش می‌کنیم."""
    return max(1, len(text) // 4)


class MockProvider(Provider):
    name = "mock"
    model = "zehnyar-mock-v1"

    def complete(self, messages: list[Message], **params) -> Completion:
        start = time.perf_counter()

        # آخرین پیام کاربر را پیدا می‌کنیم
        question = ""
        for m in reversed(messages):
            if m.role == "user":
                question = m.content
                break

        low = question.lower()
        answer = FALLBACK
        for keys, value in KNOWLEDGE.items():
            if any(k in low for k in keys):
                answer = value
                break

        time.sleep(0.05)   # وانمود می‌کنیم فکر کردن زمان می‌برد
        elapsed = (time.perf_counter() - start) * 1000
        prompt_text = " ".join(m.content for m in messages)

        return Completion(
            text=answer,
            model=self.model,
            provider=self.name,
            prompt_tokens=count_tokens(prompt_text),
            completion_tokens=count_tokens(answer),
            latency_ms=round(elapsed, 2),
            ttft_ms=round(elapsed * 0.4, 2),
            cost_usd=0.0,          # آفلاین، پس رایگان
        )

قدم ۴ — کارخانهٔ انتخاب ارائه‌دهنده

این فایل کوچک، همان «پریز استاندارد» است. بقیهٔ پروژه فقط get_provider() را صدا می‌زند و اصلاً نمی‌داند پشتش چه خبر است.

zehnyar/config.pypython
"""تنظیمات پروژه — تنها جایی که باید برای عوض کردن مدل دست بزنید."""
import os

# گزینه‌ها: mock (آفلاین) | ollama (محلی) | openai (نیازمند کلید)
PROVIDER = os.getenv("ZEHNYAR_PROVIDER", "mock")

OLLAMA_HOST  = os.getenv("OLLAMA_HOST", "http://localhost:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3.2:3b")

OPENAI_KEY   = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")

SYSTEM_PROMPT = (
    "تو «ذهن‌یار» هستی، دستیار آموزشی مهندسی هوش مصنوعی. "
    "کوتاه، دقیق و به فارسی ساده جواب بده. "
    "اگر چیزی را نمی‌دانی، صادقانه بگو نمی‌دانم."
)
zehnyar/providers/__init__.pypython
"""کارخانهٔ ارائه‌دهنده — بقیهٔ پروژه فقط با این تابع کار دارد."""
from .base import Provider, Message, Completion
from .mock import MockProvider
from .. import config


def get_provider(name: str | None = None) -> Provider:
    """ارائه‌دهندهٔ فعال را برمی‌گرداند.

    اگر ارائه‌دهندهٔ خواسته‌شده در دسترس نباشد، بی‌سروصدا
    به mock برمی‌گردیم تا آموزش هیچ‌وقت متوقف نشود.
    """
    name = (name or config.PROVIDER).lower()

    if name == "ollama":
        from .ollama import OllamaProvider
        p = OllamaProvider()
        if p.ping():
            return p
        print("[هشدار] Ollama در دسترس نیست — به mock برگشتم.")

    elif name == "openai":
        if config.OPENAI_KEY:
            from .openai_compat import OpenAIProvider
            return OpenAIProvider()
        print("[هشدار] کلید OpenAI تنظیم نشده — به mock برگشتم.")

    return MockProvider()


__all__ = ["Provider", "Message", "Completion", "get_provider"]

قدم ۵ — هستهٔ ذهن‌یار

zehnyar/core/assistant.pypython
"""هستهٔ ذهن‌یار. در هر فصل یک لایه به این کلاس اضافه می‌شود."""
from ..providers import get_provider, Message
from .. import config


class Zehnyar:
    def __init__(self, provider_name: str | None = None):
        self.provider = get_provider(provider_name)
        self.system = config.SYSTEM_PROMPT

    def ask(self, question: str):
        """یک سؤال می‌گیرد و پاسخ کامل به‌همراه سنجه‌ها برمی‌گرداند."""
        messages = [
            Message(role="system", content=self.system),
            Message(role="user", content=question),
        ]
        return self.provider.complete(messages)

    def info(self) -> dict:
        return {"provider": self.provider.name, "model": self.provider.model}

قدم ۶ — اجرا کنید

حالا لحظهٔ حقیقت. این دستور را در پاورشل بزنید:

PowerShellpowershell
.venv\Scripts\python.exe -m projects.ch01_foundation.run

باید خروجی‌ای شبیه این ببینید — پاسخ، به‌همراه سنجه‌های مهندسی:

خروجی مورد انتظارtext
========================================
 ذهن‌یار v0.1  |  provider=mock
========================================

سوال: مدل پایه چیست؟
پاسخ: مدل پایه یک مدل بزرگ و همه‌منظوره است که می‌توان
       آن را پایهٔ ساخت ده‌ها اپلیکیشن مختلف قرار داد.

  tokens   : 31 in / 27 out
  latency  : 52.4 ms   (TTFT 21.0 ms)
  cost     : $0.000000

همین‌جا امتحانش کنید — این آزمایشگاه واقعاً همان کد بالا را اجرا می‌کند:

آزمایشگاه تعاملی — این بخش در نسخه‌ی بعدی آکادمی ابزار زنده می‌شود. فعلاً مثال‌های متن همین درس را روی کاغذ یا در دفترچه‌ی پایتون خودتان امتحان کنید.

چه چیزی ساختیم؟

تمرین شما

این دو کار را خودتان انجام دهید؛ هر دو کوچک‌اند ولی مفهوم را جا می‌اندازند:

  1. در zehnyar/providers/mock.py یک ورودی تازه به KNOWLEDGE اضافه کنید — مثلاً کلید ("پرامپت", "prompt") و یک تعریف کوتاه. بعد در آزمایشگاه بالا از ذهن‌یار دربارهٔ پرامپت بپرسید.
  2. در zehnyar/config.py مقدار PROVIDER را به "ollama" عوض کنید و دوباره اجرا کنید. باید هشدار ببینید و برنامه با mock ادامه بدهد — بدون کرش. این همان تنزل آرام است که ساختیم.

خودآزمایی

آزمونک ۱

چرا لایهٔ provider را بین کد و مدل گذاشتیم؟

آزمونک ۲

چرا از همان فصل اول تأخیر و هزینه را ذخیره می‌کنیم، در حالی که ارائه‌دهندهٔ آفلاین ما اصلاً هزینه ندارد؟

مرور فلش‌کارت‌های این درس

۴ کارت · ۰ مرور

وارد شوید تا پیشرفتتان ذخیره شود و از همین‌جا ادامه دهید.

آکادمی هوش مصنوعی شیدهوش — بخشی از shidhoosh.com