अध्याय २ · खण्ड III · 24 मिनेट
त्रुटि, सीमा, र खराब प्रतिक्रिया सम्हाल्ने
वास्तविक अनुप्रयोगहरूले दिनमा हजारौँ पटक API बोलाउँछन्। केही कल असफल हुन्छन्। केहीले rate limit हिट गर्छन्। केहीले parsing पास गर्ने प्रतिक्रिया फर्काउँछन् तर तिनी गलत छन्। एप चलिरहन बनाउने बानीहरू बोरिङ, परीक्षण गरिएका, र हरेक लाइनको मूल्यका छन्।
Prompt सही छ। JSON parse हुन्छ। Pydantic मोडेल validate हुन्छ। अब कल्पना गर्नुहोस् तपाईंले उही कल महिनामा दस लाख पटक बोलाउनुहुन्छ। तिनीहरूमध्ये केही असफल हुनेछन् किनकि network संक्षिप्त रूपमा hiccup गर्छ। केही असफल हुनेछन् किनकि तपाईंले आफ्नो rate limit हिट गर्नुभयो। केही असफल हुनेछन् किनकि मोडेलले technically-मान्य प्रतिक्रिया फर्काउँछ जुन semantically गलत छ। यो खण्ड ती तिनै केसका लागि निर्माण गर्ने बोरिङ, आवश्यक कला हो — साना बानीहरू जसले घटनाबिना महिनौँसम्म अनुप्रयोगहरू चलिरहेको राख्छन्।
विफलताका तीन परिवार
तपाईंको LLM कोडले देख्ने हरेक विफलता तीन श्रेणी मध्ये एउटामा पर्छ:
- Transient विफलता — network ढिलो छ, API छिटो overloaded छ, 5xx त्रुटि फर्किन्छ। समाधान: backoff सँग retry।
- Rate limit विफलता — तपाईंले API लाई धेरै छिटो बोलाउनुभयो; provider तपाईंलाई सुस्त हुन भन्दैछ। समाधान: exponentially backoff र तिनको guidance को सम्मान गर्नुहोस्।
- Semantic विफलता — कल सफल भयो तर प्रतिक्रिया गलत छ। समाधान: validate, log, र (कहिलेकाहीँ) बलियो मोडेल वा फरक prompt सँग retry।
प्रत्येकको फरक कोड र फरक अर्थशास्त्र छ। तिनलाई अलग-अलग सिक्नुहोस्।
Transient विफलता र retry
Transient विफलताका लागि, मानक ढाँचा jitter सँग exponential backoff हो:
import time
import random
import anthropic
from anthropic import APIError, APIConnectionError, APITimeoutError
RETRIABLE_ERRORS = (APIConnectionError, APITimeoutError)
def call_with_retry(fn, max_attempts: int = 5, base_delay: float = 1.0):
"""Call fn(), retrying transient errors with exponential backoff."""
for attempt in range(1, max_attempts + 1):
try:
return fn()
except RETRIABLE_ERRORS as e:
if attempt == max_attempts:
raise
# Exponential backoff: 1s, 2s, 4s, 8s, ... with jitter
delay = base_delay * (2 ** (attempt - 1)) + random.uniform(0, 0.5)
print(f"Attempt {attempt} failed with {type(e).__name__}: {e}. "
f"Retrying in {delay:.1f}s...")
time.sleep(delay)
Usage:
def call_llm():
return client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=400,
temperature=0.0,
messages=[{"role": "user", "content": "Say hello in Nepali."}],
)
response = call_with_retry(call_llm)
Retry कोडमा तीन मुख्य विचार:
- Transient त्रुटिहरू मात्र retry गर्नुहोस्। 400 (bad request) वा 401 (auth failure) retry गर्नु बेकार छ; ती लाई कोड सुधार चाहिन्छ, थप कल होइन।
- Exponential backoff। हरेक retry ले अघिल्लो भन्दा लामो पर्खन्छ, त्यसैले तपाईंले संघर्ष गरिरहेको API लाई प्रहार गर्नुहुन्न।
- Jitter। सानो random delay थप्नाले तपाईंका सबै instances लाई ठ्याक्कै उही क्षणमा retry गर्नबाट रोक्छ, जुनले खराब स्थिति झन् खराब बनाउँछ।
Rate limits, विशेष रूपमा
जब API ले 429 Too Many Requests फर्काउँछ, प्रतिक्रियाले सामान्यतया retry-after header समावेश गर्छ तपाईंलाई ठ्याक्कै कति समय पर्खने भन्छ। Anthropic को SDK ले यो expose गर्छ:
from anthropic import RateLimitError
def call_with_rate_limit_handling(fn, max_attempts: int = 5):
for attempt in range(1, max_attempts + 1):
try:
return fn()
except RateLimitError as e:
if attempt == max_attempts:
raise
# Prefer the API's suggested wait time; fall back to exponential backoff
retry_after = float(getattr(e, "retry_after", None) or 2 ** attempt)
print(f"Rate limited. Waiting {retry_after:.1f}s...")
time.sleep(retry_after)
retry-after header को सम्मान गर्नुहोस् जब यो त्यहाँ छ। यदि छैन भने, exponentially backoff गर्नुहोस्। यदि तपाईं routine रूपमा rate limits हिट गर्दै हुनुहुन्छ भने, यो या त तपाईंको API tier upgrade गर्ने, caching थप्ने, वा उच्च throughput limits सँग सानो मोडेल प्रयोग गर्ने सङ्केत हो।
Timeouts
Anthropic SDK मा default timeout छ, तर तपाईंले user-facing कुनै पनि कुराका लागि explicit एउटा सेट गर्नुपर्छ:
client = anthropic.Anthropic(timeout=15.0) # 15 seconds default for all calls
# Or per-call:
response = client.with_options(timeout=30.0).messages.create(...)
अन्तरक्रियात्मक UI का लागि, 15-30 सेकेन्ड उदार तर तपाईंको app wedge गर्न धेरै लामो होइन। Background jobs का लागि, लामो timeouts (60-120 सेकेन्ड) उचित छन् — तर सधैँ finite।
Semantic विफलता
कल फर्कियो। JSON parse भयो। Pydantic validation पास भयो। तर प्रतिक्रिया गलत छ — मोडेलले hallucinate गर्यो, गलत वर्गीकरण गर्यो, वा भद्रसँग अस्वीकार गर्यो। यो सबैभन्दा गाह्रो विफलता मोड हो किनकि तपाईंको कोडसँग कुनै प्रत्यक्ष सङ्केत छैन।
तीन रक्षा:
1. सबै कुरा log गर्नुहोस्। उत्पादन LLM कलहरूका लागि, अनुरोध, प्रतिक्रिया, र कुनै पनि validation नतिजा log गर्नुहोस्। पछि, जब कुनै user ले खराब उत्तर रिपोर्ट गर्छ, तपाईं वास्तवमै के भयो हेर्न सक्नुहुन्छ। Logging बिना, LLM bugs debug गर्न झन्डै असम्भव छ।
logger.info(
"llm_call",
prompt=prompt[:500], # truncate for privacy
response_text=response.content[0].text[:500],
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
stop_reason=response.stop_reason,
duration_ms=elapsed_ms,
)
2. Sanity checks विरुद्ध validate गर्नुहोस्। यदि तपाईंको extraction ले 5% रसिदमा amount_npr = 0 फर्काउँछ भने, केही गलत छ — रसिदमा शून्य रकम हुँदैन। Sanity checks ले यो समात्छ:
if fields.amount_npr == 0:
logger.warning("suspicious_extraction", raw_text=receipt_text)
3. Fallback राख्नुहोस्। महत्त्वपूर्ण paths का लागि, यदि सानो मोडेल असफल हुन्छ वा शङ्कास्पद output उत्पादन गर्छ भने, बलियो मोडेलसँग retry गर्नुहोस्:
def extract_receipt_robust(text: str) -> Optional[ReceiptFields]:
result = extract_receipt(text, model="claude-haiku-4-5-20251001")
if result is None or result.amount_npr == 0:
# Escalate to a stronger model
result = extract_receipt(text, model="claude-sonnet-4-6")
return result
यो महँगो छ (Sonnet Haiku भन्दा 12x महँगो), त्यसैले यसलाई कम प्रयोग गर्नुहोस् — तर शुद्धता महत्त्वपूर्ण हुने केसका लागि, escalation मानक ढाँचा हो।
Graceful refusal केस
कहिलेकाहीँ मोडेलले भद्रसँग उत्तर दिन अस्वीकार गर्छ — सामान्यतया सुरक्षा कारणले, तर कहिलेकाहीँ किनकि तपाईंको prompt अस्पष्ट थियो:
"I'd be happy to help, but I need more context. Could you..."
तपाईंको JSON parser असफल हुनेछ। तपाईंको pydantic validator असफल हुनेछ। र user, उत्पादनमा, केही देख्नेछैन।
समाधान भनेको तपाईंको parser मा explicit रूपमा refusals पत्ता लगाउनु हो:
REFUSAL_MARKERS = [
"I'd be happy to help",
"I'm not able to",
"I cannot",
"I don't have information about",
]
def looks_like_refusal(text: str) -> bool:
return any(m.lower() in text.lower() for m in REFUSAL_MARKERS)
def parse_response(text: str):
if looks_like_refusal(text):
raise ModelRefusalError(text)
return parse_llm_json(text)
अब refusals नामित विफलता मोड हुन् जुन तपाईं जानीजानी सम्हाल्न सक्नुहुन्छ — सामान्यतया मानिसलाई escalate गरेर वा user लाई मोडेलले यो विशिष्ट अनुरोधसँग मद्दत गर्न सक्दैन भनेर व्याख्या गरेर।
सबै सँगै बाँध्ने wrapper
हरेक गम्भीर LLM अनुप्रयोगका लागि, तपाईं retries, timeouts, rate limits सम्हाल्ने र सफा नतिजा फर्काउने call_llm function चाहनुहुनेछ:
import time
import logging
from typing import Callable
from anthropic import APIError, APIConnectionError, APITimeoutError, RateLimitError
logger = logging.getLogger(__name__)
class LLMError(Exception): pass
class ModelRefusalError(LLMError): pass
class LLMBudgetExceeded(LLMError): pass
def safe_call(fn: Callable, max_attempts: int = 5) -> object:
for attempt in range(1, max_attempts + 1):
try:
start = time.time()
result = fn()
duration = time.time() - start
logger.info(f"LLM call ok (attempt {attempt}, {duration:.2f}s)")
return result
except RateLimitError as e:
retry_after = float(getattr(e, "retry_after", None) or 2 ** attempt)
logger.warning(f"Rate limited. Sleeping {retry_after}s.")
time.sleep(retry_after)
except (APIConnectionError, APITimeoutError) as e:
delay = 2 ** (attempt - 1)
logger.warning(f"Transient error: {type(e).__name__}. Retrying in {delay}s.")
time.sleep(delay)
except APIError as e:
# Non-retriable API error (auth, bad request, etc.)
logger.error(f"Non-retriable API error: {e}")
raise
raise LLMError(f"Failed after {max_attempts} attempts.")
तपाईंले लेख्ने हरेक उत्पादन LLM function ले safe_call मार्फत API लाई बोलाउनुपर्छ। एक पटक, boundary मा। Downstream सबै कुराले कल या त काम गर्यो वा यसलाई सम्हाल्न जान्ने विशिष्ट exception raise भयो भन्ने मान्न सक्छ।
लागत कडा विफलताका रूपमा
कुनै पनि उत्पादन प्रणालीका लागि, लागतमा कडा cap राख्नुहोस्। Runaway loop वा stuck script ले सयौँ डलर मिनेटमा जलाउन सक्छ:
class BudgetGuard:
def __init__(self, monthly_budget_usd: float):
self.budget = monthly_budget_usd
self.spent = 0.0
def charge(self, cost: float):
self.spent += cost
if self.spent > self.budget:
raise LLMBudgetExceeded(f"Spent ${self.spent:.2f} of ${self.budget:.2f}")
guard = BudgetGuard(monthly_budget_usd=200)
def call_and_track(fn):
response = safe_call(fn)
cost = compute_cost(response)
guard.charge(cost)
return response
Budget guard ले runaway usage लाई वास्तविक बिल बन्नुअघि समात्छ। Provider को console alerts सँग मिलेर, यो सस्तो बिमा हो।
आफ्नो बुझाइ जाँच्नुहोस्
Quick check
—एक उत्पादन LLM app ले हिजो रातिबाट garbage प्रतिक्रिया फर्काउन थाल्यो, तर कोड परिवर्तन भएको छैन। छानबिनले देखाउँछ कल सफल भयो — कुनै exceptions raise भएनन्। के बानी छुटेको छ जसले यसलाई चाँडै समात्थ्यो?
Quick check
—एक जुनियर इन्जिनियरले 429 rate-limit त्रुटिहरू रिपोर्ट गर्छन् र तिनलाई avoid गर्न API keys rotate गर्नुपर्छ कि सोध्छन्। सही प्रतिक्रिया के हो?
अब के आउँछ
अध्याय 2 सकियो। तपाईंले उत्पादन prompts लेख्न, structured JSON पाउन, र वास्तविक विफलता मोडहरू सम्हाल्न सक्नुहुन्छ। अध्याय 3 ले अर्को ठूलो architectural टुक्रा सम्बोधन गर्छ: memory। LLM हरूले कलहरू बीच बिर्सन्छन्। Multi-turn कुराकानीहरूलाई तपाईंले history स्पष्ट रूपमा manage गर्नुपर्ने चाहिन्छ। र वास्तविक chatbot निर्माण गर्नुको अर्थ बढ्दै गएको कुराकानीको token बजेटलाई सामना गर्नु हो। अध्याय 3 को अन्त्यसम्म तपाईंसँग ध्यानले memory management सहितको कार्यरत नेपाली chatbot हुनेछ।