شیدهوش
فصل ۲ · درس ۱۳ از ۱۳

۱۳. پروژه (۲): خروجی ساختاریافته و اتصال به ذهن‌یار

مدل زبانی درس قبل هر چیزی می‌تواند بگوید. در این درس خروجی‌اش را مهار می‌کنیم — با فیلتر لاجیت و تعمیر خودکار JSON — و بعد آن را پشت همان «پریز استاندارد» فصل ۱ به ذهن‌یار وصل می‌کنیم.

زمان مطالعه: ۶۱ دقیقهمرجع در کتاب: پروژهٔ طراحی‌شده برای این دوره، بر پایهٔ درس ۱۰ همین فصل

چه می‌سازیم؟

فایلتازه یا تغییر؟کارش
zehnyar/structured.pyتازهنمونه‌برداری محدودشده و تعمیر JSON
projects/ch02_sampling/run_structured.pyتازهاسکریپت اجرایی این درس
zehnyar/providers/local_lm.pyتازهمدل فصل ۲ پشت قرارداد Provider
zehnyar/providers/__init__.pyتغییرشناختن provider تازهٔ local
zehnyar/config.pyتغییرتنظیمات پیش‌فرض نمونه‌برداری
zehnyar/core/assistant.pyتغییررساندن دما و top-p به مدل

قدم ۱ — خروجی محدودشده و تعمیر JSON

دو ابزار از ابزارهای درس ۱۰ را می‌سازیم. اولی نمونه‌برداری محدودشده: در هر قدم، فقط توکن‌هایی اجازه دارند که هنوز به یکی از گزینه‌های مجاز می‌رسند. دومی پس‌پردازش دفاعی: سه اشتباه رایج JSON را خودکار درست می‌کند.

zehnyar/structured.py
"""خروجی ساختاریافته — دو ابزار از ابزارهایی که در درس ۱۰ دیدیم.

۱. نمونه‌برداری محدودشده (constrained sampling):
   در هر قدم، لاجیت توکن‌هایی که به یک خروجی مجاز نمی‌رسند حذف می‌شوند.
   مدل **نمی‌تواند** چیزی خارج از فهرست بگوید — نه اینکه «قول بدهد» نگوید.

۲. پس‌پردازش دفاعی (defensive parsing):
   مدل‌ها اشتباه‌های تکراری می‌کنند: آکولاد بسته را جا می‌اندازند، دور JSON
   علامت ``` می‌گذارند، یا ویرگول اضافه آخر فهرست می‌گذارند. این‌ها را با
   یک اسکریپت ساده درست می‌کنیم — همان کاری که لینکدین کرد و درصد خروجی
   سالمش را از ۹۰ به ۹۹٫۹۹ رساند.
"""
import json
import math
import random
import re

from . import sampling
from .lm import NGramLM


def generate_choice(lm: NGramLM, prompt: str, options: list[str],
                    temperature: float = 1.0, seed: int | None = None) -> dict:
    """یکی از گزینه‌های مجاز را تولید می‌کند — توکن به توکن، با فیلتر لاجیت."""
    rng = random.Random(seed)
    encoded = {opt: lm.tok.encode(" " + opt.strip()) for opt in options}
    context = lm.tok.encode(prompt)
    made: list[int] = []
    steps = []

    while True:
        # گزینه‌هایی که هنوز با آنچه تا اینجا ساخته‌ایم سازگارند
        alive = [o for o, ids in encoded.items() if ids[:len(made)] == made]
        done = [o for o in alive if len(encoded[o]) == len(made)]
        if done:
            choice = done[0]
            break

        allowed = {encoded[o][len(made)] for o in alive}
        logits = lm.logits(context)
        masked = {t: x for t, x in logits.items() if t in allowed}

        before = sampling.softmax(logits, 1.0)
        token, final = sampling.choose(masked, temperature, rng=rng)
        steps.append({
            "allowed": [lm.tok.vocab[t] for t in sorted(allowed)],
            "picked": lm.tok.vocab[token],
            "mass_kept": round(sum(before[t] for t in allowed), 6),
        })
        made.append(token)
        context.append(token)

    return {"choice": choice, "steps": steps}


def free_vs_constrained(lm: NGramLM, prompt: str, options: list[str],
                        temperature: float = 1.0, seed: int | None = None) -> dict:
    """یک بار آزاد تولید می‌کند، یک بار محدود — برای مقایسه در آزمایشگاه."""
    free = lm.generate(prompt, max_tokens=8, temperature=temperature, seed=seed)
    limited = generate_choice(lm, prompt, options, temperature, seed)
    scores = {}
    for o in options:
        lp, n = lm.logprob(prompt, " " + o.strip())
        scores[o] = round(lp / max(n, 1), 4)      # میانگین logprob، نه جمع
    return {
        "free": free.text,
        "free_valid": free.text.strip() in [o.strip() for o in options],
        "constrained": limited["choice"],
        "steps": limited["steps"],
        "avg_logprob": scores,
        "prob_share": _share(scores),
    }


def _share(avg_logprobs: dict) -> dict:
    exps = {k: math.exp(v) for k, v in avg_logprobs.items()}
    total = sum(exps.values())
    return {k: round(v / total, 4) for k, v in exps.items()}


# ---------------------------------------------------------------------------
#  پس‌پردازش دفاعی
# ---------------------------------------------------------------------------

def parse_json_defensive(text: str):
    """JSON نیمه‌خراب را تا جای ممکن تعمیر می‌کند. خروجی: (شیء یا None، فهرست تعمیرها)."""
    fixes = []
    s = text.strip()

    # ۱. حصار کد: ```json ... ```
    fence = re.search(r"```(?:json)?\s*(.*?)(?:```|$)", s, re.S)
    if fence:
        s = fence.group(1).strip()
        fixes.append("حذف حصار ```")

    # ۲. متن اضافهٔ قبل از اولین آکولاد: «حتماً! این هم JSON شما: {...}»
    start = min([i for i in (s.find("{"), s.find("[")) if i >= 0], default=-1)
    if start > 0:
        s = s[start:]
        fixes.append("حذف متن اضافهٔ اول")

    try:
        return json.loads(s), fixes
    except json.JSONDecodeError:
        pass

    # ۳. ویرگول آویزان: {"a": 1,}
    s2 = re.sub(r",\s*([}\]])", r"\1", s)
    if s2 != s:
        s = s2
        fixes.append("حذف ویرگول آویزان")

    # ۴. رشته یا آکولاد بسته‌نشده (معمولاً چون تولید زود متوقف شده)
    stack, in_str, esc = [], False, False
    for ch in s:
        if in_str:
            if esc:
                esc = False
            elif ch == "\\":
                esc = True
            elif ch == '"':
                in_str = False
        elif ch == '"':
            in_str = True
        elif ch in "{[":
            stack.append("}" if ch == "{" else "]")
        elif ch in "}]" and stack:
            stack.pop()
    if in_str:
        s += '"'
        fixes.append("بستن رشتهٔ نیمه‌کاره")
    if stack:
        s = re.sub(r",\s*$", "", s) + "".join(reversed(stack))
        fixes.append("بستن " + str(len(stack)) + " آکولاد/کروشه")

    try:
        return json.loads(s), fixes
    except json.JSONDecodeError:
        return None, fixes
این تکه از کدیعنی چه
encoded = {opt: lm.tok.encode(...) for opt in options}هر گزینهٔ مجاز را به فهرست توکن‌هایش تبدیل کن. مثلاً «پرامپت» ممکن است یک توکن باشد و «RAG» سه توکن
alive = [o for o ... if ids[:len(made)] == made]گزینه‌هایی که «تا این‌جا» با توکن‌های ساخته‌شده جور درمی‌آیند. اگر توکن اول ▁پ بوده، فقط گزینه‌هایی که با ▁پ شروع می‌شوند زنده می‌مانند
allowed = {...}یک مجموعه (set، آکولاد بدون :) از توکن‌هایی که در قدم بعد مجازند
masked = {t: x for t, x in logits.items() if t in allowed}قلب ماجرا: فقط لاجیت توکن‌های مجاز نگه داشته می‌شود. بقیه اصلاً به قرعه نمی‌رسند
mass_keptچند درصد از احتمال اصلی مدل مال توکن‌های مجاز بود؟ اگر خیلی کم باشد، یعنی خود مدل اصلاً به این گزینه‌ها فکر نمی‌کرد و ما به زور مجبورش کرده‌ایم
free_vs_constrainedبرای آزمایشگاه: یک تولید آزاد، یک تولید محدود، و سهم هر گزینه از روی میانگین logprob (درس ۹)
re.search(...) در بخش «۱. حصار کد»اگر خروجی داخل حصار کد (سه بک‌تیک پشت هم) بود، فقط داخلش را بردار
s.find("{")جای اولین آکولاد را پیدا کن و هرچه قبلش است («حتماً! این هم JSON شما:») را دور بریز
try: json.loads(s) except json.JSONDecodeError:«امتحان کن بخوانی؛ اگر خطای JSON داد، برو سراغ تعمیر بعدی»
re.sub(r",\s*([}\]])", r"\1", s)ویرگولی که درست قبل از } یا ] آمده را پاک کن. \s* یعنی «هر تعداد فاصله»، \1 یعنی «همان چیزی که داخل پرانتز پیدا شد»
حلقهٔ for ch in s: با stackحرف‌به‌حرف بخوان و هر آکولاد یا کروشهٔ باز را روی یک «پشته» بگذار و با هر بسته برش دار. آخر کار، هرچه روی پشته مانده یعنی بسته نشده — به همان ترتیب برعکس می‌بندیمشان. حواسش هست که علامت‌های داخل رشته را نشمارد

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

این اسکریپت دو آزمایش دارد: یک پرامپت، یک بار آزاد و یک بار فقط بین سه گزینهٔ مجاز؛ و تعمیر یک JSON نیمه‌کاره و پرحرف. در پوشهٔ projects/ch02_sampling بسازیدش:

projects/ch02_sampling/run_structured.py
"""پروژهٔ فصل ۲ (بخش ۲) — خروجی ساختاریافته.

اجرا:
    .venv\\Scripts\\python.exe -m projects.ch02_sampling.run_structured
"""
from zehnyar import _console
from zehnyar.lm import get_lm
from zehnyar.structured import generate_choice, parse_json_defensive

_console.setup()   # تا کنسول ویندوز فارسی را درست چاپ کند


def title(text: str) -> None:
    print("\n" + "=" * 52)
    print(" " + text)
    print("=" * 52)


def main() -> None:
    lm = get_lm()

    # ---------- ۱. خروجی محدودشده ----------
    title("۱. نمونه‌برداری محدودشده")
    options = ["پرامپت", "داده", "ارزیابی"]
    q = "سه ابزار برای رام کردن مدل داریم و اولی مهندسی"
    free = lm.generate(q, max_tokens=6, temperature=1.0, seed=3).text
    pick = generate_choice(lm, q, options, temperature=1.0, seed=3)["choice"]
    print(f"\n  پرامپت : {q} ...")
    print(f"  آزاد   : {free}")
    print(f"  محدود  : {pick}   (فقط از بین {options})")

    # ---------- ۲. پس‌پردازش دفاعی ----------
    title("۲. تعمیر JSON نیمه‌کاره")
    broken = 'حتماً! این هم خروجی: {"topic": "نمونه‌برداری", "tags": ["دما", "top-p",'
    obj, fixes = parse_json_defensive(broken)
    print(f"\n  ورودی  : {broken}")
    print(f"  تعمیرها: {'، '.join(fixes)}")
    print(f"  نتیجه  : {obj}")

    print("\n" + "-" * 52)
    print("مدل آزاد هرچه بخواهد می‌گوید؛ با فیلتر لاجیت و پس‌پردازش، خروجی قابل‌استفاده می‌شود.")


if __name__ == "__main__":
    main()
این تکه از کدیعنی چه
options = ["پرامپت", "داده", "ارزیابی"]فهرست گزینه‌های مجاز
lm.generate(q, max_tokens=6, ...)تولید آزاد: مدل هرچه بخواهد می‌گوید
generate_choice(...)["choice"]تولید محدود با همان پرامپت و همان بذر؛ ["choice"] یعنی از دیکشنری خروجی فقط گزینهٔ انتخاب‌شده را بردار
obj, fixes = parse_json_defensive(broken)تابع دو چیز برمی‌گرداند — شیء تعمیرشده و فهرست تعمیرها — و پایتون آن‌ها را هم‌زمان در دو متغیر می‌ریزد
'، '.join(fixes)فهرست تعمیرها را با ویرگول فارسی پشت هم بچسبان
PowerShell
.venv\Scripts\python.exe -m projects.ch02_sampling.run_structured

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

خروجی مورد انتظار

====================================================
 ۱. نمونه‌برداری محدودشده
====================================================

  پرامپت : سه ابزار برای رام کردن مدل داریم و اولی مهندسی ...
  آزاد   : که فقط‌ها آن را ک
  محدود  : پرامپت   (فقط از بین ['پرامپت', 'داده', 'ارزیابی'])

====================================================
 ۲. تعمیر JSON نیمه‌کاره
====================================================

  ورودی  : حتماً! این هم خروجی: {"topic": "نمونه‌برداری", "tags": ["دما", "top-p",
  تعمیرها: حذف متن اضافهٔ اول، بستن 2 آکولاد/کروشه
  نتیجه  : {'topic': 'نمونه\u200cبرداری', 'tags': ['دما', 'top-p']}

----------------------------------------------------
مدل آزاد هرچه بخواهد می‌گوید؛ با فیلتر لاجیت و پس‌پردازش، خروجی قابل‌استفاده می‌شود.

این دو آزمایشگاه منطق همین کد را در مرورگر خودتان اجرا می‌کنند. گزینه‌های خودتان و JSONهای خراب خودتان را امتحان کنید:

قدم ۳ — وصل کردن به ذهن‌یار

آخرین کار: مدل تازه را پشت همان «پریز استاندارد» فصل ۱ بگذاریم تا ذهن‌یار هم بتواند از آن استفاده کند. اول یک ارائه‌دهندهٔ تازه:

zehnyar/providers/local_lm.py
"""ارائه‌دهندهٔ مدل زبانی محلی — همان مدل n-gram فصل ۲، پشت قرارداد Provider.

این مدل فقط **پیش‌آموزش** دیده، پس‌آموزش ندیده. یعنی جواب نمی‌دهد؛
متن شما را **ادامه** می‌دهد — دقیقاً مثل مثال پیتزا در درس ۶.
برای همین پیش‌فرض ذهن‌یار هنوز mock است؛ این یکی برای دیدن رفتار
یک مدل خام است:

    $env:ZEHNYAR_PROVIDER = "local"
"""
import time

from .base import Provider, Message, Completion


class LocalLMProvider(Provider):
    name = "local"
    model = "zehnyar-ngram-v1"

    def __init__(self):
        from ..lm import get_lm
        self.lm = get_lm()

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

        # مدل خام «گفت‌وگو» نمی‌فهمد؛ فقط آخرین پیام کاربر را ادامه می‌دهد
        prompt = next((m.content for m in reversed(messages) if m.role == "user"), "")
        gen = self.lm.generate(
            prompt,
            max_tokens=params.get("max_tokens", 40),
            temperature=params.get("temperature", 0.7),
            top_k=params.get("top_k", 0),
            top_p=params.get("top_p", 0.9),
            seed=params.get("seed"),
            # بعد از علامت سؤال، محتمل‌ترین ادامه «پایان متن» است؛ نمی‌گذاریم خالی برگردد
            min_tokens=params.get("min_tokens", 4),
        )

        elapsed = (time.perf_counter() - start) * 1000
        return Completion(
            text=gen.text,
            model=self.model,
            provider=self.name,
            prompt_tokens=len(self.lm.tok.encode(prompt)),
            completion_tokens=len(gen.steps),
            latency_ms=round(elapsed, 2),
            # هر توکن جدا ساخته می‌شود، پس اولین توکن سهم خودش را دارد
            ttft_ms=round(elapsed / max(len(gen.steps), 1), 2),
            cost_usd=0.0,          # محلی، پس رایگان
            meta={"stop_reason": gen.stop_reason},
        )
این تکه از کدیعنی چه
class LocalLMProvider(Provider):یک ارائه‌دهندهٔ تازه که از قرارداد فصل ۱ ارث می‌برد — پس مجبور است complete داشته باشد
from ..lm import get_lm داخل __init__مدل را فقط وقتی کسی این ارائه‌دهنده را خواست بارگذاری کن، نه همیشه
next((... for m in reversed(messages) if ...), "")اولین پیام کاربر از آخر را بردار؛ اگر نبود، رشتهٔ خالی. مدل خام «پیام سیستمی» و «گفت‌وگو» نمی‌فهمد، پس فقط همان را ادامه می‌دهیم
params.get("temperature", 0.7)اگر دما داده شده همان، وگرنه ۰٫۷
min_tokens=params.get("min_tokens", 4)بعد از علامت سؤال، محتمل‌ترین ادامه در درس‌ها «پایان پاراگراف» است؛ نمی‌گذاریم پاسخ خالی برگردد
return Completion(...)همان قرارداد همیشگی، با همان سنجه‌ها. meta دلیل توقف را هم نگه می‌دارد

بعد، کارخانهٔ فصل ۱ باید اسم local را بشناسد. فایل را کامل با این عوض کنید — تنها تغییر، بلوک if name == "local": است:

zehnyar/providers/__init__.py
"""کارخانهٔ ارائه‌دهنده — بقیهٔ پروژه فقط با این تابع کار دارد."""
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 == "local":
        # مدل زبانی واقعی ولی خام فصل ۲ — همیشه در دسترس، چون روی همین دستگاه است
        from .local_lm import LocalLMProvider
        return LocalLMProvider()

    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", "MockProvider", "get_provider"]
این تکه از کدیعنی چه
if name == "local":اگر local خواسته شده، ارائه‌دهندهٔ تازه را برگردان. نیازی به ping نیست، چون روی همین کامپیوتر است و همیشه در دسترس

حالا تنظیمات پیش‌فرض نمونه‌برداری را در config.py تعریف می‌کنیم. یک بلوک SAMPLING اضافه شده؛ فایل را کامل عوض کنید:

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

# گزینه‌ها: mock (آفلاین) | local (مدل خام فصل ۲) | 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_BASE = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
OPENAI_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")

# قیمت تقریبی به ازای هر یک میلیون توکن (دلار) — برای تخمین هزینه
PRICING = {
    "gpt-4o-mini": {"in": 0.15, "out": 0.60},
}

# پارامترهای نمونه‌برداری پیش‌فرض (فصل ۲، درس ۸)
# دمای پایین برای دستیار پرسش‌وپاسخ: ثبات مهم‌تر از خلاقیت است
SAMPLING = {
    "temperature": float(os.getenv("ZEHNYAR_TEMPERATURE", "0.3")),
    "top_p": float(os.getenv("ZEHNYAR_TOP_P", "0.9")),
}

SYSTEM_PROMPT = (
    "تو «ذهن‌یار» هستی، دستیار آموزشی مهندسی هوش مصنوعی. "
    "کوتاه، دقیق و به فارسی ساده جواب بده. "
    "اگر چیزی را نمی‌دانی، صادقانه بگو نمی‌دانم."
)
این تکه از کدیعنی چه
SAMPLING = {"temperature": ..., "top_p": ...}دمای پیش‌فرض ۰٫۳ و top-p برابر ۰٫۹ — جدول «نقطهٔ شروع» درس ۸: برای دستیار پرسش‌وپاسخ، ثبات مهم‌تر از خلاقیت است
float(os.getenv("ZEHNYAR_TEMPERATURE", "0.3"))مقدار را از متغیر سیستم بخوان (اگر بود) و به عدد اعشاری تبدیل کن. متغیر سیستم همیشه متن است، پس float(...) لازم است

و آخر، هستهٔ ذهن‌یار باید این تنظیمات را به مدل برساند:

zehnyar/core/assistant.py
"""هستهٔ ذهن‌یار. در هر فصل یک لایه به این کلاس اضافه می‌شود.

فصل ۱: فقط یک فراخوانی ساده.
فصل ۲: پارامترهای نمونه‌برداری (دما، top-p) از اینجا به مدل می‌رسند.
فصل ۵: کتابخانهٔ پرامپت نسخه‌دار اینجا وصل می‌شود.
فصل ۶: بازیابی متن مرتبط (RAG) قبل از فراخوانی اضافه می‌شود.
فصل ۱۰: نگهبان، مسیریاب و کش دور همین کلاس پیچیده می‌شوند.
"""
from .. import config
from ..providers import get_provider, Message


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, **params):
        """یک سؤال می‌گیرد و پاسخ کامل به‌همراه سنجه‌ها برمی‌گرداند.

        هر پارامتری که ندهید از config.SAMPLING می‌آید؛ یعنی رفتار پیش‌فرض
        در یک جا تعریف شده و همهٔ ارائه‌دهنده‌ها همان را می‌گیرند.
        """
        messages = [
            Message(role="system", content=self.system),
            Message(role="user", content=question),
        ]
        return self.provider.complete(messages, **{**config.SAMPLING, **params})

    def info(self) -> dict:
        return {"provider": self.provider.name, "model": self.provider.model}
این تکه از کدیعنی چه
def ask(self, question, **params)دو ستاره یعنی «هر تعداد پارامتر اسم‌دار اضافه هم قبول کن» — مثلاً bot.ask("سلام", temperature=0)
{**config.SAMPLING, **params}دو دیکشنری را یکی کن: اول تنظیمات پیش‌فرض، بعد هرچه موقع صدا زدن داده شده. اگر کلیدی در هر دو بود، دومی برنده است
self.provider.complete(messages, **{...})همهٔ آن تنظیمات را به ارائه‌دهنده بده. mock آن‌ها را نادیده می‌گیرد (جدول جست‌وجو دما ندارد)، ولی local، Ollama و OpenAI از آن‌ها استفاده می‌کنند

حالا پروژهٔ فصل ۱ را با مدل تازه اجرا کنید. خط اول به پاورشل می‌گوید «تا این پنجره باز است، provider را local بگذار»:

PowerShell
$env:ZEHNYAR_PROVIDER = "local"
.venv\Scripts\python.exe -m projects.ch01_foundation.run

ذهن‌یار حالا با یک مدل زبانی واقعی کار می‌کند — و جواب‌هایش بی‌ربط است، چون فقط پیش‌آموزش دیده. برای برگشتن به mock، پاورشل را ببندید و دوباره باز کنید، یا بزنید: $env:ZEHNYAR_PROVIDER = "mock".

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

تمرین شما

  1. در run_structured.py فهرست options را به سه واژهٔ دیگر از درس‌ها عوض کنید — مثلاً ["توکن", "مدل", "داده"] — و ببینید خروجی محدود همیشه یکی از آن‌هاست، حتی اگر خروجی آزاد چیز دیگری بگوید.
  2. در آزمایشگاه تعمیرکار بالا یک JSON بنویسید که دو اشتباه هم‌زمان دارد: هم حصار کد دورش است و هم آخرش قطع شده. تعمیرکار از پسش برمی‌آید؟ حالا یک JSON بنویسید که تعمیرکار نتواند درستش کند. این همان مرز پس‌پردازش است: فقط اشتباه‌های کوچک و قابل‌حدس.
  3. با $env:ZEHNYAR_PROVIDER = "local" پروژهٔ فصل ۱ را اجرا کنید و $env:ZEHNYAR_TEMPERATURE = "1.2" را هم امتحان کنید. پاسخ‌های ذهن‌یار چقدر عوض می‌شوند؟

خودآزمایی

نمونه‌برداری محدودشده چطور تضمین می‌کند خروجی یکی از گزینه‌های مجاز باشد؟
چرا وقتی ذهن‌یار را روی local می‌گذاریم، به سؤال‌ها جواب بی‌ربط می‌دهد؟
آکادمی هوش مصنوعی شیدهوش — بخشی از shidhoosh.com