অধ্যায় 8 · খরচ, লেটেন্সি ও ক্যাশিংয়ের মূল কথা
খরচ ও লেটেন্সির মূল কথা
- পৃষ্ঠা 15 / 22
- 24 মিনিট পড়া
বোগোতার মাতেও তার টিমের জন্য EvidenceDesk প্রোটোটাইপ চালু করেছে। এক সপ্তাহ পরে বিল পরিকল্পনার চেয়ে বেশি, আর উত্তরও ধীর মনে হচ্ছে। কিছু ভাঙেনি: বাস্তব ব্যবহারে টেস্টের চেয়ে বেশি রিকোয়েস্ট ও রিট্রাই আসে। এই পাতায় সেই প্রথম অভ্যাসগুলো শেখানো হবে যা অ্যাপ্লিকেশনকে সাশ্রয়ী ও দ্রুত রাখে: খরচ কোথা থেকে আসে জানা, ব্যবহার লিখে রাখা, সময় মাপা, আর যে কলের দাম একবার দিয়েছেন তা আবার না করা। নতুন কোড দুটো মডিউল: evidencedesk/usage.py ও evidencedesk/cache.py। সবকিছু অফলাইনে চলে, আগের পাতার ফেক ক্লায়েন্ট আর বানানো একটা দামের তালিকা দিয়ে, তাই সংখ্যাগুলো কৌশল দেখায়, আসল দাম বা আসল গতি নয়।
যা শিখবেন
- খরচের চারটা কারণ (cost driver), আর নির্ভুল হিসাবে খরচ অনুমান ও রেকর্ড করা।
- ইনজেক্ট করা ক্লক দিয়ে লেটেন্সি মাপা, এবং প্রথম চাঙ্ক ও পুরো রেসপন্সের সময়ের পার্থক্য।
- নির্ধারিত ক্যাশ কি (key) ও মেমোরি ক্যাশ বানানো, যা পুনরাবৃত্ত কল এড়িয়ে যায়।
- ক্যাশ করা উত্তর কখন পুরনো হয়ে যায়, আর ব্যক্তিগত উত্তর কেন ডিফল্টে ক্যাশ হয় না।
খরচের মূল কারণগুলো চিহ্নিত করুন
বিল চারটা জিনিসে বাড়ে: ইনপুট টোকেন (প্রতিটা কলে আপনি যা পাঠান), আউটপুট টোকেন (মডেল যা লেখে), কলের সংখ্যা, আর রিট্রাই (রিট্রাই করা কলের জন্য আবার দাম দিতে হয়)। এগুলো দেখতে RAG ইনজেশন ও সাইটেশন প্রবাহ পাতার RAG রিকোয়েস্টটা নিয়ে তার অংশগুলো estimate_tokens দিয়ে মাপুন (অক্ষরসংখ্যা চার দিয়ে ভাগ করে ওপরের দিকে গোল করা: মোটামুটি মাপ, আসল টোকেনাইজার নয়)।
import json
from pathlib import Path
from evidencedesk.chunking import chunk_corpus
from evidencedesk.context import estimate_tokens
from evidencedesk.rag import RAG_RULES, SAMPLE_REPLIES, Retriever, build_request
chunks = chunk_corpus(Path("kit/corpus"))
retriever = Retriever(chunks, clock=lambda: "2026-10-12T09:00:00Z")
Q = "How long do I have to return a bike?"
found = retriever.retrieve(Q)
request = build_request(Q, found)
print("part".ljust(26), "tokens")
print("system rules".ljust(26), estimate_tokens(RAG_RULES))
for r in found:
print(f"evidence {r.chunk.id}".ljust(26), estimate_tokens(r.chunk.text))
print("whole user message".ljust(26), estimate_tokens(request.messages[1].content), "(with tags)")
reply = estimate_tokens(json.dumps(SAMPLE_REPLIES[Q]))
print("reply (JSON)".ljust(26), reply, f"(the cap is max_tokens={request.max_tokens})")
print()
print("scenario".ljust(30), "calls input output billed")
for label, k, calls in (("k=3, one call", 3, 1), ("k=1, one call", 1, 1), ("k=3, two retries", 3, 3)):
size = sum(estimate_tokens(m.content) for m in build_request(Q, retriever.retrieve(Q, k=k)).messages)
print(f"{label:<30}{calls:>5}{size * calls:>7}{reply * calls:>8}{(size + reply) * calls:>8}")part tokens
system rules 116
evidence kc-returns-v3:c0 58
evidence kc-warranty-v2:c0 41
evidence kc-returns-v3:c1 31
whole user message 210 (with tags)
reply (JSON) 39 (the cap is max_tokens=200)
scenario calls input output billed
k=3, one call 1 326 39 365
k=1, one call 1 215 39 254
k=3, two retries 3 978 117 1095নিয়মগুলো (১১৬ টোকেন) প্রতিটা কলে আবার যায়; তিনটা চাঙ্ক মিলিয়ে মাত্র ১৩০; ইউজার মেসেজ ২১০, কারণ প্রতিটা চাঙ্ক লেবেলওয়ালা ট্যাগে মোড়ানো (কনটেক্সট সাজানো ও বিশ্বাসের সীমানা)। রিপ্লাই ৩৯ টোকেন, ২০০-র সীমার চেয়ে অনেক কম: max_tokens একটা ছাদ, চার্জ নয়। তিনটার বদলে একটা প্রমাণ-চাঙ্ক ইনপুট প্রায় এক-তৃতীয়াংশ কমায়, আর দুটো রিট্রাই সবকিছুর খরচ তিনগুণ করে; এ জন্যই অ্যাডাপ্টার, কনফিগারেশন ও এরর হ্যান্ডলিং পাতায় রিট্রাই সীমিত করা হয়েছিল।
কাল্পনিক দামের তালিকা থেকে খরচ অনুমান করুন
প্রোভাইডাররা প্রতি দশ লক্ষ টোকেনের দাম জানায়, ইনপুট, আউটপুট ও ক্যাশ করা ইনপুটের আলাদা হারে। দাম বদলায়, তাই আমাদের কোডে আসল দাম থাকে না: NOT_A_REAL_PRICE_TABLE-এ দুটো বানানো মডেল ও একটা বানানো একক, ক্রেডিট। হারগুলো প্রতি 1M টোকেনের Decimal মান, তাই যোগফল নির্ভুল (ফ্লোটে সামান্য ভুল জমে: 0.1 + 0.2 হয় 0.30000000000000004)।
# file: kit/evidencedesk/usage.py
"""Cost and latency: a hypothetical price table, a usage ledger, and clocks you can inject."""
from __future__ import annotations
import time
from collections.abc import Callable, Iterable, Iterator
from dataclasses import dataclass
from decimal import Decimal
from evidencedesk.context import estimate_tokens
from evidencedesk.llm import LLMClient, LLMRequest, LLMResponse
from evidencedesk.streaming import Assembled, StreamEvent, assemble
# --- 1. A hypothetical price table ---
PER = Decimal(1_000_000) # rates are "credits per 1M tokens"; a credit is a made-up unit
@dataclass(frozen=True)
class Rate:
input: Decimal
output: Decimal
cached_input: Decimal
NOT_A_REAL_PRICE_TABLE: dict[str, Rate] = { # invented numbers for tests; never a real provider price
"fake-model": Rate(Decimal("3"), Decimal("12"), Decimal("0.75")),
"fake-small": Rate(Decimal("0.6"), Decimal("2.4"), Decimal("0.15")),
}
class UnknownModel(KeyError):
"""No rate for this model: stop, do not count it as free."""প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): প্রধান তিন প্রোভাইডারই টেক্সটের দাম প্রতি 1M টোকেনে জানায়, ইনপুট ও আউটপুট আলাদা, মডেল অনুযায়ী। ক্যাশ করা ইনপুটের দাম সাধারণ ইনপুটের চেয়ে কম; একজন ক্যাশ-রাইটের আলাদা হার যোগ করে, আরেকজন স্টোরেজ চার্জ। রিজনিং ("থিংকিং") টোকেন আউটপুট হিসেবে বিল হয়। ব্যাচ একটা সস্তা অ্যাসিনক্রোনাস স্তর; কিছু মডেল খুব লম্বা প্রম্পটে বেশি নেয়; ওয়েব সার্চের মতো টুল প্রতি কলে বাড়তি ফি যোগ করতে পারে। আমরা কোনো অঙ্ক ছাপি না: এগুলো বদলায়। বর্তমান ডকুমেন্টেশন দেখে নিন।
উপলব্ধ থাকলে ব্যবহারের মেটাডেটা রেকর্ড করুন
বেশির ভাগ প্রোভাইডার প্রতি কলে টোকেনের সংখ্যা ফেরত দেয়; প্রোভাইডার-নিরপেক্ষ LLM ক্লায়েন্ট পাতায় আমরা তা Usage-এ সাধারণ রূপে এনেছিলাম। UsageRecord যোগ করে লেজারের প্রয়োজনীয় জিনিস: রিকোয়েস্ট আইডি, মডেল, ক্যাশ থেকে কত ইনপুট টোকেন এসেছে, একটা estimated ফ্ল্যাগ আর চেষ্টার নম্বর। দুটো নিয়ম: ইনপুট টোকেনের মধ্যে ক্যাশ করাগুলোও ধরা হয়, তাই cached_tokens কখনো তার চেয়ে বেশি হতে পারে না (প্রোভাইডাররা এটা আলাদাভাবে জানায়; সবাইকে একই রূপে আনাই আসল কাজ)। আর রেসপন্সে ব্যবহারের তথ্য না থাকলে আমরা অনুমান করি এবং রেকর্ডে estimated চিহ্ন দিই, শূন্য রাখি না। দামের অভাবও এরর: UnknownModel রান থামায়, কলকে বিনামূল্যে ধরে না।
# file: kit/evidencedesk/usage.py (append)
# --- 2. Usage records ---
def request_tokens(request: LLMRequest) -> int:
"""Rough input size: ceil(chars / 4) per message."""
return sum(estimate_tokens(m.content) for m in request.messages)
@dataclass(frozen=True)
class UsageRecord:
request_id: str
model: str
input_tokens: int # ALL input tokens, cached ones included
output_tokens: int
cached_tokens: int = 0 # the part of input_tokens a provider cache served
estimated: bool = False # True: no usage from the provider, we guessed
attempt: int = 1 # 1 = first try, 2 and more = a retry
def __post_init__(self) -> None:
if min(self.input_tokens, self.output_tokens, self.cached_tokens) < 0 or self.cached_tokens > self.input_tokens:
raise ValueError("token counts must be >= 0 and cached_tokens <= input_tokens")
if self.attempt < 1:
raise ValueError("attempt starts at 1")
def record_usage(request: LLMRequest, response: LLMResponse, request_id: str, *, cached_tokens: int = 0,
attempt: int = 1) -> UsageRecord:
"""Keep reported usage; if there is none (0 + 0 with text), estimate it and flag it."""
usage = response.usage
if usage.total_tokens == 0 and response.text:
out = min(estimate_tokens(response.text), request.max_tokens)
return UsageRecord(request_id, response.model, request_tokens(request), out, 0, True, attempt)
return UsageRecord(request_id, response.model, usage.input_tokens, usage.output_tokens, cached_tokens, False, attempt)
def cost_of(record: UsageRecord, table: dict[str, Rate] = NOT_A_REAL_PRICE_TABLE) -> Decimal:
"""Exact cost in made-up credits: fresh input + cached input + output."""
try:
rate = table[record.model]
except KeyError:
raise UnknownModel(record.model) from None
fresh = record.input_tokens - record.cached_tokens
return (fresh * rate.input + record.cached_tokens * rate.cached_input + record.output_tokens * rate.output) / PER
@dataclass(frozen=True)
class Totals:
calls: int
retries: int
input_tokens: int
output_tokens: int
cached_tokens: int
estimated: int
cost: Decimal
class Ledger:
"""One record per model call. A cache hit makes no call, so it adds none."""
def __init__(self, table: dict[str, Rate] = NOT_A_REAL_PRICE_TABLE) -> None:
self.table, self.records = table, []
def add(self, record: UsageRecord) -> Decimal:
cost = cost_of(record, self.table) # raises UnknownModel first
self.records.append(record)
return cost
def summary(self) -> Totals:
r = self.records
return Totals(len(r), sum(x.attempt > 1 for x in r), sum(x.input_tokens for x in r), sum(x.output_tokens for x in r),
sum(x.cached_tokens for x in r), sum(x.estimated for x in r), sum((cost_of(x, self.table) for x in r), Decimal(0)))প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): ব্যবহারের তথ্যের নাম আলাদা। একজন প্রোভাইডার
usageঅবজেক্টেinput_tokens_details.cached_tokensজানায়। দ্বিতীয়জন জানায়input_tokens,cache_read_input_tokensওcache_creation_input_tokens, এবংrequest-idহেডারে একটা রিকোয়েস্ট আইডি ফেরত দেয়। তৃতীয়জন এক API পথেusageMetadata(promptTokenCount,cachedContentTokenCount) জানায়, নতুন পথে অন্য নাম। স্ট্রিমের ভেতরে ব্যবহারের তথ্য কোথায় আসে, তা এখানে সব প্রোভাইডারের জন্য নিশ্চিত নয়। বর্তমান ডকুমেন্টেশন দেখে নিন।
FakeClient ফিক্সচারের রিপোর্ট করা ব্যবহার, হাতে লেখা RAG ফেকের জন্য অনুমান (সে কিছু জানায় না), ৩০টা ক্যাশ করা টোকেন, আর দুবার রিট্রাই করা একটা কল। ফেক হলো বদলি: আসল মডেল কম অনুমানযোগ্য, আর ঠিক সে জন্যই আপনি টেস্ট করেন।
from dataclasses import replace
from evidencedesk.llm import FakeClient, LLMRequest, Message
from evidencedesk.rag import SAMPLE_REPLIES, RecordedReplies
from evidencedesk.usage import NOT_A_REAL_PRICE_TABLE, Ledger, UnknownModel, UsageRecord, cost_of, record_usage
for name, rate in NOT_A_REAL_PRICE_TABLE.items():
print(name, "per 1M tokens: input", rate.input, "| output", rate.output, "| cached input", rate.cached_input)
ask = LLMRequest((Message("user", "How long do I have to return a new bike?"),))
reported = record_usage(ask, FakeClient().complete(ask), "req-001")
rag_request = build_request(Q, found)
silent = record_usage(rag_request, RecordedReplies(SAMPLE_REPLIES).complete(rag_request), "req-002")
cached = UsageRecord("req-003", "fake-model", input_tokens=reported.input_tokens, output_tokens=19, cached_tokens=30)
try:
cost_of(silent)
except UnknownModel as error:
print("no rate for", error)
silent = replace(silent, model="fake-model") # price it as the model it stands in for
for rec in (reported, silent, cached):
print(rec.request_id, rec.input_tokens, rec.output_tokens, rec.cached_tokens, "estimated" if rec.estimated else "reported", cost_of(rec))
ledger = Ledger()
for attempt in (1, 2, 3): # the same call, tried three times
ledger.add(UsageRecord("req-004", "fake-model", 400, 60, attempt=attempt))
print(ledger.summary())fake-model per 1M tokens: input 3 | output 12 | cached input 0.75
fake-small per 1M tokens: input 0.6 | output 2.4 | cached input 0.15
no rate for 'recorded'
req-001 41 19 0 reported 0.000351
req-002 326 39 0 estimated 0.001446
req-003 41 19 30 reported 0.0002835
Totals(calls=3, retries=2, input_tokens=1200, output_tokens=180, cached_tokens=0, estimated=0, cost=Decimal('0.00576'))ফেকের মডেলের নাম 'recorded'-এর কোনো হার নেই, তাই কোড স্পষ্টভাবে ব্যর্থ হয়; আমরা তাকে সেই মডেলের দামে ধরি যার বদলি সে। req-002 চিহ্নিত estimated: আকার আমরা অক্ষর থেকে আন্দাজ করেছি, আর লেজারে বলা থাকা উচিত কতগুলো সারি আন্দাজ। ৪১ ইনপুট টোকেনের ৩০টা ক্যাশ থেকে এলে req-003-এর খরচ 0.000351-এর বদলে 0.0002835। শেষ লাইনে রিট্রাইয়ের প্রভাব সংখ্যায় দেখা যায়: একটা রিকোয়েস্ট, তিনবার চেষ্টা, retries=2, খরচ তিনগুণ।
লোকালি লেটেন্সি মাপুন
লেটেন্সি হলো কলকারীকে কত সময় অপেক্ষা করতে হয়। Timer একটা বাইরে থেকে দেওয়া ক্লক পড়ে। ডিফল্ট time.perf_counter(), ছোট সময়ের জন্য মনোটোনিক টাইমার; টেস্ট দেয় ManualClock, যা শুধু বললে এগোয়, তাই টেস্টে নির্ভুল সংখ্যা ধরা যায়। SlowClient সেই ক্লককে এগিয়ে দেয় একটা বেস দেরি আর প্রতি আউটপুট টোকেনের দেরি দিয়ে: নেটওয়ার্কের বদলি, তার মাপ নয়।
# file: kit/evidencedesk/usage.py (append)
# --- 3. Latency with an injected clock ---
Clock = Callable[[], float]
class ManualClock:
"""Moves only when told. No wall clock."""
def __init__(self, start: float = 0.0) -> None:
self.now = start
def __call__(self) -> float:
return self.now
def advance(self, seconds: float) -> None:
self.now += seconds
class Timer:
"""`with Timer(clock) as t: ...`, then `t.elapsed`. Tests pass a ManualClock."""
def __init__(self, clock: Clock = time.perf_counter) -> None:
self.clock, self.start, self.elapsed = clock, 0.0, 0.0
def __enter__(self) -> Timer:
self.start = self.clock()
return self
def __exit__(self, *exc: object) -> None:
self.elapsed = self.clock() - self.start
class SlowClient:
"""Moves a ManualClock: a base delay plus a delay per output token. A stand-in for the network."""
def __init__(self, inner: LLMClient, clock: ManualClock, base_s: float = 0.4, per_output_token_s: float = 0.02) -> None:
self.inner, self.clock, self.base_s, self.per_token_s = inner, clock, base_s, per_output_token_s
def complete(self, request: LLMRequest) -> LLMResponse:
response = self.inner.complete(request)
self.clock.advance(self.base_s + self.per_token_s * response.usage.output_tokens)
return responsefrom evidencedesk.usage import ManualClock, SlowClient, Timer
clock = ManualClock()
slow = SlowClient(FakeClient(), clock) # 0.4 s + 0.02 s per output token
with Timer(clock) as t:
slow.complete(ask)
print("fake clock:", round(t.elapsed, 3), "s for 19 output tokens")
with Timer() as real: # the default clock is time.perf_counter()
FakeClient().complete(ask)
print("real clock used:", real.elapsed >= 0, "(value not printed)")fake clock: 0.78 s for 19 output tokens
real clock used: True (value not printed)ফেক ক্লক প্রতিবার ঠিক 0.4 + 0.02 × 19 = 0.78 সেকেন্ড দেয়। আসল ক্লকও চলে, কিন্তু তার মান প্রতি রানে বদলায়, তাই আমরা শুধু ছাপি যে সেটা চলেছে। তার ওপর কখনো অ্যাসার্ট করবেন না: ব্যস্ত মেশিনে টেস্ট অস্থির হয়ে যাবে।
প্রথম টোকেন ও পুরো রেসপন্সের লেটেন্সি আলাদা করুন
স্ট্রিম করা উত্তরের দুটো সময় আছে। প্রথম চাঙ্কের সময় হলো ব্যবহারকারী কিছু দেখার আগে কতক্ষণ; পুরো রেসপন্সের সময় হলো উত্তর সম্পূর্ণ হতে কতক্ষণ। প্রথমটা ছোট হলে চ্যাট দ্রুত লাগে, দ্বিতীয়টা লম্বা হলেও। স্ট্রিম ছাড়া কলের সময় একটাই। timed_stream একটা স্ট্রিম পড়ে (স্ট্রিমিং ও কাঠামোবদ্ধ ফলাফল) এবং দুটোই রেকর্ড করে; slow_stream ম্যানুয়াল ক্লকে একটা ফেক স্ট্রিমকে দেরি করায়।
# file: kit/evidencedesk/usage.py (append)
# --- 4. First-chunk and full-response latency ---
@dataclass(frozen=True)
class StreamTiming:
first_chunk_s: float | None # None: no chunk arrived
total_s: float
chunks: int
def slow_stream(events: Iterable[StreamEvent], clock: ManualClock, first_s: float = 0.4, per_chunk_s: float = 0.05) -> Iterator[StreamEvent]:
"""Delay a fake stream: first chunk after first_s, each later one per_chunk_s after the last."""
first = True
for event in events:
if event.kind == "chunk":
clock.advance(first_s if first else per_chunk_s)
first = False
yield event
def timed_stream(events: Iterable[StreamEvent], clock: Clock = time.perf_counter) -> tuple[Assembled, StreamTiming]:
"""Read a stream to its end; record time to the first chunk and to the last event."""
start, first, chunks, seen = clock(), None, 0, []
for event in events:
if event.kind == "chunk":
chunks += 1
if first is None:
first = clock() - start
seen.append(event)
return assemble(seen), StreamTiming(first, clock() - start, chunks)from evidencedesk.streaming import FakeStreamClient
from evidencedesk.usage import slow_stream, timed_stream
clock = ManualClock()
events = slow_stream(FakeStreamClient(chunk_size=20).stream(ask), clock, first_s=0.4, per_chunk_s=0.1)
done, timing = timed_stream(events, clock)
print(done.status, "|", done.text)
print(f"chunks={timing.chunks} first chunk after {timing.first_chunk_s:.2f} s whole answer after {timing.total_s:.2f} s")complete | You can return a new, unridden bike within 30 days of delivery for a full refund.
chunks=5 first chunk after 0.40 s whole answer after 0.80 sব্যবহারকারী প্রথম কথাগুলোর জন্য 0.40 সেকেন্ড আর পুরো উত্তরের জন্য 0.80 সেকেন্ড অপেক্ষা করে। শুধু মোট সময় লগ করলে ধীর শুরু আর ধীর শেষের তফাত বোঝা যায় না, অথচ দুটোর প্রতিকার আলাদা (ছোট প্রম্পট, ছোট উত্তর)।
নির্ধারিত ক্যাশ কি তৈরি করুন
সবচেয়ে সস্তা কল হলো যেটা আপনি করেন না। ক্যাশ একটা কি (key)-র নিচে উত্তর রাখে, আর সেই কি-তে থাকতে হবে উত্তর বদলাতে পারে এমন সবকিছু, এবং বিনা কারণে বদলায় এমন কিছুই নয়। আমাদেরটা canonical JSON-এর SHA-256 হ্যাশ (প্রথম ১৬টা হেক্স অঙ্ক): মডেল ও max_tokens, ফাঁকা জায়গা গুটিয়ে নেওয়া মেসেজ (ছোট-বড় হাতের অক্ষর আলাদা থাকে: RD2-24 আর rd2-24 এক নয়), একটা prompt_version, chunk-id@version রূপে প্রমাণ, একটা corpus_version ও একটা scope। মেসেজে নিয়ম ও প্রমাণের লেখা আগেই আছে; লেবেলগুলো সেখানে অদৃশ্য পরিবর্তনও ধরে (নতুন পোস্ট-প্রসেসিং কোড) এবং কী মুছবেন তার নাম দেয়।
কি-তে নেই: রিকোয়েস্ট আইডি, টাইমস্ট্যাম্প বা র্যান্ডম কিছু; নইলে প্রতিটা কি নতুন হয়ে ক্যাশ কখনো হিট করবে না। কি-তে আছে যখনই উত্তর কে জিজ্ঞেস করছে তার ওপর নির্ভর করে: ব্যবহারকারী বা টেন্যান্ট আইডি, scope হিসেবে।
# file: kit/evidencedesk/cache.py
"""Exact-match response cache: a deterministic key, a policy per request class, a TTL on an injected clock."""
from __future__ import annotations
import hashlib
import json
import re
import time
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from evidencedesk.llm import LLMClient, LLMRequest, LLMResponse
from evidencedesk.rag import Retrieved
# --- 1. The key ---
def normalise(text: str) -> str:
"""Collapse whitespace; keep case (RD2-24 is not rd2-24)."""
return re.sub(r"\s+", " ", text).strip()
def evidence_refs(retrieved: Iterable[Retrieved]) -> tuple[str, ...]:
"""The evidence set as chunk-id@version."""
return tuple(f"{r.chunk.id}@{r.chunk.version}" for r in retrieved)
def cache_key(request: LLMRequest, *, prompt_version: str, evidence: Iterable[str] = (), corpus_version: str = "",
scope: str = "") -> str:
"""Hash of everything that can change the answer. Nothing random in it: no ids, no times."""
material = {
"model": request.model, "max_tokens": request.max_tokens,
"messages": [[m.role, normalise(m.content)] for m in request.messages],
"prompt_version": prompt_version, "evidence": sorted(evidence), "corpus_version": corpus_version, "scope": scope,
}
text = json.dumps(material, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]from evidencedesk.cache import cache_key, evidence_refs
base = dict(prompt_version="v1", evidence=evidence_refs(found), corpus_version="2026-10-12")
rag_a = build_request(Q, found)
variants = {
"same request again": (rag_a, {}),
"extra spaces in the question": (build_request(" How long do I have to return a bike? ", found), {}),
"different model": (LLMRequest(rag_a.messages, model="fake-small"), {}),
"different max_tokens": (LLMRequest(rag_a.messages, max_tokens=50), {}),
"different letter case": (build_request(Q.lower(), found), {}),
"prompt_version v2": (rag_a, {"prompt_version": "v2"}),
"one evidence chunk fewer": (rag_a, {"evidence": evidence_refs(found[:-1])}),
"corpus_version changed": (rag_a, {"corpus_version": "2026-10-13"}),
"scope user:nadia": (rag_a, {"scope": "user:nadia"}),
}
key0 = cache_key(rag_a, **base)
print("base key:", key0[:8], "...")
for label, (req, change) in variants.items():
print(f"{label:<30}", "same key" if cache_key(req, **{**base, **change}) == key0 else "NEW key")base key: 81f3f88d ...
same request again same key
extra spaces in the question same key
different model NEW key
different max_tokens NEW key
different letter case NEW key
prompt_version v2 NEW key
one evidence chunk fewer NEW key
corpus_version changed NEW key
scope user:nadia NEW keyবাড়তি স্পেস কিছু বদলায় না; অন্য প্রতিটা পরিবর্তন বদলায়, অক্ষরের ছোট-বড় রূপ ও max_tokens সহ (ছোট সীমা উত্তর কেটে দিতে পারে)। ক্যাশ কি ততটাই ভালো যতটা তার ইনপুটের তালিকা: যে সেটিং উত্তর বদলায় অথচ কি-তে নেই, তা দ্রুত ভুল উত্তর দেয়। ক্যাশ প্রথম যে উত্তর দেখেছে সেটাকেই আটকে রাখে, যা স্যাম্পলিং র্যান্ডম হলে গুরুত্বপূর্ণ (temperature 0-ও হুবহু একই আউটপুটের নিশ্চয়তা দেয় না: আশা নয়, ফিক্সচার দিয়ে টেস্ট করুন)।
মেমোরিতে রেসপন্স ক্যাশ তৈরি করুন
ResponseCache হলো একটা ডিকশনারি, যার আছে ইনজেক্ট করা ক্লকে সময়সীমা (TTL), আকারের সীমা, হিট ও মিসের কাউন্টার, আর প্রতিটা এন্ট্রিতে ট্যাগ (নিচে ইনভ্যালিডেশনের জন্য)। CachedClient যেকোনো ক্লায়েন্টকে মুড়ে নেয়: কি তৈরি করে, হিট হলে ক্যাশ থেকে উত্তর দেয়, মিস হলেই শুধু মডেল ডাকে। কেটে যাওয়া উত্তর (stop_reason end নয়) কখনো জমা হয় না। পলিসি গোপনীয়তার অংশে ব্যাখ্যা করা হয়েছে; এখানে FAQ_ONLY শুধু faq ক্লাসকে অনুমতি দেয়।
# file: kit/evidencedesk/cache.py (append)
# --- 2. The store ---
@dataclass
class _Entry:
response: LLMResponse
stored_at: float
tags: frozenset[str]
class ResponseCache:
"""In memory. Entries expire after ttl_s on the injected clock; the oldest goes first when full."""
def __init__(self, ttl_s: float = 300.0, clock: Callable[[], float] = time.monotonic, max_entries: int = 256) -> None:
self.ttl_s, self.clock, self.max_entries = ttl_s, clock, max_entries
self.entries: dict[str, _Entry] = {}
self.hits = self.misses = self.expired = 0
def __len__(self) -> int:
return len(self.entries)
def get(self, key: str) -> LLMResponse | None:
entry = self.entries.get(key)
if entry is not None and self.clock() - entry.stored_at >= self.ttl_s:
del self.entries[key]
self.expired += 1
entry = None
if entry is None:
self.misses += 1
return None
self.hits += 1
return entry.response
def put(self, key: str, response: LLMResponse, tags: Iterable[str] = ()) -> None:
if key not in self.entries and len(self.entries) >= self.max_entries:
del self.entries[next(iter(self.entries))]
self.entries[key] = _Entry(response, self.clock(), frozenset(tags))
def invalidate(self, tag: str) -> int:
"""Remove entries with this tag; return how many."""
doomed = [k for k, e in self.entries.items() if tag in e.tags]
for key in doomed:
del self.entries[key]
return len(doomed)
def clear(self) -> None:
self.entries.clear()# file: kit/evidencedesk/cache.py (append)
# --- 3. The policy and the client ---
@dataclass(frozen=True)
class CachePolicy:
"""Which request classes may be cached. The default: none."""
shared: frozenset[str] = frozenset() # any user may get the cached answer
per_user: frozenset[str] = frozenset() # cached inside one user's scope only
NO_CACHE = CachePolicy()
FAQ_ONLY = CachePolicy(shared=frozenset({"faq"}))
@dataclass(frozen=True)
class Cached:
response: LLMResponse
status: str # hit | miss | bypass
key: str | None = None # None for bypass
class CachedClient:
"""`ask` says what happened (hit, miss, bypass); `complete` keeps the LLMClient shape."""
def __init__(self, inner: LLMClient, cache: ResponseCache, policy: CachePolicy = NO_CACHE, prompt_version: str = "v1") -> None:
self.inner, self.cache, self.policy, self.prompt_version = inner, cache, policy, prompt_version
self.bypassed = 0
def ask(self, request: LLMRequest, *, request_class: str = "unclassified", user_id: str | None = None,
evidence: Iterable[str] = (), corpus_version: str = "") -> Cached:
if request_class in self.policy.shared:
scope = ""
elif request_class in self.policy.per_user and user_id:
scope = f"user:{user_id}"
else: # not allowed, or no user to isolate by: fail closed
self.bypassed += 1
return Cached(self.inner.complete(request), "bypass")
refs = tuple(evidence)
key = cache_key(request, prompt_version=self.prompt_version, evidence=refs, corpus_version=corpus_version, scope=scope)
found = self.cache.get(key)
if found is not None:
return Cached(found, "hit", key)
response = self.inner.complete(request)
if response.stop_reason == "end": # a cut-off answer is never cached
tags = {f"prompt:{self.prompt_version}", f"model:{request.model}", f"corpus:{corpus_version}"}
tags |= {"doc:" + ref.split(":")[0] for ref in refs}
self.cache.put(key, response, tags)
return Cached(response, "miss", key)
def complete(self, request: LLMRequest, **kwargs: object) -> LLMResponse:
return self.ask(request, **kwargs).response # type: ignore[arg-type]একই পাঁচটা প্রশ্ন, ক্যাশ ছাড়া আর FAQ ক্লাস ক্যাশ করে। হিট মডেল ডাকে না, তাই লেজারে কোনো রেকর্ড যোগ করে না।
from evidencedesk.cache import FAQ_ONLY, NO_CACHE, CachedClient, ResponseCache
from evidencedesk.usage import Ledger
def run(policy, n=5):
clock, inner = ManualClock(), FakeClient()
client, ledger = CachedClient(inner, ResponseCache(300, clock), policy), Ledger()
for i in range(n):
got = client.ask(ask, request_class="faq")
if got.status != "hit": # a hit makes no model call, so there is nothing to record
ledger.add(record_usage(ask, got.response, f"req-{i}"))
return client, inner, ledger.summary()
for label, policy in (("no cache", NO_CACHE), ("faq cached", FAQ_ONLY)):
client, inner, total = run(policy)
print(f"{label:<11} model calls={len(inner.calls)} tokens={total.input_tokens + total.output_tokens} cost={total.cost}"
f" hits={client.cache.hits} misses={client.cache.misses} bypassed={client.bypassed}")no cache model calls=5 tokens=300 cost=0.001755 hits=0 misses=0 bypassed=5
faq cached model calls=1 tokens=60 cost=0.000351 hits=4 misses=1 bypassed=0পাঁচবারের বদলে একবার মডেল কল: ৩০০-র বদলে ৬০ টোকেন, 0.001755-এর বদলে 0.000351 ক্রেডিট, ৪টা হিট ও ১টা মিস। এটা কৌশল দেখায়; একটা ফেকে পাঁচটা কল কোনো বেঞ্চমার্ক নয়। আসল সাশ্রয় নির্ভর করে ব্যবহারকারীরা কত বার একই প্রশ্ন করে তার ওপর, যা গুনেই জানা যায় (পরের পাতা তা করে)।
ক্যাশ ইনভ্যালিডেশনের নিয়ম ঠিক করুন
ক্যাশ করা উত্তর ততক্ষণই বৈধ যতক্ষণ তার উৎস অপরিবর্তিত। আমাদের নিয়ম: প্রম্পট ভার্সন, মডেল বা তার সেটিং, প্রমাণের সেট (চাঙ্ক আইডি বা ভার্সন) অথবা কর্পাস ভার্সন বদলালে, আর TTL ফুরোলে ইনভ্যালিডেট করুন। সবই কি-তে আছে, তাই পরিবর্তনে পুরনো এন্ট্রি নাগালের বাইরে চলে যায়: নতুন রিকোয়েস্ট শুধু মিস করে। পুরনো এন্ট্রি তবু মেমোরি নেয়, তাই ট্যাগ (doc:, prompt:, model:, corpus:) নাম ধরে মুছতে দেয়, যা "এই ডকুমেন্ট এখনই মুছুন" অনুরোধও মেটায়। প্রমাণে RAG ইনজেশন ও সাইটেশন প্রবাহ এবং RAG-এর নির্ভরযোগ্যতা ও মূল্যায়নের ডেটা পাতার চাঙ্ক ভার্সন থাকে; কর্পাসে এমন দুটো রিটার্ন পাতা আছে যারা একমত নয়, আর পুরনো উত্তর ভুল দিনসংখ্যা উদ্ধৃত করবে।
from dataclasses import replace
clock, inner = ManualClock(), FakeClient()
client = CachedClient(inner, ResponseCache(3600, clock), FAQ_ONLY, prompt_version="v1")
refs = evidence_refs(found)
print("chunk versions:", [ref.split("@")[1] for ref in refs], "->", [client.ask(ask, request_class="faq", evidence=refs).status for _ in range(2)])
newer = [replace(r.chunk, version=r.chunk.version + 1) for r in found] # the returns page was updated
refs_v2 = tuple(f"{c.id}@{c.version}" for c in newer)
print("evidence changed ->", client.ask(ask, request_class="faq", evidence=refs_v2).status)
client.prompt_version = "v2"
print("prompt version changed ->", client.ask(ask, request_class="faq", evidence=refs).status)
print("entries before purge:", len(client.cache), "| removed by tag doc:kc-returns-v3:", client.cache.invalidate("doc:kc-returns-v3"),
"| left:", len(client.cache))
client.prompt_version = "v1"
print("old request after purge ->", client.ask(ask, request_class="faq", evidence=refs).status)chunk versions: ['3', '2', '3'] -> ['miss', 'hit']
evidence changed -> miss
prompt version changed -> miss
entries before purge: 3 | removed by tag doc:kc-returns-v3: 3 | left: 0
old request after purge -> missএকই প্রশ্ন দ্বিতীয়বার হিট করে; নতুন চাঙ্ক ভার্সন (লেখা এক হলেও) ও নতুন প্রম্পট ভার্সন প্রতিটা মিস করে। এখন তিনটা এন্ট্রি আছে। ট্যাগ ধরে একবার মুছলে তিনটাই যায়, তাই মূল রিকোয়েস্ট পরে মিস করে।
সংবেদনশীল রেসপন্স ডিফল্টে ক্যাশ করা এড়িয়ে চলুন
ক্যাশ হলো আপনার ব্যবহারকারীদের ডেটার দ্বিতীয় কপি। তাই ডিফল্ট হলো কিছুই ক্যাশ না করা (NO_CACHE); কোনো রিকোয়েস্ট ক্লাস তখনই ক্যাশ হয় যখন পলিসি তার নাম দেয়। shared ক্লাস (পাবলিক FAQ উত্তর) যে কাউকে দেওয়া যায়। per_user ক্লাস শুধু একজন ব্যবহারকারীর স্কোপের ভেতরে ক্যাশ হয়, আর ইউজার আইডি না থাকলে ক্যাশ হয় না (ফেইল-ক্লোজড, fail closed)। বাকি সবকিছু, যেমন অ্যাকাউন্টের ডেটা, ক্যাশ এড়িয়ে যায়। TTL কপিগুলোকে স্বল্পায়ু রাখে।
from evidencedesk.cache import CachePolicy
policy = CachePolicy(shared=frozenset({"faq"}), per_user=frozenset({"orders"}))
clock, inner = ManualClock(), FakeClient()
client = CachedClient(inner, ResponseCache(60, clock), policy)
steps = [
("faq, nadia", dict(request_class="faq", user_id="nadia")),
("faq, omar (shared)", dict(request_class="faq", user_id="omar")),
("orders, nadia", dict(request_class="orders", user_id="nadia")),
("orders, nadia again", dict(request_class="orders", user_id="nadia")),
("orders, omar", dict(request_class="orders", user_id="omar")),
("orders, no user", dict(request_class="orders")),
("account (not allowed)", dict(request_class="account", user_id="nadia")),
]
for label, kw in steps:
print(f"{label:<24}{client.ask(ask, **kw).status}")
clock.advance(61)
print(f"{'faq after 61 s (ttl 60)':<24}{client.ask(ask, request_class='faq').status}")
print("model calls:", len(inner.calls), "| entries:", len(client.cache))faq, nadia miss
faq, omar (shared) hit
orders, nadia miss
orders, nadia again hit
orders, omar miss
orders, no user bypass
account (not allowed) bypass
faq after 61 s (ttl 60) miss
model calls: 6 | entries: 3ওমর নাদিয়ার ক্যাশ করা FAQ উত্তর পায় (পাবলিক লেখা), কিন্তু তার অর্ডারের উত্তর কখনো নয়: ওমরের অর্ডার রিকোয়েস্ট নিজের আলাদা মিস। ইউজারহীন রিকোয়েস্ট আর তালিকায় না থাকা ক্লাস ক্যাশ এড়িয়ে যায়। ফেক ক্লকে ৬১ সেকেন্ড পরে ৬০ সেকেন্ডের এন্ট্রি মেয়াদোত্তীর্ণ, তাই পরের রিকোয়েস্ট মিস করে।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): প্রোভাইডাররা নিজেদের দিকেও ক্যাশ করে, আমাদেরটা থেকে আলাদা। প্রম্পট ক্যাশিংয়ে প্রম্পটের শুরুর অংশ হুবহু মিলতে হয়; হিট ক্যাশড টোকেন হিসেবে জানানো হয়, কম হারে বিল হয়; একজন বলে তাদের ক্যাশ প্রতিষ্ঠানগুলোর মধ্যে আলাদা থাকে। সংরক্ষণকাল ভিন্ন: একজন ইনপুট ও আউটপুট ৩০ দিনের মধ্যে মুছে ফেলে (ব্যতিক্রমসহ); আরেকজন অপব্যবহার-পর্যবেক্ষণের লগ ৩০ দিন পর্যন্ত রাখে এবং ২৪ ঘণ্টার পর ক্যাশ ডেটা রাখে না। একজন প্রোভাইডারের বিনামূল্যের স্তরে কনটেন্ট পণ্য উন্নয়নে ব্যবহার হতে পারে এবং মানুষ পর্যালোচক পড়তে পারেন, তাই সংবেদনশীল বা ব্যক্তিগত ডেটা পাঠাবেন না; তার সংরক্ষণকাল যাচাই করা পাতাগুলোতে বলা নেই। বর্তমান ডকুমেন্টেশন ও নিজের চুক্তি দেখে নিন।
অনুশীলনী: ক্যাশড ও আনক্যাশড কল তুলনা করুন
পাঁচ প্রশ্নের রানে কল কমার হিসাব দেখা গেছে; এবার টেস্টে তা পাকা করুন (kit/tests/test_usage.py ও kit/tests/test_cache.py-এ ২৪টা)। একটা প্যারামিটারাইজড টেস্ট একবারে একটা ইনপুট বদলায় (মডেল, max_tokens, অক্ষরের ছোট-বড় রূপ, প্রম্পট ভার্সন, প্রমাণ, কর্পাস ভার্সন, স্কোপ) এবং প্রতিবার ভিন্ন কি দাবি করে; আরেকটা দাবি করে, তিনটা একই রকম কল থেকে একটাই মডেল কল হয়। sed ঘড়ির সেকেন্ড সরিয়ে দেয়।
python -m pytest -q kit/tests/test_usage.py kit/tests/test_cache.py | sed 's/ in [0-9.]*s//'........................ [100%]
24 passedটেস্ট পাস করলে বোঝা যায়, ওই সাতটা ইনপুটে কি আলাদা হয়; তালিকা যে সম্পূর্ণ, তা বোঝা যায় না। সৎ সীমা: এই ক্যাশ হুবহু একই রিকোয়েস্ট মেলায়, তাই অন্যভাবে লেখা প্রশ্ন মানেই নতুন কি; এমবেডিং দিয়ে কাছাকাছি প্রশ্ন মেলানো (সিমান্টিক ক্যাশিং) আলাদা ও বেশি ঝুঁকিপূর্ণ নকশা, যা ভুল প্রশ্নের উত্তর দিতে পারে, আর আমরা তা বানাই না।
কাজের জায়গায় এটা কোথায় দেখা যায়
টিমগুলো আগে ইউসেজ লেজার যোগ করে: ব্রেকডাউন ছাড়া বিল ("কোন ফিচার, কোন মডেল, কতবার রিট্রাই?") দিয়ে কিছু করা যায় না। প্রোডাক্ট টিম প্রথম চাঙ্কের সময় দেখে, কারণ ব্যবহারকারী সেটাই অনুভব করে। FAQ বট ক্যাশের চিরাচরিত প্রার্থী; কি-তে ব্যবহারকারী না থাকলে অ্যাকাউন্ট অ্যাসিস্ট্যান্ট হলো চিরাচরিত ফাঁস। রিভিউয়াররা প্রতিটা ক্যাশ নিয়ে জিজ্ঞেস করেন: কী জমা হয়, কত দিন, কে পড়তে পারে?
সাধারণ ভুল
- ব্যবহারকারী বা টেন্যান্ট আইডি ছাড়া ক্যাশ কি। একজন অন্যজনের উত্তর পায়। সমাধান: কি-তে স্কোপ দিন, নয়তো সেই ক্লাস ক্যাশ করবেন না।
- শুধু প্রশ্নের লেখা দিয়ে ক্যাশ। নতুন প্রম্পট, মডেল বা ডকুমেন্ট ভার্সনেও পুরনো উত্তর আসতে থাকে। সমাধান: সেগুলো কি-তে রাখুন।
- ব্যবহারের তথ্য না থাকলে শূন্য ধরা। লেজার নীরবে কম দেখায়। সমাধান: অনুমান করে
estimatedচিহ্ন দিন। - আসল সময়ের ওপর অ্যাসার্ট। টেস্ট অস্থির হয়। সমাধান: ক্লক ইনজেক্ট করুন।
- দামের হিসাবে ফ্লোট, বা কোডে আসল দাম। হিসাব সরে যায়, সংখ্যা পুরনো হয়। সমাধান:
Decimalআর বানানো তালিকা।
নিজে চেষ্টা করুন
- সহজ। প্রোভাইডার-নিরপেক্ষ LLM ক্লায়েন্ট পাতার তিনটা প্রশ্ন
fake-smallমডেলেFakeClientদিয়ে পাঠান, প্রতিটার একটা রেকর্ডLedger-এ যোগ করুন এবং সারসংক্ষেপ ছাপুন। - মাঝারি। স্ট্রিমিং ও কাঠামোবদ্ধ ফলাফল পাতার মাঝপথে থেমে যাওয়া স্ট্রিমটা নিন (
stop_after=2)। তার স্ট্যাটাস, ব্যবহার ও সময় ছাপুন, তারপর তার জন্য একটা আনুমানিকUsageRecordলিখে দাম ধরুন। - কঠিন। RAG-এর নির্ভরযোগ্যতা ও মূল্যায়নের ডেটা পাতার একটা RAG উত্তর ক্যাশ করুন: একই প্রশ্ন দুবার করুন, তারপর রিটার্ন চাঙ্কগুলো নতুন ভার্সনে রেখে রিট্রিভার আবার বানিয়ে আবার করুন। মডেল কল গুনুন।
উত্তর
সহজ।
from evidencedesk.usage import Ledger, record_usage
small, ledger = FakeClient(), Ledger()
for n, question in enumerate(small.fixtures, 1):
req = LLMRequest((Message("user", question),), model="fake-small")
resp = small.complete(req)
ledger.add(record_usage(req, resp, f"e-{n}"))
print(question[:32].ljust(34), resp.stop_reason.ljust(7), resp.usage.total_tokens)
print(ledger.summary())How long do I have to return a n end 60
How long does standard delivery end 58
How should I store the e-bike ba length 65
Totals(calls=3, retries=0, input_tokens=124, output_tokens=59, cached_tokens=0, estimated=0, cost=Decimal('0.0002160'))মাঝারি। মাঝপথে থামা স্ট্রিম done ইভেন্ট পাঠায় না, তাই ব্যবহারের তথ্য নেই: অনুমান করে চিহ্ন দিন।
from evidencedesk.context import estimate_tokens
from evidencedesk.streaming import FakeStreamClient
from evidencedesk.usage import request_tokens, slow_stream, timed_stream
clock = ManualClock()
events = slow_stream(FakeStreamClient(chunk_size=20, stop_after=2).stream(ask), clock)
done, timing = timed_stream(events, clock)
print(done.status, "| usage:", done.usage, "|", timing)
rec = UsageRecord("e-stream", "fake-model", request_tokens(ask), estimate_tokens(done.text), estimated=True)
print("estimated:", rec.estimated, "| cost:", cost_of(rec))interrupted | usage: None | StreamTiming(first_chunk_s=0.4, total_s=0.45, chunks=2)
estimated: True | cost: 0.00015কঠিন। ক্যাশ কি-তে প্রমাণের ভার্সন আছে, তাই লেখা হুবহু এক হলেও ভার্সন বাড়লে মিস হয়।
from dataclasses import replace
from evidencedesk.cache import FAQ_ONLY, CachedClient, ResponseCache, evidence_refs
from evidencedesk.rag import SAMPLE_REPLIES, RecordedReplies, Retriever, build_request
def cached_rag(client, retriever, question):
found = retriever.retrieve(question)
return client.ask(build_request(question, found), request_class="faq", evidence=evidence_refs(found))
replies = RecordedReplies(SAMPLE_REPLIES)
client = CachedClient(replies, ResponseCache(3600, ManualClock()), FAQ_ONLY)
print([cached_rag(client, retriever, Q).status for _ in range(2)], "model calls:", len(replies.calls))
bumped = [replace(c, version=c.version + 1) if c.doc_id == "kc-returns-v3" else c for c in chunks]
print(cached_rag(client, Retriever(bumped, clock=lambda: "2026-10-12T09:00:00Z"), Q).status, "model calls:", len(replies.calls))['miss', 'hit'] model calls: 1
miss model calls: 2পাস চেকলিস্ট:
- সহজ: তিনটা কল, রিট্রাই নেই, কিছুই আনুমানিক নয়; তৃতীয়টা
lengthদিয়ে শেষ হয় এবং তবু বিল হয়,fake-smallহারে। - মাঝারি: স্ট্যাটাস
interrupted, ব্যবহারNone, প্রথম চাঙ্ক 0.4 সেকেন্ড; রেকর্ডটাestimated: আন্দাজ, বিল নয়। - কঠিন: একটা মডেল কলসহ
['miss', 'hit'], তারপর ভার্সন বাড়ানোর পর একটাmissও দুটো কল।
সারসংক্ষেপ
- খরচ আসে ইনপুট টোকেন, আউটপুট টোকেন, কল ও রিট্রাই থেকে; অপ্টিমাইজ করার আগে প্রতিটা মাপুন।
- বানানো তালিকা ও
Decimalদিয়ে দাম ধরুন; প্রতি কলে ব্যবহার রেকর্ড করুন এবং অনুমানে চিহ্ন দিন। - ক্লক ইনজেক্ট করে লেটেন্সি মাপুন; প্রথম চাঙ্ক ও পুরো রেসপন্সের সময় আলাদা রাখুন; আসল সময়ে অ্যাসার্ট করবেন না।
- ক্যাশ কি-তে থাকে উত্তর বদলাতে পারে এমন সবকিছু (মডেল সেটিং, প্রম্পট, প্রমাণের ভার্সন, কর্পাস, ইউজার স্কোপ), আর র্যান্ডম কিছু নয়।
- ডিফল্টে কিছু ক্যাশ করবেন না, ইউজার ধরে আলাদা রাখুন, TTL-এ মেয়াদ শেষ করুন, ট্যাগ ধরে মুছুন; হুবহু-মিল ক্যাশই নিরাপদ শুরু।
এরপর: বাজেট, ক্যাশিং ও ব্যবহারের সীমা পাতায় এই লেজার ও ক্যাশ দিয়ে ইনপুট ও আউটপুট বাজেট বাঁধা, রিট্রাই সীমিত করা, দৈনিক ব্যবহারের সীমা কার্যকর করা, ক্যাশ হিট রেট ট্র্যাক করা এবং সৎ একটা খরচ ও লেটেন্সির সারসংক্ষেপ লেখা হবে।