ساخت ایجنت پشتیبانی مشتری با ابزارهای سمت‌کاربر کلود

ساخت ایجنت پشتیبانی مشتری؛ حلقه‌ای که مدل را از حرف‌زدن به عمل‌کردن می‌رساند

مهدی مسئول پشتیبانی یک فروشگاه اینترنتی لوازم خانگی است. هر روز صبح که پنل تیکت‌ها را باز می‌کند، سه‌چهارم پیام‌ها یک الگوی تکراری دارند: «شمارهٔ تماس ثبت‌شده‌ام را برایم بخوانید»، «سفارش من به کجا رسید؟» و «این سفارش را لغو کنید». او یک ربات هوشمند روی سایت گذاشته که با مدل زبانی جواب می‌دهد و ربات هم مؤدبانه و روان حرف می‌زند — اما وقتی مشتری می‌پرسد وضعیت سفارش ۸۳۱۲ چیست، ربات با اطمینان کامل می‌گوید «سفارش شما ارسال شده است». چنین سفارشی اصلاً وجود ندارد. مهدی فکر می‌کند مدل دروغ می‌گوید. مدل دروغ نمی‌گوید؛ فقط هیچ راهی ندارد که به پایگاه دادهٔ فروشگاه نگاه کند، و وقتی راه نگاه‌کردن نباشد، مدل زبانی محتمل‌ترین جمله را می‌سازد.

فاصلهٔ میان یک چت‌بات و یک ایجنت پشتیبانی دقیقاً همین‌جاست: ایجنت می‌تواند کاری انجام دهد. آنتروپیک برای همین سناریو یک دستور پخت رسمی دارد؛ ایجنت پشتیبانی مشتری با ابزارهای سمت‌کاربر. در این درس آن دستور پخت را باز می‌کنیم و به فروشگاه مهدی می‌بندیم.

آنچه در این درس یاد می‌گیرید

  • تعریف سه ابزار پایهٔ یک ایجنت پشتیبانی — خواندن اطلاعات مشتری، خواندن جزئیات سفارش و لغو سفارش — و اینکه توضیح هر ابزار چرا بخشی از پرامپت است.
  • حلقهٔ ایجنتی: چطور با stop_reason بفهمید مدل ابزار می‌خواهد و نتیجه را با tool_result برگردانید.
  • نقش tool_use_id در گره‌زدن پرسش و پاسخ ابزار، و اینکه چرا فراموش‌کردنش سامانه را در چندابزاری خراب می‌کند.
  • یک اصلاح مهم روی کد رسمی: چرا باید تاریخچه را «اضافه» کنید نه «بازسازی».
  • تفاوت بنیادی ابزار خواننده با ابزار اقدام‌کننده، و اینکه لغو سفارش چرا نباید بی‌واسطه به مدل سپرده شود.
  • مدیریت خطا به‌عنوان بخشی از قرارداد ابزار، نه یک استثنا.

سه ابزار، سه قرارداد روشن

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

tools = [
    {
        "name": "get_customer_info",
        "description": "Retrieves customer information based on their customer ID. Returns the customer's name, email, and phone number.",
        "input_schema": {
            "type": "object",
            "properties": {
                "customer_id": {"type": "string", "description": "The unique identifier for the customer."}
            },
            "required": ["customer_id"],
        },
    },
    {
        "name": "get_order_details",
        "description": "Retrieves the details of a specific order based on the order ID. Returns the order ID, product name, quantity, price, and order status.",
        "input_schema": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "The unique identifier for the order."}
            },
            "required": ["order_id"],
        },
    },
    {
        "name": "cancel_order",
        "description": "Cancels an order based on the provided order ID. Returns a confirmation message if the cancellation is successful.",
        "input_schema": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "The unique identifier for the order to be cancelled."}
            },
            "required": ["order_id"],
        },
    },
]

به جملهٔ آخر هر توضیح دقت کنید: «چه چیزی برمی‌گرداند». این عادت کوچک، مدل را از حدس‌زدن نجات می‌دهد. وقتی مدل بداند get_order_details وضعیت سفارش را هم برمی‌گرداند، دیگر برای پاسخ به «سفارشم کجاست؟» دو ابزار پشت سر هم صدا نمی‌زند.

پشت هر ابزار، یک تابع معمولی

عبارت «ابزار سمت‌کاربر» یعنی اجرای واقعی کار روی سرور شما اتفاق می‌افتد، نه در سمت مدل. مدل فقط می‌گوید «این تابع را با این ورودی صدا بزن». در نمونهٔ آموزشی، توابع ساختگی‌اند تا معماری دیده شود:

def get_customer_info(customer_id):
    customers = {
        "C1": {"name": "John Doe", "email": "[email protected]", "phone": "123-456-7890"},
        "C2": {"name": "Jane Smith", "email": "[email protected]", "phone": "987-654-3210"},
    }
    return customers.get(customer_id, "Customer not found")

def process_tool_call(tool_name, tool_input):
    if tool_name == "get_customer_info":
        return get_customer_info(tool_input["customer_id"])
    elif tool_name == "get_order_details":
        return get_order_details(tool_input["order_id"])
    elif tool_name == "cancel_order":
        return cancel_order(tool_input["order_id"])

ارزش این ساختگی‌بودن را دست‌کم نگیرید. مهدی می‌تواند کل رفتار ایجنت را با یک دیکشنری ده‌تایی بسنجد، همهٔ حالت‌های عجیب را ببیند و تازه بعد از آن get_order_details را به کوئری واقعی پایگاه داده وصل کند. اگر از روز اول به پایگاه داده وصل شوید، هر بار عیب‌یابی یعنی آلوده‌کردن دادهٔ واقعی.

حلقه‌ای که ایجنت را ایجنت می‌کند

منطق اصلی کوتاه است. درخواست را با فهرست ابزارها می‌فرستید. اگر stop_reason برابر tool_use بود، یعنی مدل منتظر شماست: بلوک tool_use را بردارید، تابع را اجرا کنید، نتیجه را در قالب tool_result برگردانید و دوباره درخواست بدهید. تا وقتی stop_reason از tool_use در بیاید، حلقه ادامه دارد:

messages = [{"role": "user", "content": user_message}]
response = client.messages.create(model=MODEL_NAME, max_tokens=4096, tools=tools, messages=messages)

while response.stop_reason == "tool_use":
    tool_use = next(b for b in response.content if b.type == "tool_use")
    tool_result = process_tool_call(tool_use.name, tool_use.input)

    messages.append({"role": "assistant", "content": response.content})
    messages.append({
        "role": "user",
        "content": [{
            "type": "tool_result",
            "tool_use_id": tool_use.id,
            "content": str(tool_result),
        }],
    })

    response = client.messages.create(model=MODEL_NAME, max_tokens=4096, tools=tools, messages=messages)

final = next((b.text for b in response.content if hasattr(b, "text")), None)

سه جزئیات این قطعه را باید حفظ باشید. اول، پاسخ مدل — همان که بلوک tool_use در آن است — باید عیناً به تاریخچه اضافه شود؛ نمی‌توانید فقط نتیجه را بفرستید و بلوک درخواست را حذف کنید. دوم، tool_result در نقش user می‌رود، نه assistant؛ منطقش این است که از دید مدل، نتیجهٔ ابزار خبری است که از بیرون رسیده. سوم و مهم‌تر از همه، tool_use_id است که پاسخ را به درخواست گره می‌زند. تا وقتی در هر دور فقط یک ابزار صدا زده می‌شود، بی‌دقتی در این شناسه به چشم نمی‌آید؛ اما همین که مدل در یک پاسخ دو ابزار را موازی صدا بزند، بدون شناسهٔ درست، نتیجه‌ها جابه‌جا می‌شوند.

یک اصلاح روی کد رسمی

در نوت‌بوک اصلی، فهرست پیام‌ها در هر دور از نو ساخته می‌شود؛ یعنی همیشه از پیام اول کاربر شروع می‌کند و همان یک دور آخر را می‌چیند. برای نمایش مفهوم عالی است، اما اگر همان را به تولید ببرید، ایجنت شما حافظه‌اش را در دور دوم از دست می‌دهد: نتیجهٔ ابزار قبلی دیگر در تاریخچه نیست. سناریوی واقعی مهدی دقیقاً همان جایی خراب می‌شود که مشتری می‌نویسد «سفارشم را پیدا کن و اگر هنوز ارسال نشده لغو کن» — این کار دو ابزار پشت سر هم می‌خواهد. راه‌حل همان است که در قطعهٔ بالا آمد: به‌جای بازسازی، با append به تاریخچه اضافه کنید. یک خط تفاوت، اما مرز میان یک نمایش و یک سامانه.

وقتی ابزار می‌خواند و وقتی ابزار عمل می‌کند

دو ابزار اول این ایجنت فقط می‌خوانند؛ بدترین خطایشان نمایش داده‌ای اشتباه است. ابزار سوم اما دنیای بیرون را تغییر می‌دهد. لغو سفارش برگشت‌ناپذیر است، به انبار و حسابداری و درگاه پرداخت وصل است و اگر مدل آن را با شناسهٔ اشتباه صدا بزند، عذرخواهی چیزی را برنمی‌گرداند. پس این ابزار را مثل بقیه رها نکنید.

سه محافظ ساده کافی است. نخست، تأیید صریح: پیش از اجرا، ایجنت باید خلاصهٔ سفارش را نشان دهد و از کاربر تأیید بگیرد — یعنی cancel_order فقط پس از پیام تأییدیهٔ کاربر مجاز باشد. دوم، بررسی مالکیت در سمت سرور: تابع شما باید مطمئن شود این سفارش واقعاً متعلق به همین کاربر جلسه است، چون مدل ممکن است شناسه‌ای را که کاربر در متن نوشته بی‌چون‌وچرا بپذیرد. سوم، بی‌اثری تکرار: اگر ایجنت به هر دلیل دو بار ابزار را صدا زد، بار دوم نباید کار تازه‌ای انجام دهد. قاعدهٔ کلی این است که اختیار را در سمت کد نگه دارید، نه در نیت مدل.

خطا هم یک پاسخ است

به مقدار پیش‌فرض تابع نگاه کنید: به‌جای پرتاب استثنا، رشتهٔ «Customer not found» برگردانده می‌شود. این انتخاب عمدی است. مدل می‌تواند این جمله را بخواند و به فارسی به مشتری بگوید که چنین شناسه‌ای پیدا نشد و شمارهٔ سفارش را دوباره بپرسد. اگر به‌جایش استثنا پرتاب کنید و حلقه بشکند، کاربر یک صفحهٔ خطا می‌بیند. پس متن خطا را طوری بنویسید که هم برای مدل قابل‌فهم باشد و هم راه بعدی را نشان دهد؛ مثلاً «سفارش یافت نشد؛ شمارهٔ سفارش ده‌رقمی است و در پیامک تأیید ارسال شده». همین یک جمله، نرخ حل‌شدن تیکت‌ها را جابه‌جا می‌کند.

✏️ تمرین

در حلقهٔ ایجنتی، پس از اجرای تابع، نتیجه را در قالب کدام ساختار و در کدام نقش به مدل برمی‌گردانید؟

🔒

این تمرین ویژهٔ اعضاست

برای دیدن این بخش باید عضو ویژه (VIP) باشی. با شمارهٔ موبایلت وارد شو تا ۱۴ روز دسترسی رایگان فعال شود.

🎁 ورود / ثبت‌نام و شروع ۱۴ روز رایگان
قبلاً عضو شده‌ای؟ فقط کافی است وارد شوی. اشتراک: از ۱۹۹٬۰۰۰ تومان / ماه

جمع‌بندی

ایجنت پشتیبانی مشتری، ساده‌ترین ایجنت کاملی است که می‌شود ساخت و به همین دلیل بهترین تمرین است. سه چیز را با خودتان ببرید. اول، توضیح ابزار پرامپت است؛ هر ابهامی که در آن بگذارید، مدل با حدس پر می‌کند. دوم، حلقه چیزی جز رفت‌وبرگشت میان tool_use و tool_result نیست، اما درستی‌اش به دو چیز بند است: نگه‌داشتن کامل تاریخچه و حمل صحیح شناسه. سوم، مرز میان خواندن و عمل‌کردن را خودتان بکشید؛ مدل این مرز را نمی‌شناسد و نباید هم بشناسد. ربات مهدی با همین سه اصل از یک ماشین جمله‌سازی به همکاری تبدیل می‌شود که می‌داند چه چیزی را نمی‌داند و کجا باید بپرسد.

و چرا این تکنیک کهنه نمی‌شود؟ چون آنچه اینجا ساختیم وابسته به نام هیچ مدلی نیست. الگو این است: یک تصمیم‌گیرندهٔ احتمالی داریم که به دنیای بیرون دسترسی ندارد، و ما با قراردادهای صریح به او اجازهٔ محدود می‌دهیم که بخواند و — با محافظ — عمل کند. هر نسل تازه‌ای از مدل‌ها این حلقه را سریع‌تر و باهوش‌تر می‌پیماید، اما خودِ حلقه سر جایش می‌ماند. کسی که یک‌بار آن را با دست نوشته باشد، فردا هر چارچوب آماده‌ای را هم که دستش بدهند می‌فهمد چه کار می‌کند. برای دیدن ادامهٔ همین مسیر، دورهٔ ساخت ایجنت را دنبال کنید و اگر هنوز نخوانده‌اید سری بزنید به درس اولین ابزار مدل و درس کنترل انتخاب ابزار که مکمل مستقیم همین بحث‌اند. یادگیری میان‌بر ندارد؛ اما ابزار دارد.

منبع این درس، مستندات رسمی آنتروپیک است؛ ما آن را برای فارسی‌زبان‌ها درس کرده‌ایم.

(0 رأی)

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *