অধ্যায় 7 · মৌলিক ইভ্যাল ও রিগ্রেশন টেস্টিং
রুব্রিক, রিগ্রেশন ও রিলিজ গেট
- পৃষ্ঠা 14 / 22
- 25 মিনিট পড়া
আক্রার কোয়ামে তার অ্যাসিস্ট্যান্টকে ততক্ষণ টিউন করল, যতক্ষণ না একটা ল্যাঙ্গুয়েজ মডেল, উত্তরগুলো গ্রেড করতে গিয়ে, তাকে ভালো নম্বর দিল। তারপর এক সহকর্মী লক্ষ করলেন, গ্রেডার সবচেয়ে লম্বা উত্তরগুলোই বেশি পছন্দ করে, আর একটা বাংলা প্রশ্নের উত্তর দেওয়া হয়েছে, যেখানে ফিরিয়ে দেওয়া উচিত ছিল। গড় সংখ্যাটা দুটো সমস্যাই লুকিয়ে রেখেছিল। এই পাতায় বিচারকে লিখে রাখা ও কার্যকর করার উপায় দেখব: একটা রুব্রিক, জোড়ায় জোড়ায় তুলনা, একটা রিগ্রেশন রিপোর্ট, আর এমন একটা রিলিজ গেট (release gate) যা বিল্ড ফেল করিয়ে দেয়। কোড একটা মডিউল, evidencedesk/gate.py, যা evals.py-এর ওপর দাঁড়িয়ে আছে। এখানে প্রতিটা মডেল একটা ফেক বা স্ট্যান্ড-ইন, যাতে রান পুনরাবৃত্তিযোগ্য হয়; বাস্তব মডেল কম অনুমানযোগ্য, আর সে কারণেই টেস্ট দরকার।
যা শিখবেন
- জাজ মডেল কেন একটা পক্ষপাতদুষ্ট সংকেত, আর রুব্রিক ও জোড়া-তুলনা কীভাবে ক্ষতি কমায়।
- প্রম্পট বা স্কিমা বদলের পর রিগ্রেশন কীভাবে ধরবেন, ফলাফল ভাষা ধরে কীভাবে ভাগ করবেন, এবং মান, খরচ ও লেটেন্সি কীভাবে আলাদা রাখবেন।
- ফিক্সচার দিয়ে প্রোভাইডার টেস্ট কীভাবে করবেন এবং ফলাফল JSON-এ কীভাবে রাখবেন।
- রিলিজ গেট (go, review বা block) কীভাবে ঠিক করবেন এবং সেটাকে ব্যর্থ হতে পারে এমন টেস্ট কমান্ড হিসেবে চালাবেন।
জাজ হলো পক্ষপাতযুক্ত মডেল
LLM-as-a-judge মানে একটা মডেলকে বলা: একটা উত্তরে নম্বর দাও, বা দুটোর মধ্যে ভালোটা বাছো। এটা সস্তা এবং অর্থ বোঝে, কিন্তু এটাও একটা মডেল: একই ধরনের ভুল বারবার করতে পারে। এর স্কোরকে একটা অসম্পূর্ণ সংকেত হিসেবে দেখুন, যার জন্য মানুষের লেবেলের সঙ্গে ক্যালিব্রেশন, স্পষ্ট রুব্রিক এবং নমুনার আকার জানানো দরকার।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): Zheng et al. (arXiv 2306.05685, 2023) জাজ মডেলে অবস্থান (position), শব্দবহুলতা (verbosity) ও নিজের উত্তরের প্রতি পক্ষপাত (self-enhancement) এবং সীমিত যুক্তিক্ষমতার কথা বলেছেন। তাঁদের অ্যাবস্ট্র্যাক্টে দুটো বেঞ্চমার্কে শক্তিশালী GPT-4-যুগের জাজ ও মানুষের মধ্যে ৮০%-এর বেশি মিলের কথা আছে; সেটা একটা পুরনো মডেল প্রজন্ম ও ভিন্ন কাজ, আর self-enhancement নিয়ে গবেষণাপত্রটা নিজেই বলে, প্রভাবটা আছে কি না তা তাঁদের সমীক্ষা নির্ধারণ করতে পারে না। বর্তমান ডকুমেন্টেশন দেখে নিন।
অ্যাডভান্সড টিউটোরিয়ালের মূল্যায়নের নকশা ও জাজ ক্যালিব্রেশন পাতায় এ নিয়ে গভীরে যাওয়া হয়েছে। এখানে আমরা toy_judge দিয়ে শুধু দুটো পক্ষপাত দেখব। এটা এই পাতার জন্য লেখা একটা স্ট্যান্ড-ইন: এটা বেশি শব্দের উত্তর পছন্দ করে এবং প্রথম উত্তরের পক্ষ নেয়। কোনো বাস্তব জাজ সম্পর্কে এটা কোনো প্রমাণ নয়।
def toy_judge(first: str, second: str) -> str:
"""A STAND-IN, not a real judge: it prefers more words, and gives the first answer a 3-word head start."""
a, b = len(first.split()) + 3, len(second.split())
return "first" if a > b else "second" if b > a else "tie"
short, padded = "Returns take 30 days.", "Returns take 30 days. Returns take 30 days. We are happy to explain our returns."
equal_a, equal_b = "Returns: 30 days.", "Refunds: 14 days." # same length, but only the first is current policy
for name, (x, y) in {"position": (equal_a, equal_b), "length": (short, padded)}.items():
print(f"{name:<8} x first: {toy_judge(x, y):<6} y first: {toy_judge(y, x)}")position x first: first y first: first
length x first: second y first: firstসমান দৈর্ঘ্যে প্রথম উত্তরটা দুই ক্রমেই জেতে (অবস্থানের পক্ষপাত), যদিও কেবল প্রথমটাই বর্তমান নীতি। শব্দ বাড়ানো (padded) উত্তরটাও দুই ক্রমেই জেতে (দৈর্ঘ্যের পক্ষপাত)। কোনো রায়ই সঠিকতা সম্পর্কে কিছু বলে না।
একটা সহজ, মানুষের পড়ার মতো রুব্রিক লিখুন
মূল্যায়ন ডেটাসেট ও নির্ধারিত টেস্ট পাতায় আমরা Scores পেয়েছি: প্রাসঙ্গিকতা, প্রমাণ-ব্যবহার ও ফরম্যাট, প্রতিটা ০, ১ বা ২। রুব্রিক (rubric) কথায় বলে দেয় প্রতিটা সংখ্যার মানে কী, যাতে দ্বিতীয় একজন তা প্রয়োগ করতে পারেন। নিয়মটা হলো পর্যবেক্ষণযোগ্য মানদণ্ড: মূল্যায়নকারী উত্তরে যা দেখতে পান তা লিখুন, "শুনতে ভালো" নয়। রুব্রিক JSON-এ থাকে; একটা ভ্যালিডেটর বাদ পড়া লেভেল, একই অ্যাংকরের পুনরাবৃত্তি বা অস্পষ্ট শব্দ বাতিল করে।
# file: kit/data/rubric.json
{
"name": "evidencedesk-answer-rubric",
"version": 1,
"criteria": [
{"name": "relevance", "levels": {
"0": "The answer is about something other than the question.",
"1": "The answer touches the topic but leaves the exact question open.",
"2": "The answer states the specific fact asked for, in the customer's language."}},
{"name": "evidence_use", "levels": {
"0": "A claim has no citation, or the cited source does not contain it.",
"1": "Cited sources exist, but a number or condition is not in the cited text.",
"2": "Every claim, numbers included, appears in a cited source the model was shown."}},
{"name": "format", "levels": {
"0": "The reply is not valid JSON, or a required field is missing.",
"1": "The JSON parses, but a rule is broken (status answered with no cited ids).",
"2": "The JSON parses, has all fields and obeys every rule of the contract."}}
]
}# file: kit/evidencedesk/gate.py (new file)
"""Rubric, pairwise comparison, regression compare, report and release gate. Nothing here calls a model."""
from __future__ import annotations
import argparse
import json
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
from pydantic import BaseModel, ConfigDict, model_validator
from evidencedesk.citations import MIN_MARGIN
from evidencedesk.evals import CaseModel, Dataset, Scores, build_retriever, load_dataset, load_results, run_suite
from evidencedesk.llm import LLMRequest, LLMResponse
from evidencedesk.rag import MIN_SCORE
from evidencedesk.tools import default_registry
from evidencedesk.usage import request_tokens
KIT = Path(__file__).resolve().parent.parent
DATA, MANIFEST = KIT / "data", KIT / "data" / "eval_manifest_v2.json"
METRICS = ("recall_at_k", "citation_validity", "abstention_correct", "tool_pass", "format_pass", "pass_rate") # all: higher is better
VAGUE = {"good", "nice", "great", "sounds", "better", "appropriate", "quality"}
# --- 1. A rubric: observable criteria, three levels, an anchor sentence for each level ---
class Criterion(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
name: str
levels: dict[int, str]
@model_validator(mode="after")
def anchored(self) -> Criterion:
if sorted(self.levels) != [0, 1, 2] or len(set(self.levels.values())) != 3:
raise ValueError(f"{self.name}: levels 0, 1 and 2 each need their own anchor")
for level, text in self.levels.items():
if len(text) < 25 or VAGUE & set(re.findall(r"[a-z]+", text.lower())):
raise ValueError(f"{self.name} level {level}: describe what a rater can see, not 'good' or 'nice'")
return self
class Rubric(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
name: str
version: int
criteria: tuple[Criterion, ...]
def score(self, ratings: dict[str, int]) -> Scores:
if set(ratings) != {c.name for c in self.criteria}:
raise ValueError("rate every criterion, and only those")
return Scores(**ratings) # the criteria are the Scores fields
def explain(self, ratings: dict[str, int]) -> list[str]:
return [f"{c.name}={ratings[c.name]}: {c.levels[ratings[c.name]]}" for c in self.criteria]
def load_rubric(path: Path = DATA / "rubric.json") -> Rubric:
return Rubric.model_validate_json(path.read_text(encoding="utf-8"))একটা উত্তর হাতে স্কোর করুন। প্রশ্ন: "How long do I have to return a bike?" উদ্ধৃত চাঙ্কে লেখা আছে ৩০ দিন, এবং "the delivery fee is refunded only if the bike arrived damaged"। নিচের উত্তরে সংখ্যা ঠিক, কিন্তু ফি-র কথা ভুল, তাই প্রমাণ-ব্যবহারে ১; বাকি মানদণ্ড পূরণ হয়েছে।
from pydantic import ValidationError
from evidencedesk.gate import Criterion, load_rubric
rubric = load_rubric() # kit/data/rubric.json
answer = "You have 30 days from delivery, and we also refund the delivery fee. [kc-returns-v3:c0]"
ratings = {"relevance": 2, "evidence_use": 1, "format": 2} # my judgment, written down with its reason below
print(rubric.score(ratings))
print("\n".join(rubric.explain(ratings)))
try:
Criterion(name="relevance", levels={0: "Not on topic at all.", 1: "Partly on the topic.", 2: "The answer sounds good."})
except ValidationError as error:
print("rejected:", error.errors()[0]["msg"])Scores(relevance=2, evidence_use=1, format=2)
relevance=2: The answer states the specific fact asked for, in the customer's language.
evidence_use=1: Cited sources exist, but a number or condition is not in the cited text.
format=2: The JSON parses, has all fields and obeys every rule of the contract.
rejected: Value error, relevance level 0: describe what a rater can see, not 'good' or 'nice'প্রতিটা স্কোরের পাশে অ্যাংকর ছাপা হয়, ফলে দ্বিতীয় মূল্যায়নকারী নির্দিষ্ট একটা বাক্যের সঙ্গে দ্বিমত করতে পারেন।
জোড়ায় আউটপুট তুলনা করুন
কোনো রেফারেন্স উত্তর না থাকলে, শুধু একটা পুরনো ও একটা নতুন আউটপুট থাকলে, জোড়ায় তুলনা (pairwise comparison) ব্যবহার করুন: কোনটা ভালো, কোনোটাকেই সঠিক ধরে না নিয়ে? ক্রম বদলে দুইবার জিজ্ঞেস করুন; নতুন আউটপুট জেতে কেবল তখনই, যখন সে দুই ক্রমেই জেতে, আর ক্রম বদলালে রায় উল্টে গেলে সেটা টাই, যা দ্বিমত (disagreement) হিসেবে গোনা হয়। জাজ হলো যেকোনো ফাংশন, যা দুটো টেক্সট নিয়ে first, second বা tie বলে: মানুষ, মডেল, বা আমাদের স্ট্যান্ড-ইন।
# file: kit/evidencedesk/gate.py (add)
# --- 2. Pairwise comparison: no reference answer, and the order is swapped ---
Judge = Callable[[str, str], str] # (first, second) -> "first" | "second" | "tie"
_AS_X = {"first": "x", "second": "y", "tie": "tie"}
_SWAPPED = {"first": "y", "second": "x", "tie": "tie"}
def compare_pair(judge: Judge, x: str, y: str) -> tuple[str, bool]:
"""('x' | 'y' | 'tie', order_disagreement): x wins only if the judge picks it in BOTH orders."""
one, two = _AS_X[judge(x, y)], _SWAPPED[judge(y, x)]
return (one, False) if one == two else ("tie", True)
def compare_pairs(judge: Judge, pairs: list[tuple[str, str]]) -> dict[str, int]:
"""x = new output, y = old. win/tie/loss are for x; disagree = verdicts that flipped with the order."""
done = [compare_pair(judge, x, y) for x, y in pairs]
return {"win": sum(v == "x" for v, _ in done), "tie": sum(v == "tie" for v, _ in done),
"loss": sum(v == "y" for v, _ in done), "disagree": sum(d for _, d in done), "n": len(done)}from evidencedesk.gate import compare_pairs
pairs = [(short, padded), (padded, short), (equal_a, equal_b), ("Returns take 30 days.", "Returns take 30 days.")] # (new, old)
print("one order only, wins for new:", sum(toy_judge(x, y) == "first" for x, y in pairs), "of", len(pairs))
print("both orders:", compare_pairs(toy_judge, pairs))one order only, wins for new: 3 of 4
both orders: {'win': 1, 'tie': 2, 'loss': 1, 'disagree': 2, 'n': 4}এক ক্রমে নতুন আউটপুট চারটার মধ্যে ৩টাতে "জেতে"। দুই ক্রমে সে একবার জেতে, একবার হারে, দুবার টাই করে, আর দুটো টাইই ক্রম বদলে রায় উল্টে যাওয়ার ফল। অদলবদল অবস্থানের পক্ষপাত সরায়, কিন্তু দৈর্ঘ্যের পক্ষপাত সরায় না: জয় ও হার, দুটোই এসেছে শব্দ বাড়ানো (padded) টেক্সট ছোটটাকে হারানোর কারণে। তার জন্য রুব্রিক বা মানুষ লাগে।
বিভিন্ন ভাষায় টেস্ট করুন
গড় সংখ্যা দুর্বল স্লাইস লুকাতে পারে। EvidenceDesk বাংলা ও মিশ্র-ভাষার গ্রাহকদের সেবা দেয়, তাই আমরা এমন ছয়টা কেস এবং একটা নতুন ডেটাসেট ভার্সন যোগ করি (ড্রিফট-চেক আছে মূল্যায়ন ডেটাসেট ও নির্ধারিত টেস্ট পাতায়)। kit/data/eval_lang.jsonl ফাইলে EvalCase আকারে ছয়টা লাইন আছে: বাংলা (lg01 থেকে lg03) ও মিশ্র (lg04 থেকে lg06), প্রতিটা দলে একটা করে উত্তরহীন প্রশ্ন। দুটো নমুনা:
{"id": "lg01", "kind": "rag", "input": "ডেলিভারি ফ্রি পেতে কত টাকার অর্ডার করতে হয়?", "expect": "answer", "doc_ids": ["kc-shipping-v1-bn"], "reason": "Bangla question, Bangla page."}
{"id": "lg04", "kind": "rag", "input": "Express delivery কত দিনে পৌঁছায়?", "expect": "answer", "doc_ids": ["kc-shipping-v1", "kc-shipping-v1-bn"], "reason": "Mixed: either shipping page is right."}run_gate_suite (স্লাইসের নিচে) স্থির সুইট চালায়: ডেটাসেট ভার্সন ২, RAG-এর নির্ভরযোগ্যতা ও মূল্যায়নের ডেটা পাতার হাতে লেখা রিপ্লাই ও ডিফল্ট টুল। language_of লিপি দেখে প্রশ্নের ভাষা ঠিক করে: শুধু বাংলা অক্ষর, বাংলা ও ল্যাটিন একসঙ্গে, অথবা বাংলা নয়। slice_results প্রতিটা স্লাইসের পাস রেট এবং উত্তর দেওয়া হয়ে যাওয়া উত্তরহীন প্রশ্নগুলো জানায়। ম্যানিফেস্ট eval_manifest_v2.json (নতুন ফাইল, তাই ভার্সন ১ বৈধ থাকে) তিনটা ডেটা ফাইল ও তাদের হ্যাশ তালিকাভুক্ত করে।
# file: kit/evidencedesk/gate.py (add)
# --- 3. Slices: the same run, cut by language (or any other key) ---
def language_of(text: str) -> str:
"""'bn' (Bangla letters only), 'mixed' (Bangla and Latin letters) or 'en' (no Bangla letters)."""
bangla, latin = bool(re.search(r"[\u0980-\u09ff]", text)), bool(re.search("[A-Za-z]", text))
return "mixed" if bangla and latin else "bn" if bangla else "en"
def slice_results(dataset: Dataset, result: dict[str, Any]) -> dict[str, dict]:
"""Pass rate per language of the question, and the unanswerable questions that were answered ('unsafe')."""
groups: dict[str, list[str]] = {}
for case in dataset.of_kind("rag"):
groups.setdefault(language_of(case.input), []).append(case.id)
expects = {c.id: c.expect for c in dataset.cases}
return {name: {"cases": len(ids), "pass_rate": round(sum(result["cases"][i]["passed"] for i in ids) / len(ids), 3),
"failures": sorted(i for i in ids if not result["cases"][i]["passed"]),
"unsafe": sorted(i for i in ids if expects[i] == "abstain" and not result["cases"][i]["checks"]["abstention"])}
for name, ids in sorted(groups.items())}# file: kit/evidencedesk/gate.py (add)
# --- 3b. The fixed suite: dataset version 2, hand-written replies, default tools ---
def hand_written_client(dataset: Dataset) -> CaseModel:
from tests.test_citations import REPLIES # hand-written replies from RAG Reliability and Evaluation Data
return CaseModel({c.input: REPLIES[c.id] for c in dataset.cases if c.id in REPLIES})
def run_gate_suite(min_score: float = MIN_SCORE, min_margin: float = MIN_MARGIN, k: int = 3, client=None) -> dict[str, Any]:
dataset = load_dataset(MANIFEST)
return run_suite(dataset, build_retriever(KIT / "corpus", min_score=min_score, k=k), client or hand_written_client(dataset),
default_registry(KIT / "corpus"), k=k, min_margin=min_margin)
from pathlib import Path
from evidencedesk.evals import load_dataset
from evidencedesk.gate import MANIFEST, language_of, run_gate_suite, slice_results
dataset = load_dataset(MANIFEST) # kit/data/eval_manifest_v2.json
print(len(dataset.cases), "cases, version", dataset.version, [language_of(c.input) for c in dataset.cases if c.id in ("lg01", "lg04", "rc01")])
result = run_gate_suite()
print("pass_rate", result["metrics"]["pass_rate"], "failures", result["failures"])
for name, s in slice_results(dataset, result).items():
print(f"{name:<6}{s['pass_rate']:>6} of {s['cases']:>2} cases failing: {', '.join(s['failures']) or '-':<22} unsafe: {', '.join(s['unsafe']) or '-'}")33 cases, version 2 ['bn', 'mixed', 'en']
pass_rate 0.848 failures ['lg03', 'rc03', 'rc06', 'rc08', 'rc13']
bn 0.8 of 5 cases failing: lg03 unsafe: lg03
en 0.714 of 14 cases failing: rc03, rc06, rc08, rc13 unsafe: -
mixed 1.0 of 3 cases failing: - unsafe: -গড় 0.848 কোনো সমস্যাই দেখায় না। ইংরেজিতে রেট সবচেয়ে কম, কিন্তু তার চারটা ব্যর্থতা নিরাপদ: উত্তর দেওয়া যায় এমন প্রশ্ন ফিরিয়ে দেওয়া হয়েছে (RAG-এর নির্ভরযোগ্যতা ও মূল্যায়নের ডেটা পাতার পরিচিত মিস)। বাংলায় রেট বেশি, কিন্তু একমাত্র অনিরাপদ ব্যর্থতাটা সেখানেই: lg03, উত্তরহীন প্রশ্ন, অথচ উত্তর দেওয়া হয়েছে। কম সংখ্যা সবসময় সবচেয়ে খারাপ ব্যর্থতা নয়। ৫টা বাংলা ও ৩টা মিশ্র কেসে এগুলো সংকেত মাত্র, তুলনা করার মতো রেট নয়; lg03 ঠিক করা রিট্রিভালের কাজ (হাইব্রিড রিট্রিভাল ও এরর বিশ্লেষণ)।
প্রম্পট রিগ্রেশন ধরুন
রিগ্রেশন (regression) হলো এমন কেস বা মেট্রিক, যা সংরক্ষিত রানে ঠিক ছিল, কিন্তু এখন ঠিক নেই। প্রম্পট, স্কিমা বা সেটিং বদলের পর একই কেসগুলো আবার চালান এবং তুলনা করুন। compare_runs প্রতিটা কেস ও মেট্রিককে improved, regressed বা unchanged লেবেল দেয়। ভিন্ন ডেটাসেটের রান তুলনা করতে সে রাজি হয় না, কারণ পার্থক্যের ব্যাখ্যা ডেটাই হতে পারে; আর যে মেট্রিক আর মাপা যায় না, তাকে সে রিগ্রেশন ধরে।
# file: kit/evidencedesk/gate.py (add)
# --- 4. Regression: compare two saved runs ---
class NotComparable(ValueError):
"""The runs used different datasets."""
def compare_runs(baseline: dict[str, Any], current: dict[str, Any]) -> dict[str, Any]:
if baseline["dataset"] != current["dataset"]:
was, now = baseline["dataset"], current["dataset"]
raise NotComparable(f"dataset v{was['version']} {was['sha256'][:8]} vs v{now['version']} {now['sha256'][:8]}")
cases = {}
for cid in sorted(current["cases"]):
before, after = baseline["cases"][cid]["passed"], current["cases"][cid]["passed"]
cases[cid] = "improved" if after and not before else "regressed" if before and not after else "unchanged"
metrics = {}
for name in sorted(current["metrics"]):
before, after = baseline["metrics"][name], current["metrics"][name]
rank = lambda v: -1.0 if v is None else v # a metric that can no longer be measured is a regression
status = "improved" if rank(after) > rank(before) else "regressed" if rank(after) < rank(before) else "unchanged"
metrics[name] = {"before": before, "after": after, "delta": None if None in (before, after) else round(after - before, 3), "status": status}
return {"cases": cases, "metrics": metrics,
"regressed": [c for c, s in cases.items() if s == "regressed"], "improved": [c for c, s in cases.items() if s == "improved"]}ভার্সন ১ বেসলাইন ভার্সন ২ রানের সঙ্গে তুলনা করা যায় না, তাই আমরা নতুন একটা সংরক্ষণ করি (kit/data/baseline_v2.json; একটা টেস্ট যাচাই করে যে নতুন রান একই বাইট দেয়)। NewSchema, "আমরা প্রম্পট বদলেছি"-র স্ট্যান্ড-ইন, প্রতিটা রিপ্লাইয়ের উদ্ধৃতি তালিকার নাম cited_ids-এর বদলে sources করে দেয়।
import json
from evidencedesk.evals import load_results
from evidencedesk.gate import NotComparable, compare_runs, hand_written_client
try:
compare_runs(load_results(Path("kit/data/baseline_v1.json")), result)
except NotComparable as error:
print("not comparable:", error)
baseline = load_results(Path("kit/data/baseline_v2.json"))
class NewSchema:
"""A STAND-IN for 'we changed the prompt': the new reply format calls the citation list 'sources'."""
def __init__(self, inner):
self.inner = inner
def complete(self, request):
response = self.inner.complete(request)
body = json.loads(response.text)
if "cited_ids" in body:
body["sources"] = body.pop("cited_ids")
return type(response)(json.dumps(body), response.model, response.usage, response.stop_reason)
changed = run_gate_suite(client=NewSchema(hand_written_client(dataset)))
diff = compare_runs(baseline, changed)
print("regressed:", len(diff["regressed"]), "improved:", diff["improved"], "unchanged:", sum(s == "unchanged" for s in diff["cases"].values()))
print("lg01 now:", changed["cases"]["lg01"]["detail"])
for name, m in diff["metrics"].items():
if m["status"] != "unchanged":
print(f"{name:<20}{m['before']!s:>6} -> {m['after']!s:<6} {m['status']}")
tuned = run_gate_suite(min_score=0.25)
better = compare_runs(baseline, tuned)
print("min_score 0.25: improved", better["improved"], "regressed", better["regressed"])not comparable: dataset v1 a575ab37 vs v2 4bbacf85
regressed: 13 improved: ['lg03'] unchanged: 19
lg01 now: invalid_schema
abstention_correct 0.773 -> 0.227 regressed
citation_validity 1.0 -> None regressed
pass_rate 0.848 -> 0.485 regressed
min_score 0.25: improved ['rc03'] regressed []তেরোটা কেসে রিগ্রেশন হয়েছে; যেমন lg01 এখন invalid_schema দিয়ে ফেল করে: বৈধ JSON, ভুল চুক্তি। একটা কেস "উন্নত": lg03 ফিরিয়ে দেওয়া হচ্ছে কেবল এ কারণে যে প্রতিটা রিপ্লাই বাতিল হচ্ছে, যা অগ্রগতি নয়। তালিকার পাশে মেট্রিকগুলো পড়ুন: বিরত থাকার হার (abstention) 0.773 থেকে 0.227-এ নেমেছে, আর উদ্ধৃতির বৈধতা (citation validity) হয়ে গেছে None, কারণ কোনো রিপ্লাইতে বৈধ উদ্ধৃতি ছিল না। শেষ লাইনটা ভালো ক্ষেত্র: min_score 0.25 rc03 ঠিক করে, কিছু ভাঙে না।
মান থেকে লেটেন্সি ও খরচ আলাদা করুন, এবং ফলাফল JSON-এ রাখুন
মান, খরচ ও লেটেন্সি আলাদা প্রশ্নের উত্তর দেয়, তাই একটা রিপোর্ট (report) এদের আলাদা অংশে রাখে, আর কেবল মান-অংশটা আসে মূল্যায়ন কেস থেকে। Metered মডেল কল ও আনুমানিক ইনপুট টোকেন গোনে (request_tokens আসে usage.py থেকে; এখন কিট থেকে evidencedesk/usage.py কপি করে নিন, পরের পাতা সেটা বানায়)। লেটেন্সি থাকে "not measured": ঘড়ির সময়ের ওপর কোনো অ্যাসার্শন নেই। report সাধারণ, JSON-এ রাখার মতো ডেটা ফেরত দেয় এবং report_text তা নির্দিষ্ট ক্রমে ছাপে, ফলে সংরক্ষিত রিপোর্টে ডেটাসেট ভার্সন, কনফিগারেশন, স্কোর ও ব্যর্থতা থাকে। ডিফল্ট (শীর্ষ ৩ চাঙ্ক) আর শুধু শীর্ষ চাঙ্ক পাঠানো রান তুলনা করুন:
# file: kit/evidencedesk/gate.py (add)
# --- 5. A stable report: quality, cost and latency are separate sections ---
def report(result: dict[str, Any], slices: dict | None = None, cost: dict | None = None, latency: dict | None = None) -> dict[str, Any]:
"""Only 'quality' comes from the evaluation cases; cost and latency are passed in and never mixed into it."""
return {"run": {"dataset": result["dataset"], "config": result["config"]},
"quality": {"metrics": result["metrics"], "failures": result["failures"], "slices": slices or {}},
"cost": cost or {}, "latency": latency or {}}
def report_text(rep: dict[str, Any]) -> str:
ds, q = rep["run"]["dataset"], rep["quality"]
lines = [f"dataset {ds['name']} v{ds['version']} {ds['sha256'][:8]}",
"config " + " ".join(f"{k}={v}" for k, v in sorted(rep["run"]["config"].items())), "QUALITY"]
lines += [f" {k:<24}{v}" for k, v in q["metrics"].items()]
lines += [f" slice {k:<6} {v['pass_rate']} of {v['cases']}, failing: {', '.join(v['failures']) or '-'}; unsafe: {', '.join(v['unsafe']) or '-'}"
for k, v in q["slices"].items()]
lines.append(" failures: " + (", ".join(q["failures"]) or "-"))
for section in ("cost", "latency"):
lines += [section.upper()] + [f" {k:<24}{v}" for k, v in sorted(rep[section].items())] + ([] if rep[section] else [" not measured"])
return "\n".join(lines)
class Metered:
"""Wraps a client and counts calls and estimated input tokens: a cost figure, never a quality metric."""
def __init__(self, inner) -> None:
self.inner, self.calls, self.input_tokens = inner, 0, 0
def complete(self, request: LLMRequest) -> LLMResponse:
self.calls, self.input_tokens = self.calls + 1, self.input_tokens + request_tokens(request)
return self.inner.complete(request)from evidencedesk.gate import Metered, report
runs = {}
for k in (3, 1):
meter = Metered(hand_written_client(dataset))
res = run_gate_suite(k=k, client=meter)
runs[k] = (res, report(res, slice_results(dataset, res), {"model_calls": meter.calls, "input_tokens_estimated": meter.input_tokens}))
(big, rep3), (small, rep1) = runs[3], runs[1]
tokens3, tokens1 = rep3["cost"]["input_tokens_estimated"], rep1["cost"]["input_tokens_estimated"]
print(f"input tokens k=3: {tokens3}, k=1: {tokens1} ({tokens1 / tokens3 - 1:+.1%})")
for name in ("recall_at_k", "citation_validity", "pass_rate"):
print(f"{name:<20}{big['metrics'][name]:>6} -> {small['metrics'][name]}")
print("unanswerable answered:", big["confusion"]["unanswerable"]["answered"], "->", small["confusion"]["unanswerable"]["answered"])
Path("report_k1.json").write_text(json.dumps(rep1, indent=1, sort_keys=True) + "\n", encoding="utf-8")
print("saved sections:", sorted(rep1), "| cost:", rep1["cost"])input tokens k=3: 3919, k=1: 3304 (-15.7%)
recall_at_k 0.882 -> 0.824
citation_validity 1.0 -> 0.824
pass_rate 0.848 -> 0.788
unanswerable answered: 1 -> 2
saved sections: ['cost', 'latency', 'quality', 'run'] | cost: {'model_calls': 17, 'input_tokens_estimated': 3304}k=1 রানে ইনপুট টোকেন 15.7% কম, তিনটা মান-মেট্রিকে ফল খারাপ, আর আরও একটা উত্তরহীন প্রশ্নের উত্তর দেওয়া হয়েছে। এই বিনিময় গ্রহণযোগ্য কি না, তা বলে শুধু মানের সংখ্যাগুলো।
প্রোভাইডার টেস্টে স্থির ফিক্সচার ব্যবহার করুন
লাইভ প্রোভাইডার একই উত্তর প্রতিবার ভিন্ন শব্দে বলতে পারে, temperature 0 হলেও। অফলাইন টেস্ট একটা ফিক্সচার (fixture) রিপ্লে করে: প্রোভাইডার-নিরপেক্ষ LLM ক্লায়েন্ট পাতার আকারে রেকর্ড করা রিপ্লাই (ব্যর্থতার আকার: অ্যাডাপ্টার, কনফিগারেশন ও এরর হ্যান্ডলিং)। ফিক্সচার কখনো বদলায় না, তাই তার ওপর কনটেন্ট অ্যাসার্শন স্থিতিশীল। বাস্তব প্রোভাইডারের বিরুদ্ধে ঐচ্ছিক চেকে শব্দ যাচাই করা চলবে না; যাচাই হবে চুক্তি (contract): উত্তর শেষ হলে টেক্সট আছে, স্টপ রিজন পরিচিত।
# file: kit/evidencedesk/gate.py (add)
# --- 6. Provider fixtures: assert the contract, not the wording ---
def contract_problems(response: LLMResponse) -> list[str]:
"""What must hold for ANY reply, recorded or live. Wording is not checked."""
found = []
if response.stop_reason == "end" and not response.text.strip():
found.append("a finished reply needs text")
if response.stop_reason not in ("end", "length", "other"):
found.append(f"unknown stop_reason {response.stop_reason!r}")
return foundfrom evidencedesk.gate import contract_problems
from evidencedesk.llm import FakeClient, LLMRequest, LLMResponse, Message, Usage
# FakeClient replays kit/fixtures/responses.json: the same bytes on every run
recorded = FakeClient().complete(LLMRequest((Message("user", "How long do I have to return a new bike?"),)))
reworded = LLMResponse("Thirty days from delivery.", "other-model", Usage(30, 6)) # a live provider may say it differently
for name, r in {"recorded": recorded, "reworded": reworded, "empty": LLMResponse("", "m", Usage(3, 0)),
"odd stop": LLMResponse("x", "m", Usage(3, 1), "weird")}.items(): # type: ignore[arg-type]
print(f"{name:<9} wording check: {r.text.startswith('You can return')!s:<5} contract: {contract_problems(r) or 'ok'}")recorded wording check: True contract: ok
reworded wording check: False contract: ok
empty wording check: False contract: ['a finished reply needs text']
odd stop wording check: False contract: ["unknown stop_reason 'weird'"]শব্দ-যাচাই কেবল রেকর্ড করা ফিক্সচারে পাস করে এবং ভিন্ন শব্দে লেখা রিপ্লাইকে ভুলভাবে বাতিল করে; চুক্তি-যাচাই দুটোকেই গ্রহণ করে এবং দুটো ভাঙা আকার ধরে।
একটা রিলিজ গেট ঠিক করুন
রিলিজ গেট তুলনাকে সিদ্ধান্তে বদলে দেয়: go, review বা block, কারণসহ। ক্রিটিক্যাল কেসের একটাও ফেল করলে গড় যা-ই বলুক, রিলিজ আটকে যায়। ফ্লোর হলো নির্দিষ্ট ন্যূনতম মান (absolute threshold), আর ড্রপ হলো বেসলাইনের বিপরীতে নো-রিগ্রেশন নিয়ম; প্রতিটা হয় আটকায়, নয় রিভিউ চায়।
# file: kit/data/gate_policy.json
{
"name": "evidencedesk-release-gate",
"version": 1,
"critical_cases": ["rc09", "rc14", "rc15", "rc16", "lg06", "tl03", "tl06", "fm03"],
"block_below": {"citation_validity": 0.9, "pass_rate": 0.8},
"review_below": {"recall_at_k": 0.85, "abstention_correct": 0.7},
"block_on_drop": ["citation_validity", "tool_pass", "format_pass"],
"review_on_drop": ["recall_at_k", "abstention_correct", "pass_rate"]
}# file: kit/evidencedesk/gate.py (add)
# --- 7. The release gate ---
class GatePolicy(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
name: str
version: int
critical_cases: list[str] # any failure blocks, whatever the averages say
block_below: dict[str, float] = {} # absolute floors
review_below: dict[str, float] = {}
block_on_drop: list[str] = [] # metrics that may not fall below the baseline
review_on_drop: list[str] = []
@model_validator(mode="after")
def known_metrics(self) -> GatePolicy:
named = {*self.block_below, *self.review_below, *self.block_on_drop, *self.review_on_drop}
if unknown := named - set(METRICS):
raise ValueError(f"unknown metric {sorted(unknown)}")
return self
@dataclass(frozen=True)
class Decision:
verdict: str # "go" | "review" | "block"
blockers: tuple[str, ...]
reviews: tuple[str, ...]
def exit_code(self) -> int: # review does not fail the build; it asks a person
return int(self.verdict == "block")
def text(self) -> str:
return "\n".join([f"GATE: {self.verdict.upper()}", *(f" block: {r}" for r in self.blockers), *(f" review: {r}" for r in self.reviews)])
def load_policy(path: Path = DATA / "gate_policy.json") -> GatePolicy:
return GatePolicy.model_validate_json(path.read_text(encoding="utf-8"))
def decide(policy: GatePolicy, current: dict[str, Any], baseline: dict[str, Any] | None = None) -> Decision:
block: list[str] = []
review: list[str] = []
for cid in policy.critical_cases:
case = current["cases"].get(cid)
if case is None:
block.append(f"critical case {cid} is missing from the run")
elif not case["passed"]:
block.append(f"critical case {cid} failed its {', '.join(k for k, ok in case['checks'].items() if not ok)} check")
for floors, out in ((policy.block_below, block), (policy.review_below, review)):
out += [f"{n} {current['metrics'][n]} is below the floor {f}" for n, f in sorted(floors.items()) if (current["metrics"][n] or 0) < f]
if baseline is not None:
try:
diff = compare_runs(baseline, current)
except NotComparable as error:
review.append(f"baseline not comparable: {error}")
else:
for n in policy.block_on_drop + policy.review_on_drop:
if (m := diff["metrics"][n])["status"] == "regressed":
(block if n in policy.block_on_drop else review).append(f"{n} fell from {m['before']} to {m['after']}")
review += [f"case {c} passed in the baseline and fails now" for c in diff["regressed"] if c not in policy.critical_cases]
return Decision("block" if block else "review" if review else "go", tuple(block), tuple(review))from evidencedesk.gate import decide, load_policy
policy = load_policy()
candidates = {"baseline": result, "min_score 0.25": tuned, "new schema": changed, "margin 0": run_gate_suite(min_margin=0.0),
"k=1 (cheaper)": small}
for name, run in candidates.items():
d = decide(policy, run, baseline)
print(f"{name:<15}{d.verdict:<7} pass_rate={run['metrics']['pass_rate']:<6}", (d.blockers + d.reviews + ("-",))[0])baseline go pass_rate=0.848 -
min_score 0.25 go pass_rate=0.879 -
new schema block pass_rate=0.485 critical case rc09 failed its abstention check
margin 0 block pass_rate=0.848 critical case rc15 failed its abstention check
k=1 (cheaper) block pass_rate=0.788 critical case rc15 failed its abstention checkmargin 0 পড়ুন: তার পাস রেট বেসলাইনের সমান, কারণ একটা কেস উন্নত হয়েছে (rc13) আর আরেকটা ভেঙেছে (rc15, একটা উত্তরহীন প্রশ্নের এখন উত্তর দেওয়া হচ্ছে)। গড় নড়েনি; ক্রিটিক্যাল কেস নড়েছে, তাই গেট আটকায়। k=1, সস্তা রানটাও আটকে গেছে। min_score 0.25 পার হয়ে যায়। গেট সবকিছু করে না: lg03 বেসলাইনেই ফেল করে, তাই সেটা ক্রিটিক্যাল তালিকায় নেই। গেট খারাপ হওয়া ঠেকায়, অপূর্ণতা নয় (কঠিন অনুশীলন দেখুন)।
মিনি-ল্যাব: RAG রিগ্রেশন গেট
gate.py-এর শেষ অংশ এমন একটা কমান্ড, যা রিপোর্ট ছাপে, নীতি প্রয়োগ করে এবং block হলে ১ দিয়ে বেরিয়ে যায়।
# file: kit/evidencedesk/gate.py (add)
# --- 8. The command ---
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(prog="python -m evidencedesk.gate", description="Run the fixed suite and apply the release gate.")
ap.add_argument("--min-score", type=float, default=MIN_SCORE)
ap.add_argument("--min-margin", type=float, default=MIN_MARGIN)
args = ap.parse_args(argv)
metered = Metered(hand_written_client(load_dataset(MANIFEST)))
result = run_gate_suite(args.min_score, args.min_margin, client=metered)
rep = report(result, slice_results(load_dataset(MANIFEST), result), {"model_calls": metered.calls, "input_tokens_estimated": metered.input_tokens})
decision = decide(load_policy(), result, load_results(DATA / "baseline_v2.json"))
print(report_text(rep), decision.text(), sep="\n")
return decision.exit_code()
if __name__ == "__main__":
raise SystemExit(main())প্রথমে বর্তমান সেটিংসে চালান। তারপর ইচ্ছা করে একটা রিট্রিভার সেটিং ভেঙে দিন (--min-margin 0 অস্পষ্টতার চেক বন্ধ করে); আউটপুট ছেঁকে নেওয়া হয়েছে।
python -m evidencedesk.gate; echo "exit code: $?"dataset evidencedesk-eval v2 4bbacf85
config chunks=36 k=3 min_margin=0.05 min_score=0.3
QUALITY
recall_at_k 0.882
citation_validity 1.0
abstention_correct 0.773
tool_pass 1.0
format_pass 1.0
pass_rate 0.848
slice bn 0.8 of 5, failing: lg03; unsafe: lg03
slice en 0.714 of 14, failing: rc03, rc06, rc08, rc13; unsafe: -
slice mixed 1.0 of 3, failing: -; unsafe: -
failures: lg03, rc03, rc06, rc08, rc13
COST
input_tokens_estimated 3919
model_calls 14
LATENCY
not measured
GATE: GO
exit code: 0python -m evidencedesk.gate --min-margin 0 | sed -n '/slice en/p;/^GATE/,$p'; echo "exit code: ${PIPESTATUS[0]}" slice en 0.714 of 14, failing: rc03, rc06, rc08, rc15; unsafe: rc15
GATE: BLOCK
block: critical case rc15 failed its abstention check
block: citation_validity fell from 1.0 to 0.941
exit code: 1এক্সিট কোড CI জব লাল করে। একই চেক হলো নতুন kit/tests/test_gate.py-এর test_release_gate টেস্ট (মোট ২০টা টেস্ট); এটা দুটো ঐচ্ছিক এনভায়রনমেন্ট ভ্যারিয়েবল পড়ে। পুরো ফাইল, তারপর ভাঙা সেটিংসহ সেই টেস্ট:
python -m pytest -q kit/tests/test_gate.py | sed 's/ in [0-9.]*s//'.................... [100%]
20 passedGATE_MIN_MARGIN=0 python -m pytest -q --tb=no kit/tests/test_gate.py::test_release_gate | sed 's/ in [0-9.]*s//'; echo "exit code: ${PIPESTATUS[0]}"F [100%]
=========================== short test summary info ============================
FAILED kit/tests/test_gate.py::test_release_gate - AssertionError: critical c...
1 failed
exit code: 1কোনো ব্যর্থতা গ্রাহকের ক্ষতি করলেই একটা ক্রিটিক্যাল কেস যোগ করুন।
এই চেকগুলো যা দেখাতে পারে না
তেত্রিশটা কেস বাগ খুঁজে পায়; দুটো সিস্টেমকে র্যাংক করতে বা উদ্ধৃত করার মতো শতাংশ দিতে পারে না। প্রতিটা রিপ্লাই আসে একটা নির্দিষ্ট আচরণের ফেক মডেল থেকে, তাই রান থেকে রানে তারতম্য মাপার কিছু নেই। মূল্যায়নকারী ছিলেন একজন, মিল পরিমাপ হয়নি; টয় জাজ বাস্তব জাজ সম্পর্কে কিছু বলে না; টোকেন সংখ্যা আনুমানিক। সবুজ গেটের মানে "এই কেসগুলোতে কোনো পরিচিত ক্রিটিক্যাল রিগ্রেশন নেই", "অ্যাসিস্ট্যান্ট ভালো" নয়।
কাজের জায়গায় এটা কোথায় দেখা যায়
বার্লিনের লেনা তার টিমের পুল-রিকোয়েস্ট চেকগুলোর দায়িত্বে আছেন। কোডের পাশে একটা নীতি ফাইল থাকে: কয়েকটা ক্রিটিক্যাল কেস (প্রত্যাখ্যান, পুরনো নীতি, টুলের নিরাপত্তা), দুটো ফ্লোর, আর একটা বেসলাইন, যা বদলায় কেবল রিভিউ হওয়া কমিটে। এটা একটা উদাহরণ, কেস স্টাডি নয়।
সাধারণ ভুল
- জাজ স্কোরকে পরিমাপ ধরে নেওয়া। এতে অবস্থান, দৈর্ঘ্য ও শব্দচয়নের পক্ষপাত থাকে। মানুষের লেবেলের সঙ্গে ক্যালিব্রেট করুন এবং নমুনার আকার জানান।
- এক ক্রমে বিচার করা, বা শুধু গড় দেখা। ক্রম বদলান; ভাষা ধরে ভাগ করুন; ক্রিটিক্যাল কেস আলাদা তালিকায় রাখুন। রুব্রিকে "ভালো" লিখলেও একই ভুল হয়: যা দেখা যায় তা লিখুন।
- সস্তা বা দ্রুত রানকে ভালো বলা। একই মূল্যায়ন সেটেও সেটা মিলতে হবে।
- গেট পাস করাতে বেসলাইন নতুন করে সেভ করা। কোডের মতো রিভিউ করুন।
নিজে চেষ্টা করুন
- সহজ: মূল্যায়ন ডেটাসেট ও নির্ধারিত টেস্ট পাতাটা আবার দেখুন। কমিট করা রুব্রিক দিয়ে তার "30 days, no citation" উত্তরটা স্কোর করুন। কোন অ্যাংকর খাটে?
- মাঝারি: লোকাল ইনডেক্স ও রিট্রিভালের মান (top-k) পাতাটা আবার দেখুন। k=2 ও k=1 দিয়ে গেট চালিয়ে ইনপুট টোকেন তুলনা করুন। সস্তা রান কি সবসময় ভালো?
- কঠিন: RAG-এর নির্ভরযোগ্যতা ও মূল্যায়নের ডেটা (বিরত থাকা) পাতাটা আবার দেখুন। ক্রিটিক্যাল কেসে
lg03যোগ করে বেসলাইনকে গেটে চালান। কী ঘটে, আর এগোনোর দুটো সৎ পথ কী?
উত্তর
সহজ। প্রমাণ-ব্যবহার ০: "A claim has no citation, or the cited source does not contain it." প্রাসঙ্গিকতা ও ফরম্যাট ২ থাকে।
print(rubric.score({"relevance": 2, "evidence_use": 0, "format": 2}))
print(rubric.explain({"relevance": 2, "evidence_use": 0, "format": 2})[1])Scores(relevance=2, evidence_use=0, format=2)
evidence_use=0: A claim has no citation, or the cited source does not contain it.পাস চেকলিস্ট: তিনটা রেটিং; অ্যাংকরটা উদ্ধৃত; কোনো মোট স্কোর নেই।
মাঝারি। Metered, run_gate_suite ও decide আবার ব্যবহার করুন।
for k in (2, 1):
meter = Metered(hand_written_client(dataset))
res = run_gate_suite(k=k, client=meter)
d = decide(policy, res, baseline)
print(f"k={k}: {d.verdict:<6} input tokens {meter.input_tokens}, pass_rate {res['metrics']['pass_rate']}")k=2: go input tokens 3344, pass_rate 0.848
k=1: block input tokens 3304, pass_rate 0.788k=2 রানে k=3-এর (3919) তুলনায় প্রায় ১৫% ইনপুট টোকেন বাঁচে, আর সেটা পার হয়; k=1 আরও সামান্য বাঁচায় এবং আটকে যায়। সস্তা গ্রহণযোগ্য কেবল তখনই, যখন গেট একমত। পাস চেকলিস্ট: দুটো টোকেন সংখ্যা ও দুটো রায়।
কঠিন। বেসলাইন নিজেই আটকে যায়, কারণ lg03 আজ সত্যিই ফেল করে:
from evidencedesk.gate import GatePolicy
strict = GatePolicy(**{**policy.model_dump(), "critical_cases": policy.critical_cases + ["lg03"]})
print(decide(strict, baseline, baseline).text())GATE: BLOCK
block: critical case lg03 failed its abstention checkএগোনোর পথ: বাংলা গার্ড ঠিক করুন (রিট্রিভাল পরিবর্তন, তারপর রিভিউ হওয়া কমিটে বেসলাইন নতুন করে সেভ), অথবা lg03-কে লিখিতভাবে, মালিকসহ, একটা পরিচিত খোলা ত্রুটি হিসেবে নথিভুক্ত করুন। চুপচাপ বাদ দেওয়া কোনো পথ নয়। পাস চেকলিস্ট: ব্লক পুনরুৎপাদিত; ত্রুটিটাকে বাস্তব বলা হয়েছে; দুটো পথের নাম।
সারসংক্ষেপ
- জাজ মডেল একটা পক্ষপাতদুষ্ট সংকেত: পর্যবেক্ষণযোগ্য রুব্রিক অ্যাংকর লিখুন, জোড়া দুই ক্রমে তুলনা করুন, সংখ্যা, উল্টে যাওয়া রায় ও নমুনার আকার জানান।
compare_runsপ্রতিটা রিগ্রেস হওয়া কেস ও মেট্রিকের নাম বলে এবং ভিন্ন ডেটাসেট তুলনা করতে রাজি হয় না; ভাষা ধরে স্লাইস করুন।- মান, খরচ ও লেটেন্সি আলাদা রাখুন; সস্তা রান নিজে থেকে ভালো নয়। ফিক্সচার অফলাইনে রিপ্লে করুন; লাইভ রিপ্লাইয়ে শব্দ নয়, চুক্তি যাচাই করুন।
- রিলিজ গেট কারণসহ go, review বা block দেয়; ক্রিটিক্যাল ব্যর্থতা গড় যা-ই হোক আটকায়, আর তার এক্সিট কোড বিল্ড থামায়।
এরপর: খরচ ও লেটেন্সির মূল কথা পাতায় দেখব একটা রিকোয়েস্টের খরচ কত এবং সময় কত লাগে, ব্যবহারের খতিয়ান ও একটা কাল্পনিক মূল্য তালিকা দিয়ে, যাতে এই রিপোর্টের খরচ-অংশ বাস্তব সংখ্যা পায়।