অধ্যায় 6 · LLMOps: লগিং, ট্রেসিং, খরচ ও মনিটরিং
মনিটরিং, বাজেট ও ইনসিডেন্ট
- পৃষ্ঠা 12 / 22
- 25 মিনিট পড়া
আক্রার কোয়ামে এক মঙ্গলবার সকালে সাপোর্ট অ্যাসিস্ট্যান্টের জন্য একটা নতুন প্রম্পট ডিপ্লয় করল। কিছুই ক্র্যাশ করল না। দুপুরের মধ্যে প্রতি তিনটা উত্তরের একটা ব্যর্থ, অথচ কেউ জানে না কখন থেকে: একমাত্র ড্যাশবোর্ডটি শুধু প্রতি মিনিটের রিকোয়েস্ট দেখায়, আর সেই রেখা সমান। (এটা একটা বোঝানোর গল্প।) স্ট্রাকচার্ড লগিং ও ট্রেসিং পাতার ট্রেস প্রতিটা রিকোয়েস্টে কী ঘটেছিল তা রেকর্ড করে। কিন্তু কিছু যে ভুল, কতটা ভুল, কাকে জাগাতে হবে, বা কী বন্ধ করতে হবে, তা ট্রেস বলে না।
এই পাতা ট্রেস আর লগকে সংখ্যা, লক্ষ্য, অ্যালার্ট আর পদক্ষেপে বদলায়। সবকিছু সিড-করা কৃত্রিম ট্রাফিক আর নকল ক্লকে চলে, তাই প্রতিটা সংখ্যা পুনরাবৃত্তিযোগ্য। প্রতিটা থ্রেশহোল্ড উদাহরণমাত্র: কৌশলটা নিন, তারপর সংখ্যাগুলো নিজের ডেটা থেকে ঠিক করুন।
যা শিখবেন
- ট্রেস থেকে কীভাবে ছোট একটা মেট্রিক ডেটাসেট বানাবেন, এমন হার ও পারসেন্টাইল সহ যা খুব কম ডেটায় উত্তর দিতে অস্বীকার করে।
- মান, লেটেন্সি, খরচ ও নিরাপত্তার জন্য SLO ও এরর বাজেট, আর যে অ্যালার্ট রুল বারবার দুলে না, তা কীভাবে লিখবেন।
- ড্রিফট ও রিগ্রেশন কীভাবে ধরবেন, এবং তাদের ভুল-সতর্কতার হার কত।
- রিট্রাই কীভাবে সীমিত করবেন, নিরাপদে ডিগ্রেড করবেন, সফট ও হার্ড বাজেট প্রয়োগ করবেন, অপব্যবহার চিনবেন এবং মানুষের রিভিউয়ের জন্য উত্তর নমুনা করবেন।
- রিটেনশন ও অ্যাক্সেস কীভাবে ঠিক করবেন, এবং প্রথম অ্যালার্ট থেকে পোস্টমর্টেম পর্যন্ত একটা ইনসিডেন্ট কীভাবে চালাবেন।
কী নতুন, আর কী পুনর্ব্যবহার
কোনো বিদ্যমান ফাইল বদলায় না। evidencedesk/observe/-এ যোগ হয় metrics, traffic, slo, alerts, drift, resilience, budgets, abuse, review, retention, incident ও incident_lab; data/observe/-এ যোগ হয় সাতটা ছোট JSON ফাইল (SLO, অ্যালার্ট রুল, বাজেট, সিভিয়ারিটি লেভেল, রানবুক, রিটেনশন, ইনসিডেন্ট স্ক্রিপ্ট); পাঁচটা টেস্ট ফাইলের নাম test_observe_monitor_*.py। যা আবার ব্যবহার হচ্ছে: আগের পাতার Lab, EventLog ও percentile; বাজেট, ক্যাশিং ও ব্যবহারের সীমা থেকে HitStats ও UsageLimiter-এর UTC-দিনের নিয়ম; অ্যাডাপ্টার, কনফিগারেশন ও এরর হ্যান্ডলিং থেকে RetryPolicy; খরচ ও লেটেন্সির মূল কথা-র কাল্পনিক দামের তালিকা; নিরাপদ ব্যর্থতা ও নিরাপত্তা রিগ্রেশন টেস্ট থেকে status_of; মূল্যায়নের নকশা ও জাজ ক্যালিব্রেশন থেকে kappa।
ক্যাশ হিট ও মিস মাপা
সঠিক ডিনোমিনেটর ছাড়া হিট রেট সৎ নয়। যে রিকোয়েস্ট কখনো ক্যাশে পৌঁছায়নি (রিট্রিভাল প্রত্যাখ্যান) বা ক্যাশ ব্যবহারের অনুমতি পায়নি (যেমন সারাংশ), সেটা মিস নয়। HitStats হিট, মিস (যোগ্য লুকআপ যা কিছু পায়নি) ও বাইপাস আলাদা রাখে। metrics.from_trace একটা শেষ-হওয়া ট্রেসকে সমতল Obs-এ বদলায়: স্ট্যাটাস, কোড, লেটেন্সি, টোকেন, খরচ, ক্যাশের অবস্থা, সেরা রিট্রিভাল স্কোর, ভার্সন, টেন্যান্ট; কোনো লেখা নেই, কাঁচা ইউজার আইডিও নেই।
from evidencedesk.observe.lab import Lab
from evidencedesk.observe.metrics import from_trace, summarise
Q = "How long do I have to return a bike?"
lab = Lab()
for q in (Q, Q, "What is the capital of Australia?"):
lab.ask(q)
seen = [from_trace(t, tenant="tenant-a") for t in lab.tracer.traces()]
for o in seen:
print(o.request_id, o.status, o.code, o.cache, o.tokens_in, o.tokens_out, o.cost, o.top_score)
s = summarise(seen, min_n=1)
print("hit rate:", s.hit_rate, "| lookups:", s.cache.eligible, "| p95 with 3 requests:", summarise(seen).p95_s)req-001 answered ok miss 326 39 0.001446 0.56
req-002 answered ok hit 0 0 0.0 0.56
req-003 refused no_evidence none 0 0 0.0 0.0
hit rate: 0.5 | lookups: 2 | p95 with 3 requests: Noneদ্বিতীয় রিকোয়েস্টটা বিনা খরচের হিট; প্রত্যাখ্যানটা ক্যাশ ছোঁয়নি, তাই গোনা হয়নি। তিনটা রিকোয়েস্টে ৯৫তম পারসেন্টাইল None: Summary-র প্রতিটা হার ও পারসেন্টাইল min_n-এর (এখানে ২০) নিচে এমনই করে, কারণ তিনটা সংখ্যার পারসেন্টাইল আসলে একটাই সংখ্যা।
ছোট মেট্রিক ডেটাসেট বানানো
ল্যাবে অনুশীলনের মতো রিকোয়েস্ট কম, তাই traffic.make_day() একটা সিড-করা দিন বানায়: প্রায় ৩০ জন ব্যবহারকারী ও তিন টেন্যান্ট থেকে প্রতি মিনিটে প্রায় ছয়টা রিকোয়েস্টে ২৪০ মিনিট, ১৫০তম মিনিটে একটা খারাপ রিলিজ আর ৬০তম মিনিটে একটা স্ক্র্যাপার বার্স্ট। এটা প্রোডাকশনের বদলি; আসল ট্রাফিক আরও এলোমেলো ও অদ্ভুত। buckets প্রতি সময়-বাকেটে একটা সারাংশ দেয়, ফাঁকা বাকেটসহ, কারণ হারিয়ে যাওয়া বাকেট নিজেই একটা সংকেত। Counters শুধু বাড়ে; হার মানে দুটো স্ন্যাপশটের পার্থক্য, কখনো রিসেট নয়।
from evidencedesk.observe import metrics, traffic
rows = traffic.make_day()
t0 = rows[0].ts
def hm(ts):
return f"{int(ts % 86400 // 3600):02d}:{int(ts % 3600 // 60):02d}"
print(len(rows), "requests | users:", len({o.user for o in rows}), "| versions:", sorted({o.version for o in rows}))
print("start n error% p95_s cost/req(milli) hit% naive%")
for t, b in metrics.buckets(rows, t0, t0 + 4 * 3600, 1800):
print(f"{hm(t)} {b.n:4d} {b.error_rate * 100:6.1f} {b.p95_s:6.2f} {b.cost_per_request * 1000:15.3f} {b.hit_rate * 100:5.1f} {b.cache.naive_rate * 100:6.1f}")1580 requests | users: 31 | versions: ['v1', 'v2']
start n error% p95_s cost/req(milli) hit% naive%
09:00 180 4.4 1.31 0.944 31.4 27.0
09:30 186 9.7 1.33 0.958 29.3 27.1
10:00 313 2.9 1.31 0.899 17.1 15.4
10:30 182 5.0 1.31 1.074 24.1 22.0
11:00 180 6.7 1.29 1.080 24.2 21.8
11:30 179 25.1 1.70 1.527 19.9 18.2
12:00 179 3.4 1.28 1.084 24.8 22.8
12:30 180 4.4 1.30 1.030 24.5 22.9এই টেবিলই ড্যাশবোর্ড ডেটাসেট। ১১:৩০-এর বাকেটটা এরর, p95 ও খরচে আলাদা করে চোখে পড়ে। শেষ কলামটা ভুল ডিনোমিনেটর: এটা হিট রেটকে প্রায় ১.৬ থেকে ৪.৪ পয়েন্ট কম দেখায়, কারণ সারাংশ ক্যাশ বাইপাস করে। ১০:০০-এর বাকেটে স্ক্র্যাপারের ১৩০টা বাড়তি রিকোয়েস্ট; ০৯:৩০-এর এরর বাড়া (১৮৬-র মধ্যে ১৮) শুধু কাকতালীয়।
টুল নোট (২০২৬-১০-১২ অনুযায়ী): OpenTelemetry GenAI কনভেনশনে স্প্যান পাতার পাশে একটা মেট্রিক্স পাতাও আছে, আর পুরো সেটের স্ট্যাটাস Development, তাই নাম বদলাতে পারে। এই পাতা স্প্যান ও লগ থেকে নিজের মেট্রিক হিসাব করে, কোনো মেট্রিক্স SDK ডাকে না। পরে মেট্রিক এক্সপোর্ট করলে নাম বর্তমান পাতা থেকে কপি করুন এবং বর্তমান ডকুমেন্টেশন দেখে নিন।
SLI, SLO ও এরর বাজেট ঠিক করা
SLI হলো ভালো রিকোয়েস্টের মাপা অংশ; SLO হলো একটা সময়সীমায় আপনার প্রতিশ্রুত অংশ; এরর বাজেট হলো প্রতিশ্রুতির পর যা বাকি থাকে (৯৫ শতাংশে প্রতি ১০০ রিকোয়েস্টে ৫টা খারাপ হতে পারে)। বার্ন রেট হলো খারাপ রিকোয়েস্টকে বাজেট দিয়ে ভাগ: ১.০ মানে সময়সীমার শেষে ঠিক বাজেট ফুরানো, ১৪ মানে চৌদ্দ গুণ দ্রুত ফুরানো। একটা LLM ফিচারের চারটা মাত্রা লাগে, কারণ বাকিগুলো ঠিক থাকলেও সে চার উপায়ে ব্যর্থ হতে পারে। প্রতিটা SLI একটা প্রক্সি; কোনোটাই বলে না "উত্তরটা সঠিক ছিল"।
| মাত্রা | ভালো রিকোয়েস্ট | যা ধরে না |
|---|---|---|
| মান | ব্যর্থ নয়, ভ্যালিডেশন ব্যর্থতা নেই (কাটা, ভুল JSON, অজানা সাইটেশন) | সাবলীল ভুল উত্তর |
| লেটেন্সি | সীমার মধ্যে শেষ | দ্রুত কিন্তু অকাজের |
| খরচ | প্রতি রিকোয়েস্টে ক্রেডিট সীমার নিচে | সস্তা কিন্তু ভুল |
| নিরাপত্তা | কোনো নিরাপত্তা ফ্ল্যাগ নেই, কিছু ব্লক হয়নি | যে আক্রমণ কেউ ফ্ল্যাগ করেনি |
from evidencedesk.observe import slo
for b in slo.report(slo.load_slos(), rows):
print(f"{b.slo:<16} n={b.n} bad={b.bad:<4} allowed={b.allowed:<5} sli={b.sli} budget left={b.remaining:+.2f} burn={b.burn_rate}")valid-replies n=1580 bad=115 allowed=79.0 sli=0.9272 budget left=-0.46 burn=1.46
fast-replies n=1580 bad=44 allowed=79.0 sli=0.9722 budget left=+0.44 burn=0.56
cheap-replies n=1580 bad=77 allowed=31.6 sli=0.9513 budget left=-1.44 burn=2.44
no-safety-flags n=1580 bad=114 allowed=7.9 sli=0.9278 budget left=-13.43 burn=14.43মান ও খরচ বাজেটের বেশি খরচ করেছে: খারাপ রিলিজ। নিরাপত্তা চৌদ্দ গুণ বেশি খরচ করেছে, আর সেটা একজন ব্যবহারকারী: স্ক্র্যাপার ১৪০টা রিকোয়েস্ট (অর্ধেক ব্লক) এমন বাজেটে ঢুকিয়েছে যা দিনে ৮টার অনুমতি দেয়। প্রতি-ফিচারের একটা সংখ্যা বলতে পারে না কে; অপব্যবহারের অংশ পারে। লেটেন্সির বাজেট বাকি আছে, কারণ v2-এর উত্তর ধীর হলেও বেশির ভাগ সীমার মধ্যে ছিল।
থ্রেশহোল্ড থেকে অপারেশনাল অ্যালার্ট
একটা রুল নাম দেয় একটা মেট্রিক, তার পেছনের একটা উইন্ডো, ফায়ার করার থ্রেশহোল্ড ও ক্লিয়ার করার আলাদা একটা থ্রেশহোল্ড, লঙ্ঘন কতক্ষণ টিকতে হবে, আর কত কম রিকোয়েস্টে রুল নীরব থাকবে। alert_rules.json-এ রাখা একটা রুল:
{"name": "error-rate-high", "metric": "error_rate", "op": ">", "fire": 0.10, "clear": 0.05, "for_min": 10, "window_min": 15, "min_n": 30, "severity": "SEV2"}for-duration এমন স্পাইক উপেক্ষা করে যা কেউ সাড়া দেওয়ার আগেই শেষ। হিস্টেরেসিস (ফায়ার ০.১০, ক্লিয়ার ০.০৫) এমন মেট্রিককে আটকায় যা রেখার ওপর ঘুরে বারবার পেজ করে। ডি-ডুপ্লিকেশন মানে রুল-কি প্রতি একটাই খোলা অ্যালার্ট: প্রতি পরিবর্তনে একটা নোটিফিকেশন, আর ব্যবধান ধরে একটা রিমাইন্ডার। ন্যূনতম নমুনা তিনটা রিকোয়েস্টকে অ্যালার্ম তুলতে দেয় না। Engine.tick-এর ভেতরের স্টেট মেশিন:
# excerpt of kit/evidencedesk/observe/alerts.py (Engine.tick)
st = self.state[r.name]
if st != "firing" and r.breached(v):
self.since.setdefault(r.name, now)
if now - self.since[r.name] >= r.for_min * 60:
self.state[r.name] = "firing"
self.last_note[r.name] = now
out.append(AlertEvent(now, r.name, "fired", v, r.severity))
else:
self.state[r.name] = "pending"
elif st == "firing" and r.recovered(v):
self.state[r.name] = "ok"
self.since.pop(r.name, None)
out.append(AlertEvent(now, r.name, "resolved", v, r.severity))
elif st == "pending" and not r.breached(v):দিনটা আবার চালান, প্রতি পাঁচ নকল মিনিটে:
from evidencedesk.observe import alerts
engine = alerts.run(alerts.load_rules(), rows, t0, t0 + 4 * 3600)
for e in engine.events:
print(hm(e.ts), f"{e.rule:<22} {e.kind:<9} value={e.value:.4f} {e.severity}")
print("evaluations skipped for too little data:", engine.skipped)11:45 error-rate-high fired value=0.3182 SEV2
11:55 cost-per-request-high fired value=0.0015 SEV3
12:15 error-rate-high resolved value=0.0330 SEV2
12:25 cost-per-request-high resolved value=0.0011 SEV3
evaluations skipped for too little data: 1রিলিজ শুরু ১১:৩০-এ, পেজ এল ১১:৪৫-এ: শনাক্ত হতে ১৫ মিনিট। খরচের অ্যালার্ট ফিক্সের পরে ফায়ার করল, কারণ তার উইন্ডো ৩০ মিনিটের, আর সমাধানও দেরিতে আসে। স্থিতিশীল অ্যালার্টের দাম এটাই: লম্বা উইন্ডো আর for-duration নয়েজ কমায়, কিন্তু দেরি বাড়ায়। এবার একই এরর রুল এমন ট্রাফিকে, যা ঠিক থ্রেশহোল্ডে বসে আছে:
rule = next(r for r in alerts.load_rules() if r.name == "error-rate-high")
edge = traffic.stationary(600, 3, traffic.Profile(codes={"ok": 0.9, "invalid_json": 0.1})) # error rate sits right at the threshold
for name, r in [("naive (no for, no hysteresis)", rule.naive()), ("for 10 min + hysteresis", rule)]:
eng = alerts.run([r], edge, 0, 6000)
print(f"{name:<30} notifications: {len(eng.events)} fired: {sum(e.kind == 'fired' for e in eng.events)}")naive (no for, no hysteresis) notifications: 5 fired: 3
for 10 min + hysteresis notifications: 2 fired: 1একটা অবস্থার জন্য পাঁচটা নোটিফিকেশন, বনাম দুটো।
ড্রিফট ও রিগ্রেশন ধরা
ড্রিফট মানে ইনপুট বা আউটপুট বেসলাইন থেকে আলাদা দেখাচ্ছে; রিগ্রেশন মানে খারাপ হয়েছে। একটা অন্যটাকে বোঝায় না। drift.compare পাঁচটা সংকেত পরীক্ষা করে: টপিক মিশ্রণ (চি-স্কয়ার), রিট্রিভাল স্কোরের বিন্যাস (দুই-নমুনার কোলমোগোরভ-স্মিরনভ), প্রত্যাখ্যান ও এরর হার (দুই-অনুপাতের z-টেস্ট) এবং প্রতি রিকোয়েস্টের খরচ (আপেক্ষিক পরিবর্তন)। বেসলাইন: রিলিজের আগের সবকিছু। বর্তমান: রিলিজের ২২ মিনিট।
from evidencedesk.observe import drift
base = [o for o in rows if o.ts < t0 + 150 * 60]
bad = [o for o in rows if t0 + 150 * 60 <= o.ts < t0 + 172 * 60]
for s in drift.compare(base, bad):
print(f"{s.name:<17} baseline={s.baseline:<8} current={s.current:<8} stat={s.stat:<7} limit={s.limit:<7} {'FLAG' if s.flagged else ''}")input_mix baseline=0.0 current=0.0707 stat=3.37 limit=13.28
score_dist baseline=0.532 current=0.532 stat=0.0753 limit=0.1509
refusal_rate baseline=0.1152 current=0.0992 stat=-0.54 limit=3.0
error_rate baseline=0.0537 current=0.3053 stat=9.9 limit=3.0 FLAG
cost_per_request baseline=0.000979 current=0.001698 stat=0.734 limit=0.25 FLAGএরর ও খরচ নড়েছে; ইনপুট মিশ্রণ ও রিট্রিভাল স্কোর নড়েনি। অর্থাৎ ব্যবহারকারী ও রিট্রিভাল বদলায়নি: রিট্রিভালের পরের কিছু বদলেছে। এটা প্রমাণ নয়, ইঙ্গিত। প্রতিটা ড্রিফট মনিটরকে যে প্রশ্নের উত্তর দিতে হয়: কিছু না বদলালে সে কতবার ফায়ার করে? একই বিন্যাস থেকে দুটো উইন্ডো টানুন, প্রতি আকারে ১০০ বার:
def same(n, seed):
return traffic.stationary(n, seed)
for size in (30, 100, 400):
naive = sum(drift.tv_distance([o.topic for o in same(size, 2 * t)], [o.topic for o in same(size, 2 * t + 1)]) > 0.15 for t in range(100))
print(f"windows of {size:>3}: fixed mix distance flags {naive}%, all five signals: {drift.false_positive_rate(same, size, 100)}")windows of 30: fixed mix distance flags 75%, all five signals: {'cost_per_request': 0.12, 'score_dist': 0.01, 'any': 0.13}
windows of 100: fixed mix distance flags 18%, all five signals: {'cost_per_request': 0.01, 'input_mix': 0.03, 'any': 0.04}
windows of 400: fixed mix distance flags 0%, all five signals: {'input_mix': 0.01, 'refusal_rate': 0.01, 'any': 0.02}মিশ্রণ-দূরত্বের নির্দিষ্ট থ্রেশহোল্ড ৩০ রিকোয়েস্টের অভিন্ন উইন্ডোর ৭৫ শতাংশে ফ্ল্যাগ দেয়, কারণ ছোট নমুনা দুলে; যে চি-স্কয়ার টেস্ট নমুনার আকার হিসাবে নেয়, সে দেয় না। তবু পাঁচটা সংকেত জমে: ৩০-এর অভিন্ন জোড়ার ১৩ শতাংশ কোনো একটা ফ্ল্যাগ তোলে (বেশির ভাগ খরচ, যা অসম), ৪০০-তে ২ শতাংশ। তাই বড় উইন্ডো ব্যবহার করুন, ফ্ল্যাগকে মানুষের জন্য প্রশ্ন ভাবুন, আর টিকে থাকার শর্ত দিন। নির্দোষ পরিবর্তন, যেমন গিফট কার্ড নিয়ে একটা ক্যাম্পেইন, মিশ্রণকে ফ্ল্যাগ করে কিন্তু কিছু ভাঙে না (একটা টেস্ট এটা পরীক্ষা করে), আর পুরোনো বেসলাইন ধীর পতনকে চুপচাপ শুষে নেয়।
মডেল ও প্রম্পট পরিবর্তন মনিটর করা
প্রতিটা Obs চালু ভার্সন বহন করে (প্রম্পট, মডেল, কর্পাস)। ভার্সনে ভার্সনে তুলনা সবচেয়ে সস্তা রিগ্রেশন মনিটর, আর কোনো টিউন-করা থ্রেশহোল্ড লাগে না:
for version, s in metrics.group_by(rows, lambda o: o.version).items():
print(version, f"n={s.n} error={s.error_rate} p95={s.p95_s} cost/req={s.cost_per_request} tokens_out/req={s.tokens_out / s.n:.0f}")v1 n=1448 error=0.0511 p95=1.309 cost/req=0.001002 tokens_out/req=34
v2 n=132 error=0.3106 p95=1.708 cost/req=0.00169 tokens_out/req=94নতুন প্রম্পটে প্রতি রিকোয়েস্টের খরচ ১.৭ গুণ, কারণ উত্তর প্রায় তিন গুণ লম্বা, আর ব্যর্থতা ছয় গুণ বেশি। প্রতিটা স্প্যান ও লগ লাইনে ভার্সন রাখুন, ফ্ল্যাগ দিয়ে বদলান, নতুন ভার্সন আগে অল্প অংশে পাঠান, আর দিনের বদলে ভার্সন ধরে তুলনা করুন। যে ভার্সন অফলাইনে জেতে কিন্তু অনলাইনে হারে, সেটা মূল্যায়নের নকশা ও জাজ ক্যালিব্রেশন-এর মূল্যায়ন সেট সম্পর্কে একটা পর্যবেক্ষণ।
সীমিত রিট্রাই ও ব্যাকঅফ
রিট্রাই ক্ষণস্থায়ী ত্রুটি সারায় আর দীর্ঘ ত্রুটি বাড়ায়। resilience.storm প্রতি সেকেন্ডে ১২টা নতুন রিকোয়েস্ট পাঠায় এমন প্রোভাইডারে যে সেকেন্ডে ২০টা কল পর্যন্ত সামলায় আর ৩০ সেকেন্ড কিছুই উত্তর দেয় না, আপনার চেনা RetryPolicy দিয়ে। তিন ক্লায়েন্ট: তখনই রিট্রাই, সীমাযুক্ত এক্সপোনেনশিয়াল ব্যাকঅফ ও ফুল জিটার, আর ব্যাকঅফের সঙ্গে রিট্রাই বাজেট (রিট্রাই সর্বোচ্চ প্রথম চেষ্টার ১০ শতাংশ)।
from evidencedesk.observe import resilience as rz
for name, policy, budget in [("retry at once", rz.NAIVE, None), ("backoff + full jitter", rz.BACKOFF, None), ("backoff + retry budget", rz.BACKOFF, 0.1)]:
st = rz.storm(policy, retry_budget=budget)
print(f"{name:<24} calls={st.attempts:<5} peak/s={st.peak:<3} answered={st.served:<4} gave up={st.gave_up:<4} overloaded seconds={st.overloaded_seconds}")
box = rz.Outbox()
for _ in range(3):
box.send("ak-demo", "ticket for order KC-1001")
print("side effect calls:", box.calls, "| tickets created:", len(box.sent))retry at once calls=2188 peak/s=48 answered=756 gave up=324 overloaded seconds=33
backoff + full jitter calls=1814 peak/s=38 answered=746 gave up=334 overloaded seconds=32
backoff + retry budget calls=1130 peak/s=24 answered=722 gave up=358 overloaded seconds=1
side effect calls: 3 | tickets created: 1তখনই রিট্রাই ২১৮৮টা কল পাঠায়, শীর্ষ সেকেন্ডে ৪৮, এমন সার্ভিসে যা ২০ সামলায়, আর প্রোভাইডার ৩৩ সেকেন্ড ওভারলোডেড থাকে, বিভ্রাটের চেয়ে তিন সেকেন্ড বেশি। জিটার শীর্ষ কমায়, যথেষ্ট নয়; রিট্রাই বাজেট কল অর্ধেক করে ও ওভারলোড শেষ করে। দামটা দেখা যায়: সে ৩৪টা কম রিকোয়েস্টের উত্তর দেয়। সীমিত রিট্রাই বেছে নেয় কার রিকোয়েস্ট ফেলা হবে। সার্ভারের অপেক্ষার ইঙ্গিত মানুন, শুধু পুনরায় চালানো নিরাপদ কল রিট্রাই করুন, আর প্রতিটা চেষ্টা লেজারে গুনুন। পার্শ্বপ্রভাব পুনরায় চালানো নিরাপদ শুধু আইডেমপোটেন্সি কি থাকলে, যা মডেল নয়, অ্যাপ্লিকেশন ঠিক করে (শেষ লাইন: তিনটা কল, একটা টিকিট)।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): একটা ক্লাউড প্রোভাইডারের ইঞ্জিনিয়ারিং ব্লগ সীমাযুক্ত এক্সপোনেনশিয়াল ব্যাকঅফ,
min(cap, base * 2 ** attempt), এবং তিনটা জিটার রূপ (Full, Equal, Decorrelated) বর্ণনা করে; জিটার আছে, কারণ নইলে রিট্রাইগুলো একই মুহূর্তে জড়ো হয়ে যায়। HTTPRetry-Afterহলো সেকেন্ডে অপেক্ষা বা একটা তারিখ, 503-এর সঙ্গে পাঠানো হয় ও 429-এ অনুমোদিত; MDN বলছে ক্লায়েন্টের সমর্থন এখনো অসংগত। সার্ভার সংযোগ সরাসরি প্রত্যাখ্যানও করতে পারে। রিট্রাই বাজেট ও লোড-শেডিং পরামর্শ আমাদের নিজেদের। বর্তমান ডকুমেন্টেশন দেখে নিন।
নিরাপদ ডিগ্রেডেশন নকশা
মডেল বন্ধ, ব্যয়বহুল বা সন্দেহজনক হলে ফিচারের ছোট হওয়া উচিত, ভাঙা নয়। resilience.MODES চলে full থেকে shorter_output, cheaper_model ও search_only (সেরা অংশগুলো সোর্স হিসেবে, মডেল কল ছাড়া) হয়ে unavailable পর্যন্ত, যা রিট্রাইয়ের ইঙ্গিতসহ একটা স্পষ্ট সাময়িক ব্যর্থতা। সার্কিট ব্রেকার পরপর তিন ব্যর্থতার পর খোলে যাতে কলাররা মৃত ডিপেন্ডেন্সির জন্য অপেক্ষা না করে, বিরতির পর একটা ট্রায়াল দেয়, সফল হলে বন্ধ হয়। অডিট ট্রেইলসহ একটা ফ্ল্যাগই কিল সুইচ; বদলাতে ডিপ্লয় লাগে না। একটা ফাংশন ঠিক করে, শক্তিশালী কারণ আগে:
# excerpt of kit/evidencedesk/observe/resilience.py
def choose_mode(flags: Flags, breaker: Breaker | None = None, budget_mode: str = "") -> str:
"""Strongest reason wins: kill switch, then an operator's fixed mode, then an open breaker, then the budget's degrade mode."""
if not flags.get("answer.enabled"):
return "unavailable"
if (m := flags.get("answer.mode")) != "auto":
return str(m)
if breaker is not None and not breaker.allow():
return "search_only"
return budget_mode or "full"চারটা টাইমআউট, একটা পুনরুদ্ধার, তারপর সুইচ:
lab = Lab()
flags, breaker = rz.Flags(lab.clock), rz.Breaker(lab.clock, threshold=3, reset_s=30.0)
def step(label, model_mode="ok"):
mode = rz.choose_mode(flags, breaker)
if mode == "full":
reply = lab.ask(Q, model_mode)
breaker.record(reply.status != "failed")
outcome = reply.code
elif mode == "search_only":
outcome = "sources: " + ", ".join(p["id"] for p in rz.search_only(lab.retriever, Q)["passages"][:2])
else:
outcome = rz.unavailable()["code"]
print(f"{label:<12} mode={mode:<12} breaker={breaker.state:<9} {outcome}")
for i in range(4):
step(f"call {i + 1}", "timeout")
lab.clock.advance(31)
step("after 31 s", "ok")
flags.set("answer.enabled", False, "on-call", "kill switch")
step("kill switch")
print([(h["flag"], h["to"], h["who"]) for h in flags.history])call 1 mode=full breaker=closed timeout
call 2 mode=full breaker=closed timeout
call 3 mode=full breaker=open timeout
call 4 mode=search_only breaker=open sources: kc-returns-v3:c0, kc-warranty-v2:c0
after 31 s mode=full breaker=closed ok
kill switch mode=unavailable breaker=closed temporarily_unavailable
[('answer.enabled', False, 'on-call')]তৃতীয় টাইমআউটে ব্রেকার খুলল, তাই ৪ নম্বর কল মডেল ছোঁয়নি। ৩১ নকল সেকেন্ড পর একটা ট্রায়াল সফল হলো ও ব্রেকার বন্ধ হলো। কিল সুইচ সবকিছুর ওপরে, আর ইতিহাস বলে কে ও কেন। সার্চ-অনলি সৎ কিন্তু খারাপ অভিজ্ঞতা: ব্যবহারকারীকে বলুন, আর যেদিন লাগবে তার আগেই ডিগ্রেড পথ পরীক্ষা করুন।
বাজেট খরচ ট্র্যাক করা
UsageLimiter ব্যবহারকারী ও সামগ্রিকভাবে কল ও টোকেন গুনত। একটা প্রোডাক্টে ফিচার স্কোপ আর দেয়ালের আগে নরম একটা ধাপও লাগে। BudgetKeeper দৈনিক কাউন্টার রাখে ব্যবহারকারী, টেন্যান্ট ও ফিচার ধরে, কল ও ক্রেডিটে (একটা কাল্পনিক একক)। সফট সীমার (এখানে ৮০ শতাংশ) নিচে রিকোয়েস্ট যায়; সফট ও হার্ড-এর মাঝে সস্তা মোডে সার্ভ হয়, ব্যবহার বাড়লে শক্তিশালী মোডে; হার্ড সীমায় পায় স্থির কোড budget_exceeded, স্কোপ ও রিসেটের সময় সহ। সবচেয়ে কড়া স্কোপ সিদ্ধান্ত নেয়, আর যে কল সীমা পেরোবে সেটা করার আগেই প্রত্যাখ্যাত হয়।
from evidencedesk.observe import budgets
from evidencedesk.usage import ManualClock
keeper = budgets.BudgetKeeper(ManualClock(20000 * 86400.0), budgets.Policy.load())
last = None
for call in range(1, 46):
v = keeper.check("u1", "tenant-a", "answer", 0.001)
if (v.action, v.mode) != last:
print(f"call {call:>2}: {v.action:<8} {v.mode:<15} {v.code} scope={v.scope or '-'} used={v.used}")
last = (v.action, v.mode)
if v.action != "block":
keeper.record("u1", "tenant-a", "answer", 0.001)
print("tenant-b meanwhile:", keeper.check("u2", "tenant-b", "answer").action, "| notices:", keeper.notices)call 1: allow ok scope=- used=0.03
call 32: degrade shorter_output budget_soft scope=user used=0.8
call 35: degrade cheaper_model budget_soft scope=user used=0.88
call 38: degrade search_only budget_soft scope=user used=0.95
call 41: block budget_exceeded scope=user used=1.02
tenant-b meanwhile: allow | notices: ['user:u1 crossed the soft limit']৩২ নম্বর কল থেকে ডিগ্রেড, ৪১ থেকে ব্লক; অন্য টেন্যান্ট অক্ষত। সফট সীমায় একবার নোটিস লেখা হয় যাতে ব্যবহারকারীর আগে একজন মানুষ জানে। অ্যাপ্লিকেশন কোডের সীমা একটা পাহারা, বিল নয়: প্রোভাইডারের নিজস্ব খরচ-সীমাও দিন।
অস্বাভাবিকতা ও অপব্যবহারের সংকেত
অপব্যবহার দেখতে একই জায়গার সাধারণ ট্রাফিকের মতো। abuse.signals জানায় বার্স্ট (যেকোনো ৬০ সেকেন্ডে সর্বোচ্চ রিকোয়েস্ট), সন্দেহজনক ইনজেকশন ফ্ল্যাগ, ব্যবহারকারীর ব্লকড অংশ আর ব্যবহারকারীর খরচের অংশ; অংশের জন্য ন্যূনতম রিকোয়েস্ট সংখ্যা লাগে।
from evidencedesk.observe import abuse
for s in abuse.signals(rows):
print(s.line)
print("largest request shares:", abuse.top_share(rows)[:2])u-0e6bb8a5 burst value=35.0 limit=15.0 n=140
u-0e6bb8a5 injection_flags value=77.0 limit=5.0 n=140
u-0e6bb8a5 blocked_share value=0.486 limit=0.3 n=140
largest request shares: [('u-0e6bb8a5', 0.089), ('u-3fb4ad39', 0.039)]একটা ছদ্মনাম তিনটা সংকেত তোলে; আর কেউ একটাও না। সংকেত ঠিক করে একজন মানুষ কাকে দেখবে; কাউকে নিষিদ্ধ করার জন্য নয়। একটা স্কুল ত্রিশজন শিক্ষার্থীকে একই টুলে পাঠালে সেটা বার্স্টের মতো দেখায়। ধাপে ধাপে সাড়ার সঙ্গে জুড়ুন (ধীর করা, ডিগ্রেড, ব্লক)।
মান নমুনা ও মানুষের রিভিউ কিউ
কোনো থ্রেশহোল্ড সাবলীল ভুল উত্তর দেখে না; দেখে শুধু উত্তর পড়া মানুষ, আর মানুষের সময় একটা বাজেট। review.select কিউ ভরে দুই লেনে: র্যান্ডম লেন, মানের একমাত্র পক্ষপাতহীন দৃশ্য, আর টার্গেটেড লেন (ব্যর্থতা, ফ্ল্যাগ, নিচু রিট্রিভাল স্কোরেও দেওয়া উত্তর, বিরত থাকা, নতুন ভার্সন), গুরুত্ব অনুযায়ী। সংরক্ষিত র্যান্ডম লেন ছাড়া ব্যর্থতায় কিউ ভরে যায় ও মানের হিসাব পক্ষপাতদুষ্ট হয়। ভার্ডিক্টে কোনো লেখা থাকে না; রিভিউয়াররা ছোট রিটেনশনের বাইরের কনটেন্ট স্টোরে উত্তর পড়েন। Queue.agreement দুজন রিভিউয়ারকে kappa দিয়ে মাপে (পরীক্ষিত, এখানে দেখানো হয়নি)।
from evidencedesk.observe import review
items = review.select(rows, capacity=40, new_versions=frozenset({"v2"}))
by_id = {o.request_id: o for o in rows}
sampled = [by_id[i.request_id] for i in items if i.reason == "sampled"]
good = sum(o.truth == "good" for o in sampled)
print("queue:", {r: sum(i.reason == r for i in items) for r in sorted({i.reason for i in items})})
print(f"random lane: {good}/{len(sampled)} good, 95% interval {review.wilson(good, len(sampled))}")
for n in (10, 20, 100, 400):
print(f"if 80% of {n} reviewed items are good, the 95% interval is {review.wilson(round(0.8 * n), n)}")queue: {'blocked': 5, 'failed': 11, 'flagged': 4, 'sampled': 20}
random lane: 16/20 good, 95% interval (0.584, 0.919)
if 80% of 10 reviewed items are good, the 95% interval is (0.49, 0.943)
if 80% of 20 reviewed items are good, the 95% interval is (0.584, 0.919)
if 80% of 100 reviewed items are good, the 95% interval is (0.711, 0.867)
if 80% of 400 reviewed items are good, the 95% interval is (0.758, 0.836)নমুনা-করা ২০টা উত্তরের ১৬টা ভালো, কিন্তু ব্যবধান ৫৮ থেকে ৯২ শতাংশ। কয়েক পয়েন্টের মান-পরিবর্তন ধরতে শত শত রিভিউ লাগে, তাই ৫ শতাংশ নমুনা ২২ মিনিটের রিগ্রেশন দেখতে পারে না। সে কারণে জোরালো সংকেত আর ভার্সন-ধরে তুলনা আগে আসে, আর নমুনা ধরে ধীর, নীরব ক্ষয়। রিভিউ-করা প্রতিটা ব্যর্থতা মূল্যায়ন সেটের একটা কেস হয়।
লগ রিটেনশন ও অ্যাক্সেস পরিকল্পনা
অবজার্ভেবিলিটি ডেটাও ডেটা। retention.json প্রতি শ্রেণিতে (মেট্রিক, লগ, ট্রেস, কনটেন্ট, রিভিউ লেবেল, পোস্টমর্টেম) বলে: কী ধারণ করে, কত দিন রাখে, কোন ভূমিকা পড়তে পারে, কীভাবে মোছা হয় এবং এটা সংবেদনশীল কি না। সংখ্যাগুলো উদাহরণ; আপনার নিয়ম আসে আপনার আইনি, নিরাপত্তা ও গ্রাহক-প্রতিশ্রুতি থেকে। চেকগুলো পরিকল্পনার ভুল ধরে, আইনি ভুল নয়:
from evidencedesk.observe import retention
plan = retention.load_plan()
print("plan problems:", retention.plan_problems(plan))
print({c.name: (c.days, "/".join(c.access)) for c in plan if c.name in ("logs", "content")})
broken = [retention.DataClass("logs", "events", 30, ("everyone",), "", False), retention.DataClass("content", "text", 90, ("reviewers",), "purge", True)]
for p in retention.plan_problems(broken):
print(" -", p)
day = 86400.0
kept, gone = retention.purge([("c1", "content", 0.0), ("c2", "content", 9 * day), ("l1", "logs", 0.0), ("x1", "scratch", 9 * day)], 10 * day, plan)
print("kept:", [r[0] for r in kept], "| deleted:", gone, "| product may read content:", retention.may_read(plan, "product", "content"))plan problems: []
{'logs': (30, 'engineering'), 'content': (3, 'reviewers/incident-lead')}
- metrics: no rule
- traces: no rule
- logs: needs days > 0 and a way to delete
- logs: access must name roles, not everyone
- content: sensitive data kept 90 days (illustrative cap: 7)
- content must be kept for less time than metadata
kept: ['c2', 'l1'] | deleted: {'content': 1, 'scratch': 1} | product may read content: Falseচেকার ধরেছে একটা অনুপস্থিত নিয়ম, একটা অনুপস্থিত মোছার পদ্ধতি, দর্শক হিসেবে "everyone", খুব বেশি দিন রাখা সংবেদনশীল ডেটা, আর মেটাডেটার চেয়ে বেশি টেকা কনটেন্ট। purge যার কোনো শ্রেণি নেই তাকেও মোছে: নিয়ম নেই, রিটেনশনও নেই। ব্যবহারকারীর মোছার অনুরোধ কনটেন্ট স্টোরেও পৌঁছাতে হবে।
ইনসিডেন্ট ট্রায়াজ চেকলিস্ট লেখা
পেজ এলে প্রথম কাজ ইনসিডেন্টের ধরন চেনা, কারণ প্রতিটার প্রথম পদক্ষেপ আলাদা। চেকলিস্টটা কোড, তাই মেট্রিক থেকে উত্তর দেয়, আর নিরাপত্তা আগে আসে কারণ তাতে এমন পদক্ষেপ লাগতে পারে যা বাকিগুলোতে লাগে না।
| ধরন | প্রমাণ | প্রথম পদক্ষেপ |
|---|---|---|
| নিরাপত্তা | ব্লক বা ফ্ল্যাগ-করা রিকোয়েস্ট, একজনের বার্স্ট | সার্চ-অনলি বা বন্ধ; প্রমাণ রাখুন |
| প্রোভাইডার বিভ্রাট | মডেল-ধাপের এরর, রিট্রিভাল ঠিক | সার্চ-অনলি; সীমিত রিট্রাই; নোটিস |
| রিট্রিভাল ব্যর্থতা | no_evidence বেড়েছে বা সেরা স্কোর কমেছে | ইনডেক্স রোলব্যাক; রিট্রিভাল মূল্যায়ন আবার চালান |
| রিগ্রেশন | নতুন ভার্সনে ভ্যালিডেশন ব্যর্থতা বেড়েছে | ফ্ল্যাগ দিয়ে ভার্সন রোলব্যাক |
from evidencedesk.observe import incident
normal = traffic.stationary(300, 1)
cases = {"model keeps timing out": {"ok": 0.6, "timeout": 0.25, "server": 0.1}, "nothing is retrieved": {"ok": 0.5, "no_evidence": 0.4},
"blocked requests spike": {"ok": 0.4, "not_allowed": 0.5}, "quiet window": {"ok": 0.9}}
for label, codes in cases.items():
t = incident.triage(traffic.stationary(300, 2, traffic.Profile(codes=codes)), normal)
print(f"{label:<24} -> {t.category:<10} matches={t.matches}")
levels = incident.load_levels()
print("SEV2:", levels["SEV2"]["impact"], "| respond within", levels["SEV2"]["respond_min"], "min")
print("regression runbook:", incident.load_runbook()["regression"]["steps"][1])model keeps timing out -> provider matches=['provider']
nothing is retrieved -> retrieval matches=['retrieval']
blocked requests spike -> security matches=['security']
quiet window -> unknown matches=[]
SEV2: The feature is down or failing for many users | respond within 15 min
regression runbook: Set prompt.version back to the previous valueসিভিয়ারিটি ব্যবহারকারীর ক্ষতি অনুসরণ করে, কারণ নিয়ে আগ্রহ নয়: এখানে ২৫ শতাংশ খারাপ SEV2, ৫ শতাংশ SEV3 (উদাহরণ), আর সন্দেহজনক ফাঁস SEV1। রানবুক একটা ডেটা, যাতে মালিক, ক্রমানুসারে ধাপ আর একটা "verify" লাইন আছে: আগে ক্ষতি থামান, তারপর কারণ খুঁজুন, আর বলুন কীভাবে বুঝবেন কাজ হয়েছে। একসঙ্গে কয়েকটা ধরন সত্য হতে পারে; চেকলিস্ট সব মিল ফেরত দেয় এবং প্রথমটাকে সামনে রাখে।
মিনি-ল্যাব: একটা খারাপ রিলিজ রিপ্লে
incident_lab দিনটা শুরু থেকে শেষ চালায়: ট্রাফিক, অ্যালার্ট ইঞ্জিন, পেজের দুই মিনিট পর অ্যাকনলেজমেন্ট, ট্রায়াজ, পেজের সাত মিনিট পর ফ্ল্যাগ বদলে রোলব্যাক, পুনরুদ্ধার, লগ ইভেন্ট ও ট্রেসের তথ্য মিলিয়ে একটা টাইমলাইন, আর তা থেকে বানানো পোস্টমর্টেম। রোলব্যাক কখন পৌঁছায় তা নিয়ে স্ক্রিপ্ট ও সাড়া না মিললে এটা চলতে অস্বীকার করে। লগ আগের পাতার EventLog: কোড ও সংখ্যা, কোনো লেখা নেই, কাঁচা ইউজার আইডি নেই।
python -m evidencedesk.observe.incident_labalerts: 11:45 error-rate-high fired; 11:55 cost-per-request-high fired; 12:15 error-rate-high resolved; 12:25 cost-per-request-high resolved
triage: regression | validation failure share 0.322 (baseline 0.028); new versions: ['v2']
# Postmortem INC-001: Release v2 raised invalid replies
Severity SEV2. Category regression.
## Summary
Between 11:30 and 11:52 41 of 132 requests failed (31%); replies that parsed but were wrong are not counted here. 52% of the day's quality budget was spent.
## Detection and response
- Started 11:30, detected 11:45 (time to detect 15 min).
- Mitigated 11:52 (time to mitigate 7 min), resolved 12:15.
## Timeline
- 11:30 [log] deploy.started {"change": "prompt text and reply template", "version": "v2"}
- 11:30 [trace] first request on version v2
- 11:30 [trace] first failed request on v2 (truncated)
- 11:45 [log] alert.fired {"rule": "error-rate-high", "severity": "SEV2", "value": 0.3222}
- 11:47 [log] oncall.ack {"rule": "error-rate-high"}
- 11:50 [trace] last failed request on v2 (invalid_json)
- 11:52 [log] flag.changed {"flag": "prompt.version", "reason": "rollback", "to": "v1"}
- 11:55 [log] alert.fired {"rule": "cost-per-request-high", "severity": "SEV3", "value": 0.0016}
- 12:15 [log] alert.resolved {"rule": "error-rate-high", "severity": "SEV2", "value": 0.0333}
- 12:25 [log] alert.resolved {"rule": "cost-per-request-high", "severity": "SEV3", "value": 0.0011}
## Cause
The v2 prompt and reply template made the model leave the JSON format and run longer. No check on validation failures per version ran before release.
## What helped
- The error-rate alert fired 15 minutes after the release
- Rollback was a flag change: no build, no deploy
## What did not
- Nothing compared validation failures per version before release
- The cost alert fired after the fix, because its window is 30 minutes
## Actions
| What | Owner (role) | Due |
|---|---|---|
| Ship prompt changes to 5% of requests first and compare per version | release owner | 2 weeks |
| Alert on the validation-failure rate per version | app on-call | 1 week |
| Add the failed v2 cases to the regression set | evaluation owner | 1 week |পোস্টমর্টেম ভূমিকার নাম বলে, কখনো মানুষের নাম নয়, আর blameless_problems দোষারোপের শব্দ ও নাম খোঁজে। এর সংখ্যায় সেই উত্তর বাদ যা পার্স হয়েছে কিন্তু ভুল ছিল; সেগুলোর জন্য রিভিউ কিউ। ১৫ মিনিটের দেরি আর দেরিতে আসা খরচের অ্যালার্ট এখানে বেছে নেওয়া থ্রেশহোল্ডের ফল, ইনসিডেন্টের কোনো নিয়ম নয়।
python -m pytest kit/tests/test_observe_monitor_{metrics,alerts,budgets,review,incident}.py | tail -n 1 | sed 's/ in [0-9.]*s//'48 passedটেস্ট কোডের পথ প্রমাণ করে। আসল ট্রাফিকের আকার এমন কি না, বা আপনার থ্রেশহোল্ড আপনার ব্যবহারকারীদের জন্য ঠিক কি না, তা তারা দেখাতে পারে না।
কাজের জায়গায় এটা কোথায় দেখা যায়
পুনের প্রিয়া এমন একটা দলে যোগ দিল যার অ্যাসিস্ট্যান্টের একটাই ড্যাশবোর্ড ও কোনো অ্যালার্ট ছিল না (একটা বোঝানোর গল্প)। দুই সপ্তাহে সে যোগ করল ভার্সন-ধরে এরর ও খরচের প্যানেল, ১০ মিনিটের for-duration সহ একটা এরর অ্যালার্ট, একটা কিল সুইচ ফ্ল্যাগ আর এক পাতার রানবুক। প্রথম আসল পেজ, একটা প্রোভাইডার টাইমআউট ঝড়, অ্যালার্ট থেকে সার্চ-অনলি পর্যন্ত ছয় মিনিট নিল।
সাধারণ ভুল
- নমুনার আকার ছাড়া কাঁচা থ্রেশহোল্ডে অ্যালার্ট। তিন রিকোয়েস্টে একটা ব্যর্থতা মানে ৩৩ শতাংশ। ন্যূনতম সংখ্যা ও for-duration চাই।
- ফায়ার আর ক্লিয়ারের জন্য একই সংখ্যা। অ্যালার্ট দুলতে থাকে। হিস্টেরেসিস দিন এবং প্রতি ইনসিডেন্টে নোটিফিকেশন গুনুন।
- ড্রিফট ফ্ল্যাগকে ত্রুটি ভাবা। এটা একটা প্রশ্ন; ছোট উইন্ডো আর অনেক সংকেত ভুল সতর্কতা তোলে। বড় উইন্ডো ও টিকে থাকার শর্ত ব্যবহার করুন।
- সীমা বা কি ছাড়া রিট্রাই। এটা লোড গুণ করে ও পার্শ্বপ্রভাব দ্বিগুণ করে। চেষ্টা সীমিত করুন, রিট্রাই বাজেট ও আইডেমপোটেন্সি কি দিন।
- শুধু ব্যর্থতা রিভিউ করা। মানের হিসাব পক্ষপাতদুষ্ট হয়। র্যান্ডম লেন রাখুন এবং শুধু অংশ নয়, ব্যবধানও জানান।
নিজে চেষ্টা করুন
- সহজ। খরচ ও লেটেন্সির মূল কথা পাতায় ফিরে যান। কৃত্রিম দিনের প্রতি ভার্সনে প্রতি রিকোয়েস্টের খরচ বের করুন। চালানোর আগে অনুমান করুন v2-র খরচ v1-এর ১.৫ গুণের বেশি কি না।
- মাঝারি। বাজেট, ক্যাশিং ও ব্যবহারের সীমা পাতায় ফিরে যান। প্রতিটা ব্যবহারকারীকে দিনে ১০ কল দিন, একই ৮০ শতাংশ সফট সীমায়। কোন কল নম্বরে মোড বদলায় তা অনুমান করুন, তারপর মিলিয়ে দেখুন।
- কঠিন। স্ট্রাকচার্ড লগিং ও ট্রেসিং পাতায় ফিরে যান। যেহেতু প্রতিটা পর্যবেক্ষণে ভার্সন আছে, v2-র প্রথম রিকোয়েস্টগুলো ৩০তম থেকে প্রতি দশে v1 বেসলাইনের সঙ্গে তুলনা করুন।
error_rateপ্রথম কখন ফ্ল্যাগ হয় তা বের করুন এবং উইন্ডো অ্যালার্টের সঙ্গে তুলনা করুন।
উত্তর
সহজ। প্রায় ১.৭ গুণ: v2-র উত্তর প্রায় তিন গুণ লম্বা, আর দামের তালিকায় আউটপুট টোকেনের দাম ইনপুটের চেয়ে বেশি।
from evidencedesk.observe import metrics
per = metrics.group_by(rows, lambda o: o.version)
print({v: round(s.cost_per_request * 1000, 3) for v, s in per.items()}, "| ratio v2/v1:", round(per["v2"].cost_per_request / per["v1"].cost_per_request, 2)){'v1': 1.002, 'v2': 1.69} | ratio v2/v1: 1.69মাঝারি। ১০-এর ৮০ শতাংশ হলো ৮ নম্বর কল; তিন মোডের প্রতিটা এক কল টেকে; ১১ নম্বর কল প্রত্যাখ্যাত।
from dataclasses import replace
small = replace(budgets.Policy.load(), user=budgets.Limit(10, 1.0))
k2 = budgets.BudgetKeeper(ManualClock(20000 * 86400.0), small)
marks, last = [], None
for call in range(1, 14):
v = k2.check("u", "t", "answer")
if (v.action, v.mode) != last:
marks.append((call, v.action, v.mode))
last = (v.action, v.mode)
if v.action != "block":
k2.record("u", "t", "answer", 0.0)
print(marks)[(1, 'allow', ''), (8, 'degrade', 'shorter_output'), (9, 'degrade', 'cheaper_model'), (10, 'degrade', 'search_only'), (11, 'block', '')]কঠিন। ভার্সন-ধরে তুলনায় মাত্র ৩০ রিকোয়েস্ট লাগে ও সমানে সমান তুলনা হয়, তাই ১১:৩৪-এ ফ্ল্যাগ হয়, উইন্ডো অ্যালার্টের এগারো মিনিট আগে। প্রতিটা রিকোয়েস্টে ভার্সন লেবেল লাগে।
v1 = [o for o in rows if o.version == "v1" and o.ts < t0 + 150 * 60]
v2 = [o for o in rows if o.version == "v2"]
for k in range(30, len(v2) + 1, 10):
if "error_rate" in drift.flagged(drift.compare(v1, v2[:k])):
print(f"flagged after {k} requests on v2, at {hm(v2[k - 1].ts)}; the window alert fired at 11:45")
breakflagged after 30 requests on v2, at 11:34; the window alert fired at 11:45পাস চেকলিস্ট:
- সহজ: প্রতি রিকোয়েস্টে ১.০০২ ও ১.৬৯ মিলি-ক্রেডিট, অনুপাত ১.৬৯, আর আপনি কারণ হিসেবে উত্তরের দৈর্ঘ্য বলেছেন।
- মাঝারি: ৮, ৯, ১০ ও ১১ নম্বর কলে পরিবর্তন (ছোট আউটপুট, সস্তা মডেল, সার্চ-অনলি, ব্লক)।
- কঠিন: ৩০ রিকোয়েস্টের পর ১১:৩৪-এ ফ্ল্যাগ, আর আপনি বলেছেন এতে কী লাগে (ভার্সন লেবেল) এবং কী দেখা যায় না (ভার্সনের সঙ্গে যুক্ত নয় এমন ত্রুটি)।
সারসংক্ষেপ
- ট্রেসকে সমতল পর্যবেক্ষণে বদলান, সঠিক ডিনোমিনেটরে গুনুন, ন্যূনতম নমুনার নিচে হার ও পারসেন্টাইল দিতে অস্বীকার করুন, আর বাকেট (ফাঁকাগুলোসহ) ও ভার্সন-ধরে গ্রুপকে ড্যাশবোর্ড ডেটাসেট করুন।
- মান, লেটেন্সি, খরচ ও নিরাপত্তার SLO লিখুন, বাজেট ও বার্ন রেট দেখুন, আর for-duration, হিস্টেরেসিস, ডি-ডুপ্লিকেশন ও ন্যূনতম নমুনা সহ অ্যালার্ট বানান; লম্বা উইন্ডো নয়েজের বদলে দেরি দেয়।
- ড্রিফট ফ্ল্যাগ মাপা ভুল-সতর্কতার হারসহ প্রশ্ন; ভার্সন তুলনা করুন, বড় উইন্ডো ব্যবহার করুন, আর র্যান্ডম রিভিউ লেন রাখুন কারণ কোনো মেট্রিক সাবলীল ভুল উত্তর দেখে না।
- ব্যাকঅফ, রিট্রাই বাজেট ও আইডেমপোটেন্সি কি দিয়ে রিট্রাই সীমিত করুন; ব্রেকার ও কিল সুইচ দিয়ে ধাপে ধাপে ডিগ্রেড করুন; ব্যবহারকারী, টেন্যান্ট ও ফিচার ধরে সফট ও হার্ড বাজেট প্রয়োগ করুন।
- রিটেনশন ও অ্যাক্সেস ডেটা হিসেবে পরিকল্পনা করুন, প্রমাণ দিয়ে ট্রায়াজ করুন, রানবুক মানুন, আর টাইমলাইন থেকে দোষারোপহীন পোস্টমর্টেম লিখুন।
এরপর: থ্রেট মডেলিং ও ইনজেকশন টেস্টিং পুরো অ্যাসিস্ট্যান্টের অ্যাসেট, অ্যাক্টর ও ট্রাস্ট বাউন্ডারি ম্যাপ করে এবং নিরাপদ লোকাল আক্রমণ দিয়ে ডাইরেক্ট ও ইনডাইরেক্ট প্রম্পট ইনজেকশন পরীক্ষা করে।