Part 05 · Building LLM Applications

Prompt Engineering

System promptsFew-shotInstructionsPrompt versioning

Prompt হলো এমন code যেটা model চালায়। পরিষ্কার instruction, উদাহরণ, system prompt, আর অন্য code এর মতোই prompt কে version করা আর test করা।

⏱ 1-2 ঘণ্টা · Station 34 / 92

জিনিসটা কী, কেন দরকার

একজন নতুন সহকর্মী বুদ্ধিমান, কিন্তু তোমার company সম্পর্কে কিছুই জানে না। "এই complaint গুলো সাজাও" বললে একটা ভদ্র রচনা পাবে। "প্রতিটা complaint এই চারটা বাক্সের একটায় রাখো, কোন বাক্সে কী যায় এই যে, শুধু বাক্সের নামটা বলো" বললে এমন কিছু পাবে যেটা কাজে লাগানো যায়। একই সহকর্মী, আলাদা নির্দেশ।

Prompt হলো model কে পাঠানো নির্দেশ আর context, আর একটা LLM application এ তোমার হাতে থাকা মূল logic এটাই। তাই একে code এর মতো দেখো: নিখুঁতভাবে লেখো, version রাখো, আর প্রতিটা change একই example এর উপর test করো। এটা না থাকলে prompt এর কাজ আন্দাজে পরিণত হয়: একটা change একটা case ঠিক করে চুপচাপ আরো তিনটা ভাঙে, আর user রা complain না করা পর্যন্ত কেউ টের পায় না।

ভেতরে কীভাবে চলে

Message আর role। Chat model message এর একটা list নেয়:

  • system: স্থায়ী নির্দেশ: role, নিয়ম, output এর format। User রা এটা দেখে না; পুরো কথোপকথনে খাটে।
  • user: মানুষটার input।
  • assistant: model এর আগের reply (আর few-shot prompt এ তোমার লেখা উদাহরণ reply)।

Chat template এই list কে special marker সহ একটা token sequence বানায় (Part 4 এর lesson 5)। Model শুধু সেটা টেনে নেয়।

নির্দেশ কাজ করে কীসে:

  • কাজ আর কার জন্য সেটা বলো। "You route customer messages for a food delivery app", "What is this about?" এর চেয়ে ভালো।
  • Output এর format বেঁধে দাও। "এই list থেকে ঠিক একটা শব্দ দিয়ে reply দাও" free text কে এমন কিছুতে বদলায় যেটা তোমার code parse করতে পারে।
  • Category গুলোর সংজ্ঞা দাও। Model এর কাছে "account" মানে যা, তোমার কাছে হয়তো তা না।
  • উদাহরণ দেখাও (few-shot)। এক দুইটা input → output এর জোড়া প্রায়ই আরো নিয়মের চেয়ে দ্রুত format আর সীমানা শেখায়।
  • পরিবর্তনশীল data পরিষ্কার ঘরে রাখো ({msg} placeholder), user এর text কখনো নির্দেশের সাথে জুড়ে দিয়ে না।

Prompt হলো code। নাম সহ version control এ রাখো (v2-instructions), একটা template function দিয়ে render করো, আর label দেওয়া input এর একটা নির্দিষ্ট test set এ প্রতিটা version এর score নাও। এই test set ই evaluation এর শুরু, Part 8।

Prompt এর সীমা আছে। Prompting বদলায় model যা আগে থেকেই পারে সেটা কীভাবে ব্যবহার করবে। Model এর যে ক্ষমতা নেই সেটা যোগ করতে পারে না। Prompt এর version বদলেও score আর না বাড়লে পরের উপায় হলো বড় model, retrieval (এই part এর lesson 6) বা fine-tuning (Part 9)।

আগে অনুমান করো

4/8 পাওয়া একটা prompt এ দুইটা উদাহরণ যোগ করলে, আর নতুন version একই test set এ 3/8 পেল। কী করবে?

ছবিতে দেখো

8 labelled messagessame test set every timev1: vague question0/8v1v2: role + label list4/8v2v3: + 2 examples3/8v3v4: examples per label4/8v4v3 scored lower: without the test set, it would have shipped
একই prompt এর চারটা version, একই label দেওয়া 8 টা message এর উপর চলে। অস্পষ্ট প্রশ্ন পায় 0, role আর label এর list যোগ করলে 4, দুইটা উদাহরণ যোগ করলে নেমে 3, আর প্রতিটা label এর উদাহরণ দিলে আবার 4। Test set না থাকলে v3 এর regression টা ship হয়ে যেত।

আসল model এ চারটা prompt

একটা আসল ছোট open model (Qwen2.5-0.5B-Instruct, CPU তে local এ চলছে, greedy decoding যাতে output প্রতিবার একই আসে) 8 টা customer message কে চারটা category তে route করছে। চারটা prompt version, একই message, একই check। ছোট একটা llm.py model টাকে মুড়ে রাখে; messages এর format হুবহু সেটাই যেটা hosted LLM API গুলো ব্যবহার করে।

"""A tiny chat client for a small open model running locally (Qwen2.5-0.5B-Instruct on CPU).

Greedy decoding by default, so the same prompt always gives the same answer and the lesson
output is reproducible. The same messages format works with hosted LLM APIs.
"""
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

torch.set_num_threads(4)
MODEL = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype=torch.float32)
model.generation_config.temperature = model.generation_config.top_p = model.generation_config.top_k = None

def chat(messages, max_new_tokens=80, temperature=0.0, seed=0):
    ids = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt")
    kwargs = {"do_sample": False} if temperature == 0 else {"do_sample": True, "temperature": temperature, "top_p": 0.9}
    torch.manual_seed(seed)
    out = model.generate(ids, attention_mask=torch.ones_like(ids), max_new_tokens=max_new_tokens,
                         pad_token_id=tokenizer.eos_token_id, **kwargs)
    return tokenizer.decode(out[0, ids.shape[1]:], skip_special_tokens=True).strip()

def count_tokens(messages):
    return len(tokenizer.apply_chat_template(messages, add_generation_prompt=True))
"""Three versions of the same prompt, tested on the same labelled messages."""
from llm import chat, count_tokens

LABELS = ["delivery", "refund", "account", "other"]
TESTS = [  # (customer message, correct label)
    ("My biryani still hasn't arrived after an hour", "delivery"),
    ("I was charged twice, please return my money", "refund"),
    ("I can't log in, the OTP never comes", "account"),
    ("Do you deliver to Savar?", "other"),
    ("The rider went to the wrong address", "delivery"),
    ("Order was cancelled but bKash still shows the payment", "refund"),
    ("How do I change my phone number?", "account"),
    ("amar khabar ekhono ashe nai", "delivery"),
]

PROMPTS = {
    "v1-vague": [
        {"role": "user", "content": "What is this message about? {msg}"},
    ],
    "v2-instructions": [
        {"role": "system", "content": "You route customer messages for a food delivery app. "
         "Reply with exactly one word from this list: delivery, refund, account, other. No other text."},
        {"role": "user", "content": "{msg}"},
    ],
    "v3-few-shot": [
        {"role": "system", "content": "You route customer messages for a food delivery app. "
         "Reply with exactly one word from this list: delivery, refund, account, other. No other text.\n"
         "delivery = food is late, missing, wrong address. refund = money back, double charge. "
         "account = login, OTP, profile. other = anything else."},
        {"role": "user", "content": "rider is not picking up my call"}, {"role": "assistant", "content": "delivery"},
        {"role": "user", "content": "taka ferot chai"}, {"role": "assistant", "content": "refund"},
        {"role": "user", "content": "{msg}"},
    ],
    "v4-examples-per-label": [
        {"role": "system", "content": "Classify the customer message for a food delivery app into one category.\n"
         "Categories and examples:\n"
         "delivery: \"food is late\", \"rider can't find my house\", \"khabar ashe nai\"\n"
         "refund: \"charged twice\", \"I want my money back\"\n"
         "account: \"OTP not coming\", \"change my email\"\n"
         "other: \"are you open on Eid?\", \"do you deliver to Gazipur?\"\n"
         "Answer with the category name only."},
        {"role": "user", "content": "{msg}"},
    ],
}

def render(template, msg):
    return [{**m, "content": m["content"].format(msg=msg)} for m in template]

if __name__ == "__main__":
    for version, template in PROMPTS.items():
      correct = 0
      print(f"--- {version} ({count_tokens(render(template, TESTS[0][0]))} prompt tokens) ---")
      for msg, want in TESTS:
          got = chat(render(template, msg), max_new_tokens=20)
          ok = got.strip().lower().rstrip(".") == want
          correct += ok
          print(f"  {'ok ' if ok else 'BAD'} {msg[:42]:<42} -> {got[:60]!r}")
      print(f"  score: {correct}/{len(TESTS)}")

Output:

--- v1-vague (46 prompt tokens) ---
  BAD My biryani still hasn't arrived after an h -> 'This message suggests that your biryani has not yet been del'
  BAD I was charged twice, please return my mone -> "I'm sorry to hear that you've been charged twice for somethi"
  BAD I can't log in, the OTP never comes        -> "I'm sorry to hear that you're having trouble logging into yo"
  BAD Do you deliver to Savar?                   -> "I'm sorry, but I don't have enough context to determine what"
  BAD The rider went to the wrong address        -> 'This message suggests that the rider was unable to find thei'
  BAD Order was cancelled but bKash still shows  -> 'This message indicates that there was an issue with the orde'
  BAD How do I change my phone number?           -> 'This message appears to be from an unknown source and does n'
  BAD amar khabar ekhono ashe nai                -> "I'm sorry, but I don't have enough context to understand wha"
  score: 0/8
--- v2-instructions (55 prompt tokens) ---
  BAD My biryani still hasn't arrived after an h -> 'account'
  ok  I was charged twice, please return my mone -> 'refund'
  ok  I can't log in, the OTP never comes        -> 'account'
  BAD Do you deliver to Savar?                   -> 'delivery'
  BAD The rider went to the wrong address        -> 'account'
  ok  Order was cancelled but bKash still shows  -> 'refund'
  ok  How do I change my phone number?           -> 'account'
  BAD amar khabar ekhono ashe nai                -> 'account'
  score: 4/8
--- v3-few-shot (122 prompt tokens) ---
  BAD My biryani still hasn't arrived after an h -> 'account'
  ok  I was charged twice, please return my mone -> 'refund'
  ok  I can't log in, the OTP never comes        -> 'account'
  BAD Do you deliver to Savar?                   -> 'account'
  BAD The rider went to the wrong address        -> 'account'
  BAD Order was cancelled but bKash still shows  -> 'account'
  ok  How do I change my phone number?           -> 'account'
  BAD amar khabar ekhono ashe nai                -> 'account'
  score: 3/8
--- v4-examples-per-label (116 prompt tokens) ---
  BAD My biryani still hasn't arrived after an h -> 'refund'
  ok  I was charged twice, please return my mone -> 'refund'
  ok  I can't log in, the OTP never comes        -> 'account'
  BAD Do you deliver to Savar?                   -> 'delivery'
  BAD The rider went to the wrong address        -> 'refund'
  ok  Order was cancelled but bKash still shows  -> 'refund'
  ok  How do I change my phone number?           -> 'account'
  BAD amar khabar ekhono ashe nai                -> 'refund'
  score: 4/8

কী দেখলাম:

  • v1 (অস্পষ্ট) পায় 0/8, আর model ভুল বুঝেছে বলে না। সে ভদ্র paragraph লিখেছে ("This message suggests that your biryani..."), যেটা কোনো program route করতে পারে না। Format বাঁধা না থাকলে ঠিক answer ও code এর কাছে অকাজের।
  • v2 (role + label এর list + "শুধু এক শব্দ") পায় 4/8, আর এখন প্রতিটা reply একটাই valid label। Format এর সমস্যা এক ধাপেই মিটেছে, মাত্র 9 টা বাড়তি prompt token এ।
  • v3 (দুইটা উদাহরণ) নেমে যায় 3/8 এ। Model প্রায় সবকিছুতে "account" বলা শুরু করেছে। এত ছোট model এ উদাহরণ তাকে একটা label এর দিকে হেলিয়ে দিতে পারে। Test set এটা ধরেছে; হাতে এক দুইটা message check করলে ধরা পড়ত না।
  • v4 (প্রতিটা label এর সংজ্ঞা আর উদাহরণ) আবার 4/8 এ, কিন্তু এবার ভুলগুলো "refund" এর দিকে হেলে। Prompt দ্বিগুণ হয়েছে (55 এর জায়গায় 116 token), লাভ শূন্য।
  • আসল শিক্ষা হলো এই থেমে যাওয়া। তিনটা যুক্তিসঙ্গত prompt এ 0.5 billion parameter এর model মোটামুটি অর্ধেক ঠিকেই আটকে আছে, আর তার ভুলগুলো শুধু এদিক ওদিক সরে। এটাই model এর সীমা। পরের ধাপ পঞ্চম একটা prompt না; শক্তিশালী একটা model (বা fine-tuning), হুবহু এই harness দিয়ে test করা। বড় instruct model এই কাজ সহজেই পারে, আর কতটা পারে সেটা এই harness ই দেখিয়ে দেবে।

Production এ যা মাথায় রাখবে

  • Code এর মতো prompt version আর test করো। Repo তে রাখো, change review করো, আর CI তে test set চালাও (Part 8)। কোন answer কোন prompt version থেকে এসেছে log করো।
  • প্রতিটা call এ token এর খরচ। একই score এ v4 v2 এর দ্বিগুণ লম্বা। লম্বা system prompt আর অনেক উদাহরণ লাখ লাখ call এ গুণ হয়; prompt caching (Part 9) সাহায্য করে, কিন্তু বিনা খরচে না।
  • Prompt model নির্ভর। এক model এ tune করা prompt আরেক model এ, বা model upgrade এর পরে, হঠাৎ অনেক খারাপ করতে পারে। Model বদলালেই test set আবার চালাও।
  • সাবধানে parse করো। "শুধু এক শব্দ" বললেও model মাঝে মাঝে দাঁড়ি, বড় হাতের অক্ষর বা ব্যাখ্যা জুড়ে দেয়। Output normalise করো, অনুমোদিত set এর সাথে validate করো, আর একটা fallback রাখো (structured output, পরের lesson, এটা ঠিকঠাক সমাধান করে)।
  • User এর text হলো data, নির্দেশ না। নিজের message এ বা স্পষ্ট চিহ্ন দেওয়া ঘরে রাখো। যে user (বা document) বলে "ignore previous instructions", সেটা security এর সমস্যা, Part 11 এ আছে।

এক কথায়

Prompt হলো code: role, কাজ, হুবহু output format আর সংজ্ঞা দাও, কাজে লাগলে উদাহরণ দাও, version রাখো, আর প্রতিটা change একটা নির্দিষ্ট test set এ score করো; prompt বদলেও score না বাড়লে সীমাটা model এর, শব্দের না।

নিজে করে দেখো

Test set বাড়িয়ে 20 টা message করো, তার 4 টা Banglish আর 3 টা সত্যিই দ্ব্যর্থক (যেমন "my order is late, I want a refund")। কিছু চালানোর আগেই প্রতিটা দ্ব্যর্থক message এর সঠিক label ঠিক করো, কেন সেটা লিখে রাখো, তারপর v2 আর v4 এর score নাও। আগে label লেখা তোমাকে কী সিদ্ধান্ত নিতে বাধ্য করে?

Answer

দ্ব্যর্থক message একটা product এর নিয়ম ঠিক করতে বাধ্য করে: "late, I want a refund" কি delivery তে যাবে (delivery ঠিক করো) নাকি refund এ (টাকাটাই আসল)? Team সিদ্ধান্ত না নেওয়া পর্যন্ত কোনো prompt ই "ঠিক" হতে পারে না, আর score এর কোনো মানে থাকে না। আগে label লিখলে model এর answer দেখার পরে উদারভাবে নম্বর দেওয়াও আটকায়। 20 টা message এ প্রতিটা case এর দাম 12.5 এর বদলে 5 point, তাই version গুলোর পার্থক্য বেশি বিশ্বাসযোগ্য হয় (Part 1 এর standard-error এর নিয়ম), যদিও production এর সিদ্ধান্তের জন্য 20 এখনো ছোট।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →