سیلف اسیسنگ AI سسٹم کیسے بنایا جائے: LLM ایپلی کیشنز کے لیے خودکار ٹیسٹنگ اور ایویلیوایشن پائپ لائن

لہذا، ہم نے AI فیچر کو متعارف کرایا ہے اور یہ ڈیمو میں کام کر رہا ہے۔ آپ کی ٹیم متاثر ہوئی ہے۔ پھر، جب صارف کوئی سوال پوچھتا ہے جو ٹیسٹ کیس سے تھوڑا سا ہٹ جاتا ہے، تو ماڈل اعتماد کے ساتھ کچھ مکمل طور پر غلط بتاتا ہے۔

بڑے پیمانے پر زبان کے ماڈل بنانے کے بارے میں سچائی یہ ہے کہ روایتی سافٹ ویئر ٹیسٹنگ الگ ہوجاتی ہے۔ جب سسٹم ہر بار چلتا ہے تو مختلف متن تیار کرتا ہے، متوقع سادہ دعویٰ آؤٹ پٹ لکھنا ممکن نہیں ہوتا ہے۔

زیادہ تر ٹیوٹوریلز آپ کو سکھائیں گے کہ چیٹ بوٹ کیسے بنایا جائے یا RAG پائپ لائن کو کیسے جوڑنا ہے، اور پھر رک جانا ہے۔ ہم کہتے ہیں "پروڈکشن میں لگائیں” گویا مشکل حصہ ختم ہو گیا ہے۔ لیکن اصل مشکل حصہ یہ جاننا ہے کہ AI کب اچھا ہے اور پکڑنا کب اچھا نہیں ہے۔

اس مضمون میں، میں آپ کو ایک مکمل تشخیصی پائپ لائن بنانے کے عمل سے آگاہ کروں گا۔ ہم تین تشخیصی حکمت عملیوں کا بھی احاطہ کریں گے جو لاگت اور گہرائی کی مختلف سطحوں پر کام کرتی ہیں۔

ہم کیا احاطہ کریں گے:

آپ کو کیا ضرورت ہے

جاری رکھنے کے لیے، آپ کو Python 3.10 یا اس سے زیادہ اور LLM API کالز کے ساتھ کچھ بنیادی تجربے کی ضرورت ہوگی۔ اس سے کوئی فرق نہیں پڑتا کہ آیا آپ OpenAI، Anthropic، یا مقامی ماڈلز استعمال کرتے ہیں، کیونکہ تشخیص کا پیٹرن اسی طرح کام کرتا ہے۔

جج کی مثال کے طور پر LLM کے لیے آپ کو OpenAI API کلید کی بھی ضرورت ہوگی (ہم gpt-4o-mini کیونکہ یہ سستا ہے اور اسکور کرنے کے لیے کافی اچھا ہے)۔

اگر آپ کے پاس پہلے سے ہی LLM پر مبنی ایپ ہے جس کا آپ جائزہ لینا چاہتے ہیں، تو ایک بہت چھوٹی ایپ بھی بہترین ہے۔ اگر نہیں تو، مثالیں خود ساختہ ہیں لہذا آپ ہر چیز کے ساتھ پیروی کر سکتے ہیں۔

یہاں سے انحصار حاصل کریں۔

pip install openai numpy pandas scikit-learn python-dotenv

ایل ایل ایم ایپلی کیشنز میں روایتی ٹیسٹ کیوں ناکام ہوتے ہیں۔

اگر آپ نے کبھی عام سافٹ ویئر کے لیے ٹیسٹ لکھے ہیں، تو آپ ڈرل کو جانتے ہیں۔ اگر کوئی فنکشن اندر جاتا ہے اور ایک قدر نکلتی ہے، تو ہم دعویٰ کرتے ہیں کہ دونوں مماثل ہیں۔ صاف، سادہ اور مکمل۔

لیکن ایل ایل ایم پورے ماڈل کو توڑ دیتا ہے۔ اور وہ ایک دوسرے کے ساتھ ایک طرح سے نہیں بلکہ کئی طریقوں سے ملتے ہیں۔

سب سے پہلے، آؤٹ پٹ تعییناتی نہیں ہے۔ آپ بالکل وہی پرامپٹ دو بار بھیج سکتے ہیں اور مختلف الفاظ وصول کر سکتے ہیں۔ یہاں تک کہ ترتیب temperature=0 چونکہ ماڈل فراہم کنندہ ماڈل کو پردے کے پیچھے اپ ڈیٹ کرتا ہے، اس لیے یہ صارف کو مکمل طور پر محفوظ نہیں کرتا ہے۔ جنوری اور مارچ میں ایک ہی API کال مختلف طریقے سے برتاؤ کر سکتی ہے۔

دوسرا، کوئی صحیح جواب نہیں ہے۔ اگر ایک ایپ کسی دستاویز کا خلاصہ کرتی ہے، تو ایک اچھا خلاصہ کیسا لگتا ہے؟ اگر دو افراد مختلف خلاصے لکھتے ہیں، تو وہ دونوں بالکل ٹھیک ہو سکتے ہیں۔ تم نہیں کر سکتے assertEqual اپنے راستے سے۔

تیسرا، کوئی قابل توجہ بندش نہیں ہے اور لاگز میں کوئی خرابیاں، کریش یا سرخ لکیریں نہیں ہیں۔ ماڈل صرف خاموشی سے ایک بہتر اور پر اعتماد غلط جواب دیتا ہے۔ اپ ٹائم ڈیش بورڈ 100% کہتا ہے، لیکن صارفین بکواس سن رہے ہیں۔ یہ وہ چیز ہے جو واقعی مدد کرتی ہے جب آپ کا LLM ناکام ہوجاتا ہے۔

لہذا، آپ اپنے LLM ایپ کی جانچ نہیں کر سکتے جس طرح آپ اپنے REST API کی جانچ کرتے ہیں۔ پاس/فیل کی بجائے گریڈنگ درکار ہے۔ آپ کو انفرادی آؤٹ پٹ کے بجائے بیچوں میں آؤٹ پٹ کا اندازہ لگانا چاہیے۔ اور اسے ایسی چیز کی ضرورت ہے جو مسلسل چلتی رہے، کیونکہ اس کا معیار وقت کے ساتھ ساتھ کوڈ کی ایک لائن کو تبدیل کیے بغیر بدل سکتا ہے۔

ایل ایل ایم کی تشخیص کے تین درجات

بہت آزمائش اور غلطی کے بعد میں نے جو طریقہ منتخب کیا ہے، وہ ہے تین پرتوں کا استعمال کرنا، سستی اور تیز سے مہنگی اور مکمل۔

  1. پرت 1 فیصلہ کن جانچ ہے۔ اس کے بارے میں سوچیں کہ دروازے پر ایک سیکورٹی گارڈ ہے۔ کیا آؤٹ پٹ درست JSON ہے؟ کیا یہ مشتبہ طور پر چھوٹا ہے یا مضحکہ خیز لمبا؟ کیا اس میں سائیکیڈیلک یو آر ایل ہے؟ یہ اسکین فوری، مفت ہیں، اور آپ کی توقع سے کہیں زیادہ مسائل تلاش کرتے ہیں۔

  2. پرت 2 جج کے طور پر ایل ایل ایم ہے۔ یہاں ہم ایک علیحدہ LLM کال کا استعمال کرتے ہوئے بیس LLM کے نتائج کا جائزہ لیتے ہیں۔ "کیا یہ جواب متعلقہ تھا؟ کیا یہ درست تھا؟ کیا یہ واقعی مددگار تھا؟” ایک ہی ماڈل gpt-4o-mini یہ دوسرے ماڈلز کے کام کو اسکور کرنے میں حیرت انگیز طور پر اچھا ہے، جب تک کہ آپ واضح روبرک فراہم کریں۔

  3. پرت 3 انسانی تشخیص ہے۔ حقیقی لوگ حقیقی نتائج کا جائزہ لے رہے ہیں۔ میں یہ ہر جواب کے لیے نہیں کروں گا کیونکہ یہ ناممکن طور پر سست ہوگا۔ لیکن ہم یہ وقتاً فوقتاً اس بات کو یقینی بنانے کے لیے کرتے ہیں کہ ہماری خودکار پرتیں واقعی اچھی سمجھ سے بھٹک نہ جائیں۔

راز یہ جاننا ہے کہ کون سی پرت استعمال کرنی ہے اور کب، اور ہم ان میں سے ہر ایک پر تعمیر کریں گے۔

پرت 1 کی تعمیر کیسے کریں: عزم کی جانچ

جب میں نے پہلی بار اپنی تشخیصی پائپ لائن بنانا شروع کی تو میں نے سیدھا ٹھنڈی چیزوں پر چھلانگ لگا دی: LLM ججز، مماثلت کے اسکورز ڈالنا، وغیرہ۔ اس دوران، میری ایپ کبھی کبھار بالکل خالی سٹرنگ لوٹتی تھی اور میں نے اسے دو ہفتوں تک محسوس نہیں کیا۔ 2 ہفتے!

یہی وجہ ہے کہ اب میں ہر پراجیکٹ کا آغاز ایک تعییناتی جانچ کے ساتھ کرتا ہوں۔ یہ بہت آسان ہے۔ کوئی ML یا API کال نہیں، صرف سادہ Python آؤٹ پٹ کے بارے میں بنیادی سنجیدگی کے سوالات پوچھتا ہے۔ کیا یہ موجود ہے؟ کیا یہ درست فارمیٹ ہے؟ مشکوک طور پر مختصر؟ کیا ماڈل نے یو آر ایل کو دھوکہ دیا؟

آپ کو لگتا ہے کہ یہ اتنا بنیادی ہے کہ اس سے کوئی فرق نہیں پڑتا ہے۔ میں نے بھی ایسا ہی سوچا۔ اس کے بعد میں نے ایک ماہ کے پروڈکشن لاگ کے ذریعے دوڑایا اور دریافت کیا کہ تقریباً ایک تہائی غلط آؤٹ پٹ جو میں نے چھوٹ دیے تھے وہ ایک چیک کے ذریعے پکڑے گئے تھے جسے میں پانچ منٹ میں لکھ سکتا تھا۔

Deterministic Evaluator کلاس جسے میں اب پہلے دن ہر پروجیکٹ میں شامل کرتا ہوں وہ ہے:

import json
import re
from dataclasses import dataclass


@dataclass
class EvalResult:
    """Holds the result of a single evaluation check."""
    check_name: str
    passed: bool
    score: float  # 0.0 to 1.0
    details: str


class DeterministicEvaluator:
    """Layer 1: Fast, rule-based checks for LLM outputs."""

    def check_json_validity(self, output: str) -> EvalResult:
        """Verify the output is valid JSON when JSON is expected."""
        try:
            json.loads(output)
            return EvalResult("json_validity", True, 1.0, "Valid JSON")
        except json.JSONDecodeError as e:
            return EvalResult("json_validity", False, 0.0, f"Invalid JSON: {e}")

    def check_length_bounds(
        self, output: str, min_chars: int = 10, max_chars: int = 5000
    ) -> EvalResult:
        """Check that output length falls within acceptable bounds."""
        length = len(output)
        if length < min_chars:
            return EvalResult(
                "length_bounds", False, 0.0,
                f"Too short: {length} chars (minimum: {min_chars})"
            )
        if length > max_chars:
            return EvalResult(
                "length_bounds", False, 0.0,
                f"Too long: {length} chars (maximum: {max_chars})"
            )
        return EvalResult("length_bounds", True, 1.0, f"Length OK: {length} chars")

    def check_no_hallucinated_links(self, output: str) -> EvalResult:
        """Detect URLs in output that the model may have fabricated."""
        url_pattern = r'https?://[^s)]}"'<>]+'
        urls = re.findall(url_pattern, output)
        if urls:
            return EvalResult(
                "no_hallucinated_links", False, 0.0,
                f"Found {len(urls)} URLs that may be hallucinated: {urls[:3]}"
            )
        return EvalResult("no_hallucinated_links", True, 1.0, "No URLs found")

    def check_required_sections(
        self, output: str, required: list[str]
    ) -> EvalResult:
        """Verify that required sections or keywords appear in the output."""
        missing = [s for s in required if s.lower() not in output.lower()]
        if missing:
            score = 1.0 - (len(missing) / len(required))
            return EvalResult(
                "required_sections", False, score,
                f"Missing sections: {missing}"
            )
        return EvalResult("required_sections", True, 1.0, "All sections present")

    def check_no_refusal(self, output: str) -> EvalResult:
        """Detect if the model refused to answer when it should not have."""
        refusal_phrases = [
            "i cannot", "i can't", "i'm unable to", "as an ai",
            "i don't have access", "i'm not able to"
        ]
        output_lower = output.lower()
        for phrase in refusal_phrases:
            if phrase in output_lower:
                return EvalResult(
                    "no_refusal", False, 0.0,
                    f"Possible refusal detected: '{phrase}'"
                )
        return EvalResult("no_refusal", True, 1.0, "No refusal detected")

    def run_all(self, output: str, config: dict = None) -> list[EvalResult]:
        """Run all deterministic checks and return results."""
        config = config or {}
        results = [
            self.check_length_bounds(
                output,
                config.get("min_chars", 10),
                config.get("max_chars", 5000)
            ),
            self.check_no_hallucinated_links(output),
            self.check_no_refusal(output),
        ]
        if config.get("expect_json"):
            results.append(self.check_json_validity(output))
        if config.get("required_sections"):
            results.append(
                self.check_required_sections(output, config["required_sections"])
            )
        return results


if __name__ == "__main__":
    evaluator = DeterministicEvaluator()

    # Test with a normal output
    good_output = "Python is a high-level programming language known for its readability."
    results = evaluator.run_all(good_output)
    for r in results:
        print(f"  {r.check_name}: {'PASS' if r.passed else 'FAIL'} ({r.details})")

    # Test with a suspicious output
    bad_output = "Visit https://fake-docs.example.com/api for more details."
    results = evaluator.run_all(bad_output)
    for r in results:
        print(f"  {r.check_name}: {'PASS' if r.passed else 'FAIL'} ({r.details})")

یہ تمام ٹیسٹ ایک ملی سیکنڈ سے بھی کم وقت میں چلتے ہیں اور اس کی کوئی قیمت نہیں ہے۔ لیکن اس کی سادگی سے بیوقوف نہ بنیں۔ کیونکہ سائیکیڈیلک لنک کی اکیلے جانچ نے مجھے اپنے صارفین کو اس سے زیادہ ڈاکومنٹڈ یو آر ایل دینے سے بچایا ہے جتنا میں تسلیم کرنا چاہوں گا۔

ایک چیز جس پر زور دینا بھی ضروری ہے وہ یہ ہے کہ یہ ایک نقطہ آغاز ہے۔ مندرجہ بالا عمومی چیکس تمام LLM ایپس کے لیے کام کریں گے۔ لیکن سب سے بڑی جیت ڈومین کے ساتھ مخصوص کامیابی سے حاصل ہوتی ہے۔ اگر آپ کی ایپ ایس کیو ایل تیار کرتی ہے تو ایک نحوی تجزیہ کار شامل کریں۔ ای میل کا مسودہ تیار کرتے وقت، یقینی بنائیں کہ آپ کے پاس سبجیکٹ لائن اور سلام ہے۔ کوڈ آؤٹ پٹ ہونے کے بعد، اسے لنٹر کے ذریعے چلانے کی کوشش کریں۔

آپ جو بھی چیک یہاں شامل کرتے ہیں وہ ایک کم غلط آؤٹ پٹ ہے جو مہنگی نیچے کی تہوں تک پہنچتا ہے، یا اس سے بھی بدتر، صارفین۔

پرت 2 کیسے بنائیں: جج کے طور پر ایل ایل ایم کی تشخیص

بہت اچھا آؤٹ پٹ نے سنٹی چیک پاس کیا۔ یہ درست JSON ہے، اس کی لمبائی مناسب ہے، اور اس میں کوئی جوڑ توڑ لنک نہیں ہے۔ لیکن یہاں ایک سوال ہے جس کا جواب پرت 1 نہیں دے سکتا۔ کیا واقعی کوئی جواب ہے؟ مددگار?

یہاں تک کہ اگر آؤٹ پٹ مکمل طور پر تشکیل شدہ ہے اور تمام تعییناتی جانچوں کو پاس کرتا ہے، تب بھی یہ پڑھنے والے کے لیے مکمل طور پر بیکار ہو سکتا ہے۔ "فرانس کا دارالحکومت برلن ہے” درست متن ہے، اس کی لمبائی درست ہے، اور اس میں کوئی hallucinatory URLs نہیں ہیں۔ لیکن یہ بھی غلط ہے۔

یہ وہ جگہ ہے جہاں چیزوں کو تھوڑا سا میٹا ملتا ہے۔ ایل ایل ایم جج کا بنیادی خیال یہ ہے کہ یہ ایک الگ ایل ایل ایم کال کرتا ہے جس کا واحد کام مین ماڈل کے آؤٹ پٹ کو پڑھنا اور اسکور کرنا ہے۔ ہاں، ہم AI کو گریڈ AI کے لیے استعمال کر رہے ہیں۔ یہ ایک طالب علم سے دوسرے طالب علم کے ہوم ورک کو گریڈ کرنے کے لیے کہنے کے مترادف ہے۔ لیکن یہ حقیقت میں حیرت انگیز طور پر اچھی طرح سے کام کرتا ہے، اور انتھروپک اور گوگل جیسی لیبز کی تحقیق سے یہ بات سامنے آئی ہے کہ ایل ایل ایم ججوں کا انسانی ریٹرز کے ساتھ گہرا تعلق ہوتا ہے جب انہیں درجہ بندی کے واضح معیار فراہم کیے جاتے ہیں۔

کلیدی جملہ ہے ‘کلیئر گریڈنگ کا معیار’ ورنہ یہ سارا نقطہ نظر الگ ہو جاتا ہے۔

گریڈنگ روبرک کو کیسے ڈیزائن کریں۔

آپ اپنے LLM کو کہہ سکتے ہیں، "مجھے 1 سے 10 تک درجہ دیں” اور آپ کو ہر جگہ سے اسکور واپس ملیں گے۔ 7 ایک بار اگلی بار 5 بن جاتا ہے۔ اسکور بنیادی طور پر بے معنی ہیں کیونکہ ماڈل کی کوئی مشترکہ تعریف نہیں ہے کہ ہر نمبر کا کیا مطلب ہے۔

فکس مخصوص اینکر کی تفصیل کے ساتھ ایک روبرک ہے۔ یہاں کچھ ہے جو مدد کر سکتا ہے.

Score 1 - The response is completely irrelevant, incorrect, or harmful.
Score 2 - The response addresses the topic but contains major errors or omissions.
Score 3 - The response is partially correct but misses key information.
Score 4 - The response is correct and helpful with minor issues.
Score 5 - The response is comprehensive, accurate, and directly addresses the question.

اب دیکھیں کہ ہر سطح آؤٹ پٹ میں کسی ایسی چیز کو کس طرح بیان کرتا ہے جس کی طرف ماحول کے بجائے اشارہ کیا جا سکتا ہے۔ ایک "مکمل طور پر موضوع سے ہٹ کر” رجحان دیکھا گیا ہے۔ یہ "قسم کی بری” نہیں ہے۔ یہ خاصیت وہی ہے جو ریفریوں کو رنز میں مستقل بناتی ہے۔

جج کو کیسے نافذ کیا جائے۔

مکمل LLMJudge کلاس درج ذیل ہے: ہم ڈیزائن کے اہم فیصلوں کو بعد میں دیکھیں گے۔

import json
import os
from openai import OpenAI
from dataclasses import dataclass

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))


@dataclass
class JudgeResult:
    """Holds the result of an LLM judge evaluation."""
    criterion: str
    score: int
    max_score: int
    reasoning: str


RUBRICS = {
    "relevance": {
        "description": "Does the response directly address the user's question?",
        "levels": {
            1: "Completely off-topic or addresses a different question entirely.",
            2: "Tangentially related but misses the core question.",
            3: "Addresses the question but includes significant irrelevant content.",
            4: "Directly addresses the question with minor tangents.",
            5: "Precisely and completely addresses the question asked.",
        },
    },
    "accuracy": {
        "description": "Is the factual content of the response correct?",
        "levels": {
            1: "Contains critical factual errors that would mislead the reader.",
            2: "Multiple factual errors on important points.",
            3: "Mostly accurate but contains one notable error.",
            4: "Accurate with only trivial imprecisions.",
            5: "Completely accurate with no factual errors.",
        },
    },
    "completeness": {
        "description": "Does the response cover all important aspects of the question?",
        "levels": {
            1: "Addresses less than 20 percent of what the question requires.",
            2: "Covers some aspects but misses major required components.",
            3: "Covers the basics but lacks depth on important points.",
            4: "Comprehensive coverage with minor gaps.",
            5: "Thoroughly covers all aspects the question requires.",
        },
    },
}


class LLMJudge:
    """Layer 2: Uses a separate LLM to evaluate response quality."""

    def __init__(self, model: str = "gpt-4o-mini"):
        self.model = model

    def evaluate(
        self, question: str, response: str, criterion: str
    ) -> JudgeResult:
        """Evaluate a single response on a single criterion."""
        rubric = RUBRICS[criterion]
        levels_text = "n".join(
            f"Score {score}: {desc}"
            for score, desc in rubric["levels"].items()
        )

        judge_prompt = f"""You are an expert evaluator. Your job is to score an AI assistant's response.

CRITERION: {rubric['description']}

SCORING RUBRIC:
{levels_text}

USER QUESTION:
{question}

AI RESPONSE:
{response}

Evaluate the response on the criterion above. You must respond with valid JSON only:
{{"score": , "reasoning": "<2-3 sentence explanation>"}}"""

        judge_response = client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": judge_prompt}],
            temperature=0.0,
            response_format={"type": "json_object"},
        )

        result = json.loads(judge_response.choices[0].message.content)
        return JudgeResult(
            criterion=criterion,
            score=result["score"],
            max_score=5,
            reasoning=result["reasoning"],
        )

    def evaluate_all(
        self, question: str, response: str, criteria: list[str] = None
    ) -> list[JudgeResult]:
        """Evaluate a response across all specified criteria."""
        criteria = criteria or list(RUBRICS.keys())
        return [self.evaluate(question, response, c) for c in criteria]


if __name__ == "__main__":
    judge = LLMJudge()

    question = "What is a Python decorator and when should you use one?"
    good_response = (
        "A Python decorator is a function that takes another function as input "
        "and extends its behavior without modifying it. You define a decorator "
        "with the @decorator_name syntax above a function definition. Use "
        "decorators when you need to add cross-cutting concerns like logging, "
        "authentication checks, or caching to multiple functions without "
        "duplicating code in each one."
    )

    results = judge.evaluate_all(question, good_response)
    for r in results:
        print(f"  {r.criterion}: {r.score}/{r.max_score} - {r.reasoning}")

اس کوڈ کے بارے میں چند چیزیں قابل توجہ ہیں:

  1. درجہ حرارت 0 ہے: ہم ججوں سے تخلیقی ہونے کو نہیں کہہ رہے ہیں۔ ہم چاہتے ہیں کہ ایک ہی ان پٹ ہر بار ایک ہی سکور پیدا کرے، یا جتنا ممکن ہو اس کے قریب ہو۔

  2. آؤٹ پٹ JSON کی تشکیل شدہ ہے۔ میں نے یہ مشکل طریقے سے سیکھا۔ اگر آپ جج کو مفت متن میں جواب دینے دیتے ہیں، تو آپ سکور نکالنے کے لیے ٹوٹنے والا پارسنگ کوڈ لکھتے ہیں۔ JSON آؤٹ پٹ کو مجبور کرنا آپ کی زندگی کو بہت آسان بنا دیتا ہے۔

  3. روبرک تمام اشارے پر لاگو ہوتا ہے۔ ججز کبھی بھی اپنے خیالات کا استعمال نہیں کرتے ہیں کہ اچھے معنی کیا ہیں۔ یہ دوبارہ پیدا کرنے کے قابل ہے کیونکہ اسکورز کا موازنہ ہمیشہ روبرک سے کیا جاتا ہے۔

ججوں کی ساکھ سے کیسے نمٹا جائے؟

اس سب کے باوجود سنگل جج کی کال بھی بلند ہو سکتی ہے۔ میں نے ایک کال پر ایک ہی جوابی سکور 4 اور اگلی کال پر 3 دیکھا ہے۔ اگر آپ ان اسکورز کی بنیاد پر فیصلے کر رہے ہیں، تو فرق اہم ہے۔ دو چیزیں مدد کر سکتی ہیں:

پہلا یہ ہے۔ ججوں کی اکثریت نے اتفاق کیا۔. اس کا مطلب ہے ایک ہی تشخیص کو تین بار چلانا اور میڈین لینا۔ ہاں، اس کی قیمت 3 گنا زیادہ ہے۔ تاہم، سکور بہت زیادہ مستحکم ہو جاتا ہے، اور CI/CD گیٹنگ کے فیصلوں کے لیے، استحکام چند سینٹ بچانے سے زیادہ اہم ہے۔

دوسرا ہے۔ پروف ریڈنگ سیٹ. جوابات کے ایک چھوٹے سیٹ کو برقرار رکھیں (شاید 20-30) جن کے پاس پہلے سے ہی قابل اعتماد انسانی اسکور ہوں۔ وقتاً فوقتاً اس پر جج چلائیں۔ اگر جج انسانوں سے اختلاف کرنے لگتے ہیں تو کچھ بدل گیا ہے اور اس کی تحقیقات ہونی چاہیے۔

آپ اتفاق رائے کے نفاذ پر ایک نظر ڈال سکتے ہیں جو یہ بتاتا ہے کہ اسے کیسے ہینڈل کیا جائے۔

import numpy as np


def evaluate_with_consensus(
    judge: LLMJudge,
    question: str,
    response: str,
    criterion: str,
    num_judges: int = 3,
) -> JudgeResult:
    """Run multiple judge evaluations and return the median."""
    results = [
        judge.evaluate(question, response, criterion)
        for _ in range(num_judges)
    ]
    scores = [r.score for r in results]
    median_score = int(np.median(scores))
    median_result = min(results, key=lambda r: abs(r.score - median_score))
    return JudgeResult(
        criterion=criterion,
        score=median_score,
        max_score=5,
        reasoning=f"Consensus ({scores}): {median_result.reasoning}",
    )

پرت 3 بنانے کا طریقہ: انسانی تشخیص کا لوپ

ایک موقع پر، LLM جج نے درستگی کے لیے 5/5، مطابقت کے لیے 5/5، اور مکمل ہونے کے لیے 4/5 جواب دیا۔ اسکور اس وقت تک کامل لگ رہا تھا جب تک کہ ایک ساتھی نے جواب نہیں پڑھا اور کہا، "یہ تکنیکی طور پر درست ہے، لیکن یہ کسی ایسے شخص کے لیے الجھن میں پڑے گا جو ابھی تک ماہر نہیں ہے۔” اور وہ صحیح تھا۔

جواب میں آپ کو معلوم نہ ہونے کے الفاظ استعمال کیے گئے، کلیدی نکات کو تین پیراگراف گہرائی میں دفن کیا گیا، اور مددگار جواب کے بجائے نصابی کتاب کی طرح پڑھیں۔

یہ خودکار تشخیص کی ایک حد ہے۔ LLM ججز حقائق پر مبنی غلطیوں اور ساختی مسائل کا پتہ لگانے میں بہت اچھے ہیں، لیکن ان کے لہجے، سامعین کی وضاحت، اور ‘درست’ اور ‘دراصل مددگار’ کے درمیان ٹھیک ٹھیک فرق کے بارے میں اندھا دھبہ ہوتا ہے۔ یہ اندھے مقامات ہیں جہاں انسانی تشخیص ہوتی ہے۔

واضح ہونے کے لیے، اس کا مطلب ہر ایک جواب کا جائزہ لینے کے لیے ٹیم کی خدمات حاصل کرنا نہیں ہے۔ یہ پیمانہ نہیں ہے اور آپ کو اس کی ضرورت نہیں ہے۔ مقصد زیادہ تنگ ہے۔ ہم باقاعدگی سے انسانی اسکور کے چھوٹے بیچ حاصل کرتے ہیں اور ان اسکورز کو اپنی خودکار پرتوں کے لیے حقیقت کی جانچ کے طور پر استعمال کرتے ہیں۔

ہلکا پھلکا تشریحی انٹرفیس کیسے بنایا جائے۔

اس کے لیے آپ کو واقعی لیبل اسٹوڈیو یا کسی فینسی اینوٹیشن پلیٹ فارم کی ضرورت نہیں ہے۔ جوابات ظاہر کرنے اور اسکور کی درخواست کرنے کے لیے آپ کو صرف ایک ازگر اسکرپٹ کی ضرورت ہے۔

یہاں یہ ہے کہ یہ اعلی سطح پر کیسے کام کرتا ہے: اسکرپٹ سوال جواب کے جوڑے لیتا ہے، انہیں ٹرمینل میں دکھاتا ہے، جائزہ لینے والوں سے 1 سے 5 کے پیمانے پر اسکور کرنے کو کہتا ہے، اور پھر نتائج کو فائل میں محفوظ کرتا ہے۔ ہر تشریح کو JSON کی ایک لائن کے طور پر ذخیرہ کیا جاتا ہے، جسے JSONL فارمیٹ کہا جاتا ہے، لہذا آپ اسے بعد میں آسانی سے دوبارہ لوڈ کر سکتے ہیں، تجزیات چلا سکتے ہیں اور ڈیش بورڈز کو فیڈ کر سکتے ہیں۔

import json
import random
from pathlib import Path
from dataclasses import dataclass, asdict


@dataclass
class Annotation:
    """A single human annotation for an LLM response."""
    question: str
    response: str
    annotator: str
    score: int
    notes: str


class AnnotationCollector:
    """Collects and stores human evaluations."""

    def __init__(self, output_file: str = "annotations.jsonl"):
        self.output_path = Path(output_file)

    def collect_annotation(
        self, question: str, response: str, annotator: str
    ) -> Annotation:
        """Present a question-response pair and collect a human score."""
        print("n" + "=" * 60)
        print(f"QUESTION: {question}")
        print("-" * 60)
        print(f"RESPONSE: {response}")
        print("-" * 60)
        print("Score this response (1-5):")
        print("  1 = Terrible  2 = Poor  3 = Acceptable  4 = Good  5 = Excellent")

        while True:
            try:
                score = int(input("Score: "))
                if 1 <= score <= 5:
                    break
                print("Please enter a number between 1 and 5.")
            except ValueError:
                print("Please enter a valid number.")

        notes = input("Notes (optional, press Enter to skip): ").strip()

        annotation = Annotation(
            question=question,
            response=response,
            annotator=annotator,
            score=score,
            notes=notes,
        )
        self.save(annotation)
        return annotation

    def save(self, annotation: Annotation) -> None:
        """Append annotation to JSONL file."""
        with open(self.output_path, "a") as f:
            f.write(json.dumps(asdict(annotation)) + "n")

    def load_all(self) -> list[Annotation]:
        """Load all saved annotations."""
        annotations = []
        if self.output_path.exists():
            with open(self.output_path) as f:
                for line in f:
                    data = json.loads(line)
                    annotations.append(Annotation(**data))
        return annotations

آئیے اس اسکرپٹ میں کیا ہو رہا ہے اس پر ایک نظر ڈالتے ہیں۔

کہ Annotation ڈیٹاکلاس صرف ایک کنٹینر ہے جس میں ایک جائزہ، اصل سوال، ماڈل کا جواب، کس نے اس کا جائزہ لیا، اس نے جو اسکور دیا، اور اس میں شامل کیے گئے کسی بھی نوٹس کے بارے میں سب کچھ رکھتا ہے۔ یہ فینسی نہیں ہے، لیکن تشکیل شدہ فارمیٹ بعد میں جائزہ لینے والوں کے درمیان اسکورز کا موازنہ کرنا آسان بناتا ہے۔

کہ collect_annotation طریقہ وہ ہے جہاں حقیقی جائزہ لیا جاتا ہے۔ بصری جداکاروں کا استعمال کرتے ہوئے ٹرمینل پر سوالات اور جوابات پرنٹ کریں تاکہ جائزہ لینے والے انہیں واضح طور پر پڑھ سکیں، پھر اسکور کے لیے اشارہ کریں۔

ان پٹ کی توثیق کے ساتھ جبکہ حقیقی لوپ یہاں اہم ہے۔ تبصروں کی فائل میں کوڑے کے ڈیٹا سے بچنے کے لیے، ہم جائزہ لینے والوں سے اس وقت تک پوچھتے رہتے ہیں جب تک کہ وہ 1 اور 5 کے درمیان درست نمبر فراہم نہ کریں۔

محفوظ کرنے کا طریقہ Annotation.jsonl فائل میں ہر ایک تشریح کو ایک واحد JSON لائن کے طور پر شامل کرتا ہے۔ میں سادہ JSON سرنی کے بجائے JSONL (ایک JSON آبجیکٹ فی لائن) استعمال کر رہا ہوں کیونکہ اسے شامل کرنا آسان ہے۔ آپ پوری فائل کو پڑھے اور دوبارہ لکھے بغیر نئے تبصرے شامل کر سکتے ہیں، جو وقت کے ساتھ سینکڑوں جائزے جمع کرتے وقت اہم ہے۔

اور load_all سب کچھ دوبارہ پڑھتا ہے اور ہر لائن کو تشریح آبجیکٹ میں پارس کرتا ہے۔ جب آپ تبصروں کا تجزیہ کرنا چاہتے ہیں اور ان کا LLM جائزہ لینے والے کے اسکور سے موازنہ کرنا چاہتے ہیں یا جائزہ لینے والوں کے درمیان معاہدے کا حساب لگانا چاہتے ہیں تو اسے کال کریں۔

عملی طور پر، آپ اسے پروڈکشن لاگ یا گولڈن ڈیٹاسیٹ سے سوال جواب کے جوڑوں کا ایک سیٹ کھلا کر استعمال کرتے ہیں۔ آپ یہ ہفتہ وار جائزہ سیشن کے دوران کر سکتے ہیں جہاں ٹیم کے ممبران 30 منٹ میں 20 سے 30 جوابات کو گریڈ دیتے ہیں۔ یہ چھوٹی سی سرمایہ کاری آپ کی خودکار تہوں کو کیلیبریٹ کرنے کے لیے قابل اعتماد، حقیقی دنیا کی معلومات فراہم کرتی ہے۔

تشریح کرنے والوں کے درمیان معاہدے کا حساب کیسے لگائیں۔

اب یہاں ایک مسئلہ ہے جس میں آپ کو جلدی ہو گی۔ اگر آپ دو لوگوں سے ایک ہی جواب کی درجہ بندی کرنے کو کہیں گے تو وہ آپ کو مختلف سکور دیں گے۔ کیا جواب مبہم ہے، یا روبرک مبہم ہے؟

ہمیں اس کی پیمائش کرنے کے لیے ایک طریقہ درکار ہے، اور کوہن کا کپا ایسا کرنے کے لیے معیاری ٹول ہے۔ یہ بنیادی طور پر آپ کو بتاتا ہے کہ دونوں تشریح کنندگان کتنے متفق ہیں، معاہدے کی ڈگری کے لیے ایڈجسٹ کیا گیا جس کی آپ اتفاق سے توقع کریں گے۔

from sklearn.metrics import cohen_kappa_score


def measure_agreement(
    scores_annotator_1: list[int], scores_annotator_2: list[int]
) -> dict:
    """Calculate inter-annotator agreement using Cohen's Kappa."""
    kappa = cohen_kappa_score(scores_annotator_1, scores_annotator_2)

    interpretation = "poor"
    if kappa > 0.8:
        interpretation = "almost perfect"
    elif kappa > 0.6:
        interpretation = "substantial"
    elif kappa > 0.4:
        interpretation = "moderate"
    elif kappa > 0.2:
        interpretation = "fair"

    exact_agreement = sum(
        a == b for a, b in zip(scores_annotator_1, scores_annotator_2)
    ) / len(scores_annotator_1)

    return {
        "cohens_kappa": round(kappa, 3),
        "interpretation": interpretation,
        "exact_agreement": round(exact_agreement, 3),
    }


if __name__ == "__main__":
    # two annotators scored the same 10 responses
    annotator_a = [5, 4, 3, 4, 5, 2, 3, 4, 5, 4]
    annotator_b = [5, 4, 4, 4, 5, 3, 3, 4, 5, 3]

    agreement = measure_agreement(annotator_a, annotator_b)
    print(f"Cohen's Kappa: {agreement['cohens_kappa']}")
    print(f"Interpretation: {agreement['interpretation']}")
    print(f"Exact Agreement: {agreement['exact_agreement']:.0%}")

آپ 0.6 یا اس سے زیادہ کا کپپا چاہیں گے۔ مندرجہ ذیل مواد اور روبرک مسئلہ ہیں، تشریح کرنے والے کا نہیں۔ واپس جائیں اور ہر سکور کی سطح کے لیے مزید مخصوص مثالیں شامل کریں۔ اس وقت تک بہتری لاتے رہیں جب تک کہ لوگ مستقل طور پر متفق نہ ہوں۔ عام طور پر 2-3 تکرار کی ضرورت ہوتی ہے۔

ریگریشن ٹیسٹنگ پائپ لائن کیسے بنائی جائے۔

آپ ان منظرناموں کو دیکھ سکتے ہیں جو آپ کے ساتھ یا آپ کے جاننے والے دوسرے لوگوں کے ساتھ ہوا ہو گا۔ آپ کو جو بھی غلط آؤٹ پٹ ملتا ہے اسے ٹھیک کرنے کے لیے پرامپٹ کو ایڈجسٹ کریں۔ یہ اب کام کرتا ہے اور کچھ آؤٹ پٹ بہتر ہیں۔ بعد میں، آپ نے اسے بھیجنے کے ایک ہفتہ بعد، آپ کو احساس ہوا کہ تبدیلیوں نے تین دوسرے جوابات کو روک دیا ہے جن پر آپ نے غور نہیں کیا تھا۔

یہ ایک بہت عام واقعہ ہے۔ واحد راستہ ریگریشن ٹیسٹنگ ہے۔ اگر آپ نے کبھی روایتی سافٹ ویئر ڈویلپمنٹ کیا ہے، تو آپ شاید پہلے ہی جان چکے ہوں گے کہ ریگریشن ٹیسٹنگ کا کیا مطلب ہے۔ اس کا مطلب ہے کہ جب بھی آپ کوئی تبدیلی کرتے ہیں تو ٹیسٹوں کا ایک مقررہ سیٹ دوبارہ چلانا۔ خاص طور پر، اس بات کو یقینی بنانا کہ جو چیز پہلے سے کام کر رہی ہے اس میں خلل نہ پڑے۔

رجعت کا لفظی معنی پیچھے کی طرف جانا ہے۔ سسٹم سوال کو درست طریقے سے پروسیس کر رہا تھا لیکن اب تبدیلی کے بعد ایسا نہیں ہے۔

عام سافٹ ویئر میں، ریگریشن ٹیسٹ عام طور پر یونٹ ٹیسٹ یا انضمام ٹیسٹ ہوتے ہیں۔ LLM ایپلیکیشنز کے لیے، چیزیں تھوڑی مختلف طریقے سے کام کرتی ہیں۔ درست آؤٹ پٹ کی تصدیق کرنے کے بجائے، آپ جوابات کا ایک سیٹ اسکور کرتے ہیں اور ان سکور کا پچھلے رنز سے موازنہ کرتے ہیں۔ اگر آپ کا سکور گر جاتا ہے، تو کچھ پیچھے ہٹ گیا ہے۔ خیال ایک ہی ہے، لیکن طریقہ کار پاس/فیل دعووں کی بجائے گریڈنگ کے ارد گرد بنایا گیا ہے۔

سنہری ڈیٹا سیٹ کیسے بنایا جائے۔

گولڈن ڈیٹاسیٹ سوالات کی تیار کردہ فہرست ہے جو اس بات کی نمائندگی کرتی ہے کہ آپ کی ایپ کو اصل میں کیا ہینڈل کرنا چاہیے۔ جب بھی کچھ تبدیل ہوتا ہے (ایک نیا پرامپٹ، ایک نیا ماڈل، یا اپ ڈیٹ شدہ سرچ منطق)، ہم سسٹم کو اس فہرست کے خلاف چلاتے ہیں اور آخری رن سے اسکور کا موازنہ کرتے ہیں۔

import json
from pathlib import Path
from dataclasses import dataclass, asdict


@dataclass
class GoldenExample:
    """A single test case in the golden dataset."""
    id: str
    question: str
    reference_answer: str
    category: str
    difficulty: str  # "easy", "medium", "hard"
    criteria: list[str]  # which criteria to evaluate


class GoldenDataset:
    """Manages a curated evaluation dataset."""

    def __init__(self, filepath: str = "golden_dataset.json"):
        self.filepath = Path(filepath)
        self.examples: list[GoldenExample] = []
        if self.filepath.exists():
            self.load()

    def add(self, example: GoldenExample) -> None:
        """Add a new example to the dataset."""
        self.examples.append(example)
        self.save()

    def get_by_category(self, category: str) -> list[GoldenExample]:
        """Filter examples by category."""
        return [e for e in self.examples if e.category == category]

    def save(self) -> None:
        """Persist dataset to disk."""
        data = [asdict(e) for e in self.examples]
        with open(self.filepath, "w") as f:
            json.dump(data, f, indent=2)

    def load(self) -> None:
        """Load dataset from disk."""
        with open(self.filepath) as f:
            data = json.load(f)
            self.examples = [GoldenExample(**item) for item in data]

    def summary(self) -> dict:
        """Return dataset statistics."""
        categories = {}
        for e in self.examples:
            categories[e.category] = categories.get(e.category, 0) + 1
        return {
            "total_examples": len(self.examples),
            "categories": categories,
        }

اس کی تعمیر کے دوران میں نے جو چیزیں سیکھی ہیں ان میں سے ایک یہ ہے کہ آپ کو 50 سے 100 مثالوں سے شروع کرنا چاہیے۔ یہ بامعنی رجعت کو حاصل کرنے کے لیے کافی ہے بغیر ہر تشخیص کے لیے طویل وقت کی ضرورت کے۔

آپ کو ایج کیسز بھی شامل کرنا چاہئے – عجیب سوالات جنہوں نے پہلے آپ کے ماڈل کو توڑا ہے۔ اگر آپ کا ڈیٹا سیٹ 90% آسان سوالات ہیں، تو آپ کو معلوم نہیں ہوگا کہ مشکل سوالات کب ناکام ہونا شروع ہو جائیں گے۔

آخر میں، اس کو ایک زندہ دستاویز سمجھیں۔ جب بھی آپ کو پروڈکشن میں کوئی مسئلہ درپیش ہو تو اسے سنہری ڈیٹاسیٹ کی مثال میں بدل دیں۔ کئی مہینوں کے دوران، ڈیٹا سیٹ عام ٹیسٹ کے سوالات سے تفصیلی نقشوں میں تیار ہوتا ہے جو ظاہر کرتا ہے کہ آپ کی ایپ کہاں کمزور ہے۔

CI/CD میں اسسمنٹ کیسے چلائیں۔

اب ہم سب کچھ ایک ساتھ جوڑتے ہیں۔ یہ ریگریشن پائپ لائن کلاس سنہری ڈیٹاسیٹ کے خلاف سسٹم کو چلاتی ہے، تمام جوابات کو اسکور کرتی ہے اور نتائج کا پچھلے رنز سے موازنہ کرتی ہے۔

import json
from datetime import datetime, timezone
from dataclasses import dataclass, asdict


@dataclass
class EvalRun:
    """Records the results of one full evaluation run."""
    run_id: str
    timestamp: str
    model: str
    prompt_version: str
    total_examples: int
    avg_scores: dict  # criterion -> average score
    pass_rate: float  # percentage of examples above threshold
    failures: list[dict]  # examples that scored below threshold


class RegressionPipeline:
    """Runs evaluation against golden dataset and detects regressions."""

    def __init__(
        self,
        deterministic_eval: "DeterministicEvaluator",
        llm_judge: "LLMJudge",
        threshold: float = 3.5,
    ):
        self.det_eval = deterministic_eval
        self.judge = llm_judge
        self.threshold = threshold

    def run(
        self,
        golden_dataset: "GoldenDataset",
        generate_fn: callable,
        model_name: str,
        prompt_version: str,
    ) -> EvalRun:
        """Run full evaluation pipeline against golden dataset.

        Args:
            golden_dataset: The dataset to evaluate against.
            generate_fn: A function that takes a question string and
                         returns the model's response string.
            model_name: Identifier for the model being tested.
            prompt_version: Identifier for the prompt version.
        """
        all_scores = {}
        failures = []

        for example in golden_dataset.examples:
            # Generate response
            response = generate_fn(example.question)

            # Layer 1: Deterministic checks
            det_results = self.det_eval.run_all(response)
            det_failures = [r for r in det_results if not r.passed]

            if det_failures:
                failures.append({
                    "id": example.id,
                    "question": example.question,
                    "layer": "deterministic",
                    "details": [r.details for r in det_failures],
                })
                continue

            # Layer 2: LLM judge
            judge_results = self.judge.evaluate_all(
                example.question, response, example.criteria
            )

            for result in judge_results:
                if result.criterion not in all_scores:
                    all_scores[result.criterion] = []
                all_scores[result.criterion].append(result.score)

                if result.score < self.threshold:
                    failures.append({
                        "id": example.id,
                        "question": example.question,
                        "layer": "llm_judge",
                        "criterion": result.criterion,
                        "score": result.score,
                        "reasoning": result.reasoning,
                    })

        avg_scores = {
            criterion: sum(scores) / len(scores)
            for criterion, scores in all_scores.items()
        }

        total_evaluated = len(golden_dataset.examples)
        pass_count = total_evaluated - len(failures)

        return EvalRun(
            run_id=f"eval_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}",
            timestamp=datetime.now(timezone.utc).isoformat(),
            model=model_name,
            prompt_version=prompt_version,
            total_examples=total_evaluated,
            avg_scores=avg_scores,
            pass_rate=pass_count / total_evaluated if total_evaluated else 0,
            failures=failures,
        )

    def compare_runs(self, baseline: EvalRun, current: EvalRun) -> dict:
        """Compare two evaluation runs to detect regressions."""
        regressions = {}
        improvements = {}

        for criterion in current.avg_scores:
            if criterion in baseline.avg_scores:
                diff = current.avg_scores[criterion] - baseline.avg_scores[criterion]
                if diff < -0.2:  # Score dropped by more than 0.2
                    regressions[criterion] = {
                        "baseline": baseline.avg_scores[criterion],
                        "current": current.avg_scores[criterion],
                        "change": round(diff, 3),
                    }
                elif diff > 0.2:
                    improvements[criterion] = {
                        "baseline": baseline.avg_scores[criterion],
                        "current": current.avg_scores[criterion],
                        "change": round(diff, 3),
                    }

        return {
            "verdict": "REGRESSION" if regressions else "PASS",
            "regressions": regressions,
            "improvements": improvements,
            "pass_rate_change": current.pass_rate - baseline.pass_rate,
        }

اب آپ اسے اپنی CI/CD پائپ لائن سے جوڑ سکتے ہیں تاکہ جب بھی کوئی پرامپٹ یا ماڈل کنفیگریشن کو تبدیل کرے تو یہ چلتا ہے۔ اگر compare_runs رپورٹ REGRESSIONتعمیر ناکام ہو جاتی ہے۔ کوئی بھی اسے اس وقت تک تعینات نہیں کرے گا جب تک کہ وہ یہ نہ جان لیں کہ کیا غلط ہے۔

یہ کیسے بتایا جائے کہ آیا واقعی AI بہتر ہو رہا ہے: شماریاتی اہمیت

لہذا ہم نے اشارے کو ایڈجسٹ کیا اور اوسط سکور 3.8 سے 4.0 پر چلا گیا۔ یہ منانے کا وقت ہے، ٹھیک ہے؟ شاید یا ہوسکتا ہے کہ 0.2 کی بہتری صرف بے ترتیب شور ہے۔

50 سے 100 مثالوں کے سنہری ڈیٹاسیٹ کے ساتھ، اکیلے تغیرات آسانی سے اتنے بڑے اسکور کے فرق کو جنم دے سکتے ہیں۔ یہ تعین کرنے کے لیے حقیقی شماریاتی جانچ کی ضرورت ہے کہ آیا تبدیلیاں حقیقی ہیں۔

اگر آپ نے تھوڑی دیر میں اعدادوشمار نہیں کیے ہیں، تو یہ ایک فوری پرائمر ہے۔ کوئی راستہ نہیں پیئرڈ ٹی ٹیسٹ پیمائش کے دو سیٹوں کا موازنہ کرنے کا ایک طریقہ جو آپس میں جڑے ہوئے ہیں۔ ہمارے معاملے میں، ہر جوڑا ایک ہی سوال ہے جو سسٹم کے دو مختلف ورژن (پرانا پرامپٹ اور نیا پرامپٹ) میں بنایا گیا ہے۔

ٹیسٹ ہر جوڑے کو دیکھتا ہے، اس کا حساب لگاتا ہے کہ ہر سوال پر اسکور میں کتنا فرق آیا، اور پھر پوچھتا ہے کہ "کیا یہ تبدیلیاں مستقل طور پر ایک سمت میں ہوتی ہیں، یا وہ تصادفی طور پر تقسیم ہوتی ہیں؟”

اگر تبدیلیاں مطابقت رکھتی ہیں (زیادہ تر سوالات نئے پرامپٹ کے ساتھ زیادہ اسکور کرتے ہیں)، تو ٹیسٹ کم پی-ویلیو دے گا، یعنی بہتری کا امکان حقیقی ہے۔ اگر تبدیلیاں پوری جگہ پر ہیں (کچھ سوالات بہتر ہیں، کچھ بدتر ہیں، کوئی واضح نمونہ نہیں ہے)، p-value زیادہ ہوگی، یعنی ہم اس بات کا یقین نہیں کر سکتے کہ نیا ورژن اصل میں بہتر ہے۔

ہم کیوں استعمال کرتے ہیں a جوڑا بنایا ٹی ٹیسٹ اور عام ٹی ٹیسٹ کے درمیان فرق یہ ہے کہ یہ مسئلہ کی مشکل کو بیان کرتا ہے۔ کچھ سوالات فطری طور پر دوسروں کے مقابلے میں زیادہ مشکل ہوتے ہیں اور جوڑی کے ذریعے ماپا جا سکتا ہے۔ فی سوال تبدیل کریں۔ آپ محض اسکور کے دو غیر متعلقہ بیچوں کا موازنہ نہیں کر رہے ہیں۔

اس کو نافذ کرنے کا طریقہ یہاں ہے:

from scipy import stats
import numpy as np


def is_improvement_significant(
    scores_before: list[float],
    scores_after: list[float],
    alpha: float = 0.05,
) -> dict:
    """Test whether a score improvement is statistically significant.

    Uses a paired t-test since the same questions are evaluated in both runs.
    """
    t_stat, p_value = stats.ttest_rel(scores_after, scores_before)
    mean_diff = np.mean(scores_after) - np.mean(scores_before)

    return {
        "mean_before": round(np.mean(scores_before), 3),
        "mean_after": round(np.mean(scores_after), 3),
        "mean_difference": round(mean_diff, 3),
        "p_value": round(p_value, 4),
        "is_significant": p_value < alpha,
        "direction": "improvement" if mean_diff > 0 else "regression",
        "recommendation": (
            "Safe to deploy"
            if p_value < alpha and mean_diff > 0
            else "Do not deploy - change is not a significant improvement"
        ),
    }


if __name__ == "__main__":
    # scores on 20 golden examples, before and after a prompt change
    before = [3, 4, 3, 5, 4, 3, 4, 4, 3, 5, 4, 3, 4, 3, 4, 5, 3, 4, 4, 3]
    after =  [4, 4, 4, 5, 5, 3, 4, 5, 4, 5, 4, 4, 4, 4, 5, 5, 4, 4, 5, 4]

    result = is_improvement_significant(before, after)
    print(f"Mean: {result['mean_before']} -> {result['mean_after']}")
    print(f"p-value: {result['p_value']}")
    print(f"Significant: {result['is_significant']}")
    print(f"Recommendation: {result['recommendation']}")

اگر p-value 0.05 سے نیچے واپس آجاتی ہے، تو 5% سے کم امکان ہے کہ بہتری قسمت کی وجہ سے تھی۔ اس وقت جب اس کی ترسیل ہوتی ہے۔ اس سے آگے کی کوئی بھی چیز اور کوئی بھی بہتری محض شور ہو سکتی ہے، لہذا اوسط کو تقسیم نہ کریں چاہے وہ کتنا ہی اچھا کیوں نہ ہو۔

یہ سب ایک ساتھ کیسے باندھیں: ایک مکمل تشخیصی فن تعمیر

آئیے تینوں تہوں کو ایک سنگل آرکیسٹریٹر میں جوڑتے ہیں۔ یہ وہ طبقہ ہے جو ہر چیز کو جوڑتا ہے۔ یہ سب سے پہلے ایک تعییناتی جانچ چلاتا ہے، پاس یا ناکام ہونے کے لیے LLM فیصلے کی طرف بڑھتا ہے، اور تدارک کے لیے اختیاری طور پر انسانی تشخیص کرتا ہے۔

class EvaluationOrchestrator:
    """Coordinates all three evaluation layers into a single pipeline."""

    def __init__(self):
        self.det_eval = DeterministicEvaluator()
        self.llm_judge = LLMJudge()
        self.annotation_collector = AnnotationCollector()

    def evaluate_response(
        self,
        question: str,
        response: str,
        run_human_eval: bool = False,
    ) -> dict:
        """Run the complete evaluation pipeline on a single response."""

        # Layer 1: Deterministic (always runs, every request)
        det_results = self.det_eval.run_all(response)
        det_passed = all(r.passed for r in det_results)

        if not det_passed:
            return {
                "status": "FAIL",
                "layer": "deterministic",
                "details": [r for r in det_results if not r.passed],
                "recommendation": "Fix structural issues before deeper eval",
            }

        # Layer 2: LLM Judge (runs on sample or in CI)
        judge_results = self.llm_judge.evaluate_all(question, response)
        avg_score = sum(r.score for r in judge_results) / len(judge_results)

        if avg_score < 3.5:
            return {
                "status": "FAIL",
                "layer": "llm_judge",
                "avg_score": avg_score,
                "details": judge_results,
                "recommendation": "Response quality below threshold",
            }

        # Layer 3: Human eval (periodic calibration)
        if run_human_eval:
            annotation = self.annotation_collector.collect_annotation(
                question, response, annotator="reviewer"
            )
            return {
                "status": "PASS" if annotation.score >= 4 else "REVIEW",
                "layer": "human",
                "automated_score": avg_score,
                "human_score": annotation.score,
            }

        return {
            "status": "PASS",
            "layer": "llm_judge",
            "avg_score": avg_score,
            "details": judge_results,
        }

وہ چیزیں جو کاش مجھے جلد معلوم ہوتی

میں کچھ چیزوں کے ساتھ ختم کرنا چاہتا ہوں کاش کسی نے مجھے اپنے تشخیصی نظام کی تعمیر شروع کرنے سے پہلے بتا دیا ہوتا۔

سب سے پہلے، ایک ہی وقت میں تینوں تہوں کی تعمیر نہ کریں۔ عزمی جانچ کے ساتھ شروع کریں، پھر جہاز بھیجیں۔ آپ حیران ہوں گے کہ آپ خود کتنے مسائل دریافت کرتے ہیں، اور جیسے ہی آپ انہیں لکھتے ہیں، آپ کو واقعی اس بات کی وضاحت کرنے کی ضرورت ہے کہ آپ کی ایپ کے لیے صحیح آؤٹ پٹ کا کیا مطلب ہے۔ جب آپ کو ضرورت ہو تو LLM ججوں کو شامل کریں اور بعد میں انسانی تشخیصات شامل کریں۔

دوسرا، مہینے میں ایک بار اپنے ججوں کو انسانوں کے خلاف چیک کریں۔ اپنے LLM ججوں کو 20-30 جوابات پر چلائیں جن کے پہلے ہی انسانی اسکور ہیں۔ اگر ججز اوسطاً 0.5 پوائنٹس سے زیادہ بڑھے تو کچھ بدل گیا ہے۔ ہو سکتا ہے جج ماڈل کو اپ ڈیٹ کر دیا گیا ہو یا روبرک نئے فیل موڈ کا احاطہ نہ کرے۔ کسی بھی طرح سے، ری کیلیبریشن ضروری ہے۔

تیسرا، ہر پیداواری ناکامی ایک ٹیسٹ کیس بن جاتی ہے۔ یہ شاید سب سے زیادہ مفید عادت ہے۔ کیا کچھ ٹوٹ گیا ہے؟ بہت اچھا یہ ایک نئی سنہری ڈیٹا سیٹ کی مثال ہے۔ چند مہینوں کے بعد، آپ کا ڈیٹا سیٹ اب صرف عام ٹیسٹوں کا مجموعہ نہیں ہے، بلکہ آپ کی ایپ کے ناکام ہونے کے تمام طریقوں کا تفصیلی نقشہ ہے۔

اور آخر میں، کامل جائزے کے اسکور کا پیچھا نہ کریں۔. میں نے دیکھا ہے کہ ٹیمیں اپنی ریٹنگ 4.2 سے 4.5 تک حاصل کرنے کے لیے اپنے اشارے کو لامتناہی طور پر موافقت کرتی ہیں، صرف یہ جاننے کے لیے کہ ان کے روبرکس پر بلائنڈ دھبے تھے اور صارفین اب بھی مطمئن نہیں تھے۔ اسکورز ایک ٹول ہیں، مقصد نہیں، لہٰذا انسانی تشخیص موجود ہے تاکہ یہ معلوم کیا جا سکے کہ نمبروں کی کمی کیا ہے۔

ختم

ہم نے اس مضمون میں بہت کچھ کا احاطہ کیا ہے، تو آئیے اسے ایک ساتھ رکھیں۔ بنیادی مسئلہ یہ ہے کہ ایل ایل ایم ایپلی کیشنز روایتی سافٹ ویئر سے مختلف طریقے سے ناکام ہو جاتی ہیں۔ کوئی کریش نہیں، کوئی ایرر لاگ نہیں، کوئی اسٹیک ٹریس نہیں۔ یہ صرف ایک پر اعتماد، اچھی طرح سے تیار کردہ غلط جواب ہے۔

اور چونکہ آؤٹ پٹ غیر متعین ہے، اس لیے اسے ایک سادہ دعوے سے جانچا نہیں جا سکتا۔ ایک بالکل مختلف نقطہ نظر کی ضرورت ہے۔

یہ نقطہ نظر ایک تہہ دار تشخیصی پائپ لائن ہے۔

  • پرت 1 (حتمی جانچ) ہم بنیادی باتوں کا خیال رکھیں گے۔ کیا آؤٹ پٹ درست ہے، مناسب لمبائی کا، اور سائیکیڈیلک یو آر ایل سے پاک ہے؟ یہ تیز، مفت ہے اور آپ کی توقع سے زیادہ مسائل کو پکڑتا ہے۔

  • پرت 2 (LLM-جج) معنوی تشخیص حاصل کریں۔ کیا آپ کے جوابات واقعی متعلقہ، درست اور مکمل ہیں؟ اپنے جج ماڈل کو اسکورنگ کے مخصوص معیار کے ساتھ واضح روبرک فراہم کرکے، آپ ناقابل یقین حد تک قابل اعتماد اور خودکار معیار کے اسکور حاصل کرسکتے ہیں۔

  • پرت 3 (انسانی تشخیص) پورے نظام کو کیلیبریٹ رکھتا ہے۔ باقاعدگی سے طے شدہ انسانی جائزوں کی ایک چھوٹی سی تعداد ایسے لطیف مسائل کو پکڑتی ہے جو خودکار اسکورنگ سے محروم رہتے ہیں، جیسے کہ لہجہ، وضاحت، اور "درست” اور "واقعی مددگار” کے درمیان فرق۔

تین تہوں کے علاوہ، آپ نے سیکھا کہ سنہری ڈیٹاسیٹ کو ریگریشن ٹیسٹنگ پائپ لائن بنانے کے لیے کس طرح استعمال کرنا ہے تاکہ پیداوار تک پہنچنے سے پہلے معیار کی گراوٹ کو پکڑ سکے۔ آپ نے یہ بھی سیکھا کہ شماریاتی اہمیت کے ٹیسٹوں کا استعمال کیسے کریں تاکہ یہ یقینی بنایا جا سکے کہ بہتری حقیقی ہیں نہ کہ صرف شور۔

اگر ایک چیز ہے جو میں چاہتا ہوں کہ آپ اسے چھین لیں، تو یہ چھوٹی سے شروع کرنا ہے۔ ایک ہفتے کے آخر میں یہ سب بنانے کی کوشش نہ کریں۔ آج ہی اپنے پروجیکٹ میں DeterministicEvaluator کلاس شامل کریں۔ اس میں تقریباً 5 منٹ لگتے ہیں اور اس وقت غائب ہونے والی کسی بھی چیز کو فوری طور پر پکڑنا شروع کر دے گا۔ پھر، جب آپ گہرائی سے جانچ کے لیے تیار ہوں، تو ایک LLM جج شامل کریں۔ اس کے بعد ہم ایپ کے پختہ ہونے کے ساتھ ہی انسانی جائزوں اور ریگریشن ٹیسٹنگ میں پرت دیتے ہیں۔

وہ ٹیمیں جو قابل اعتماد AI پروڈکٹس بھیجتی ہیں وہ بہترین ماڈلز کے ساتھ نہیں ہیں۔ وہ وہی ہیں جو جانتے ہیں کہ ماڈل کب ناکام ہو جاتا ہے اور صارفین کے کرنے سے پہلے اسے پکڑنے کے لیے سہاروں کو بنایا ہے۔

اوپر تک سکرول کریں۔