درس ۱۲ از ۱۲، بخش ۳: مهندسی پرامپت
زنجیرهی تفکر و مدلهای استدلالی
در درس ۸ دیدیم که فکر کردن مدل به ازای تولید توکن است و اگر گامهای حل را بدهیم، جواب بهتری میگیریم. این درس یک قدم جلوتر میرود: بهجای اینکه ما گامها را بنویسیم، از خود مدل میخواهیم مسیر استدلالش را بنویسد. این ایده آنقدر مؤثر بود که امروز تقریباً همهی چتباتها دکمهای به اسم «Thinking» یا «Reason» دارند.
نوتبوکهای این درس
03_12_chain_of_thought_prompting.ipynb و 03_13-Reasoning_models.ipynb
زنجیرهی تفکر (Chain of Thought)#
مقالهی Chain-of-Thought Prompting Elicits Reasoning in Large Language Models (Wei و همکاران، ۲۰۲۲) یک آزمایش ساده انجام داد. در پرامپت few-shot، بهجای اینکه مثال فقط جواب نهایی را داشته باشد، راهحل را هم آورد:
Q: Roger has 5 tennis balls. He buys 2 more cans of tennis balls. Each can has 3 tennis balls.
How many tennis balls does he have now?
A: Roger started with 5 balls. 2 cans of 3 tennis balls each is 6 tennis balls. 5 + 6 = 11. The answer is 11.
مدل هم در جواب سؤال جدید همین الگو را دنبال کرد: اول استدلال، بعد جواب. دقت در مسائل ریاضی و منطقی جهش بزرگی داشت.
بدون CoT: سؤال ─◄ جواب با CoT: سؤال ─◄ گام ۱ ─◄ گام ۲ ─◄ گام ۳ ─◄ جواب
یک تابع کمکی برای این درس:
def ask(prompt, model="ollama/llama3.1:8b", system=None, temperature=0.1):
messages = [{"role": "system", "content": system}] if system else []
messages.append({"role": "user", "content": prompt})
return completion(model=model, messages=messages, temperature=temperature).choices[0].message.content
آزمایش: بدون CoT و با CoT#
PROBLEM = """
علی ۵ تا سیب داشت. ۳ تا از سیب ها را مریم ازش دزدید، بعد ۷ تا خرید.
سپس نصف سیبهایش را به دوستش داد.
الان علی چند تا سیب دارد؟
"""
ask(f"سوال: {PROBLEM}\nلطفا فقط فقط پاسخ نهایی را بده\nجواب:")
۵ + ۷ - ۳ = ۹
مدل نصف کردن را جا انداخت. جواب درست ۴٫۵ است. حالا با CoT:
ask(f"سوال: {PROBLEM}\nمرحله به مرحله فکر کن و هر قدم را توضیح بده، سپس جواب نهایی را بنویس.\nمراحل حل:")
مدل این بار مراحل را نوشت و در بیشتر اجراها به ۴٫۵ رسید. در بعضی اجراها ۴ یا ۵ گفت، چون عدد را گرد کرده بود. یک جملهی «گرد نکن» این را هم درست میکند. زمان پاسخ هم از ۰٫۵ ثانیه به ۴٫۲ ثانیه رسید. استدلال مجانی نیست.
Zero-shot CoT: عبارت جادویی#
مقالهی Large Language Models are Zero-Shot Reasoners (Kojima و همکاران، ۲۰۲۲) نشان داد حتی مثال هم لازم نیست. کافی است به آخر سؤال اضافه کنید:
Let's think step by step.
بیا گام به گام فکر کنیم.
def zero_shot_cot(question, lang="fa"):
magic = {"en": "Let's think step by step.", "fa": "بیا گام به گام فکر کنیم."}[lang]
return ask(f"{question}\n\n{magic}")
دو نکته از آزمایشهای کلاس:
- زبان عبارت مهم است. با عبارت انگلیسی، مدل حتی برای سؤال فارسی معمولاً به انگلیسی استدلال میکند. با عبارت فارسی، به فارسی.
- مدل کوچک همیشه اطاعت نمیکند. برای مسئلهی منطقی سه برادر (احمد از باقر بزرگتر است، حسن از احمد کوچکتر و از باقر بزرگتر است؛ بزرگترین کیست؟) Llama 3.1 8B بدون نوشتن هیچ گامی جواب داد. جوابش درست بود، اما دستور را اجرا نکرد. few-shot در این موارد قابلاعتمادتر است، چون مدل الگو را میبیند.
Few-shot CoT#
FEW_SHOT_EXAMPLES = [
{
"question": "دانا ۱۰ کتاب داشت. ۴ تا قرض داد و ۶ تا خرید. چند کتاب دارد؟",
"reasoning": """
گام ۱: دانا ابتدا ۱۰ کتاب دارد.
گام ۲: ۴ کتاب قرض میدهد → ۱۰ - ۴ = ۶ کتاب
گام ۳: ۶ کتاب میخرد → ۶ + ۶ = ۱۲ کتاب
جواب: ۱۲ کتاب""",
},
{
"question": "یک پارکینگ ۳ ردیف دارد. هر ردیف ۸ جا دارد. ۱۳ ماشین پارک است. چند جا خالی است؟",
"reasoning": """
گام ۱: کل ظرفیت = ۳ × ۸ = ۲۴ جا
گام ۲: جاهای خالی = ۲۴ - ۱۳ = ۱۱ جا
جواب: ۱۱ جا خالی است""",
},
]
برای سؤال «مغازهای روزانه ۱۵۰ محصول میفروشد … در ۵ روز با قیمت ۲۰۰۰ تومان درآمد چقدر است؟» مدل دقیقاً همان قالب را دنبال کرد:
گام ۱: کل فروش = ۱۵۰ × ۵ = ۷۵۰
گام ۲: درآمد کل = ۷۵۰ × ۲۰۰۰ = ۱,۵۰۰,۰۰۰ تومان
خودسازگاری (Self-Consistency)#
اگر یک مسئله را چند بار با CoT حل کنید، مسیر استدلال هر بار کمی فرق میکند و گاهی به جواب متفاوت میرسد. ایدهی خودسازگاری (Wang و همکاران، ۲۰۲۲) ساده است: چند بار حل کن و رأی اکثریت را بردار.
from collections import Counter
def self_consistency_cot(question, n_samples=3, temperature=0.7):
prompt = f"""{question}
بیا گام به گام حل کنیم:
(لطفاً در انتهای پاسخ، جواب نهایی را دقیقاً با این فرمت بنویس:
جواب نهایی: [عدد])"""
answers = [extract_final_answer(ask(prompt, temperature=temperature)) for _ in range(n_samples)]
winner, votes = Counter(answers).most_common(1)[0]
return winner, votes, answers
- دما باید بیشتر از صفر باشد، وگرنه هر بار همان مسیر تکرار میشود.
- قالب جواب نهایی را تعیین کنید («جواب نهایی: [عدد]») تا بشود با regex استخراجش کرد. اگر سؤال فارسی باشد، مدل ممکن است یک بار با رقم فارسی و یک بار با رقم لاتین جواب بدهد. قبل از شمارش، رقمها را یکسان کنید.
برای سؤال «کالای ۵۰٬۰۰۰ تومانی با ۲۰٪ تخفیف و سپس ۱۵٪ مالیات روی قیمت تخفیفخورده» (جواب درست: ۴۶٬۰۰۰):
در یکی از اجراها جوابها ۴۶۰۰۰، ۱۳۸ و ۴۶۰۰۰ بود. در اجرای دیگر ۴۶۰۰۰، ۴۶۰۰۰ و ۵۶۰۰۰. هر دو بار، اکثریت درست بود.
خودسازگاری هزینهی چندبرابری دارد و امروز کمتر از CoT استفاده میشود. اما برای سؤالهای حساس عددی یا وقتی فقط مدل کوچک در اختیار دارید، ابزار مفیدی است.
مدلهای استدلالی#
بعد از موفقیت CoT، شرکتها مدلها را طوری آموزش دادند که خودشان پیش از جواب نهایی استدلال کنند: o1 و o3 از OpenAI، DeepSeek-R1، Qwen3، Gemma 4 و gpt-oss. این مدلها:
- استدلال را بین توکنهای ویژه تولید میکنند، مثلاً
<think> … </think>، و بعد جواب نهایی را میدهند. چتباتها معمولاً استدلال را پنهان یا جمعشده نشان میدهند. - با یک تنظیم، و در واقع یک توکن ویژه در پشت صحنه، فکر کردن را روشن یا خاموش میکنند.
پشت این روند یک مشاهدهی مهم است. قبلاً میدانستیم مدل بزرگتر، دادهی بیشتر و محاسبهی بیشتر در زمان آموزش، دقت را بالا میبرد (قوانین مقیاس). حالا معلوم شده محاسبهی بیشتر در زمان استنتاج هم همین اثر را دارد: هر چه مدل بیشتر فکر کند، دقیقتر جواب میدهد. به این test-time compute میگویند.
think روشن و خاموش#
def get_completion(prompt, model="ollama/gemma4:e4b", temperature=0.2, think=False):
response = completion(model=model, messages=[{"role": "user", "content": prompt}],
temperature=temperature, think=think)
return response.choices[0].message.content
QUESTION = "جواب را در یک کلمه بگو، اگر یک قطار با سرعت 120 کیلومتر در ساعت حرکت کند و بعد از 45 دقیقه توقف کند، چند کیلومتر طی کرده؟"
| زمان | جواب | |
|---|---|---|
think=False |
۰٫۸ ثانیه | ۲۴ ✗ |
think=True |
۹٫۵ ثانیه | ۹۰ ✓ |
با اینکه گفتیم «در یک کلمه بگو»، مدل استدلالی قبل از آن یک کلمه، پشت صحنه فکر کرده است: 120 × 0.75 = 90.
اگر usage پاسخ را ببینید، تعداد توکنهای خروجی برای جوابی به کوتاهی «۹۰» صدها توکن است. توکنهای استدلال هم شمرده میشوند و پولشان را میدهید، حتی اگر نمایش داده نشوند.
سطح تلاش استدلالی#
بعضی مدلها بهجای روشن و خاموش، سطح فکر کردن را هم میپذیرند. gpt-oss سه سطح دارد:
completion(model="ollama/gpt-oss:20b", messages=messages,
options={"reasoning_effort": "low"}) # low | medium | high
برای سؤال «یک تابع پایتونی بنویس که یک لیست را در سریعترین زمان ممکن سورت کند»:
| سطح | زمان |
|---|---|
| low | ۱۷٫۵ ثانیه |
| medium | ۲۵٫۶ ثانیه |
| high | ۲۱٫۴ ثانیه |
high کوتاهتر از medium شد. سطح تلاش مدل را تشویق به فکر بیشتر میکند، اما مدل احتمالاتی است و ممکن است زودتر به نتیجه برسد. در چتباتهای تجاری هم همین تنظیم را با نامهایی مثل «Thinking» یا «Extended thinking» میبینید.
کی استدلال لازم نیست؟#
- سؤالهای واقعیتمحور: «پایتخت فلان کشور کجاست؟» یا «بازیگر این فیلم کیست؟». جواب یا در حافظهی مدل هست یا نیست و فکر کردن کمکی نمیکند. فقط کندتر و گرانتر میشود.
- احوالپرسی و کارهای ساده: برای «سلام» با
think=Trueجواب ۸٫۵ ثانیه طول کشید و بدون آن ۱٫۴ ثانیه. مدل برای جواب دادن به یک سلام کلی فکر کرده بود. - کارهای حساس به تأخیر، مثل چتبات زنده یا تکمیل خودکار.
استدلال داخلی مدل یا CoT دستی؟
اگر مدل استدلالی در اختیار دارید، روشن کردن think معمولاً از نوشتن «گام به گام فکر کن» در پرامپت بهتر است، چون مدل برای همین کار آموزش دیده است. CoT دستی برای مدلهای غیراستدلالی و وقتی میخواهید قالب استدلال را کنترل کنید (مثل few-shot CoT) همچنان ابزار مهمی است.
خلاصه#
- زنجیرهی تفکر یعنی مدل پیش از جواب، استدلالش را بنویسد. zero-shot («بیا گام به گام فکر کنیم») ساده است. few-shot قابلاعتمادتر است.
- خودسازگاری یعنی چند استدلال با دمای غیرصفر و رأی اکثریت. دقیقتر است، اما چند برابر هزینه دارد.
- مدلهای استدلالی خودشان فکر میکنند.
thinkوreasoning_effortمقدار فکر را تنظیم میکنند و توکنهای استدلال هزینه دارند. - برای سؤالهای واقعیتمحور و کارهای ساده، استدلال را خاموش کنید.
این درس پایان دورهی اول است. در دورهی LangChain همین مفاهیم، یعنی پیامها، خروجی ساختیافته، حافظه و زنجیره، را با یک فریمورک استاندارد و قابلتوسعه پیاده میکنیم.