stop the figure guard from throwing away four fifths of what it caught

محافظِ «وابسته به شکل» یک regexِ تخت بود (شکل|جدول|روبه‌رو|مقابل|زیر را) و
مسئله‌ها را بدونِ هیچ فراخوانی کنار می‌گذاشت. فاجعه‌بار گشاد بود:

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

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

قاعده‌ی تازه دو شرط دارد و هر دو باید برقرار باشند: ارجاعِ صریح به چیزی بیرونِ
متن، و نیامدنِ هیچ توصیفی بعد از آن. شرطِ دوم همان چیزی را می‌گیرد که قاعده‌ی
self-contained تولید می‌کند: «با توجه به شکل مقابل که شامل {a,1} است».

اثرش روی دادهٔ واقعی: unjudgeable از ۱٬۴۴۱ به ۵۰۸ و agrees از ۷۹۱ به ۲٬۴۶۶.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 12:36:16 +03:30
parent fa347915c4
commit 96bda905db

View File

@@ -82,12 +82,46 @@ def is_account_limit(exc) -> bool:
return '403' in text and 'insufficient' in text return '403' in text and 'insufficient' in text
# مسئله‌ای که به شکل/جدولِ کتاب ارجاع می‌دهد بدونِ آن شکل قابلِ حل نیست. # مسئله‌ای که به شکل/جدولِ کتاب ارجاع می‌دهد و **محتوای آن را ننوشته** بدونِ
# فرستادنش هم پول می‌سوزاند هم آمار را خراب می‌کند. # تصویر قابلِ حل نیست؛ فرستادنش هم پول می‌سوزاند هم آمار را خراب می‌کند.
NEEDS_FIGURE = re.compile( #
r'شکل|جدول|نمودار|روبه‌رو|روبرو|مقابل|تصویر|نقشه|رسم کنید|بکشید|' # ⚠️ نسخه‌ی اولِ این محافظ یک regexِ تخت بود:
r'رنگ[‌ ]?آمیزی|زیر را|بالا را|وِن' # شکل|جدول|نمودار|روبه‌رو|روبرو|مقابل|...|زیر را|بالا را
# و فاجعه‌بار گشاد بود. سه نمونه از چیزهایی که بی‌دلیل رد می‌کرد:
# «زاویه‌های روبه‌رو در لوزی برابرند؟» ← «روبه‌رو» یعنی مقابل، نه شکل
# «کسرهای زیر را گویا کنید: ۲/√۳» ← داده همان‌جاست
# «به شکل مخروطی با شعاع ۴ و ارتفاع ۱۲» ← «به شکلِ ...» یعنی شبیهِ
# اندازه‌گیری (۱۴۰۵/۰۶/۲۹، نمونه‌ی ۵۰تایی از کتاب‌های بازخوانی‌شده): از
# مسئله‌هایی که این محافظ رد کرده بود، **۸۴٪ واقعاً قابلِ قضاوت بودند** —
# یعنی ~۷۸۴ مسئله‌ی سالم بی‌صدا از چرخه‌ی ساختِ سوال بیرون مانده بودند.
#
# قاعده‌ی تازه دو شرط دارد، و هر دو باید برقرار باشند:
# ۱. ارجاعِ صریح به چیزی بیرونِ متن باشد («شکل زیر»، «جدول روبه‌رو»)
# ۲. بعد از آن ارجاع، توصیفی نیامده باشد (نه «که ...»، نه عدد، نه نماد)
# شرطِ دوم همان چیزی است که قاعده‌ی self-contained تولید می‌کند: مدل حالا
# می‌نویسد «با توجه به شکل مقابل **که شامل {a,1} است**».
_FIGURE_REFERENCE = re.compile(
r'(شکل|جدول|نمودار|تصویر|نقشه)\s*(ِ|های|‌های)?\s*'
r'(زیر|بالا|روبه\u200cرو|روبرو|مقابل|فوق|داده\s*شده)'
r'|با توجه به\s*(شکل|جدول|نمودار|تصویر)'
) )
# نشانه‌ی اینکه محتوای شکل داخلِ متن آمده.
_FIGURE_DESCRIBED = re.compile(r'که\s|شامل|اعضای|مقادیر|برابر|=|\{|\d')
# کارهایی که ذاتاً تصویری‌اند و هیچ متنی نجاتشان نمی‌دهد.
_ART_TASK = re.compile(r'رنگ[‌ ]?آمیزی|رنگ کنید|رنگ\u200cآمیزی')
def needs_figure(problem: str) -> bool:
"""آیا این مسئله بدونِ دیدنِ صفحه‌ی کتاب واقعاً قابلِ قضاوت نیست؟"""
text = problem or ''
if _ART_TASK.search(text):
return True
match = _FIGURE_REFERENCE.search(text)
if not match:
return False
# ۱۶۰ نویسه بعد از ارجاع: جایی که توصیفِ شکل اگر بیاید، می‌آید.
return not _FIGURE_DESCRIBED.search(text[match.end():match.end() + 160])
PROMPT = """تو یک ریاضی‌دانِ دقیق هستی که راه‌حل‌های کتابِ درسیِ ایران را بازبینی می‌کند. PROMPT = """تو یک ریاضی‌دانِ دقیق هستی که راه‌حل‌های کتابِ درسیِ ایران را بازبینی می‌کند.
@@ -183,7 +217,7 @@ class Command(BaseCommand):
rows = list(qs.order_by('collection', 'source_page', 'order')) rows = list(qs.order_by('collection', 'source_page', 'order'))
skipped = self._mark_figure_dependent(rows) skipped = self._mark_figure_dependent(rows)
rows = [r for r in rows if not NEEDS_FIGURE.search(r.problem or '')] rows = [r for r in rows if not needs_figure(r.problem or '')]
if opts['limit']: if opts['limit']:
rows = rows[:opts['limit']] rows = rows[:opts['limit']]
if skipped: if skipped:
@@ -203,7 +237,7 @@ class Command(BaseCommand):
"""قبل از خرجِ یک ریال، آن‌هایی که ذاتاً قابلِ قضاوت نیستند کنار می‌روند.""" """قبل از خرجِ یک ریال، آن‌هایی که ذاتاً قابلِ قضاوت نیستند کنار می‌روند."""
marked = [] marked = []
for chunk in rows: for chunk in rows:
if NEEDS_FIGURE.search(chunk.problem or ''): if needs_figure(chunk.problem or ''):
chunk.solution_verdict = LessonChunk.VERDICT_UNJUDGEABLE chunk.solution_verdict = LessonChunk.VERDICT_UNJUDGEABLE
chunk.check_note = 'به شکل/جدولِ کتاب ارجاع می‌دهد؛ از متن قابلِ قضاوت نیست.' chunk.check_note = 'به شکل/جدولِ کتاب ارجاع می‌دهد؛ از متن قابلِ قضاوت نیست.'
chunk.checked_by = 'pre-filter' chunk.checked_by = 'pre-filter'