অধ্যায় 5 · সিকিউরিটি ও অপারেশন
প্রম্পট অপারেশন: মালিক, ভার্সন, গ্রহণযোগ্যতার শর্ত ও রোলব্যাক
- পৃষ্ঠা 21 / 24
- 18 মিনিট পড়া
কোনো পণ্যের জন্য গুরুত্বপূর্ণ একটি প্রম্পট তখন আর শুধু লেখা থাকে না, সেটি একটি অ্যাসেট (asset) হয়ে যায়: অন্য মানুষ তার ওপর নির্ভর করে, সময়ের সঙ্গে সেটি বদলায়, আর একটি পরিবর্তন নীরবে অবস্থা খারাপও করে দিতে পারে। AI-এর জন্য Git, API ও ব্যাকএন্ড: অ্যাডভান্সড-এ দেখানো হয়েছে, ব্যাকএন্ড কীভাবে ট্র্যাক করে কোন প্রম্পট ও মডেল ভার্সন কোন উত্তর দিয়েছে। এই পাতায় তা আবার শেখানো হচ্ছে না। এখানে প্রম্পট অ্যাসেটের পুরো জীবনচক্র: কার মালিকানা, ভার্সনের নাম ও কারণ কীভাবে লেখা হবে, এডিটের আগে "ভালো" বলতে কী বোঝায়, আগের ভার্সনের সঙ্গে পরিবর্তন কীভাবে তুলনা হবে, আর কীভাবে পরিবর্তন ফিরিয়ে নেওয়া যাবে।
আপনি একটি ছোট টুলকিট, promptops.py, বানাবেন একটি কাল্পনিক অ্যাসেটের জন্য: একটি টিকিট-ট্রায়াজ প্রম্পট, যা সাপোর্ট টিকিটকে billing, bug বা other লেবেল দেয়। লাগবে Python ৩.১২ বা নতুন এবং আপনার ফোল্ডারে promptlab.py। যে ব্লকের প্রথম লাইনে file: লেখা, সেটি আপনার তৈরি করার ফাইল; বাকি ব্লকগুলো ক্রমানুসারে চলে এবং ভেরিয়েবল ভাগ করে নেয়। একমাত্র "মডেল" হলো একটি টয় প্রোভাইডার (toy provider), অর্থাৎ নির্ধারিত আচরণের একটি বদলি ফাংশন, যাতে টুলকিট বারবার একই ফল দিয়ে পরীক্ষা করা যায়। আসল মডেল কম অনুমানযোগ্য, আর তাই আপনি পরীক্ষা করেন; টয়ের ফল আসল মডেল সম্পর্কে কিছু বলে না।
যা শিখবেন
- মালিকসহ একটি প্রম্পট অ্যাসেট কীভাবে সংজ্ঞায়িত করবেন, এক্সপেরিমেন্টের আউটপুট থেকে আলাদা রাখবেন, এবং প্রতিটি ভার্সনকে পাঠযোগ্য আইডি ও লিখিত কারণ দেবেন।
- টেস্ট ইনপুট ও গ্রহণযোগ্যতার শর্ত (acceptance criteria) প্রম্পটের সঙ্গে রাখা, এবং এডিটের আগে শর্ত লেখা।
- নির্দিষ্ট রিগ্রেশন সেটে আগের ভার্সনের সঙ্গে পরিবর্তন তুলনা করা এবং শর্ত দেখে গ্রহণ বা বাতিল করা।
- অনুমান (assumption), ভাষা ও ব্যবহার-ক্ষেত্র অনুযায়ী মান, এবং প্রতিটি ভার্সনের ল্যাটেন্সি ও খরচের ট্রেড-অফ লিখে রাখা।
এক ফাইলে টুলকিট
প্রথমে এই মডিউলটি তৈরি করুন; নিচের প্রতিটি অংশ এর একেকটি অংশ ব্যবহার করে। ভেতরের টয় প্রোভাইডারটি একটি বদলি: প্রম্পট যে ভাষার নাম বলে শুধু সেগুলোই সে পড়ে এবং প্রম্পটে যেমন বলা আছে তেমন করে টাই ভাঙে, ফলে প্রম্পট এডিট করলে তার উত্তর দৃশ্যমানভাবে বদলায়।
# file: promptops.py
"""promptops: a tiny prompt registry (prompts/registry.json plus prompts/<name>/ files)."""
import difflib, hashlib, json
from pathlib import Path
from promptlab import Client, estimate_tokens, render
ROOT = Path("prompts")
PRICE_PER_1K_TOKENS = 1.0 # placeholder units, not a real price
def _read(path):
return json.loads(Path(path).read_text(encoding="utf-8"))
def _save(reg):
text = json.dumps(reg, indent=1, sort_keys=True, ensure_ascii=False)
(ROOT / "registry.json").write_text(text + "\n", encoding="utf-8")
def load(name, version=None):
"""Prompt text plus metadata; no version means the 'current' pointer."""
asset = _read(ROOT / "registry.json")[name]
version = version or asset["current"]
entry = asset["versions"][version]
return {"version": version, "entry": entry, "asset": asset,
"text": (ROOT / name / entry["file"]).read_text(encoding="utf-8")}
def lint(name):
"""What a teammate needs before touching an asset. Empty list = complete."""
asset, folder = _read(ROOT / "registry.json").get(name, {}), ROOT / name
problems = [f"no {key}" for key in ("purpose", "owner", "assumptions", "current") if not asset.get(key)]
problems += [f"missing file {f}" for f in ("cases.json", "criteria.json") if not (folder / f).exists()]
entry = asset.get("versions", {}).get(asset.get("current"))
if entry is None or not (folder / entry["file"]).exists():
problems.append("the current version has no file")
return problems
def next_version(name, breaking=False):
major, minor = max(tuple(int(n) for n in v[1:].split(".")) for v in load(name)["asset"]["versions"])
return f"v{major + 1}.0" if breaking else f"v{major}.{minor + 1}"
def add_version(name, text, reason, date, breaking=False):
"""Write the next version file and register it; no reason, no version."""
if not reason.strip():
raise ValueError("a new version needs a change reason")
reg, version = _read(ROOT / "registry.json"), next_version(name, breaking)
(ROOT / name / f"{version}.txt").write_text(text, encoding="utf-8")
reg[name]["versions"][version] = {"file": f"{version}.txt", "reason": reason, "date": date, "status": "candidate"}
_save(reg)
return version
def set_status(name, version, status):
reg = _read(ROOT / "registry.json")
reg[name]["versions"][version]["status"] = status
_save(reg)
def set_current(name, version, date, why):
"""Move the pointer (files never change) and record the move."""
reg = _read(ROOT / "registry.json")
asset = reg[name]
asset.setdefault("pointer_history", []).append(
{"from": asset["current"], "to": version, "date": date, "why": why})
asset["current"] = version
asset["versions"][version]["status"] = "released"
_save(reg)
def diff(name, a, b):
old, new = (load(name, v)["text"].splitlines() for v in (a, b))
return "\n".join(difflib.unified_diff(old, new, a, b, lineterm="", n=0))
KEYWORDS = {"en": {"billing": ("charged", "invoice", "refund"), "bug": ("crash", "error")},
"es": {"billing": ("cobr", "factura"), "bug": ("falla", "falló", "error")},
"bn": {"billing": ("চার্জ",), "bug": ("ক্র্যাশ",)}}
LANGUAGES = {"en": "English", "es": "Spanish", "bn": "Bangla"}
def toy_triage(messages, **settings):
"""STAND-IN, not a model: reads only the languages the prompt names; breaks ties as told.
Planted weakness: without the context line, it loses tickets longer than 12 words."""
rules, ticket = messages[-1]["content"].split("Ticket:\n", 1)
if "software shop" not in rules and len(ticket.split()) > 12:
return "other"
langs = [code for code, word in LANGUAGES.items() if word in rules]
hits = [label for label in ("billing", "bug")
if any(w in ticket.lower() for c in langs for w in KEYWORDS[c][label])]
if len(hits) > 1 and "choose bug" in rules:
return "bug"
return hits[0] if hits else "other"
def run_set(name, version, provider=toy_triage, text=None):
"""Run the fixed regression set. `text` tries an unsaved draft instead."""
raw = (ROOT / name / "cases.json").read_bytes()
prompt, cases = text or load(name, version)["text"], json.loads(raw)
client, rows = Client(provider, model="toy"), []
for c in cases:
reply = client.complete([{"role": "user", "content": render(prompt, ticket=c["ticket"])}])
rows.append({**{k: c[k] for k in ("id", "language", "use_case")}, "ok": reply.text.strip() == c["expected"],
"tokens": reply.usage.input_tokens, "ms": reply.latency_ms})
def by(key):
groups = {}
for r in rows:
groups.setdefault(r[key], []).append(r["ok"])
return {k: round(sum(v) / len(v), 2) for k, v in sorted(groups.items())}
n, tokens = len(rows), sum(r["tokens"] for r in rows) / len(rows)
report = {"name": name, "version": version, "cases": hashlib.sha256(raw).hexdigest()[:8],
"accuracy": round(sum(r["ok"] for r in rows) / n, 3), "by_language": by("language"),
"by_use_case": by("use_case"), "failed": [r["id"] for r in rows if not r["ok"]],
"mean_input_tokens": round(tokens, 1), "mean_latency_ms": round(sum(r["ms"] for r in rows) / n, 1),
"cost_per_1000_calls": round(tokens * PRICE_PER_1K_TOKENS, 1)}
if version: # experiment output goes to runs/, never to prompts/
out = Path("runs") / name
out.mkdir(parents=True, exist_ok=True)
(out / f"{version}.json").write_text(json.dumps(report, indent=1, sort_keys=True) + "\n")
return report
def check(report, baseline, criteria):
"""Judge a run by criteria written BEFORE the edit. Empty list = accepted."""
if baseline["cases"] != report["cases"]:
return [f"different case set ({baseline['cases']} vs {report['cases']}): results are not comparable"]
problems = []
if report["accuracy"] < criteria["min_accuracy"]:
problems.append(f"accuracy {report['accuracy']} is below {criteria['min_accuracy']}")
for lang, acc in report["by_language"].items():
if acc < criteria["min_accuracy_per_language"]:
problems.append(f"{lang} accuracy {acc} is below {criteria['min_accuracy_per_language']}")
new = sorted(set(report["failed"]) - set(baseline["failed"]))
if len(new) > criteria["max_new_failures"]:
problems.append(f"new failures: {new}")
if report["mean_input_tokens"] > criteria["max_mean_input_tokens"]:
problems.append(f"mean input {report['mean_input_tokens']} tokens is above {criteria['max_mean_input_tokens']}")
return problemsপ্রম্পট অ্যাসেট ও তার মালিক নির্ধারণ করুন
কেউ নির্ভর করলেই একটি প্রম্পট অ্যাসেট হয়। তখন শুধু লেখাটুকু যথেষ্ট নয়। রেজিস্ট্রি ফাইলে সেই তথ্যগুলো থাকে, যা একজন সহকর্মীর ছোঁয়ার আগে জানা দরকার:
| অংশ | কোথায় থাকে | কেন |
|---|---|---|
| প্রম্পটের লেখা, প্রতি ভার্সনে একটি ফাইল | prompts/ticket-triage/v1.0.txt | যেটি আসলে চালু হয় |
| উদ্দেশ্য, মালিক, রিভিউয়ার, অনুমান | registry.json | একজন নামধারী ব্যক্তি সিদ্ধান্ত নেন ও প্রশ্নের উত্তর দেন |
| প্রতিনিধিত্বকারী টেস্ট ইনপুট | cases.json | লেখার পাশে প্রমাণ |
| গ্রহণযোগ্যতার শর্ত | criteria.json | "ভালো" বলতে কী বোঝায় |
মালিক একজন নামধারী ব্যক্তি, যিনি পরিবর্তন গ্রহণ বা বাতিল করেন; কেউ পড়ে না এমন টিমের ডাকনাম নয়। রিভিউয়ার হলেন দ্বিতীয় জোড়া চোখ। রেজিস্ট্রি ও প্রথম ভার্সন দিয়ে শুরু করুন:
// file: prompts/registry.json
{
"ticket-triage": {
"purpose": "Label an incoming support ticket as billing, bug or other, so it reaches the right queue.",
"owner": "Nadia Rahman, support tools team (Dhaka)",
"reviewer": "Mateo Silva, support operations (Bogotá)",
"current": "v1.0",
"assumptions": {
"model": "any chat model that follows a one-word output instruction (a toy stand-in in this tutorial)",
"provider": "none here; write your provider and model name, and the date you checked them",
"settings": "default sampling settings; the reply is one word",
"input": "one ticket, up to a few hundred words, in English, Spanish or Bangla"
},
"versions": {
"v1.0": {"file": "v1.0.txt", "reason": "First release: labels only.", "date": "2026-09-01", "status": "released"}
}
}
}# file: prompts/ticket-triage/v1.0.txt
You label customer support tickets for a software shop.
Tickets are written in English.
Labels: billing, bug, other.
Reply with the label only.
Ticket:
{ticket}অ্যাসেটটি ইচ্ছা করেই অসম্পূর্ণ; lint বলে দেয় কী নেই।
from promptops import *
print(lint("ticket-triage"))['missing file cases.json', 'missing file criteria.json']প্রম্পট এক্সপেরিমেন্টের আউটপুট থেকে আলাদা রাখুন
উত্তর, লগ ও স্কোর দ্রুত জমে যায়। দুটি জায়গা রাখুন, দুটি নিয়মসহ:
prompts/ small, reviewed; every change has a reason
registry.json owner, purpose, assumptions, versions, pointer
ticket-triage/
v1.0.txt v1.1.txt one file per version; released files never change
cases.json criteria.json
runs/ outputs: many, regenerated, safe to delete
ticket-triage/v1.0.json one report per run (run_set writes it)prompts/-এর সবকিছু মানুষ রিভিউ করে। runs/-এর সবকিছু সেট আবার চালালে আবার তৈরি হয়, তাই সাধারণত তা ভার্সন কন্ট্রোলের বাইরে থাকে, অথবা বিল্ড আর্টিফ্যাক্ট হিসেবে রাখা হয়। দুটো মিশিয়ে ফেললে ডজনখানেক আউটপুট ফাইলের ভিড়ে প্রম্পটের পরিবর্তন হারিয়ে যায়। ব্যাকএন্ডের দিক (কোন ভার্সন কোন অনুরোধ সামলেছে) আছে AI-এর জন্য Git, API ও ব্যাকএন্ড: অ্যাডভান্সড-এ।
প্রতিটি ভার্সনকে পাঠযোগ্য আইডি ও কারণ দিন
আইডি দেখে একজন মানুষের বোঝা উচিত পরিবর্তনটি কী ধরনের। একটি স্কিম যা কাজ করে; final2-এর মতো নাম এড়িয়ে চলুন:
| পরিবর্তন | আইডি | উদাহরণ |
|---|---|---|
| শব্দ বা নিয়ম; আউটপুট ফরম্যাট ও ভেরিয়েবল একই | মাইনর: v1.1, v1.2 | টাই-নিয়ম যোগ করা |
| আউটপুট ফরম্যাট, লেবেল বা ভেরিয়েবল বদলায়, ফলে কলারকেও বদলাতে হয় | মেজর: v2.0 | এক শব্দের বদলে JSON-এ উত্তর |
যে নিয়ম ব্যবস্থাটিকে সৎ রাখে: লেখা বদলালেই কেবল আইডি বদলায়, আর রিলিজ হওয়া ফাইল কখনো বদলায় না। প্রতিটি ভার্সনে কেন সেটি আছে তাও লেখা থাকে। ডিফ দেখায় কী বদলেছে; কেন বদলেছে তা শুধু কারণই বলে। add_version পরের আইডি বেছে নেয় এবং ফাঁকা কারণ প্রত্যাখ্যান করে। এই হলো প্রথম আসল এডিট। নাদিয়ার টিম দেখেছে স্প্যানিশ ও বাংলা টিকিট other লেবেল পাচ্ছে এবং মিশ্র টিকিট চলে যাচ্ছে billing-এ, তাই নতুন প্রম্পটে তিন ভাষার নাম আছে আর একটি টাই-নিয়ম যোগ হয়েছে।
V11 = """You label customer support tickets for a software shop.
Tickets may be written in English, Spanish or Bangla.
Labels: billing, bug, other.
If a ticket fits more than one label, choose bug.
Reply with the label only.
Ticket:
{ticket}
"""
reason = "Spanish and Bangla tickets were labelled other; mixed tickets went to billing."
print(add_version("ticket-triage", V11, reason, "2026-09-14"))
print(diff("ticket-triage", "v1.0", "v1.1"))
try:
add_version("ticket-triage", V11, " ", "2026-09-14")
except ValueError as exc:
print("refused:", exc)v1.1
--- v1.0
+++ v1.1
@@ -2 +2 @@
-Tickets are written in English.
+Tickets may be written in English, Spanish or Bangla.
@@ -3,0 +4 @@
+If a ticket fits more than one label, choose bug.
refused: a new version needs a change reasonপ্রত্যাখ্যান দেখায়, অভ্যাসটি শুভ ইচ্ছায় নয়, কোডে বলবৎ হচ্ছে। নতুন ভার্সনটি একটি candidate: current পয়েন্টার এখনো v1.0-কে দেখাচ্ছে।
প্রতিটি প্রম্পটের সঙ্গে প্রতিনিধিত্বকারী টেস্ট ইনপুট রাখুন
একটি ভার্সন ততটুকুই নির্ভরযোগ্য, যতটুকু ইনপুটে তা পরীক্ষা করা হয়েছে। cases.json থাকে প্রম্পটের ফোল্ডারেই, ফলে যিনি প্রম্পট বদলান তিনি প্রমাণটিও দেখেন। এমন কেস বাছুন, যাতে আপনার সমর্থিত প্রতিটি ভাষা, প্রতিটি লেবেল এবং জটিল মিশ্র টিকিট থাকে; আসল সেট আরও বড় হয়। লেখাগুলো বানিয়ে নিন বা পরিচয় মুছে ফেলুন; টেস্ট সেটে ব্যক্তিগত ডেটার প্রসঙ্গে টিম হ্যান্ডওভার: প্রম্পট লাইব্রেরি, চেঞ্জ লগ, হ্যান্ডওভার ডকুমেন্ট ও প্রস্তুতি পাতায় ফিরে আসা হবে।
// file: prompts/ticket-triage/cases.json
[
{"id": "en-billing", "language": "en", "use_case": "billing", "expected": "billing", "ticket": "I was charged twice for my March invoice."},
{"id": "en-bug", "language": "en", "use_case": "bug", "expected": "bug", "ticket": "The app crashes when I upload a photo."},
{"id": "en-other", "language": "en", "use_case": "other", "expected": "other", "ticket": "How do I change my account email?"},
{"id": "en-mixed", "language": "en", "use_case": "mixed", "expected": "bug", "ticket": "Charged twice and then the app crashed."},
{"id": "es-billing", "language": "es", "use_case": "billing", "expected": "billing", "ticket": "Me cobraron dos veces la factura de marzo."},
{"id": "es-bug", "language": "es", "use_case": "bug", "expected": "bug", "ticket": "La aplicación falla al subir una foto."},
{"id": "es-other", "language": "es", "use_case": "other", "expected": "other", "ticket": "¿Cómo cambio mi correo?"},
{"id": "es-mixed", "language": "es", "use_case": "mixed", "expected": "bug", "ticket": "Me cobraron dos veces y luego la app falló."},
{"id": "bn-billing", "language": "bn", "use_case": "billing", "expected": "billing", "ticket": "আমার কার্ডে দুইবার চার্জ করা হয়েছে।"},
{"id": "bn-bug", "language": "bn", "use_case": "bug", "expected": "bug", "ticket": "অ্যাপটি বারবার বন্ধ হয়ে যায়।"},
{"id": "bn-other", "language": "bn", "use_case": "other", "expected": "other", "ticket": "আমার পাসওয়ার্ড কীভাবে বদলাব?"},
{"id": "bn-mixed", "language": "bn", "use_case": "mixed", "expected": "bug", "ticket": "দুইবার চার্জ হয়েছে আর অ্যাপ ক্র্যাশ করেছে।"}
]| কেস | টিকিটের ইংরেজি অনুবাদ |
|---|---|
es-billing | "They charged me twice for the March invoice." (মার্চের ইনভয়েসের জন্য দুইবার টাকা কেটেছে।) |
es-bug | "The application fails when uploading a photo." (ছবি আপলোডের সময় অ্যাপ ব্যর্থ হয়।) |
es-other | "How do I change my email?" (আমার ইমেইল কীভাবে বদলাব?) |
es-mixed | "They charged me twice and then the app failed." (দুইবার টাকা কেটেছে, তারপর অ্যাপ ব্যর্থ হয়েছে।) |
bn-billing | "My card was charged twice." (আমার কার্ডে দুইবার চার্জ করা হয়েছে।) |
bn-bug | "The app keeps closing." (অ্যাপটি বারবার বন্ধ হয়ে যায়।) |
bn-other | "How do I change my password?" (আমার পাসওয়ার্ড কীভাবে বদলাব?) |
bn-mixed | "I was charged twice and the app crashed." (দুইবার চার্জ হয়েছে আর অ্যাপ ক্র্যাশ করেছে।) |
এডিটের আগে গ্রহণযোগ্যতার শর্ত ঠিক করুন
নতুন ফল দেখার আগে ঠিক করুন "যথেষ্ট ভালো" বলতে কী বোঝায়; পরে প্রায় যেকোনো ফলকেই গ্রহণযোগ্য বলে ব্যাখ্যা করা যায়। শর্ত সংখ্যায় লিখুন এবং কোডকে তা প্রয়োগ করতে দিন:
// file: prompts/ticket-triage/criteria.json
{"written_before": "v1.1", "min_accuracy": 0.85, "min_accuracy_per_language": 0.75,
"max_new_failures": 0, "max_mean_input_tokens": 70}min_accuracy_per_language: গড় ভালো দেখালেও কোনো ভাষা পেছনে পড়ে থাকতে পারবে না।max_new_failures: আগের ভার্সন যে কেস ঠিক উত্তর দিয়েছিল, তা নতুন করে ব্যর্থ হতে পারবে না, মোট ফল যতই ভালো হোক।max_mean_input_tokens: আকারের বাজেট, হেল্পারের মোটামুটি অনুমান (চার অক্ষরে এক টোকেন) থেকে; ভার্সনের তুলনার জন্য ভালো, বিল আগাম বলার জন্য নয়।
এখন অ্যাসেটটি সম্পূর্ণ:
print(lint("ticket-triage") or "asset complete")asset completeনির্দিষ্ট রিগ্রেশন সেটের সঙ্গে পরিবর্তন তুলনা করুন
রিগ্রেশন সেট (regression set) হলো কেসের একটি স্থির তালিকা, যা প্রতিটি প্রার্থী ভার্সনকে পাস করতে হয়। রানগুলোর মাঝে কেস বদলে গেলে স্কোরের পরিবর্তন প্রম্পট থেকে নয়, কেস থেকেও আসতে পারে। run_set প্রতিটি রিপোর্টে cases.json-এর একটি ছোট ডাইজেস্ট রাখে, আর check ভিন্ন ডাইজেস্টের রিপোর্ট তুলনা করতে অস্বীকার করে। বেসলাইন v1.0 চালান, তারপর প্রার্থী ভার্সন, তারপর শর্ত প্রয়োগ করুন। দ্বিতীয় প্রার্থী v1.2 টাই-নিয়ম বাদ দিয়ে টোকেন বাঁচাতে চায়।
crit = json.loads(Path("prompts/ticket-triage/criteria.json").read_text())
r0 = run_set("ticket-triage", "v1.0")
r1 = run_set("ticket-triage", "v1.1")
V12 = V11.replace("If a ticket fits more than one label, choose bug.\n", "")
add_version("ticket-triage", V12, "Shorter prompt to cut input tokens.", "2026-09-20")
r2 = run_set("ticket-triage", "v1.2")
for r in (r0, r1, r2):
print(r["version"], r["accuracy"], r["failed"])
print("v1.1 against v1.0:", check(r1, r0, crit) or "accepted")
print("v1.2 against v1.1:", check(r2, r1, crit) or "accepted")v1.0 0.417 ['en-mixed', 'es-billing', 'es-bug', 'es-mixed', 'bn-billing', 'bn-bug', 'bn-mixed']
v1.1 0.917 ['bn-bug']
v1.2 0.667 ['en-mixed', 'es-mixed', 'bn-bug', 'bn-mixed']
v1.1 against v1.0: accepted
v1.2 against v1.1: ['accuracy 0.667 is below 0.85', 'bn accuracy 0.5 is below 0.75', "new failures: ['bn-mixed', 'en-mixed', 'es-mixed']"]রিভিউয়ারের চোখে ফলটি পড়ুন। v1.1, v1.0-এর সাতটি ব্যর্থতার ছয়টি ঠিক করে এবং কিছু ভাঙে না; bn-bug এখনো ব্যর্থ, তবে এখন সেটি একটি জানা ব্যর্থতা। v1.2 সস্তা, কিন্তু তিনটি মিশ্র টিকিট হারায়: একসঙ্গে তিনটি শর্ত ভাঙে। সিদ্ধান্ত কে ভালো যুক্তি দেয় তার ওপর নির্ভর করে না। প্রথমটি রিলিজ করুন, দ্বিতীয়টি বাতিল চিহ্নিত করুন; শুধু পয়েন্টার সরে, আর বাতিল ফাইলটি রেকর্ডের জন্য থেকে যায়। রোলব্যাকও একইভাবে হয়: পয়েন্টারটি আগের কোনো রিলিজ হওয়া ভার্সনে ফিরিয়ে নিন (পরের পাতায় এর অনুশীলন আছে)।
set_current("ticket-triage", "v1.1", "2026-09-21", "Passed all criteria on the 12-case set.")
set_status("ticket-triage", "v1.2", "rejected")
asset = load("ticket-triage")["asset"]
print(asset["current"], {v: e["status"] for v, e in asset["versions"].items()})
print(asset["pointer_history"])v1.1 {'v1.0': 'released', 'v1.1': 'released', 'v1.2': 'rejected'}
[{'date': '2026-09-21', 'from': 'v1.0', 'to': 'v1.1', 'why': 'Passed all criteria on the 12-case set.'}]আসল মডেলে প্রতি কেসে একবার চালানো দুর্বল সংকেত; কম temperature রাখলেও উত্তর বদলায়। সীমারেখার কেস বারবার চালান, যেমন স্যাম্পলিং ও রিজনিং কন্ট্রোল: temperature, top-p, effort ও পুনরাবৃত্তিযোগ্যতা দেখায়। টয় প্রোভাইডার নির্ধারিত আচরণের, তাই এখানে একবার চালানোই যথেষ্ট।
মডেল ও প্রোভাইডারের অনুমান লিখে রাখুন
একটি প্রম্পট লেখা হয় একটি মডেলের জন্য, নির্দিষ্ট সেটিংসে, নির্দিষ্ট ধরনের ইনপুটের জন্য। এর কোনোটি বদলালে পুরনো টেস্টের ফল আর প্রমাণ থাকে না। রেজিস্ট্রির assumptions ব্লকে লেখা থাকে আপনি যে মডেল পরীক্ষা করেছেন তার সঠিক নাম, প্রোভাইডার, গুরুত্বপূর্ণ সেটিংস, ইনপুট (ভাষা, দৈর্ঘ্য), প্রত্যাশিত আউটপুট এবং কবে যাচাই করেছেন। প্রোভাইডার মডেল বদল বা বন্ধ ঘোষণা করলে রিভিউয়ার সহজে দেখতে পান কোন প্রম্পট প্রভাবিত। এখানে মডেলটি একটি বদলি, এন্ট্রিতে তা-ই লেখা; আপনার রেজিস্ট্রিতে আসল নাম বসান।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): কোনো প্রোভাইডারের প্রম্পট-রেজিস্ট্রি বা ভার্সনিং ফিচার এই টিউটোরিয়ালের তথ্য-যাচাইয়ে নিশ্চিত হয়নি। আপনার প্ল্যাটফর্ম এমন ফিচার দেয় কি না এবং কী কী ভার্সন করে (শুধু লেখা, নাকি মডেল, সেটিংস ও টেস্ট ডেটাও) তা যাচাই করে নিন। সাধারণ ফাইল যেকোনো প্ল্যাটফর্মে কাজ করে। সর্বশেষ ডকুমেন্টেশন দেখুন।
ভাষা ও ব্যবহার-ক্ষেত্র অনুযায়ী মান ট্র্যাক করুন
একটিমাত্র accuracy সংখ্যা লুকিয়ে রাখে কারা ক্ষতিগ্রস্ত। রিপোর্টে প্রতিটি কেসের language ও use_case ফিল্ড অনুযায়ী ফল ভাগ করা থাকে:
for r in (r0, r1, r2):
print(r["version"], "language", r["by_language"], "use case", r["by_use_case"])v1.0 language {'bn': 0.25, 'en': 0.75, 'es': 0.25} use case {'billing': 0.33, 'bug': 0.33, 'mixed': 0.0, 'other': 1.0}
v1.1 language {'bn': 0.75, 'en': 1.0, 'es': 1.0} use case {'billing': 1.0, 'bug': 0.67, 'mixed': 1.0, 'other': 1.0}
v1.2 language {'bn': 0.5, 'en': 0.75, 'es': 0.75} use case {'billing': 1.0, 'bug': 0.67, 'mixed': 0.0, 'other': 1.0}v1.0 ইংরেজিতে চলনসই (০.৭৫) কিন্তু স্প্যানিশ ও বাংলায় ব্যর্থ। v1.2 মোটে ০.৬৬৭ পায়, আর মিশ্র টিকিটে শূন্য। কোনোটিই গড়ে চোখে পড়ে না। দুটি সতর্কতা। প্রতি ভাষায় চারটি কেস শুধু পদ্ধতি দেখায়: একটি কেস একটি ভাষার স্কোর ০.২৫ সরিয়ে দেয়। আর টোকেনের সংখ্যা ভাষাভেদে আলাদা: ইংরেজির তুলনায় অনেক অ-ল্যাটিন লিপিতে একই বাক্যে বেশি টোকেন লাগে, তাই হেল্পারের মোটামুটি নিয়ম বাংলার মতো লিপির খরচ কম দেখাতে পারে। আপনার প্রোভাইডারের নিজস্ব কাউন্টার দিয়ে ভাষা ধরে ধরে মাপুন।
ল্যাটেন্সি ও খরচের ট্রেড-অফ লিখে রাখুন
প্রতিটি ভার্সনের একটি দাম আছে। মানের পাশে সেটি লিখে রাখুন, যাতে ট্রেড-অফ একটি দৃশ্যমান সিদ্ধান্ত হয়। run_set গড় ইনপুট টোকেন, গড় ল্যাটেন্সি এবং একটি খরচের সংখ্যা রাখে, যেখানে PRICE_PER_1K_TOKENS একটি প্লেসহোল্ডার; আপনার প্রোভাইডারের সর্বশেষ দামের তালিকা থেকে বসান।
for r in (r0, r1, r2):
print(r["version"], r["accuracy"], r["mean_input_tokens"], r["mean_latency_ms"], r["cost_per_1000_calls"])v1.0 0.417 47.6 55.8 47.6
v1.1 0.917 65.6 56.2 65.6
v1.2 0.667 53.2 57.5 53.2টয় ল্যাটেন্সি হেল্পারের বানানো সংখ্যা, যা উত্তরের দৈর্ঘ্যের ওপর নির্ভর করে, প্রম্পটের ওপর নয়; তাই এখানে আসল ট্রেড-অফ দেখানো যায় না। খরচের কলামটি একটি প্লেসহোল্ডার একক, টাকা নয়। টেবিলটি সিদ্ধান্তের আকার দেখায়: v1.1-এর আনুমানিক ইনপুট v1.0-এর চেয়ে প্রায় ৪০ শতাংশ বেশি, বিনিময়ে বড় মানের উন্নতি, যা সাধারণত লাভজনক; v1.2 সেই খরচের প্রায় এক-পঞ্চমাংশ বাঁচায় আর মিশ্র টিকিট ছেড়ে দেয়, যা লাভজনক নয়। আসল মডেলে আসল সময় মেপে চালান, আর মনে রাখুন আউটপুট টোকেন ও লুকানো রিজনিং টোকেনেরও সময় ও টাকা লাগে (দেখুন প্রম্পট ক্যাশিং ও খরচ: স্থির প্রিফিক্স, ব্যবহারের ডেটা ও আনুমানিক হিসাব)।
কাজের জায়গায় এটি কোথায় দেখা যায়
টিমগুলো অ্যাপ্লিকেশনের পাশে এমন একটি রেজিস্ট্রি রাখে এবং রিগ্রেশন রানের ওপর রিলিজ আটকে রাখে। নতুন সহকর্মী একটি শব্দ বদলানোর আগে মালিক, অনুমান, শর্ত ও সর্বশেষ রিপোর্ট পড়েন।
সাধারণ ভুল
- নামধারী মালিক নেই। সবাই এডিট করতে পারে, কেউ সিদ্ধান্ত নেয় না। সমাধান: একজন মালিক ও একজন রিভিউয়ার, রেজিস্ট্রিতে লেখা।
- রিলিজ হওয়া ফাইল এডিট করা। ইতিহাস অনুমানে পরিণত হয়। সমাধান: কারণসহ নতুন ভার্সন; পয়েন্টার সরান।
- ফল দেখার পর শর্ত ঠিক করা। তখন সংখ্যা যা ঘটেছে তাকেই ন্যায্য করে। সমাধান: এডিটের আগে
criteria.jsonকমিট করুন। - গড় দেখে বিচার করা। মোট বাড়লেও একটি ভাষা বা কেসের ধরন ধসে যেতে পারে। সমাধান: ভাষা ও ব্যবহার-ক্ষেত্র ধরে রিপোর্ট করুন; নতুন ব্যর্থতা গুনুন।
- টেস্ট সেট আর প্রম্পট একসঙ্গে বদলানো। কোনটি স্কোর সরাল বোঝা যায় না। সমাধান: একবারে একটি পরিবর্তন; ডাইজেস্ট মিলিয়ে দেখুন।
নিজে চেষ্টা করুন
- সহজ। দ্বিতীয় একটি অ্যাসেট,
reply-draft, নিবন্ধন করুন, যাতে শুধু উদ্দেশ্য এবংv1.0নামের current ভার্সন আছে, তারপর তার ওপরlintচালান। কী জানায় পড়ুন। - মাঝারি।
v1.3লিখুন, যাv1.1-এর চেয়ে ছোট কিন্তু তিন ভাষা ও টাই-নিয়ম রাখে। চালান এবং শর্ত দিয়েv1.1-এর সঙ্গে তুলনা করুন। - কঠিন। একটি তেরো নম্বর কেস যোগ করুন (স্প্যানিশ,
"Aparece un error al iniciar sesión.", অর্থ "লগইনের সময় একটি এরর দেখা যায়", প্রত্যাশিতbug)।v1.3আবার চালান এবং পুরনোv1.1রিপোর্টের সঙ্গেcheckকরুন। কী ঘটে, আর এরপর কী করবেন?
উত্তর
সহজ। রিপোর্টে অনুপস্থিত মালিক, অনুমান, দুটি ফাইল এবং অনুপস্থিত ভার্সন ফাইলের নাম থাকা উচিত।
reg = json.loads(Path("prompts/registry.json").read_text(encoding="utf-8"))
reg["reply-draft"] = {"purpose": "Draft a reply to a customer.", "current": "v1.0"}
Path("prompts/registry.json").write_text(json.dumps(reg), encoding="utf-8")
print(lint("reply-draft"))['no owner', 'no assumptions', 'missing file cases.json', 'missing file criteria.json', 'the current version has no file']পাস চেকলিস্ট: প্রতিটি সমস্যা কী নেই তা নাম ধরে বলে; সমাধান হলো তা পূরণ করা, lint বদলানো নয়।
মাঝারি।
V13 = """You label customer support tickets for a software shop.
Tickets may be in English, Spanish or Bangla.
Labels: billing, bug, other. Mixed ticket: choose bug.
Reply with the label only.
Ticket:
{ticket}
"""
add_version("ticket-triage", V13, "Same behaviour as v1.1 in fewer tokens.", "2026-09-28")
r3 = run_set("ticket-triage", "v1.3")
print(r3["accuracy"], r3["failed"], r3["mean_input_tokens"])
print(check(r3, r1, crit) or "accepted")0.917 ['bn-bug'] 57.6
acceptedপাস চেকলিস্ট: ভার্সনের কারণ আছে; একই স্থির সেট; তুলনা সবচেয়ে পুরনোটির সঙ্গে নয়, current ভার্সনের সঙ্গে; "accepted" এসেছে শর্ত থেকে।
কঠিন।
path = Path("prompts/ticket-triage/cases.json")
cases = json.loads(path.read_text(encoding="utf-8"))
cases.append({"id": "es-bug2", "language": "es", "use_case": "bug", "expected": "bug",
"ticket": "Aparece un error al iniciar sesión."})
path.write_text(json.dumps(cases, ensure_ascii=False, indent=1), encoding="utf-8")
r3b = run_set("ticket-triage", "v1.3")
print(check(r3b, r1, crit))
r1b = run_set("ticket-triage", "v1.1")
print(check(r3b, r1b, crit) or "accepted on the new set")['different case set (f125750a vs 80efc886): results are not comparable']
accepted on the new setপাস চেকলিস্ট: ডাইজেস্ট বদলে গেছে বলে check তুলনা প্রত্যাখ্যান করে; পরের ধাপ হলো নতুন সেটে বেসলাইন আবার চালানো। কেস যোগ করা বৈধ, তবে প্রম্পট এডিট থেকে তা একটি আলাদা পরিবর্তন।
সারসংক্ষেপ
- প্রম্পট অ্যাসেটের মালিক, উদ্দেশ্য, অনুমান, টেস্ট কেস ও শর্ত তার সঙ্গেই রাখা হয়।
- রিভিউ হওয়া প্রম্পট আর আবার তৈরি হওয়া এক্সপেরিমেন্ট আউটপুট আলাদা রাখুন।
- আইডি বলে পরিবর্তন কী ধরনের, রিলিজ হওয়া ফাইল কখনো বদলায় না, আর প্রতিটি ভার্সনে কারণ লেখা থাকে।
- এডিটের আগে গ্রহণযোগ্যতার শর্ত লিখুন, তারপর নির্দিষ্ট রিগ্রেশন সেটে প্রতিটি প্রার্থীকে আগের ভার্সনের সঙ্গে তুলনা করুন।
- ভাষা ও ব্যবহার-ক্ষেত্র ধরে মান জানান, পাশে ল্যাটেন্সি ও খরচ রাখুন;
currentপয়েন্টার সরিয়ে রিলিজ করুন।
এরপর: টিম হ্যান্ডওভার: প্রম্পট লাইব্রেরি, চেঞ্জ লগ, হ্যান্ডওভার ডকুমেন্ট ও প্রস্তুতি, যেখানে আপনি মডেল-নির্দিষ্ট ভ্যারিয়েন্ট, রিভিউ গেট, রোলব্যাক, চেঞ্জ লগ ও হ্যান্ডওভার ডকুমেন্ট যোগ করবেন।