پروژه: تولد ذهنیار
وقت کد است. در این درس اسکلت محصولی را میسازیم که تا فصل ۱۰ همراهمان خواهد بود — و مهمترین تصمیم معماری کل دوره را میگیریم.
چه میسازیم؟
ذهنیار یک دستیار پرسشوپاسخ فارسی است که به سؤالهای شما دربارهٔ همین دوره جواب میدهد. منبع دانشش، درسنامههای همین پنل است. یعنی هرچه بیشتر جلو بروید، دستیارتان هم باهوشتر میشود.
در فصل ۱ فقط اسکلت را میسازیم: یک لایهٔ تمیز که با «مدل» حرف میزند. هنوز هیچ هوشی در کار نیست — و این عمدی است.
قدم ۱ — محیط را آماده کنید
پوشهٔ پروژه از قبل ساخته شده و یک محیط مجازی پایتون هم دارد. محیط مجازی یعنی یک جعبهٔ جدا که کتابخانههای این پروژه داخلش نصب میشوند و با بقیهٔ سیستم شما قاطی نمیشوند.
پاورشل را در پوشهٔ پروژه باز کنید و این را بزنید تا مطمئن شوید همهچیز سر جایش است:
.venv\Scripts\python.exe --versionباید چیزی شبیه Python 3.12.10 ببینید.
قدم ۲ — قرارداد مشترک را تعریف کنید
اول باید بگوییم «هر مدلی، هر که باشد، باید بتواند این کار را بکند». این همان قرارداد است. در پایتون به این کار کلاس پایه میگویند.
دو مفهوم در این فایل هست که احتمالاً برایتان تازه است:
@dataclass— یک میانبر پایتون برای ساختن کلاسی که فقط داده نگه میدارد. بهجای نوشتن ده خط کد تکراری، فقط نام و نوع فیلدها را مینویسید.ABCو@abstractmethod— یعنی «این کلاس فقط یک قالب است، نمیشود مستقیم ازش نمونه ساخت؛ هر کس ارثش را میبرد باید این متد را بنویسد». این همان چیزی است که قرارداد را الزامآور میکند.
"""قرارداد مشترک همهٔ ارائهدهندههای مدل."""
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قدم ۳ — ارائهدهندهٔ آفلاین
حالا اولین پیادهسازی واقعی. این یکی هیچ هوشی ندارد: بر اساس کلمات کلیدی جواب میدهد. ولی قطعی است (هر بار همان ورودی، همان خروجی) و همین آن را برای تست و یادگیری عالی میکند.
"""ارائهدهندهٔ آفلاین و رایگان — برای یادگیری و تست."""
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() را صدا میزند و اصلاً نمیداند پشتش چه خبر است.
"""تنظیمات پروژه — تنها جایی که باید برای عوض کردن مدل دست بزنید."""
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 = (
"تو «ذهنیار» هستی، دستیار آموزشی مهندسی هوش مصنوعی. "
"کوتاه، دقیق و به فارسی ساده جواب بده. "
"اگر چیزی را نمیدانی، صادقانه بگو نمیدانم."
)"""کارخانهٔ ارائهدهنده — بقیهٔ پروژه فقط با این تابع کار دارد."""
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"]قدم ۵ — هستهٔ ذهنیار
"""هستهٔ ذهنیار. در هر فصل یک لایه به این کلاس اضافه میشود."""
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}قدم ۶ — اجرا کنید
حالا لحظهٔ حقیقت. این دستور را در پاورشل بزنید:
.venv\Scripts\python.exe -m projects.ch01_foundation.runباید خروجیای شبیه این ببینید — پاسخ، بههمراه سنجههای مهندسی:
========================================
ذهنیار v0.1 | provider=mock
========================================
سوال: مدل پایه چیست؟
پاسخ: مدل پایه یک مدل بزرگ و همهمنظوره است که میتوان
آن را پایهٔ ساخت دهها اپلیکیشن مختلف قرار داد.
tokens : 31 in / 27 out
latency : 52.4 ms (TTFT 21.0 ms)
cost : $0.000000همینجا امتحانش کنید — این آزمایشگاه واقعاً همان کد بالا را اجرا میکند:
چه چیزی ساختیم؟
تمرین شما
این دو کار را خودتان انجام دهید؛ هر دو کوچکاند ولی مفهوم را جا میاندازند:
- در
zehnyar/providers/mock.pyیک ورودی تازه بهKNOWLEDGEاضافه کنید — مثلاً کلید("پرامپت", "prompt")و یک تعریف کوتاه. بعد در آزمایشگاه بالا از ذهنیار دربارهٔ پرامپت بپرسید. - در
zehnyar/config.pyمقدارPROVIDERرا به"ollama"عوض کنید و دوباره اجرا کنید. باید هشدار ببینید و برنامه با mock ادامه بدهد — بدون کرش. این همان تنزل آرام است که ساختیم.
خودآزمایی
چرا لایهٔ provider را بین کد و مدل گذاشتیم؟
چرا از همان فصل اول تأخیر و هزینه را ذخیره میکنیم، در حالی که ارائهدهندهٔ آفلاین ما اصلاً هزینه ندارد؟
مرور فلشکارتهای این درس
۴ کارت · ۰ مروروارد شوید تا پیشرفتتان ذخیره شود و از همینجا ادامه دهید.