Knaph

یک گفت‌وگو5 از 375 دقیقه مطالعه

نگه داشتنِ تاریخچه

لیلا برنامه‌اش را اجرا می‌کند و می‌پرسد: «یک اسمِ خوب برای گزارشِ فروشِ فصلِ سوم چیست؟» جواب را دوست دارد. سؤال را به «کوتاه‌ترش کن» تغییر می‌دهد و دوباره اجرا می‌کند. مدل هیچ نمی‌داند منظور از «کوتاه‌ترش» چیست. همان مدل، همان برنامه، با چند ثانیه فاصله.


لیلا در اصل می‌دانم چرا. مدل بین دو درخواست هیچ چیز نگه نمی‌دارد. ولی پس در یک برنامه‌ی گفت‌وگو چه کسی گفت‌وگو را نگه می‌دارد؟

امید خودِ برنامه. خب، در برنامه‌ی ما برنامه کیست؟

لیلا خودمان. پس فهرستی از همه‌ی حرف‌ها نگه می‌داریم و هر بار کلِ فهرست را می‌فرستیم.

امید درست است. حالا سؤالِ طراحی: وقتی پاسخ برگشت، دقیقاً چه چیزی به فهرست اضافه می‌کنی؟

لیلا فکر می‌کنم متنِ جواب را. چون همان است که گفته شد.

امید بلوکِ فکر کردن چه؟ و بعدها فراخوانی‌های ابزار؟

لیلا مگر مهم‌اند؟ کاربر که هیچ‌وقت آن‌ها را نمی‌بیند.

امید API می‌بیند. وقتی مدل ابزاری می‌خواهد، درخواستِ بعدی باید دقیقاً همان درخواست را، با شناسه‌اش، داشته باشد و نتیجه هم پشتِ سرش بیاید. بلوک‌های فکر کردن هم به گفت‌وگویی که آن‌ها را ساخته بسته‌اند. اگر فقط متن را نگه داری، حرفِ مدل را از نو نوشته‌ای.

لیلا پس محتوای پاسخ را همان‌طور که آمده اضافه می‌کنم، با همه‌ی بلوک‌هایش. نه خلاصه‌ی خودم را.

امید دقیقاً. این فهرست سندِ گفت‌وگوست، نه متنی برای اینکه آدم‌ها بخوانند.


فهرستِ messages

آرگومانِ messages گفت‌وگو تا این لحظه است، به شکلِ یک فهرست. هر پیام یک دیکشنری است با یک role (نقش)، که "user" یا "assistant" است، و یک content، که یا رشته است یا فهرستی از بلوک‌های محتوا. برنامه‌ی شما فهرست را نگه می‌دارد، در هر نوبت به آن اضافه می‌کند و همه‌اش را با هر درخواست می‌فرستد.

حلقه‌ی یک برنامه‌ی گفت‌وگو: شما یک خط می‌نویسید، به‌عنوان پیامِ user به فهرست اضافه می‌شود، کلِ فهرست فرستاده می‌شود، پاسخ به‌عنوان پیامِ assistant اضافه می‌شود، متنِ پاسخ چاپ می‌شود و دوباره از اول.

این یک برنامه‌ی گفت‌وگوی کامل است:

import anthropic
 
MODEL = "claude-opus-5-5"
client = anthropic.Anthropic()
messages = []
 
while True:
    text = input("you: ")
    if text in ("", "quit"):
        break
    messages.append({"role": "user", "content": text})
    reply = client.messages.create(model=MODEL, max_tokens=2000, messages=messages)
    messages.append({"role": "assistant", "content": reply.content})
    print("claude:", "".join(b.text for b in reply.content if b.type == "text"))
Python

به این‌ها دقت کنید:

  • در هر نوبت دو append هست: خطِ شما به‌عنوان پیامِ user پیش از درخواست، و پاسخ به‌عنوان پیامِ assistant بعد از آن.
  • reply.content همان‌طور که آمده اضافه می‌شود، یعنی فهرستی از بلوک‌ها، با بلوکِ فکر کردن. SDK شیءهای بلوکِ خودش را دوباره می‌پذیرد، پس چیزی برای تبدیل کردن نیست.
  • درخواست messages را می‌فرستد، همه‌اش را. هیچ چیزِ دیگری نوبت‌های قبلی را با خود نمی‌برد. اگر فقط خطِ تازه را می‌فرستادید، مدل دوباره هیچ چیز نمی‌دانست.

سه درخواست در یک گفت‌وگو. درخواستِ ۱ یک پیام دارد. درخواستِ ۲ همان پیام، پاسخِ مدل و پیامی تازه را دارد. درخواستِ ۳ همه‌ی این‌ها و دو پیامِ دیگر را دارد. فهرستِ messages هر بار کامل فرستاده می‌شود.

شکلِ فهرست

فهرست شکلِ مشخصی دارد و mini آن را دقیق رعایت می‌کند:

  • با یک پیامِ user شروع می‌شود. مدل همیشه به چیزی جواب می‌دهد.
  • نقش‌ها به نوبت می‌آیند: user، assistant، user، assistant. به هر پیامِ user یک پیامِ assistant جواب می‌دهد.
  • وقتی فرستاده می‌شود، با یک پیامِ user تمام می‌شود، چون مدل باید به همان جواب بدهد.
  • به هر فراخوانیِ ابزار در همان پیامِ user بعدی جواب داده می‌شود، با نتیجه‌ای که همان شناسه را دارد. (این بخش را در بخشِ ابزارها می‌نویسید.)

فهرستی که این شکل را بشکند اشکالی در هارنس است، و همیشه فوراً خطا نمی‌دهد. API فراخوانیِ ابزاری را که نتیجه ندارد رد می‌کند، ولی پاسخی که دو بار اضافه شده شاید فقط مدل را گیج کند. در تمرینِ «تاریخچه را بررسی کنید» بررسی‌ای را می‌نویسید که mini می‌تواند پیش از هر درخواست انجام دهد تا این اشکال‌ها همان‌جا که پیش می‌آیند پیدا شوند.

پرامپت سیستمی، یعنی دستورهایی که پیش از همه‌چیز می‌آیند، اصلاً در این فهرست نیست. آرگومانِ جداگانه‌ی خودش را دارد، در درسِ بعد.

فهرست صورت‌حساب هم هست

هر درخواست کلِ فهرست را می‌فرستد، پس ورودی در هر نوبت بزرگ‌تر می‌شود: درخواستِ دهم نُه نوبتِ پیش از خودش را هم با خود می‌برد. می‌توانید این را ببینید: بعد از هر نوبت reply.usage.input_tokens را چاپ کنید تا ببینید نوبت به نوبت بالا می‌رود، مثلِ پله. همین رشد است که گفت‌وگوهای بلند و جلسه‌های کار با عامل را گران می‌کند، و حالا در کدی است که خودتان نوشته‌اید: هزینه‌ی توکنیِ یک گفت‌وگو جمعِ ورودیِ همه‌ی درخواست‌هاست، نه فقط آخرینِ آن‌ها.

برای یک برنامه‌ی گفت‌وگو این تا مدتِ زیادی مشکلی نیست. برای یک عامل، که نوبت‌هایش فایل‌های کامل و خروجیِ دستورها را با خود دارند، فهرست خیلی سریع‌تر بزرگ می‌شود. بعدتر در این دوره دو راه به mini می‌دهید تا آن را مهار کند: کش کردنِ بخشی که تکرار می‌شود، و فشرده کردنِ تاریخچه وقتی خیلی بزرگ شد.


لیلا پس «شروعِ یک گفت‌وگوی تازه» یعنی فقط messages = [].

امید همیشه همین بوده. «ویرایشِ یک پیامِ قبلی» در برنامه‌های گفت‌وگو هم یعنی بریدنِ فهرست در همان نقطه و اضافه کردنِ پیامی دیگر.

لیلا عجیب است که چقدر از چیزهایی که فکر می‌کردم کارِ مدل است، فقط یک فهرستِ پایتون است.

امید راستش، بیشترِ این دوره همین است.


امتحان کنید

اگر کلید دارید، برنامه‌ی گفت‌وگو را اجرا کنید، اسمتان را بگویید و بعد بپرسید اسمتان چیست. بعد print(len(messages), reply.usage.input_tokens) را بعد از هر پاسخ اضافه کنید، پنج نوبت گفت‌وگو کنید و ببینید هر دو عدد بزرگ می‌شوند. آخر سر، درخواست را طوری تغییر دهید که messages[-1:] (فقط تازه‌ترین پیام) را بفرستد و دوباره بپرسید: دیگر نمی‌داند. اگر کلید ندارید، فهرستی را که در نوبتِ سومِ یک گفت‌وگو فرستاده می‌شود با دست بنویسید.

درست به کار بردن

  1. کلِ محتوای پاسخ را اضافه کنید، نه فقط متنش را. بلوک‌های فکر کردن و فراخوانیِ ابزار باید دقیقاً همان‌طور که آمده‌اند برگردند، وگرنه درخواست‌های بعدی خطا می‌دهند یا معنایشان را از دست می‌دهند.
  2. شکلِ فهرست را دقیق نگه دارید. اول user، نقش‌ها به نوبت، و در آخر user: فهرستی که همیشه این شکل را دارد قابلِ بررسی است و اشکال‌ها همان‌جا که ساخته می‌شوند پیدا می‌شوند.
  3. رشدِ input_tokens را زیرِ نظر داشته باشید. روشن‌ترین نشانه است که گفت‌وگو کی گران شده، و همان عددی است که بعدها بر اساسش کاری می‌کنید.
  4. برای کارِ تازه فهرستِ تازه بسازید. گفت‌وگوی قبلیِ بی‌ربط در هر درخواست توکن خرج می‌کند و چیزهایی جلوی مدل می‌گذارد که گیجش می‌کنند.

واژه‌های این درس

  • فهرستِ messages (messages list) - گفت‌وگو تا این لحظه، به شکلِ فهرستی از پیام‌ها، که برنامه نگهش می‌دارد و با هر درخواست کامل می‌فرستد.
  • نقش (role) - پیام از طرفِ کیست: "user" (شما، و بعدها نتیجه‌ی ابزارها) یا "assistant" (مدل).

ایده‌ی اصلی

مدل هیچ چیز نگه نمی‌دارد، پس برنامه‌ی شما گفت‌وگو را نگه می‌دارد: فهرستی که در هر نوبت به آن اضافه می‌کند و با هر درخواست کامل می‌فرستد. پاسخ‌ها را دقیقاً همان‌طور که آمده‌اند اضافه کنید، شکلِ فهرست را دقیق نگه دارید، و یادتان باشد که پولِ هر پیامِ آن در هر نوبت دوباره پرداخت می‌شود.

دانش خود را بسنجید

ورود برای سنجش دانش خود از این درس و پیگیری پیشرفتتان.