অধ্যায় 2 · চেইন ও স্ট্রাকচার্ড আউটপুট
প্রম্পট চেইনিং: ধাপ, নির্দিষ্ট স্কিমা ও promptlab হেল্পার
- পৃষ্ঠা 5 / 24
- 18 মিনিট পড়া
একটা বড় প্রম্পট যদি গোলমেলে গ্রাহক-বার্তা পড়ে, কী করতে হবে ঠিক করে এবং উত্তরও লেখে, তাহলে তাতে ভুল ধরা কঠিন। উত্তর ভুল হলে বোঝা যায় না মডেল বার্তাটা ভুল বুঝেছে, ভুল নিয়ম খাটিয়েছে, নাকি শুধু খারাপ লিখেছে। প্রম্পট চেইন (prompt chain) কাজটাকে ছোট ছোট ধাপে ভাগ করে। প্রতিটা ধাপের ইনপুট আর আউটপুট পরিষ্কার, আর ধাপগুলোর মাঝখানে সাধারণ Python কোড ফলাফল যাচাই করে ঠিক করে পরের কাজ কী হবে।
এই পাতাে সাপোর্ট টিকেটের জন্য তিন ধাপের একটা চেইন বানানো হবে: এক্সট্র্যাক্ট, ভ্যালিডেট, ড্রাফট। সঙ্গে পরিচয় হবে promptlab.py-এর সঙ্গে। এটা একটা ছোট হেল্পার, যা পরের সব কোড পাতাে একটা "মডেল" কল করে, কী ঘটল তা রেকর্ড করে, আর পরীক্ষা বারবার একই ফল দেওয়ার মতো রাখে।
যা শিখবেন
- স্পষ্ট ইনপুট ও আউটপুট দিয়ে চেইন কীভাবে ডিজাইন করবেন, এবং এক্সট্র্যাকশন, ইন্টারপ্রিটেশন, ক্লাসিফিকেশন, ব্যাখ্যা আর ড্রাফটিং কেন আলাদা ধাপে রাখবেন।
- শেয়ার্ড
promptlabহেল্পার কীভাবে কাজ করে, এবং টয় প্রোভাইডার (toy provider) কী আর কী নয়। - সাধারণ Python-এ যাচাই করা নির্দিষ্ট স্কিমা দিয়ে ধাপগুলোর মধ্যে শুধু যাচাই-করা ফলাফল কীভাবে পাঠাবেন।
- শাখা (branch) আর থামার সিদ্ধান্ত প্রম্পটে না রেখে কোডে কীভাবে নেবেন, এবং প্রতিটা ধাপের ব্যর্থতা কীভাবে সামলাবেন।
- প্রতিটা ধাপে বড় কনটেক্সট বারবার না পাঠানোর উপায়।
কেন চেইন, আর ধাপ (stage) মানে কী
ধাপ হলো একটা প্রম্পট (বা একটুকরো কোড), যার ঘোষিত ইনপুট, ঘোষিত আউটপুট আর একটাই কাজ। আমাদের চেইন কাঁচা টিকেটকে উত্তরের খসড়ায় বদলায়:
ticket text
| stage 1: extract (model) -> JSON record: order_id, issue, action, email
| stage 2: validate (Python) -> accepted record, or a stop with a reason
| branch: missing info? unusual issue? (Python) -> stop or hand to a person
| stage 3: draft (model) -> reply text for a person to review
reply draft (never sent automatically)| ধাপ | ইনপুট | আউটপুট | কে করে | ব্যর্থ হলে |
|---|---|---|---|---|
| এক্সট্র্যাক্ট | টিকেটের লেখা | চারটা নির্দিষ্ট কি-সহ রেকর্ড | মডেল | শুধু ট্রান্সপোর্ট এরর রিট্রাই, তারপর থামা |
| ভ্যালিডেট | এক্সট্র্যাক্টের কাঁচা উত্তর | যাচাই-করা রেকর্ড, অথবা সমস্যার তালিকা | Python | থামা; সমস্যায় ফিল্ডের নাম থাকে |
| শাখা | যাচাই-করা রেকর্ড | চালিয়ে যাওয়া, গ্রাহককে জিজ্ঞেস করা, বা মানুষের হাতে দেওয়া | Python | সবসময় একটা নামযুক্ত স্ট্যাটাসে শেষ হয় |
| ড্রাফট | শুধু যে তথ্য উত্তরে ব্যবহার করা যাবে | উত্তরের লেখা | মডেল | থামা; কিছু পাঠানো হয় না |
কোনো প্রম্পট লেখার আগে এই টেবিলটা লিখে ফেলা এই পাতাের সবচেয়ে কাজের অভ্যাস। "ইনপুট" আর "আউটপুট" ঘর ভরতে না পারলে ধাপটা এখনো ঠিকমতো সংজ্ঞায়িত হয়নি।
promptlab হেল্পার
এই টিউটোরিয়ালের প্রতিটা কোড পাতা একটা ছোট ফাইল promptlab.py ইমপোর্ট করে। এখানে সেটা একবারই পুরোটা দেখানো হলো। এতে শুধু Python স্ট্যান্ডার্ড লাইব্রেরি আছে, তাই কিছু ইনস্টল করতে হয় না। এটা আপনার কাজের ফোল্ডারে promptlab.py নামের ফাইলে কপি করে রাখুন।
"""promptlab: a tiny, stdlib-only helper for prompt experiments (Python 3.12).
A Client wraps any provider function, records a Response for every call and can
append one JSON line per attempt to a log file. Tutorials use toy providers, so
all numbers here are repeatable stand-ins, never measurements of a real model.
"""
from __future__ import annotations
import json
import string
import time
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Callable, Mapping, Sequence
Message = Mapping[str, str]
Provider = Callable[..., "str | dict[str, Any]"]
@dataclass(frozen=True)
class Usage:
"""Token counts for one call; cached_input_tokens is the part of the input served from a cache."""
input_tokens: int
output_tokens: int
cached_input_tokens: int = 0
@dataclass
class Response:
"""What complete() returns: the text, its Usage, a latency in milliseconds and extra metadata."""
text: str
usage: Usage
latency_ms: int
meta: dict[str, Any] = field(default_factory=dict)
class ProviderError(RuntimeError):
"""Raised by Client.complete() after the failure was logged and all retries were used."""
def estimate_tokens(text: str) -> int:
"""Roughly estimate tokens as one per four characters (at least 1). A rule of thumb, not a tokenizer."""
return max(1, round(len(text) / 4))
class Client:
"""Call a provider(messages, **settings) function and keep a record of every attempt.
The provider returns a str or a dict with the key "text" and optionally "cached_input_tokens".
Latency is a deterministic fake derived from a call counter unless real_timing=True.
"""
def __init__(self, provider: Provider, model: str = "toy", log_path: str | Path | None = None,
retries: int = 0, real_timing: bool = False) -> None:
self.provider, self.model, self.retries, self.real_timing = provider, model, retries, real_timing
self.log_path = Path(log_path) if log_path else None
self.calls = 0 # attempts made so far; drives the fake latency and numbers the log lines
def _log(self, record: dict[str, Any]) -> None:
if self.log_path is None:
return
self.log_path.parent.mkdir(parents=True, exist_ok=True)
with self.log_path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, sort_keys=True, ensure_ascii=False) + "\n")
def complete(self, messages: Sequence[Message], **settings: Any) -> Response:
"""Send messages to the provider; retry on any exception; log every attempt, failures included."""
if not messages or any("role" not in m or "content" not in m for m in messages):
raise ValueError("messages must be a non-empty list of {'role': ..., 'content': ...} items")
prompt = [dict(m) for m in messages]
for attempt in range(1, self.retries + 2):
self.calls += 1
base = {"attempt": attempt, "call": self.calls, "messages": prompt, "model": self.model, "settings": settings}
started = time.perf_counter()
try:
raw = self.provider(prompt, **settings)
data = {"text": raw} if isinstance(raw, str) else dict(raw)
text = str(data["text"]) # a reply without "text" counts as a provider failure
except Exception as exc: # the provider may fail in any way; record it, then retry or give up
self._log({**base, "error": f"{type(exc).__name__}: {exc}", "status": "error"})
if attempt > self.retries:
raise ProviderError(f"{type(exc).__name__}: {exc} (after {attempt} attempt(s))") from exc
continue
tokens_in = sum(estimate_tokens(m["content"]) for m in prompt)
cached = min(int(data.get("cached_input_tokens", 0)), tokens_in)
usage = Usage(tokens_in, estimate_tokens(text), cached)
if self.real_timing:
latency = round((time.perf_counter() - started) * 1000)
else:
latency = 40 + 5 * usage.output_tokens + 10 * (self.calls % 3)
response = Response(text, usage, latency, {"attempt": attempt, "call": self.calls, "model": self.model})
self._log({**base, "latency_ms": latency, "meta": response.meta, "status": "ok", "text": text,
"usage": asdict(usage)})
return response
raise AssertionError("unreachable") # pragma: no cover
def render(template: str, **variables: Any) -> str:
"""Fill {name} fields in template; raise ValueError if a variable is missing or not used. To print a literal brace, write it twice."""
names = {name for _, name, _, _ in string.Formatter().parse(template) if name}
if names - variables.keys():
raise ValueError(f"missing variables: {sorted(names - variables.keys())}")
if variables.keys() - names:
raise ValueError(f"unused variables: {sorted(variables.keys() - names)}")
return template.format(**variables)অংশগুলোর সংক্ষিপ্ত পরিচয়:
- provider: যেকোনো ফাংশন
provider(messages, **settings), যা টেক্সট (বা"text"কি-সহ ডিকশনারি) ফেরত দেয়। আসল প্রোভাইডার আপনার SDK কলকে মুড়ে নেবে;Client-এর তাতে কিছু আসে যায় না। মডেল শুধু এখানেই জড়িত। Client.complete(messages): প্রোভাইডারকে কল করে এবংtext,usage,latency_msওmeta-সহ একটাResponseফেরত দেয়। কিছু কল করার আগেই ভুল গঠনের মেসেজ ফিরিয়ে দেয়।retries: যেকোনো এক্সেপশনে ক্লায়েন্ট আবার চেষ্টা করে। সব চেষ্টা ব্যর্থ হলেProviderErrorতোলে। ব্যর্থতাসহ প্রতিটা চেষ্টা লগ হয়।log_path: প্রতি চেষ্টায় একটা JSON লাইন যোগ করে (মেসেজ, সেটিংস, স্ট্যাটাস, টেক্সট, usage)। পরে চেইন ডিবাগ করার কাঁচামাল এটাই।estimate_tokens: মোটামুটি নিয়ম, চার অক্ষরে এক টোকেন। এটা টোকেনাইজার নয়; আকার তুলনা করতে ব্যবহার করুন, বিল আন্দাজ করতে নয়।real_timing=Trueনা দিলে ল্যাটেন্সি একটা নির্ধারিত নকল সংখ্যা।render(template, **variables):{name}ফিল্ড ভরে, আর কোনো ভেরিয়েবল না থাকলে বা অব্যবহৃত থাকলে এরর তোলে, তাই বানান ভুলে চুপচাপ ভাঙা প্রম্পট তৈরি হয় না।
টয় প্রোভাইডার: মডেলের বদলি, মডেল নয়
এই টিউটোরিয়ালের প্রোভাইডারগুলো টয় প্রোভাইডার: পাতােই লেখা ছোট নির্ধারিত (deterministic) ফাংশন, যারা একটা আচরণের নকল করে। এগুলো একটা বদলি (stand-in), যাতে আপনার চেইন, যাচাই আর টেস্ট বারবার একই ফলে চালানো যায়। আসল মডেল কম পূর্বানুমেয়, আর ঠিক সে কারণেই টেস্ট করতে হয়। টয় প্রোভাইডারের ফল থেকে জানা যায় আপনার কোড একটা ঘটনা সামলাতে পারে; আসল মডেল কতটা ভালো করত, তা থেকে কিছুই জানা যায় না।
from promptlab import Client, ProviderError, estimate_tokens, render
def toy_echo(messages, **settings):
"""Stand-in provider: repeats the last message in capitals."""
return messages[-1]["content"].upper()
client = Client(toy_echo, model="toy-echo", log_path="logs/demo.jsonl")
reply = client.complete([{"role": "user", "content": render("Say hi to {name}.", name="Aiko")}])
print(reply.text)
print(reply.usage)
print(reply.latency_ms, reply.meta)
print(open("logs/demo.jsonl", encoding="utf-8").read().count("\n"), "log line")SAY HI TO AIKO.
Usage(input_tokens=4, output_tokens=4, cached_input_tokens=0)
70 {'attempt': 1, 'call': 1, 'model': 'toy-echo'}
1 log lineল্যাটেন্সি এখানে নকল, বারবার একই সংখ্যা। লগে প্রতি চেষ্টায় একটা লাইন থাকে, তাই পাঁচ কলের চেইন পাঁচটা লাইন রেখে যায় যা আপনি পড়তে পারবেন।
ধাপ ১: এক্সট্র্যাকশন, ইন্টারপ্রিটেশন থেকে আলাদা
এক্সট্র্যাক্ট প্রম্পট শুধু জিজ্ঞেস করে টিকেটে কী বলা আছে, আর যা নেই তার জন্য null দিতে বলে। "আমরা কী করব?" সে প্রশ্ন এখানে নেই। এক্সট্র্যাকশনকে ইন্টারপ্রিটেশন থেকে আলাদা রাখলে ভুল ফিল্ড একটা ছোট প্রম্পট পর্যন্ত খুঁজে নেওয়া যায়, আর সেই তথ্যে কী করা হবে তার নীতি থাকে আপনার পড়া যায় এমন কোডে।
চেইনের অন্য জায়গাতেও একই ভাগ খাটে:
| আলাদা করুন | এর থেকে | কেন |
|---|---|---|
| এক্সট্র্যাকশন (লেখায় কী বলা আছে?) | ইন্টারপ্রিটেশন (আমরা কী করব?) | তথ্য লেখার সঙ্গে মিলিয়ে যাচাই করা যায়; সিদ্ধান্ত হলো নীতি। |
| ক্লাসিফিকেশন (চার ধরনের কোনটা?) | ব্যাখ্যা (গ্রাহকের জন্য কেন, কথায়) | লেবেল জানা উত্তরের সঙ্গে টেস্ট করা যায়; গদ্য যায় না। |
| ড্রাফটিং (প্রথম সংস্করণ) | রিভিশন (রুব্রিকের বিপরীতে উন্নত করা) | রিভিশনের জন্য এমন কিছু লাগে যার বিপরীতে বিচার করা যায়। পরের পাতা চেইনের নির্ভরযোগ্যতা: এরর রেট, শুরুর ভুল, সংশোধন লুপ ও ফলব্যাক সেই ধাপ যোগ করে। |
আমাদের চেইনে issue লেবেলটা ধাপ ১-এ নির্দিষ্ট তালিকা থেকে ক্লাসিফাই হয়, আর ধাপ ৩ শুধু তা ব্যাখ্যা করে। ধাপ ৩ একবারই ড্রাফট করে; রিভিশন ধাপ ইচ্ছে করেই বাদ রাখা হয়েছে, যতক্ষণ না মাপা যায় সেটা কাজে লাগে কি না।
নিচে দুটো প্রম্পট আর দুটো টয় প্রোভাইডার। এক্সট্র্যাক্টর সহজ নিয়মে ফিল্ড খোঁজে; ড্রাফটার পাওয়া তথ্য থেকে একটা বাক্য ভরে।
import json, re
EXTRACT = """Read the support ticket. Reply with JSON only, with the keys order_id, issue, action, email.
issue is one of: damaged, late, wrong_item, other. action is one of: refund, replace, none.
Use null for anything the ticket does not state. Do not guess.
### TICKET
{ticket}"""
DRAFT = """Write a polite reply of at most 60 words to a customer.
You may use only these facts:
order: {order_id}
problem: {issue}
our next step: {step}
Do not promise anything that is not listed."""
def toy_extract(messages, **settings):
"""Stand-in: finds fields with simple rules and answers in JSON. A real model reads more loosely."""
text = messages[-1]["content"].split("### TICKET", 1)[1].lower()
order = re.search(r"order\s*#?\s*(\d{4,})", text)
mail = re.search(r"[\w.]+@[\w.]+\w", text)
issue = next((k for w, k in [("cracked", "damaged"), ("late", "late"), ("wrong", "wrong_item")] if w in text), "other")
action = next((k for w, k in [("refund", "refund"), ("replace", "replace")] if w in text), "none")
return json.dumps({"order_id": order and order.group(1), "issue": issue, "action": action,
"email": mail and mail.group(0)})
def toy_draft(messages, **settings):
"""Stand-in: fills a sentence from the facts it is given."""
facts = dict(line.split(": ", 1) for line in messages[-1]["content"].splitlines() if ": " in line)
return f"Hello, thank you for writing about order {facts['order']}. We are sorry about the {facts['problem']} problem. Our next step: {facts['our next step']}."ধাপ ২: নির্দিষ্ট স্কিমা ও যাচাই-করা ফলাফল
ধাপ ১ প্রতিবার একই চারটা কি ফেরত দেবে। এই নির্দিষ্ট স্কিমাই ধাপগুলোর মধ্যে চুক্তি: ধাপ ৩ কখনো মডেলের কাঁচা উত্তর দেখে না, দেখে শুধু যাচাই-পাস করা রেকর্ড। স্কিমা শুধু প্রম্পটে না লিখে কোডেও লিখলেই চুক্তিটা সত্যিকারের হয়।
মনে রাখবেন, JSON চাওয়া আর JSON-এর নিশ্চয়তা এক জিনিস নয়। কিছু প্রোভাইডার স্কিমা-নিয়ন্ত্রিত আউটপুট দেয়, যা ফরম্যাটের নিশ্চয়তা দেয় কিন্তু মান সঠিক হওয়ার নয়, আর প্রত্যাখ্যান বা মাঝপথে কেটে যাওয়া উত্তরে তাও ব্যর্থ হতে পারে। যেটাই ব্যবহার করুন, তিনটা জিনিস নিজে যাচাই করুন: সিনট্যাক্স (এটা JSON কি না?), স্কিমা (কি, টাইপ, অনুমোদিত মান) আর বিজনেস রুল (এখানে: অর্ডার নম্বর আর ইমেইল ঠিকানা আবশ্যক)। এরপরের পাতা JSON ও স্কিমা: ফিল্ড, টাইপ, enum ও অনুপস্থিত মান এবং তার পরেরটা আরও গভীরে যায়; এখানে সাধারণ Python যাচাই ধারণাটা চোখের সামনে রাখে।
SCHEMA = { # the fixed contract between stage 1 and the rest of the chain
"order_id": (str, None),
"issue": (str, {"damaged", "late", "wrong_item", "other"}),
"action": (str, {"refund", "replace", "none"}),
"email": (str, None),
}
REQUIRED = ["order_id", "email"]
STEP = {"refund": "we will start your refund", "replace": "we will send a replacement",
"none": "a colleague will review your case"}
def check_record(text):
"""Stage 2 (plain Python): return (record, problems). No model is involved."""
try:
record = json.loads(text)
except json.JSONDecodeError as exc:
return None, [f"not valid JSON ({exc.msg})"]
if not isinstance(record, dict) or set(record) != set(SCHEMA):
return None, [f"keys must be exactly {sorted(SCHEMA)}"]
problems = []
for key, (kind, allowed) in SCHEMA.items():
value = record[key]
if value is not None and not isinstance(value, kind):
problems.append(f"{key}: expected {kind.__name__}")
elif allowed and value not in allowed:
problems.append(f"{key}: {value!r} is not one of {sorted(allowed)}")
return record, problems
def run_chain(ticket, extractor, writer):
"""Three stages: extract (model) -> validate and branch (Python) -> draft (model)."""
try:
raw = extractor.complete([{"role": "user", "content": render(EXTRACT, ticket=ticket)}]).text
except ProviderError as exc:
return {"status": "failed", "stage": "extract", "why": str(exc)}
record, problems = check_record(raw)
if problems:
return {"status": "failed", "stage": "validate", "why": "; ".join(problems)}
missing = [key for key in REQUIRED if record[key] is None]
if missing: # stop: do not let a model invent the gap
return {"status": "needs_info", "ask_customer_for": missing}
if record["issue"] == "other": # branch: this case is not ours to automate
return {"status": "human", "record": record}
prompt = render(DRAFT, order_id=record["order_id"], issue=record["issue"].replace("_", " "),
step=STEP[record["action"]])
try:
reply = writer.complete([{"role": "user", "content": prompt}]).text
except ProviderError as exc:
return {"status": "failed", "stage": "draft", "why": str(exc)}
return {"status": "drafted", "to": record["email"], "reply": reply}run_chain-কে চেইনের সূচিপত্র হিসেবে পড়ুন। প্রতিটা return চেইন শেষ করে এমন একটা স্ট্যাটাসে, যা আপনি গুনতে, লগ করতে ও টেস্ট করতে পারেন: failed, needs_info, human বা drafted।
শাখা আর থামার নিয়ম থাকে Python-এ
run_chain-এর দুটো সিদ্ধান্ত প্রম্পটের বাইরে নেওয়া হয়। order_id বা email না থাকলে চেইন থামে এবং জানায় গ্রাহককে কী জিজ্ঞেস করতে হবে; কোনো মডেলকে "মানানসই" অর্ডার নম্বর বানাতে দেয় না। সমস্যাটা other হলে টিকেট যায় মানুষের কাছে। কোড এই সিদ্ধান্ত প্রম্পটের চেয়ে ভালো নেয়: এগুলো বিনা খরচে (মডেল কল লাগে না), প্রতিবার একই আচরণ করে, ইউনিট টেস্টে ধরা যায়, আর চতুর কোনো টিকেট এগুলো এড়িয়ে যেতে পারে না।
তিনটা বানানো টিকেটে চেইন চালান। লক্ষ করুন, টিকেট B কখনো ধাপ ৩-এ পৌঁছায় না।
from pprint import pprint
TICKETS = {
"A": "Hello, order 48213 arrived with a cracked screen. Please send a refund. Kwame, kwame@example.com",
"B": "My parcel never came and nobody answers. Please help. Lena, lena@example.org",
"C": "Order #51877 is fine, but how do I change the language of the app? Mateo, mateo@example.net",
}
extractor, writer = Client(toy_extract), Client(toy_draft, log_path="logs/chain.jsonl")
for name, ticket in TICKETS.items():
print(name)
pprint(run_chain(ticket, extractor, writer), width=76)A
{'reply': 'Hello, thank you for writing about order 48213. We are sorry '
'about the damaged problem. Our next step: we will start your '
'refund.',
'status': 'drafted',
'to': 'kwame@example.com'}
B
{'ask_customer_for': ['order_id'], 'status': 'needs_info'}
C
{'record': {'action': 'none',
'email': 'mateo@example.net',
'issue': 'other',
'order_id': '51877'},
'status': 'human'}টিকেট A পর্যালোচনার জন্য একটা খসড়া দেয়। টিকেট B থামে needs_info-তে: টিকেটে অর্ডার নম্বর নেই, তাই সঠিক পরের পদক্ষেপ গ্রাহককে একটা প্রশ্ন করা। টিকেট C-তে নম্বর আছে, কিন্তু সমস্যাটা চেইনের সামলানোর মতো নয়, তাই এক্সট্র্যাক্ট করা রেকর্ডসহ মানুষের হাতে যায়।
প্রতিটা ধাপের ব্যর্থতা সামলানো
ব্যর্থতা ঘটার আগেই ঠিক করুন, প্রতিটা ধাপে তার মানে কী। উপরের চেইন তিন রকম আলাদা করে:
- কল ব্যর্থ হয়েছে (টাইমআউট, নেটওয়ার্ক): রিট্রাই যুক্তিসংগত, কারণ একই ইনপুটে এবার সফল হতে পারে।
Client(retries=1)তা-ই করে, আর লগে দুই চেষ্টাই থাকে। - কল চলেছে কিন্তু বিষয়বস্তু ব্যবহারের অযোগ্য (JSON নয়, ভুল কি, তালিকার বাইরের মান): চোখ বুজে রিট্রাই করবেন না। ধাপের নাম আর সঠিক সমস্যা জানিয়ে থামুন। ভ্যালিডেশন ও রিপেয়ার: jsonschema, বিজনেস রুল ও সীমিত রিট্রাই সাবধানে মেরামত দেখায়।
- বিষয়বস্তু বৈধ কিন্তু অসম্পূর্ণ (আবশ্যক ফিল্ড null): এটা স্বাভাবিক ফল, এরর নয়।
needs_infoজানান।
নিচের টয় প্রোভাইডারগুলো প্রত্যেকে একটা নিয়ম ভাঙে, যাতে আসল মডেল ছাড়াই তিনটা পথ দেখা যায়।
def chatty(messages, **settings): # breaks the contract: prose around the JSON
return "Sure! Here is the data: " + toy_extract(messages)
calls = {"n": 0}
def flaky(messages, **settings): # fails once, then works
calls["n"] += 1
if calls["n"] == 1:
raise TimeoutError("provider timed out")
return toy_extract(messages)
def always_down(messages, **settings):
raise ConnectionError("provider unreachable")
writer = Client(toy_draft)
for label, provider, retries in [("chatty", chatty, 0), ("flaky, 1 retry", flaky, 1), ("down, 1 retry", always_down, 1)]:
result = run_chain(TICKETS["A"], Client(provider, retries=retries), writer)
print(f"{label}: {result['status']} {result.get('stage', '')} {result.get('why', '')[:70]}".rstrip())chatty: failed validate not valid JSON (Expecting value)
flaky, 1 retry: drafted
down, 1 retry: failed extract ConnectionError: provider unreachable (after 2 attempt(s))chatty প্রোভাইডার JSON-কে একটা বন্ধুত্বপূর্ণ বাক্যে মুড়েছে, যা আসল মডেলের পরিচিত অভ্যাস; চেইন আবর্জনা এগিয়ে না দিয়ে validate-এ থেমেছে। flaky প্রোভাইডার রিট্রাইয়ে সেরে উঠেছে। নাগালের বাইরের প্রোভাইডারে চেইন extract-এ থেমেছে, বার্তায় দুই চেষ্টাই গোনা আছে।
বড় কনটেক্সট বারবার পাঠাবেন না
সাপোর্ট টিকেটের সঙ্গে প্রায়ই আগের বার্তার লম্বা সুতো আসে। শুধু ধাপ ১-এর সেটা পড়া দরকার। এক্সট্র্যাকশনের পর পরের ধাপগুলোর লাগে কয়েকটা যাচাই-করা তথ্য, তাই সুতো না পাঠিয়ে তথ্য পাঠান। এতে পরের প্রম্পট ছোট, সস্তা ও পড়তে সহজ থাকে, আর ড্রাফটিং মডেলের সামনে মনোযোগ নষ্ট করার মতো অপ্রাসঙ্গিক লেখা কমে। কোনো ধাপে সত্যিই বড় অপরিবর্তিত অংশ লাগলে সেটা আগে রাখুন এবং কলের মধ্যে অভিন্ন রাখুন; যেসব প্রোভাইডার প্রম্পট ক্যাশ করে, তারা অভিন্ন শুরুর অংশ পুনর্ব্যবহার করে, যা প্রম্পট ক্যাশিং ও খরচ: স্থির প্রিফিক্স, ব্যবহারের ডেটা ও আনুমানিক হিসাব দেখায়।
history = "\n".join(f"> message {i}: thanks, still waiting for an update" for i in range(60))
long_ticket = TICKETS["A"] + "\n" + history
extract_prompt = render(EXTRACT, ticket=long_ticket)
draft_prompt = render(DRAFT, order_id="48213", issue="damaged", step=STEP["refund"])
print("extract stage reads :", estimate_tokens(extract_prompt), "tokens (rough)")
print("draft, facts only :", estimate_tokens(draft_prompt), "tokens (rough)")
print("draft, ticket again :", estimate_tokens(draft_prompt + long_ticket), "tokens (rough)")extract stage reads : 836 tokens (rough)
draft, facts only : 50 tokens (rough)
draft, ticket again : 822 tokens (rough)এই সংখ্যাগুলো চার-অক্ষরের মোটামুটি নিয়মে, তাই এগুলো তুলনা, মাপ নয়। শুধু-তথ্যের ড্রাফট প্রম্পট সুতোসহ ড্রাফট প্রম্পটের একটা ছোট ভগ্নাংশ।
কাজের জায়গায় এটা কোথায় দেখা যায়
মানুষ উত্তর দেওয়ার আগে সাপোর্ট টিমগুলো "পড়ো, যাচাই করো, খসড়া করো" চেইন বানায়। ফিন্যান্স টিম চেইন করে "ইনভয়েস থেকে ফিল্ড বের করো, কোডে যোগফল মেলাও, সারাংশ লেখো"। ডেটা টিম করে "রেকর্ড ক্লাসিফাই করো, তারপর লেবেল ব্যাখ্যা করো"। সবগুলোতে কাজের গুণ একই: ফল ভুল হলে লগ বলে দেয় কোন ধাপ তা বানিয়েছে, আর একটা টেস্ট সেই ধাপের ভুল আলাদা করে ধরতে পারে।
সাধারণ ভুল
- মডেলের কাঁচা উত্তর সরাসরি পরের ধাপে দেওয়া। একটা খারাপ উত্তর তখন পরের সবকিছু নষ্ট করে। ধাপের মাঝে যাচাই করুন, শুধু যাচাই-করা রেকর্ড এগিয়ে দিন।
- শাখার সিদ্ধান্ত প্রম্পটকে দেওয়া। "অর্ডার নম্বর না থাকলে একটা বানিয়ে নাও" টেস্ট করা যায় না, আর এ নিয়ে তর্ক চলে। শাখা কোডে রাখুন।
- প্রতিটা ব্যর্থতায় রিট্রাই। রিট্রাই টাইমআউটে কাজ করে, ভুল বোঝাবুঝি সারায় না। অবৈধ বিষয়বস্তুতে রিট্রাই আসল ত্রুটি লুকাতে আর খরচ দ্বিগুণ করতে পারে।
- প্রতিটা ধাপে পুরো কনটেক্সট আবার পাঠানো। খরচ বাড়ে আর গোলমাল জোটে। যাচাই-করা তথ্য পাঠান।
- টয় প্রোভাইডারের ফলে ভরসা করা। তা দেখায় আপনার কোড একটা ঘটনা সামলায়। আসল মডেলের নিজস্ব টেস্ট লাগে, যা পরের পাতাগুলো বানায়।
নিজে চেষ্টা করুন
পাতাের কোড ব্যবহার করুন: promptlab.py পাশে রেখে একটা Python ফোল্ডারে ব্লকগুলো পরপর চালান।
- সহজ। টিকেট D যোগ করুন,
"Order 60412 arrived late. Please replace it. Priya", এবং চালানোর আগে স্ট্যাটাস কী হবে অনুমান করুন। - মাঝারি। একটা শাখার নিয়ম যোগ করুন: দেরিতে আসা অর্ডারে রিফান্ডের অনুরোধ গেলে মানুষের কাছে যাবে, কারণ দেরির রিফান্ডে অনুমোদন লাগে। এটাকে একটা ফাংশন হিসেবে লিখে দুটো রেকর্ডে টেস্ট করুন।
- কঠিন। এমন একটা টয় এক্সট্র্যাক্টর লিখুন যা
issue-এর জন্য অনুমোদিত তালিকার বাইরের মান ফেরত দেয়, এবং দেখান যে চেইনvalidate-এ থামে ও ফিল্ডের নাম বলে। তারপর বলুন Beginner টিউটোরিয়ালের কোন আগের ধারণা ("না থাকলে null, আন্দাজ নয়") এটা রক্ষা করে।
উত্তর
সহজ। টিকেটে অর্ডার নম্বর আছে কিন্তু ইমেইল ঠিকানা নেই, আর ইমেইল আবশ্যক, তাই স্ট্যাটাস needs_info এবং গ্রাহকের কাছে ইমেইল চাওয়া হবে।
ticket_d = "Order 60412 arrived late. Please replace it. Priya"
print(run_chain(ticket_d, Client(toy_extract), Client(toy_draft))){'status': 'needs_info', 'ask_customer_for': ['email']}মাঝারি। ভালো উত্তর হলো একটা ছোট খাঁটি ফাংশন, যা যাচাই-করা রেকর্ড নিয়ে সত্য বা মিথ্যা ফেরত দেয়; run_chain-এ other নিয়মের পাশে সেটা বসান (if needs_human(record): return {"status": "human", ...})।
def needs_human(record):
return record["issue"] == "other" or (record["issue"] == "late" and record["action"] == "refund")
late_refund = {"issue": "late", "action": "refund"}
late_replace = {"issue": "late", "action": "replace"}
print(needs_human(late_refund), needs_human(late_replace))True Falseপাস চেকলিস্ট: এটা মডেল কল ছাড়া সাধারণ Python; যে ঘটনায় নিয়মটা ধরে আর যে ঘটনায় ধরে না, দুটোতেই টেস্ট করা হয়েছে; এটা ভ্যালিডেশনের পরে চলে, তাই ফিল্ডের মানে ভরসা করতে পারে।
কঠিন।
def wrong_enum(messages, **settings):
return json.dumps({"order_id": "1", "issue": "broken", "action": "refund", "email": "a@b.co"})
result = run_chain(TICKETS["A"], Client(wrong_enum), Client(toy_draft))
assert result["stage"] == "validate"
print(result["why"])issue: 'broken' is not one of ['damaged', 'late', 'other', 'wrong_item']পাস চেকলিস্ট: ব্যর্থতার স্ট্যাটাসে ধাপ (validate) আর ফিল্ড (issue) দুটোর নাম আছে; ড্রাফট ধাপ কখনো ডাকা হয়নি; কোনো রিট্রাই হয়নি। এটা Beginner টিউটোরিয়ালের সেই নিয়মই: এক্সট্র্যাক্টর null বা বলা মান ফেরত দেবে, কখনো আন্দাজ নয়, আর সে তা করেছে কি না তা যাচাই করে আপনার কোড।
সারসংক্ষেপ
- চেইন মানে ঘোষিত ইনপুট-আউটপুটওয়ালা ছোট ধাপ। প্রম্পটের আগে ইনপুট/আউটপুট টেবিল লিখুন।
- এক্সট্র্যাকশন, ইন্টারপ্রিটেশন, ক্লাসিফিকেশন, ব্যাখ্যা, ড্রাফটিং ও রিভিশন আলাদা রাখুন, যাতে প্রতিটা একা টেস্ট করা যায়।
- নির্দিষ্ট স্কিমা ব্যবহার করুন, আর পরের ধাপ ফল দেখার আগে কোডে সিনট্যাক্স, স্কিমা ও বিজনেস রুল যাচাই করুন।
- শাখা ও থামার নিয়ম Python-এ রাখুন: আবশ্যক তথ্য না থাকলে থামুন, আর কারণের নাম বলুন।
- প্রতিটা ধাপের ব্যর্থতার নিয়ম ঠিক করুন; ট্রান্সপোর্ট এররে রিট্রাই, অযোগ্য বিষয়বস্তুতে থামা।
- পরের ধাপে পুরো কনটেক্সট নয়, যাচাই-করা তথ্য পাঠান। টয় প্রোভাইডার কোডকে টেস্টযোগ্য করে; আসল মডেল সম্পর্কে তারা কিছু বলে না।
এরপর: চেইনের নির্ভরযোগ্যতা: এরর রেট, শুরুর ভুল, সংশোধন লুপ ও ফলব্যাক, যেখানে আপনি মাপবেন পুরো চেইন কতবার সফল হয়, আর শুরুর ভুল ছড়ানো থামাবেন।