Files
MathApp/rag/services/clients.py
Zareian b26aca0f63 label every textbook solution, and build quizzes at the level the app plays
سه کارِ به‌هم‌پیوسته که از یک کشف شروع شدند: راه‌حل‌هایی که vision از کتاب
درآورده گاهی غلط‌اند، و این خطا در بازیابی دیده نمی‌شود — فقط وقتی کسی به
`solution` اعتماد کند خودش را نشان می‌دهد.

۱) بازبینیِ راه‌حل‌ها (rag_verify_solutions)

هر chunk دوباره حل می‌شود و با کتاب مقایسه. نتیجه سه‌حالته است، نه بله/خیر:
اجرای اول با بولی ۱۹٪ خطا گزارش کرد؛ خواندنِ دستیِ نمونه‌ها نشان داد ۳۵٪ از
آن‌ها مسئله‌هایی بودند که به شکلِ کتاب ارجاع می‌دادند و اصلاً قابلِ قضاوت
نبودند، و ۲۴٪ موردی که «۳/۱۰۴» را کسرِ ۳÷۱۰۴ خوانده بود نه عددِ ۳٫۱۰۴. با دو
حالت، «نمی‌دانم» ناچار به «غلط است» ترجمه می‌شد.

با سه حالت و پرامپتی که این دو تله را می‌شناسد: ۲٬۱۱۹ chunk بررسی شد،
۱٬۱۱۲ درست، ۱۲۳ غلط (۵٫۸٪)، ۸۸۱ غیرقابلِ قضاوت. آن ۸۸۱ رایگان برچسب خوردند
چون پیش‌فیلتر قبل از هر فراخوانی کنارشان گذاشت.

`solution` هرگز بازنویسی نمی‌شود — کتاب باید کتاب بماند. برچسب در payloadِ
Qdrant هم می‌نشیند، ولی با نامی که `retrieval._payload_text` نمی‌خواند: یک
نویسه هم به متنی که به مدل می‌رسد اضافه نشد.

نمونه‌ی خطاهای واقعیِ کتاب که پیدا شد:
    BMI = ۱۰۰/(۲٫۷۴)²          کتاب: ۳۳٫۲      درست: ۱۳٫۳
    ۱۶ آبی + ۲۴ صورتی          کتاب: «کل ۵۰»   درست: ۴۰
    بزرگ‌ترین عددِ ۴رقمیِ بی‌تکرار کتاب: ۸۵۳۲     درست: ۹۸۷۶
    g(x) = x³ − ۲              کتاب: «نمایی»   درست: چندجمله‌ای

۲) سوال‌ها فقط از راه‌حل‌های تأییدشده

۱٬۱۹۹ سوال در content/seed/questions.json، همه از chunkهایی که برچسبِ
«درست» گرفته‌اند. هر شش پایه برنامه‌ی درسی دارند (۱٬۹۲۸ درس) — هشتم و دهم
تازه ساخته شدند.

۳) GET /api/nodes/{id}/quiz

واحدِ بازی در اپ «مبحث» است ولی سوال‌ها به «درس» بسته‌اند، و درس اینجا
ریزترین دانه است: یک نوعِ مسئله، نه یک جلسه. نتیجه‌اش این بود که اپ برای یک
آزمونِ چهارسوالی باید ده‌ها درخواست می‌زد. این اندپوینت از همه‌ی درس‌های مبحث
نمونه برمی‌دارد، حداکثر یکی از هر درس تا آزمون روی یک مهارت گیر نکند.

درهم‌ریزی با بذرِ nodeId:userId پایدار است: رفرشِ صفحه همان آزمون را می‌دهد،
نه مجموعه‌ی تازه‌ای که پیشرفت را بی‌معنا کند. `questionCount` جدا از
`requested` می‌آید تا مبحثِ کم‌سوال وانمود نکند کامل است.

──────────
اصلاحاتِ جانبی که سرِ راه لازم شدند:

• QDRANT_TIMEOUT_SECONDS: کلاینت بدونِ timeout ساخته می‌شد و پیش‌فرضِ ۵
  ثانیه‌ایِ کتابخانه ingestِ کتابِ هشتم را روی اولین فراخوانی کشت.
• پاک‌سازیِ نقاطِ نیمه‌نوشته: upsert با wait=True ممکن است نوشته باشد و فقط
  جوابش تایم‌اوت شده باشد؛ مسیرِ جبران فقط ردیف‌های DB را پس می‌گرفت و هر
  retry یک نسخه‌ی تکراری در Qdrant جا می‌گذاشت (۸ مورد در پایه نهم، پاک شد).
• تشخیصِ سقفِ حساب: «insufficient user quota» با فاصله می‌آید نه زیرخط، و
  محافظِ اول نمی‌گرفتش — یک اجرا ۷۷۰ فراخوانیِ محکوم‌به‌شکست زد.
• پرامپتِ vision حالا می‌خواهد محتوای شکل داخلِ متنِ مسئله نوشته شود. ۴۲٪ از
  محتوای موجود به شکلی ارجاع می‌دهد که در متن نیست؛ مدل آن را دیده بود ولی
  از او خواسته نشده بود بنویسدش. کتاب‌های موجود قبل از این قاعده وارد شده‌اند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:11:15 +03:30

57 lines
2.2 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
کلاینت‌های OpenAI و Qdrant به‌صورتِ lazy (نه در سطحِ ماژول).
مخزنِ Rag_backend این‌ها رو موقعِ import می‌ساخت؛ نتیجه این بود که بدونِ
کلید/شبکه، خودِ import هم crash می‌کرد. اینجا کلاینت فقط وقتی واقعاً لازم
بشه ساخته می‌شه و بینِ فراخوانی‌ها cache می‌مونه.
"""
from functools import lru_cache
from django.conf import settings
class RagNotConfigured(RuntimeError):
"""کلید/کتابخانه‌ی لازم برای مسیرِ RAG موجود نیست."""
@lru_cache(maxsize=1)
def get_openai_client():
if not settings.OPENAI_API_KEY:
raise RagNotConfigured('OPENAI_API_KEY تنظیم نشده است.')
try:
from openai import OpenAI
except ModuleNotFoundError as exc:
raise RagNotConfigured('پکیجِ openai نصب نیست (pip install -r requirements.txt).') from exc
return OpenAI(
base_url=settings.OPENAI_BASE_URL,
api_key=settings.OPENAI_API_KEY,
timeout=settings.OPENAI_TIMEOUT_SECONDS,
)
@lru_cache(maxsize=1)
def get_qdrant_client():
if not settings.QDRANT_API_KEY:
raise RagNotConfigured('QDRANT_API_KEY تنظیم نشده است.')
try:
from qdrant_client import QdrantClient
except ModuleNotFoundError as exc:
raise RagNotConfigured('پکیجِ qdrant-client نصب نیست (pip install -r requirements.txt).') from exc
# check_compatibility=False: کلاینتِ نصب‌شده (1.19) با سرورِ تیمِ دیگه
# (1.17) اختلافِ minor داره و کلاینت warning می‌ده؛ API‌هایی که استفاده
# می‌کنیم (get_collections/upsert/query_points/scroll) پایدارن.
return QdrantClient(
url=settings.QDRANT_URL,
api_key=settings.QDRANT_API_KEY,
check_compatibility=False,
timeout=settings.QDRANT_TIMEOUT_SECONDS,
)
def reset_clients_cache():
"""برای تست‌ها / بعد از تغییرِ settings در زمانِ اجرا."""
get_openai_client.cache_clear()
get_qdrant_client.cache_clear()