Files
MathApp/docs/extraction-playbook.md
Zareian 6a6d0200d2 complete the quiz path for grades 8 through 12, and close three gaps in the API contract
Content
-------
Every node in grades 8, 9, 10, 11 and 12 now holds a full four-question
quiz — 121 more questions, filling the two حسابان دوازدهم nodes in
فصل ۵: کاربردهای مشتق that were empty and the two ریاضی هشتم nodes that
were short. The repo's own seed guard was already failing on those two
empty nodes; it passes now.

Grades 3, 6 and 7 are the other team's books and were left alone. 39 of
their 145 nodes still hold fewer than four questions, but none is empty,
so every node on the map still plays.

An audit over all 1,593 questions in grades 8–12 found no structural
fault: no question with the wrong number of options, no question with
zero or two correct answers, no empty or duplicated option, no leftover
LaTeX, and no two identical questions inside one lesson.

Contract
--------
Three endpoints were missing or malformed in the OpenAPI schema, which
matters more than it looks: that schema is the contract the frontend
generates its client from, so a view missing there is a view the other
repo cannot call.

- NotificationMarkReadView was dropped from the schema entirely, because
  spectacular could not guess a request body for its PATCH and gives up
  on the whole view rather than the one field. It declares request=None.
- PaymentRedirectView was dropped for the same reason; it answers with a
  302 and no JSON body, which it now says.
- /api/health and /api/health/ collided on operationId and were emitted
  as health_retrieve and health_retrieve_2. The slash variant exists only
  for a monitor that calls it that way, so it is excluded from the schema.

`spectacular` now runs clean — no errors, no warnings.

Tidying
-------
Condensed the two longest comment blocks (18 and 14 lines) in
rag_verify_solutions and settings, keeping the measurement and the rule
and dropping the retelling. README gains the bulk question-authoring
workflow; the playbook's subagent section now points at the real tool
instead of describing the steps by hand.

458 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 03:49:32 +03:30

8.9 KiB
Raw Permalink Blame History

روشِ استخراجِ ارزان و دقیق از کتابِ درسی

این فایل نتیجه‌ی یک روزِ اندازه‌گیریِ واقعی روی پنج کتابِ ریاضی است (۱۴۰۵/۰۶/۲۹). هر عددی که اینجا می‌بینید از پنلِ صورتحساب یا از دادهٔ واقعی درآمده، نه از جدولِ قیمتِ رسمی و نه از تخمین.

برای تیم‌های دیگر: اگر کتابِ درسی را با مدلِ vision استخراج می‌کنید، این چهار تصمیم تفاوتِ چند برابری در هزینه و کیفیت می‌سازند.


۱. مدل را اندازه بگیرید، از جدولِ قیمت نخوانید

قیمتِ GapGPT با قیمتِ رسمیِ OpenAI یکی نیست. اگر از جدولِ رسمی تخمین بزنید، گران‌ترین گزینه را ارزان می‌بینید.

اندازه‌گیریِ واقعی روی سه صفحه‌ی یکسان، با خواندنِ /v1/dashboard/billing/usage قبل و بعد:

مدل سنت/صفحه (واقعی) عددهای درست¹ مسئله‌ی جعلی
gpt-4.1-mini ۰٫۲۲ ۹۸٪ خیر
gpt-4o ۰٫۷۷ ۹۷٪ بله
gemini-3.6-flash ۲٫۱۳ ۱۰۰٪ خیر
gpt-4o-mini — ۰٪ مختصات را از خود ساخت

¹ سنجه‌ی رایگان: هر عددی که مدل در متنِ مسئله می‌نویسد باید در لایه‌ی متنیِ همان صفحه‌ی PDF وجود داشته باشد. عددی که آنجا نیست یا از شکل خوانده شده (خوب) یا جعل شده (بد)، و نرخِ نسبیِ مدل‌ها قابلِ مقایسه است.

نتیجه: gpt-4.1-mini هم دقیق‌تر از gpt-4o بود هم ۳٫۵ برابر ارزان‌تر. gemini دقیق‌ترین بود ولی سه برابرِ gpt-4o گران. gpt-4o-mini صریحاً مختصات را از خود درمی‌آورد («با فرض مختصات A(1,2)…») — شکستِ بی‌صدا، بدترین نوع.

Batch API روی GapGPT وجود ندارد (/v1/batches → ۴۰۴)، پس تخفیفِ ۵۰٪ در دسترس نیست. هر ابزاری که آن را وعده بدهد دارد گمراه می‌کند.

DPI اهرمِ هزینه نیست. OpenAI تصویر را به ضلعِ کوتاهِ ۷۶۸ نرمال می‌کند، پس رندر با ۲۰۰ یا ۱۱۰ DPI توکنِ یکسانی می‌دهد. ۲۰۰ را نگه دارید.

۲. قاعده‌ی self-contained را در پرامپت بگذارید

مدلِ vision شکل را می‌بیند؛ فقط از او خواسته نشده بنویسدش. بدونِ این قاعده، ۴۲٪ مسئله‌ها به «شکلِ زیر» ارجاع می‌دهند و برای هیچ‌کس قابلِ حل نیستند.

قاعده (در rag/services/pdf.py): هرجا مسئله به شکل/جدول ارجاع می‌دهد، اندازه‌ها، برچسبِ رأس‌ها، زاویه‌ها و مقادیرِ جدول را داخلِ متنِ مسئله بنویس؛ و اگر شکلی واقعاً قابلِ توصیف نیست (رسمِ آزاد، رنگ‌آمیزی) مسئله را رد کن به‌جای نوشتنِ ارجاعِ بی‌فایده.

اثرش با عدد: نرخِ unjudgeable در بازبینی از ۴۰٪ به ۳٪ رسید.

۳. بک‌اسلشِ LaTeX را تعمیر کنید، وگرنه صفحه‌های پرفرمول را می‌بازید

مدل در متنِ ریاضی \frac و \sqrt می‌نویسد و بک‌اسلشِ تنها JSON را می‌شکند. json.loads کلِ صفحه را دور می‌ریزد — یعنی دقیقاً باارزش‌ترین صفحه‌ها.

⚠️ تله‌ی نسخه‌ی اولِ تعمیر: \f، \t، \b، \r و \n از نظرِ JSON معتبراند، پس «سالم» رد می‌شوند و \frac به formfeed+«rac» و \theta به tab+«heta» تبدیل می‌شود — خرابیِ بی‌صدا به‌جای خطای بلند.

قاعده‌ی تفکیک: دستورِ LaTeX همیشه با حرفِ ASCII ادامه می‌یابد (\frac, \times)، ولی escapeِ واقعی با رقم، فاصله، فارسی یا پایانِ رشته می‌آید (\n۲.). پیاده‌سازی: rag/services/pdf.py::repair_json_escapes.

۴. محافظِ «وابسته به شکل» را با regexِ تخت نسازید

برای صرفه‌جویی، مسئله‌های وابسته به شکل را قبل از فرستادن کنار می‌گذاشتیم. regexِ اولیه شاملِ شکل|جدول|روبه‌رو|مقابل|زیر را بود و فاجعه‌بار گشاد:

متن چرا رد می‌شد واقعیت
«زاویه‌های روبه‌رو در لوزی برابرند؟» کلمه‌ی «روبه‌رو» یعنی زاویه‌ی مقابل
«کسرهای زیر را گویا کنید: ۲/√۳» «زیر را» داده همان‌جاست
«ظرفی به شکل مخروطی با شعاع ۴» کلمه‌ی «شکل» یعنی شبیهِ مخروط

اندازه‌گیری روی نمونه‌ی ۵۰تایی: ۸۴٪ از رد‌شده‌ها واقعاً قابلِ قضاوت بودند — حدود ۷۸۴ مسئله‌ی سالم بی‌صدا از چرخه بیرون مانده بود.

قاعده‌ی درست دو شرط دارد و هر دو باید برقرار باشند: (۱) ارجاعِ صریح به چیزی بیرونِ متن، و (۲) بعد از آن ارجاع هیچ توصیفی نیامده باشد. پیاده‌سازی: rag/management/commands/rag_verify_solutions.py::needs_figure.

۵. ساختِ سوال: subagent به‌جای API

ساختِ سوالِ چهارگزینه‌ای با o4-mini روی GapGPT ۰٫۷۷ سنت برای هر سوال تمام می‌شود (چهار برابرِ تخمینِ خودِ ابزار، چون مدلِ ریزنینگ است).

همان کار با subagentهای Claude Code (مدلِ Sonnet) رایگان است و بازده‌اش روی ۳۴۵ ورودی ۹۲٪ بود. Haiku برای این کار مناسب نیست — جوابِ نهایی را از راه‌حل بیرون نمی‌کشد.

مسیر حالا ابزار دارد و دستی نیست:

# ۱. جفت‌های (درس، مسئله‌ی تأییدشده) را بیرون بده — رایگان، هیچ فراخوانیِ پولی
python manage.py generate_questions_from_chunks \
    --subject حسابان --grade یازدهم --export /tmp/batches
# ۲. برای هر فایلِ دسته یک subagentِ Sonnet بگذار که خروجی را در /tmp/out بنویسد
# ۳. وارد کن (اعتبارسنجی دارد؛ فایلِ بد هیچ‌چیز نمی‌نویسد)
#    از پنلِ ادمین: /admin/content/question/ → «وارد کردنِ سوال‌ها از فایل»
# ۴. به فایلِ ریپو منتقل کن و کامیت کن
python manage.py seed_content --export --only questions

--export کنارِ دسته‌ها یک *-RULES.txt هم می‌نویسد که عینِ همان PROMPTی است که به مدلِ پولی می‌رفت؛ پس کیفیتِ دو شاخه از هم جدا نمی‌افتد. اندازه‌ی دسته با --batch-size تنظیم می‌شود (پیش‌فرض ۳۰).

⚠️ سقفِ هفتگیِ Sonnet واقعی است و ناگهانی می‌رسد. هر ۳۰ سوال حدودِ ۹۵ هزار توکن می‌برد، پس ۲۰ subagentِ هم‌زمان ≈ ۲ میلیون توکن در یک نشست. قبل از شروع حساب کنید؛ دسته‌ها را کوچک نگه دارید و بینشان نتیجه را وارد کنید.

یک سوال بسازید، نه یک ترجمه. subagentها چند بار خطای حسابِ خودِ کتاب را گرفتند (راه‌حلی که ریشه را ۶ نوشته بود ولی ۶٫۵ بود؛ راه‌حلی که می‌گفت حد وجود ندارد درحالی‌که داشت). یعنی solution_verdict='agrees' فیلترِ اول است، نه تضمین — در پرامپت صریح بخواهید که جواب را مستقل حل کند و اگر نخواند، ردیف را رد کند.


درسِ فراگیر

هر چهار موردِ بالا یک وجهِ مشترک دارند: هیچ‌کدام خطا تولید نمی‌کردند. نه در لاگ، نه در خروجی، نه در تست. فقط دادهٔ کمتر یا بدتر می‌ساختند.

پس: قبل از اعتماد به هر ران انبوه، یک نمونه‌ی ۴۰ تا ۵۰تایی را با چشم بخوانید. هر چهار موردِ این فایل با همین کار پیدا شدند، نه با کدخوانی.