অধ্যায় 2 · চেইন ও স্ট্রাকচার্ড আউটপুট
ভ্যালিডেশন ও রিপেয়ার: jsonschema, বিজনেস রুল ও সীমিত রিট্রাই
- পৃষ্ঠা 8 / 24
- 18 মিনিট পড়া
যে উত্তর পার্স হয়, সেটা এখনো ভরসা করার মতো রেকর্ড নয়। বাস্তব পাইপলাইন উত্তরটা স্তরে স্তরে যাচাই করে: মডেল শেষ করেছে কি না, এটা বৈধ JSON কি না, স্কিমায় মেলে কি না, মানগুলো বিজনেস রুল মানে কি না। প্রতিটা স্তর আলাদা ধরনের ব্যর্থতা ধরে, আর ব্যর্থতার পরে কী করবেন (রিপেয়ার, থামা, নাকি মানুষকে জিজ্ঞেস করা) তা ধরে ফেলার মতোই গুরুত্বপূর্ণ।
এই পাতা ইনভয়েস-এক্সট্র্যাকশন কাজের চারপাশে সেই স্তরগুলো বানায়, JSON ও স্কিমা: ফিল্ড, টাইপ, enum ও অনুপস্থিত মান পাতাের ধারাবাহিকতায়। দরকার Python ৩.১২ বা নতুন, promptlab.py আর pip install jsonschema; ব্লকগুলো ক্রমে টাইপ করুন (এগুলো একই নাম ভাগ করে নেয়)। প্রতিটা "মডেল" একটা টয় প্রোভাইডার (toy provider): সাধারণ ভুল বসানো নির্ধারিত বদলি ফাংশন, যাতে পাইপলাইন বারবার একইভাবে পরীক্ষা করা যায়। আসল মডেল কম অনুমানযোগ্য, তাই পরীক্ষা করতে হয়; টয়ের ফল আসল মডেল সম্পর্কে কিছু বলে না।
যা শিখবেন
- প্রোভাইডারের স্ট্রাকচার্ড-আউটপুট ডকুমেন্টেশন থেকে আপনার কোডের নির্ভর করা তথ্যগুলো কীভাবে পড়বেন।
- Python-এ সিনট্যাক্স, স্কিমা ও বিজনেস রুল কীভাবে যাচাই করবেন, আর অনুপস্থিত, বাড়তি ও ভুল ফিল্ড কীভাবে আলাদা করবেন।
- প্রত্যাখ্যান (refusal) ও মাঝপথে কেটে যাওয়া উত্তর কীভাবে সামলাবেন, আর সীমিত রিট্রাইয়ে রিপেয়ার কীভাবে করবেন।
- কখন রিপেয়ার করবেন না, আর একাধিক মডেলের স্কিমা-সম্মতি ন্যায্যভাবে কীভাবে তুলনা করবেন।
প্রোভাইডারের স্ট্রাকচার্ড-আউটপুট ডকুমেন্টেশন পড়ুন
স্কিমা-সীমাবদ্ধ আউটপুটের ওপর ভরসা করার আগে প্রোভাইডারের বর্তমান ডকুমেন্টেশনে নিচের প্রশ্নগুলোর উত্তর খুঁজে নিন। প্রতিটা উত্তর আপনার কোড বদলে দেয়।
| ডকুমেন্টেশনের কাছে প্রশ্ন | উত্তর পেয়ে আপনার কোড কী করবে |
|---|---|
| কোন স্কিমা-কিওয়ার্ড সমর্থিত? | স্কিমা সেই উপসেটের মধ্যে রাখুন; অসমর্থিত শর্ত (পরিসর, দৈর্ঘ্য) Python-এ আবার যাচাই করুন। |
| সব ফিল্ড required হতে হবে কি না? বাড়তি প্রপার্টি নিষিদ্ধ করতে হবে কি না? | ঐচ্ছিক ফিল্ডকে "null হতে পারে" হিসেবে লিখুন; required ফ্ল্যাগ বসান। |
| প্রত্যাখ্যান কীভাবে জানানো হয়? | পার্স করার আগে পরীক্ষা করুন। |
| টোকেন-সীমায় কেটে যাওয়া কীভাবে জানানো হয়? | স্টপ স্ট্যাটাস পরীক্ষা করুন; কেটে যাওয়া উত্তর পার্সিংয়ের সমস্যা নয়। |
| ঠিক কী নিশ্চিত করা হয়? | শুধু ফরম্যাট। মান যাচাই আপনার দায়িত্ব। |
| আকার, নেস্টিং বা প্রথম-অনুরোধের সীমা আছে কি না? অসমর্থিত স্কিমায় এরর আসে কি না? | স্কিমা ছোট রাখুন; প্রোডাকশনে নয়, আগেই পরীক্ষা করুন। |
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): OpenAI-এর ডকুমেন্টেশনে আছে: প্রত্যাখ্যান আসে আলাদা ফিল্ডে এবং তা স্কিমা না-ও মানতে পারে; টোকেন-সীমায় কাটা উত্তর অসম্পূর্ণ (incomplete) হিসেবে জানানো হয় (কারণ
max_output_tokens); সব ফিল্ড required এবংadditionalPropertiesfalse হতে হয়; আর নতুন স্কিমা নিয়ে প্রথম অনুরোধে বাড়তি ল্যাটেন্সি থাকে। Anthropic-এর ডকুমেন্টেশনে আছে:stop_reasonrefusalবাmax_tokensহলে স্কিমায় না-মেলা আউটপুট আসতে পারে (আগেstop_reasonদেখুন); অসমর্থিত স্কিমা-ফিচারে এরর আসে; আর SDK হেল্পার ক্লায়েন্টে যাচাই করতে পারে। Google-এর ডকুমেন্টেশনে একটা সমর্থিত উপসেট আছে (enum, minimum/maximum, items ও required সহ); খুব বড় বা গভীর নেস্টেড স্কিমা বাতিল হতে পারে বলে সতর্কতা আছে; title ও description পথ দেখায় কিন্তু প্রয়োগ হয় না; আর মান নিজের অ্যাপ্লিকেশনে যাচাই করতে বলা হয়েছে। যে পাতা দেখা হয়েছে সেখানে কেটে যাওয়ার বিবরণ নেই, তাই বর্তমান ডকুমেন্টেশনে খুঁজে দেখুন। নাম ও সীমা বদলায়: বর্তমান ডকুমেন্টেশন দেখুন।
নিচে টয় মডেলগুলো API-র আকারের ফলাফল দেয়: {"stop": ..., "text": ...}। ফিল্ডের নাম প্রোভাইডারভেদে আলাদা; ধারণাটা এক। আগে ডেটা: ১২টা বানানো ইনভয়েস, প্রতিটার উদ্দিষ্ট রেকর্ড জানা। দুটোতে উৎসেই সমস্যা আছে (INV-1006 নিজের লাইনের সঙ্গে মেলে না; INV-1009-এর তারিখ 06/04/2026, যা ৬ এপ্রিল বা ৪ জুন দুটোই হতে পারে)।
import json, re
from jsonschema import Draft202012Validator
from promptlab import render
DATE = r"^\d{4}-\d{2}-\d{2}$"
SCHEMA = {"type": "object", "additionalProperties": False,
"required": ["invoice_number", "issue_date", "due_date", "currency", "lines", "total"],
"properties": {
"invoice_number": {"type": "string", "pattern": r"^INV-\d{4}$"},
"issue_date": {"type": "string", "pattern": DATE},
"due_date": {"type": ["string", "null"], "pattern": DATE},
"currency": {"enum": ["USD", "EUR", "BDT"]},
"total": {"type": "number", "minimum": 0},
"lines": {"type": "array", "minItems": 1, "items": {
"type": "object", "additionalProperties": False, "required": ["description", "quantity", "unit_price"],
"properties": {"description": {"type": "string", "minLength": 1},
"quantity": {"type": "integer", "minimum": 1}, "unit_price": {"type": "number", "minimum": 0}}}}}}
VALIDATOR = Draft202012Validator(SCHEMA)
QUIRK = {5: "the invoice total disagrees with its own lines", 8: "the date 06/04/2026 can be read two ways"}
def make_invoice(i):
lines = [{"description": f"Item {'ABC'[j]}", "quantity": 1 + (i + j) % 3, "unit_price": float(10 + 7 * ((3 * i + j) % 5))} for j in range(1 + i % 3)]
total = float(sum(l["quantity"] * l["unit_price"] for l in lines)) + (5.0 if i == 5 else 0.0)
gold = {"invoice_number": f"INV-{1001 + i}", "issue_date": f"2026-03-{10 + i:02d}",
"due_date": None if i % 4 == 3 else f"2026-04-{10 + i:02d}", "currency": ["USD", "EUR", "BDT"][i % 3],
"lines": lines, "total": total}
shown = "06/04/2026" if i == 8 else gold["issue_date"]
head = f"Invoice {gold['invoice_number']} | Date: {shown} | Due: {gold['due_date'] or 'on receipt'} | Currency: {gold['currency']}"
body = [f"{l['quantity']} x {l['description']} @ {l['unit_price']:.2f}" for l in lines]
return "\n".join([head, *body, f"Total: {total:.2f}"]), gold
SOURCES, GOLD = zip(*(make_invoice(i) for i in range(12)))
print(SOURCES[1]); print("---"); print(SOURCES[8])Invoice INV-1002 | Date: 2026-03-11 | Due: 2026-04-11 | Currency: EUR
2 x Item A @ 31.00
3 x Item B @ 38.00
Total: 176.00
---
Invoice INV-1009 | Date: 06/04/2026 | Due: 2026-04-18 | Currency: BDT
3 x Item A @ 38.00
1 x Item B @ 10.00
2 x Item C @ 17.00
Total: 158.00Python-এ JSON সিনট্যাক্স যাচাই
প্রথম স্তর শুধু জিজ্ঞেস করে "এটা JSON কি না?"। json.loads ভুল হলে বার্তা, লাইন ও কলামসহ JSONDecodeError তোলে। এর দুটো ডিফল্ট অনেককে অবাক করে: সে NaN মেনে নেয়, আর একই কি দুবার থাকলে চুপচাপ শেষ মানটা রাখে। যে ডেটায় ভরসা করবেন তাতে দুটোই নিষিদ্ধ করুন। কোড ফেন্স, শেষের কমা আর কেটে যাওয়া স্ট্রিং এই স্তরেই ব্যর্থ হয়।
def parse_json(text):
"""Strict syntax check: returns (document, None) or (None, reason). Rejects NaN and duplicate keys."""
def no_dupes(pairs):
keys = [k for k, _ in pairs]
if len(keys) != len(set(keys)): raise ValueError("duplicate key")
return dict(pairs)
def no_constant(name): raise ValueError(f"{name} is not valid JSON")
try: return json.loads(text, object_pairs_hook=no_dupes, parse_constant=no_constant), None
except json.JSONDecodeError as exc: return None, f"not JSON: {exc.msg} (line {exc.lineno}, column {exc.colno})"
except ValueError as exc: return None, f"not JSON: {exc}"
for text in ['{"total": 300.0}', '```json\n{"total": 300.0}\n```', '{"total": 300.0,}', '{"total": NaN}', '{"total": 1, "total": 2}', '{"total": 30']:
print(f"{text[:26]!r:30}", parse_json(text)[1] or "ok")
print(json.loads('{"total": NaN}')) # the standard parser accepts NaN unless you forbid it'{"total": 300.0}' ok
'```json\n{"total": 300.0}\n`' not JSON: Expecting value (line 1, column 1)
'{"total": 300.0,}' not JSON: Expecting property name enclosed in double quotes (line 1, column 17)
'{"total": NaN}' not JSON: NaN is not valid JSON
'{"total": 1, "total": 2}' not JSON: duplicate key
'{"total": 30' not JSON: Expecting ',' delimiter (line 1, column 13)
{'total': nan}jsonschema দিয়ে স্কিমার বিরুদ্ধে যাচাই
দ্বিতীয় স্তর কাঠামো দেখে: কি, টাইপ, enum, প্যাটার্ন, সর্বনিম্ন মান। Draft202012Validator.iter_errors প্রতিটা ভুল দেয়, সঙ্গে পথ, ব্যর্থ কিওয়ার্ড আর খারাপ মান। মানুষের দরকারি তিন দলে ভাগ করুন: অনুপস্থিত ফিল্ড (required কিন্তু নেই), বাড়তি ফিল্ড (অনুমোদিত নয়) এবং ভুল ফিল্ড (আছে কিন্তু টাইপ, মান বা ফরম্যাট ভুল)। শেষ লাইনটা দেখুন: "date"-এর মতো format কিওয়ার্ড ফরম্যাট চেকার না দিলে প্রয়োগ হয় না, তাই এখানকার মতো pattern আর কোডের চেক ব্যবহার করুন। (Pydantic v2 Python ক্লাস থেকে একই স্তর দেয়; ধারণা বদলায় না।)
def diagnose(doc):
"""Schema layer: sort jsonschema errors into missing, extra and incorrect fields."""
found = {"missing": [], "extra": [], "incorrect": []}
for e in VALIDATOR.iter_errors(doc):
where = "/".join(map(str, e.path)) or "(top)"
if e.validator == "required":
found["missing"] += [f"{where}/{m}" for m in re.findall(r"'(\w+)' is a required", e.message)]
elif e.validator == "additionalProperties":
found["extra"] += [f"{where}/{x}" for x in re.findall(r"'(\w+)'", e.message.split("(")[1])]
else:
found["incorrect"].append(f"{where}: {e.instance!r} fails {e.validator}")
return {k: sorted(v) for k, v in found.items() if v}
ok_doc = json.loads(json.dumps(GOLD[1]))
print(diagnose(ok_doc) or "no problems")
bad = {**ok_doc, "currency": "eur", "lines": [{"description": "Item A", "quantity": "2", "unit_price": 10.0, "sku": "A1"}]}
del bad["total"]
print(diagnose(bad))
print(Draft202012Validator({"type": "string", "format": "date"}).is_valid("14 March"), "<- 'format' is not enforced by default")no problems
{'missing': ['(top)/total'], 'extra': ['lines/0/sku'], 'incorrect': ["currency: 'eur' fails enum", "lines/0/quantity: '2' fails type"]}
True <- 'format' is not enforced by defaultস্কিমার বাইরে বিজনেস রুল যাচাই
স্কিমা বলতে পারে না যে মোট সংখ্যা লাইনগুলোর যোগফলের সমান, বা নির্ধারিত তারিখ ইস্যুর তারিখের আগে নয়। এগুলো বিজনেস রুল: আপনার ক্ষেত্রের তথ্য। এগুলো চালান শুধু সেই ডকুমেন্টে, যা আগেই স্কিমায় মিলেছে, আর প্রতিটা ব্যর্থতায় মানুষ পড়তে পারে এমন বার্তা দিন।
TODAY = "2026-12-31" # a fixed "today" keeps the example repeatable; in real code pass the current date in
def rules(doc):
"""Business rules: facts the schema cannot express. Run only on a document that already fits the schema."""
found = []
expected = sum(l["quantity"] * l["unit_price"] for l in doc["lines"])
if abs(expected - doc["total"]) > 0.005: found.append(f"total {doc['total']} but the lines add up to {expected}")
if doc["due_date"] and doc["due_date"] < doc["issue_date"]: found.append("due_date is before issue_date")
if doc["issue_date"] > TODAY: found.append("issue_date is in the future")
return found
print(rules(GOLD[1]), rules(GOLD[5]))
print(rules({**GOLD[1], "due_date": "2026-03-01", "issue_date": "2027-01-05"}))[] ['total 100.0 but the lines add up to 95.0']
['due_date is before issue_date', 'issue_date is in the future']2026-02-31-এর মতো তারিখ তারিখ-প্যাটার্নে মেলে, তবু আসল তারিখ নয়; প্রথম অনুশীলনীতে এর চেক দেখানো হয়েছে।
প্রত্যাখ্যান ও অসম্পূর্ণ আউটপুট সামলান
মডেল প্রত্যাখ্যান করতে পারে, অথবা JSON-এর মাঝখানে টোকেন ফুরিয়ে যেতে পারে। দুই ক্ষেত্রেই লেখাটা রিপেয়ারের প্রার্থী নয়, তাই পার্স করার আগে স্টপ স্ট্যাটাস দেখুন। প্রত্যাখ্যান একটা সিদ্ধান্ত (থামুন এবং মানুষকে জিজ্ঞেস করুন); কেটে যাওয়ায় বেশি সীমা দিয়ে একবার আবার চেষ্টা করা যেতে পারে। দুটোকে "অবৈধ JSON, ঠিক করো" ভাবলে কল নষ্ট হয় আর আসল কারণ চাপা পড়ে। নিচের মডেলগুলো সাধারণ ভুল নকল করে: model_a-তে একটা কেটে-যাওয়া উত্তর আছে, model_b উত্তর ফেন্সে মোড়ায়, সংখ্যার জায়গায় স্ট্রিং পাঠায় এবং একবার প্রত্যাখ্যান করে, আর model_c বিশ্বাসযোগ্য কিন্তু ভুল মান লেখে।
EXTRACT = """Extract the invoice as ONE JSON object and nothing else (no Markdown).
Fields: invoice_number, issue_date (YYYY-MM-DD), due_date (or null), currency (USD, EUR, BDT),
lines (a list of objects with description, quantity, unit_price), total. Use null if missing; never guess.
### INVOICE
{source}"""
REPAIR = EXTRACT + "\n### PROBLEMS WITH YOUR LAST ANSWER\n{problems}\nFix only these problems, using the invoice text above."
def read_source(source):
"""What every toy 'reads' from the text. Note the silent guess: a slash date is read as day/month/year."""
head, *rest = source.splitlines()
f = dict(p.strip().split(": ", 1) for p in head.split("|")[1:])
d = re.fullmatch(r"(\d\d)/(\d\d)/(\d{4})", f["Date"])
lines = [dict(zip(("quantity", "description", "unit_price"), (int(m[0]), m[1], float(m[2]))))
for m in (re.fullmatch(r"(\d+) x (.+) @ ([\d.]+)", l).groups() for l in rest[:-1])]
return {"invoice_number": head.split()[1], "issue_date": f"{d[3]}-{d[2]}-{d[1]}" if d else f["Date"],
"due_date": None if f["Due"] == "on receipt" else f["Due"], "currency": f["Currency"],
"lines": lines, "total": float(rest[-1].split(": ")[1])}
def answer(stop, text=""):
return {"stop": stop, "text": text} # API-shaped result; the stop field is named differently by each provider
def parts(prompt):
"""Invoice number, what a careful reader would extract, and whether this is a repair request."""
doc = read_source(prompt.split("### INVOICE\n", 1)[1].split("\n### PROBLEMS")[0])
return int(doc["invoice_number"][4:]) - 1001, doc, "### PROBLEMS" in prompt
def model_a(prompt):
"""Stand-in 'strict' model: clean JSON, except one reply cut off by a token limit."""
n, doc, repair = parts(prompt)
return answer("max_tokens", json.dumps(doc)[:60]) if n == 7 and not repair else answer("end", json.dumps(doc))
def model_b(prompt):
"""Stand-in 'wrapper' model: fences on even invoices, a string quantity on every third, one refusal."""
n, doc, repair = parts(prompt)
if not repair:
if n == 9: return answer("refusal")
if n % 3 == 0: doc["lines"][0]["quantity"] = str(doc["lines"][0]["quantity"])
if n % 2 == 0: return answer("end", "```json\n" + json.dumps(doc) + "\n```")
return answer("end", json.dumps(doc))
def model_c(prompt):
"""Stand-in 'sloppy values' model: lower-case currency, a prose date, an invented total; clean when told what is wrong."""
n, doc, repair = parts(prompt)
if not repair:
if n % 3 == 1: doc["currency"] = doc["currency"].lower()
if n % 4 == 0: doc["issue_date"] = "14 Mar 2026"
if n % 5 == 2: doc["total"] += 1.0
return answer("end", json.dumps(doc))
def model_stuck(prompt):
return answer("end", '{"total": "unknown"}') # never valid, whatever you tell itসীমিত রিপেয়ার প্রম্পটে অবৈধ আউটপুট আবার চেষ্টা করুন
উত্তর শেষ হয়েছে কিন্তু সিনট্যাক্স, স্কিমা বা রুলে ব্যর্থ হলে একবার-দুবার সংশোধন চাইতে পারেন। ভালো রিপেয়ার প্রম্পটে থাকে মূল কাজ ও উৎস, আপনার ভ্যালিডেটরের পাওয়া সঠিক সমস্যাগুলো ("আবার চেষ্টা করো" নয়), আর শুধু সেগুলোই বদলানোর নির্দেশ।
দুর্বল প্রম্পট:
Your JSON was invalid. Try again.ভালো প্রম্পট:
[the original task and the invoice text, unchanged]
### PROBLEMS WITH YOUR LAST ANSWER
not JSON: Expecting property name enclosed in double quotes (line 1, column 17)
Fix only these problems, using the invoice text above. Output one JSON object and nothing else.দুর্বল প্রম্পট কারণ দেয় না, আর পুরোটা নতুন করে লিখতে উসকায়। ভালোটা উৎস আবার দেয়, ভ্যালিডেটরের পাওয়া সমস্যার নাম বলে, আর পরিবর্তন সীমিত রাখে; নিচের কোড এটা REPAIR থেকে বানায়।
লুপ সীমিত রাখুন: max_repairs=2 মানে সর্বোচ্চ তিনটা কল, তারপর নামকরা ব্যর্থতা। যে মডেল কখনো উন্নতি করে না, যেমন model_stuck, ঠিক তিনটা কল খরচ করে।
উৎস অস্পষ্ট হলে রিপেয়ার করবেন না
রিপেয়ার ফরম্যাটের ভুলের জন্য, যা মডেল লেখাটা আবার পড়ে ঠিক করতে পারে। উৎসই সমস্যা হলে এটা বিপজ্জনক। 06/04/2026 তারিখের যেকোনো উত্তর আন্দাজ, আর সেই আন্দাজ সব স্তর পার হয়ে যায়, কারণ ভুল তারিখও প্যাটার্ন আর রুলে মেলে। তাই পাইপলাইন আগে উৎস পরীক্ষা করে (ambiguous_source) এবং কোনো মডেল-কলের আগেই ইনভয়েসটা মানুষের কাছে পাঠায়। INV-1006-এ মডেল মোট সংখ্যাটা বিশ্বস্তভাবে কপি করেছে, আর উৎস নিজেই নিজের সঙ্গে অমিল; grounded ধরে যে খারাপ মোটটা উৎসেই আছে, তাই "ঠিক করো" বললে মডেলকে একটা বানানো সংখ্যা দিতে উসকানি দেওয়া হতো। নিয়ম: ফরম্যাট রিপেয়ার করুন, বিষয়বস্তু এসক্যালেট করুন।
def ambiguous_source(source):
"""A rule on the SOURCE, before any model call: dd/mm vs mm/dd cannot be known from '06/04/2026'."""
m = re.search(r"\b(\d\d)/(\d\d)/\d{4}\b", source)
return "ambiguous date " + m.group(0) if m and int(m.group(1)) <= 12 and int(m.group(2)) <= 12 else None
def grounded(doc, source):
return f"{doc['total']:.2f}" in source # the model copied the total that the source really shows
def process(source, model, max_repairs=2):
if why := ambiguous_source(source): # do not ask a model to guess; do not repair
return {"status": "needs_review", "why": why, "calls": 0}
prompt, calls = render(EXTRACT, source=source), 0
while True:
result, calls = model(prompt), calls + 1
if result["stop"] != "end": # check the stop status BEFORE parsing
return {"status": "failed", "why": "stop=" + result["stop"], "calls": calls}
doc, error = parse_json(result["text"])
if error: problems = [error]
elif schema := diagnose(doc): problems = [f"{k}: {', '.join(v)}" for k, v in schema.items()]
elif not (problems := rules(doc)):
return {"status": "ok" if calls == 1 else "repaired", "doc": doc, "calls": calls}
elif grounded(doc, source): # the source itself is inconsistent: repairing would be a guess
return {"status": "needs_review", "why": "; ".join(problems), "calls": calls}
if calls > max_repairs:
return {"status": "failed", "why": problems[0].split(":")[0], "calls": calls}
prompt = render(REPAIR, source=source, problems="\n".join(problems))
for i in (0, 4, 5, 8, 7, 9):
for name, model in (("a", model_a), ("b", model_b)):
r = process(SOURCES[i], model); print(f"INV-{1001 + i} model {name}:", r["status"], r.get("why", ""), "| calls", r["calls"])
r = process(SOURCES[0], model_stuck)
print("stuck model:", r["status"], "|", r["why"], "| calls", r["calls"])INV-1001 model a: ok | calls 1
INV-1001 model b: repaired | calls 2
INV-1005 model a: ok | calls 1
INV-1005 model b: repaired | calls 2
INV-1006 model a: needs_review total 100.0 but the lines add up to 95.0 | calls 1
INV-1006 model b: needs_review total 100.0 but the lines add up to 95.0 | calls 1
INV-1009 model a: needs_review ambiguous date 06/04/2026 | calls 0
INV-1009 model b: needs_review ambiguous date 06/04/2026 | calls 0
INV-1008 model a: failed stop=max_tokens | calls 1
INV-1008 model b: ok | calls 1
INV-1010 model a: ok | calls 1
INV-1010 model b: failed stop=refusal | calls 1
stuck model: failed | missing | calls 3লাইনগুলো ফলাফলের তালিকা হিসেবে পড়ুন: INV-1001-এ দুই মডেলই ঠিক (model b-র একবার রিপেয়ার লেগেছে), উৎসের দুই সমস্যা রিপেয়ার ছাড়াই মানুষের কাছে গেছে, কেটে-যাওয়া আর প্রত্যাখ্যান এক কলের পরে নামকরা ব্যর্থতা হিসেবে শেষ হয়েছে, আর আটকে-থাকা মডেল তিন কলের পর missing কারণে শেষ হয়েছে।
একাধিক মডেলের স্কিমা-সম্মতি তুলনা করুন
মডেল ন্যায্যভাবে তুলনা করতে একই ইনপুট একই স্তরে চালান এবং প্রতিটা স্তর আলাদা রিপোর্ট করুন: শেষ হয়েছে, বৈধ JSON, স্কিমায় মেলে, রুল পাস করে, উদ্দিষ্ট রেকর্ডের সমান। নিচের টেবিলে উৎস-সমস্যাহীন ১০টা ইনভয়েস ব্যবহার হয়েছে। এগুলো তিনটা টয় প্রোভাইডার, তাই এটা একটা পদ্ধতি দেখায়, কোনো আসল মডেলের মান নয়।
from collections import Counter
CLEAN = [i for i in range(12) if i not in QUIRK] # the 10 invoices without a built-in source problem
def layers(model):
"""First-attempt compliance, layer by layer (no repair): stop, syntax, schema, rules, exact match."""
n = Counter()
for i in CLEAN:
result = model(render(EXTRACT, source=SOURCES[i]))
n["stop"] += result["stop"] == "end"
doc, error = parse_json(result["text"]) if result["stop"] == "end" else (None, "x")
n["syntax"] += error is None
fits = error is None and not diagnose(doc)
n["schema"] += fits
n["rules"] += fits and not rules(doc)
n["exact"] += doc == GOLD[i]
return n
print("model stop syntax schema rules exact (first attempt, 10 invoices)")
for name, model in (("model_a", model_a), ("model_b", model_b), ("model_c", model_c)):
n = layers(model)
print(f"{name:9}", *(f"{n[k]:>{len(k) + 1}}" for k in ("stop", "syntax", "schema", "rules", "exact")))
final = Counter(process(SOURCES[i], model)["status"] for i in range(12))
print(" with bounded repair:", dict(sorted(final.items())))model stop syntax schema rules exact (first attempt, 10 invoices)
model_a 9 9 9 9 9
with bounded repair: {'failed': 1, 'needs_review': 2, 'ok': 9}
model_b 9 4 3 3 3
with bounded repair: {'failed': 1, 'needs_review': 2, 'ok': 3, 'repaired': 6}
model_c 10 10 5 4 4
with bounded repair: {'needs_review': 2, 'ok': 4, 'repaired': 6}স্তর ধরে পড়ুন। model_a প্রথম চেষ্টায় প্রায় নিখুঁত (একটা কেটে-যাওয়া)। model_b বেশি ব্যর্থ হয় সিনট্যাক্স ও টাইপে, যা রিপেয়ার ঠিক করে। model_c প্রতিবারই বৈধ JSON, কিন্তু স্কিমায় মেলে মাত্র ৫টা, আর বানানো মোট স্কিমা পেরিয়ে যায়, ধরা পড়ে শুধু রুলে। আসল তুলনায় অনেক বেশি ইনপুট নিন, প্রতিটা কয়েকবার চালান, প্রম্পট একই রাখুন, আর খরচ ও ল্যাটেন্সিও লিখুন। দশটা কেসে একবার চালানো শুধু একটা অনুমানকে সমর্থন করে।
কাজের জায়গায় এটা কোথায় দেখা যায়
ফাইন্যান্স, লজিস্টিকস ও সাপোর্ট দলগুলো ডকুমেন্ট থেকে ফিল্ড বের করতে ঠিক এই লুপ চালায়, আর রিপোর্ট করে কত শতাংশ ডকুমেন্ট স্বয়ংক্রিয়ভাবে গৃহীত, রিপেয়ার-করা, বা মানুষের কাছে গেছে। শেষ সংখ্যাটাই প্রায়ই সবচেয়ে কাজের: এটা অটোমেশনের সৎ খরচ।
সাধারণ ভুল
- স্টপ স্ট্যাটাস না দেখে পার্স করা। কেটে-যাওয়া উত্তর সিনট্যাক্স বাগের মতো দেখায়; প্রত্যাখ্যান দেখায় ফিল্ড-না-থাকার মতো।
- স্কিমায় থেমে যাওয়া। বৈধ রেকর্ডও ভুল হতে পারে; বিজনেস রুল চালান।
- "আবার চেষ্টা করো" ধরনের রিপেয়ার। মডেলকে সঠিক সমস্যাগুলো বলুন, আর চেষ্টার সংখ্যা সীমিত রাখুন।
- খারাপ উৎস রিপেয়ার করা। আত্মবিশ্বাসী বানানো মান পাবেন। অস্পষ্টতা ও স্ববিরোধ এসক্যালেট করুন।
- একবার চালিয়ে মডেল তুলনা। স্তর ধরে, অনেক ইনপুটে, বারবার চালিয়ে তুলনা করুন।
নিজে চেষ্টা করুন
ওপরের একই সেশনে।
- সহজ। দেখান যে
2026-02-31তারিখ স্কিমা পাস করে, আর এটা বাতিল করে এমন একটা চেক লিখুন। - মাঝারি। এমন একটা খামখেয়ালি (flaky) বদলি মডেল লিখুন যার প্রথম উত্তরে শেষের কমা আছে, আর সমস্যাগুলো দেখলে যা বৈধ উত্তর দেয়।
max_repairs=2ওmax_repairs=0দিয়ে স্ট্যাটাস দেখান। - কঠিন (মূল অনুশীলনী: ইনভয়েস থেকে যাচাই-করা স্কিমায় তথ্য বের করা)। পাতা পুরো পাইপলাইন বানিয়েছে: স্টপ-চেক, সিনট্যাক্স, স্কিমা, রুল, সীমিত রিপেয়ার, এসক্যালেশন। এটা শুরু থেকে শেষ ব্যবহার করুন।
model_cদিয়েprocess-এ ১২টা ইনভয়েসই চালান। প্রতি ইনভয়েসে এক লাইন ছাপুন (স্ট্যাটাস, কল, কারণ), তারপর আলাদাভাবে যাচাই করুন যে গৃহীত প্রতিটা রেকর্ড স্কিমা ও রুল পাস করে এবং উদ্দিষ্ট রেকর্ডের সমান। কোন ইনভয়েসে মানুষ লাগে এবং কেন, বলুন।
উত্তর
সহজ।
from datetime import date
def real_date(text):
try: date.fromisoformat(text); return True
except ValueError: return False
doc = {**GOLD[1], "issue_date": "2026-02-31"}
print("schema:", diagnose(doc) or "no problems", "| real date:", real_date(doc["issue_date"]))schema: no problems | real date: Falseপাস চেকলিস্ট: স্কিমা কিছু জানায় না; বাড়তি চেক আসল তারিখ-পার্সার ব্যবহার করে; আপনি বলছেন চেকটা রুল-স্তরের।
মাঝারি।
def model_flaky(prompt): # stand-in: broken first answer, valid once it is told what is wrong
return answer("end", '{"total": 1,}') if "### PROBLEMS" not in prompt else model_a(prompt)
for limit in (2, 0):
r = process(SOURCES[1], model_flaky, max_repairs=limit)
print(f"max_repairs={limit}:", r["status"], "| calls", r["calls"], r.get("why", ""))max_repairs=2: repaired | calls 2
max_repairs=0: failed | calls 1 not JSONপাস চেকলিস্ট: একবার রিপেয়ারে সফল; রিপেয়ার নিষিদ্ধ হলে এক কলের পরে নামকরা ব্যর্থতা; কোনো অসীম লুপ নেই।
কঠিন।
rows = [(f"INV-{1001 + i}", process(SOURCES[i], model_c)) for i in range(12)]
for number, r in rows:
print(number, f"{r['status']:13}", r["calls"], r.get("why", ""))
accepted = [r["doc"] for _, r in rows if "doc" in r]
print("accepted:", len(accepted), "| all pass schema and rules:", all(not diagnose(d) and not rules(d) for d in accepted),
"| equal to the intended record:", sum(d == GOLD[int(d["invoice_number"][4:]) - 1001] for d in accepted))INV-1001 repaired 2
INV-1002 repaired 2
INV-1003 repaired 2
INV-1004 ok 1
INV-1005 repaired 2
INV-1006 needs_review 1 total 100.0 but the lines add up to 95.0
INV-1007 ok 1
INV-1008 repaired 2
INV-1009 needs_review 0 ambiguous date 06/04/2026
INV-1010 ok 1
INV-1011 repaired 2
INV-1012 ok 1
accepted: 10 | all pass schema and rules: True | equal to the intended record: 10পাস চেকলিস্ট: INV-1006 আর INV-1009 মানুষের কাছে যায় (অসামঞ্জস্যপূর্ণ উৎস; অস্পষ্ট তারিখ); বাকি দশটা গৃহীত; পাইপলাইনের নিজের স্ট্যাটাস নয়, আলাদা চেকই নিশ্চিত করে যে গৃহীত সব রেকর্ড বৈধ ও সঠিক; কোনো গৃহীত রেকর্ডে অনুমোদিত রিপেয়ারের বেশি লাগেনি।
সারসংক্ষেপ
- সমর্থিত উপসেট, প্রত্যাখ্যান ও কেটে-যাওয়ার সংকেত এবং নিশ্চয়তা প্রোভাইডারের ডকুমেন্টেশনে পড়ুন; প্রতিটা উত্তরের জন্য কোড লিখুন।
- স্তরে স্তরে যাচাই করুন: স্টপ স্ট্যাটাস, সিনট্যাক্স, স্কিমা (অনুপস্থিত, বাড়তি, ভুল), তারপর বিজনেস রুল।
- ফরম্যাটের ভুল রিপেয়ার করুন সঠিক সমস্যা বলে এবং চেষ্টার কঠোর সীমায়; অস্পষ্ট বা স্ববিরোধী উৎস এসক্যালেট করুন।
- মডেল তুলনা করুন স্তর ধরে, অনেক ইনপুটে; টয় প্রোভাইডার পদ্ধতি দেখায়, র্যাঙ্কিং নয়।
এরপর: টেমপ্লেট ও ভেরিয়েবল: রেন্ডারিং, ভ্যালিডেশন ও নিরাপদ সীমানা, যেখানে প্রম্পটগুলো নিজেই যাচাই-করা ভেরিয়েবলসহ পুনর্ব্যবহারযোগ্য টেমপ্লেট হয়ে ওঠে।