مبانی API مدل‌های زبانی برای برنامه‌نویس دات‌نت: توکن، کانتکست و خروجی ساختاریافته

پاسخ کوتاه: API یک مدل زبانی متن می‌گیرد و متن برمی‌گرداند و قیمت و محدودیت‌هایش بر حسب توکن است. هر چیزی که می‌فرستید (System Prompt، گفت‌وگو، اسناد) باید در پنجره‌ی کانتکست جا شود؛ طول پاسخ با max_tokens محدود می‌شود؛ و دلیل توقف (Stop Reason) می‌گوید چرا مدل ایستاد. برای هر خروجی‌ای که کدتان مصرف می‌کند از خروجی ساختاریافته استفاده کنید تا پاسخ JSON مطابق Schema شما باشد. این پنج مفهوم را که مسلط شوید، بقیه‌ی AI Engineering روی همین‌ها ساخته می‌شود.

یک مثال واقعی: دسته‌بندی ایمیل‌های پشتیبانی

یک فروشگاه کوچک نرم‌افزار ایمیل‌هایی مثل این می‌گیرد: «دیروز دو بار ۷۵۰ هزار تومان برای DiskWatch پرداخت کردم (سفارش AB12CD34EF). لطفاً یکی را برگردانید.» می‌خواهید هر ایمیل به یک دسته، کد سفارش (اگر هست)، مبلغ و خلاصه‌ی یک‌خطی تبدیل شود تا آدم درست اول آن را ببیند.

متن آزاد مدل اینجا به درد نمی‌خورد. یک شیء تایپ‌دار لازم دارید و باید مطمئن باشید مدل اگر کد سفارشی نبود، خالی بگذاردش و از خودش نسازد.

پنج مفهوم

۱. توکن. مدل‌ها توکن می‌خوانند و می‌نویسند؛ تکه‌هایی از کلمه. یک کلمه‌ی کوتاه انگلیسی اغلب یک توکن است و فارسی معمولاً برای همان معنا توکن بیشتری می‌گیرد. برای هر توکن ورودی و خروجی هزینه می‌دهید و هر دو در هر پاسخ گزارش می‌شوند.

۲. پنجره‌ی کانتکست. بیشترین تعداد توکنی که مدل در یک درخواست در نظر می‌گیرد: System Prompt، کل گفت‌وگوی تا این لحظه، اسناد و خود پاسخ. پنجره‌ی مدل‌های فعلی ابزار هوش مصنوعی خیلی بزرگ است (ابزار هوش مصنوعی یک میلیون توکن)، ولی بزرگ یعنی رایگان نیست: هر توکنی که می‌فرستید هزینه دارد و تأخیر اضافه می‌کند. همان را بفرستید که کار لازم دارد.

۳. System Prompt در برابر پیام کاربر. System Prompt قواعد و نقش را نگه می‌دارد («ایمیل پشتیبانی را دسته‌بندی کن؛ فقط واقعیت‌ها را استخراج کن») و پیام کاربر ورودی را. جدا نگه داشتن این دو پرامپت را قابل استفاده‌ی دوباره می‌کند و اولین دفاع در برابر ورودی‌ای است که سعی می‌کند قواعد را عوض کند.

۴. دلیل توقف. هر پاسخ می‌گوید چرا تولید متن متوقف شد: end_turn (تمام شد)، max_tokens (قطع شد و جواب ناقص است)، tool_use (مدل می‌خواهد ابزاری صدا بزند؛ در مقاله‌ی ایجنت‌ها) یا refusal. پیش از استفاده از محتوا آن را بررسی کنید.

۵. خروجی ساختاریافته. به‌جای اینکه مؤدبانه JSON بخواهید، یک JSON Schema می‌فرستید و API پاسخ را به آن محدود می‌کند. کدتان آن را مثل پاسخ هر API دیگری Deserialize می‌کند.

کد قابل‌اجرا

یک برنامه‌ی کنسول بسازید (dotnet new console)، SDK رسمی را اضافه کنید (dotnet add package Anthropic)، ANTHROPIC_API_KEY را تنظیم کنید و Program.cs را جایگزین کنید:

using System.Text.Json;
using Anthropic;
using Anthropic.Models.Messages;

// Reads ANTHROPIC_API_KEY from the environment.
AnthropicClient client = new();

var ticket = "Hi, I paid 750,000 Toman twice for DiskWatch yesterday (order AB12CD34EF). Please refund one. - Sara";

var response = await client.Messages.Create(new MessageCreateParams
{
    Model = "claude-opus-5",
    MaxTokens = 2048,
    System = "You triage support emails for a small software shop. Extract facts only; never invent an order number.",
    Messages = [new UserMessageParam { Content = ticket }],
    // Structured output: the reply is guaranteed to be JSON that matches this schema.
    OutputConfig = new OutputConfig
    {
        Format = new JsonOutputFormat
        {
            Schema = new Dictionary<string, JsonElement>
            {
                ["type"] = JsonSerializer.SerializeToElement("object"),
                ["properties"] = JsonSerializer.SerializeToElement(new
                {
                    category = new { type = "string", @enum = new[] { "refund", "bug", "license", "other" } },
                    orderCode = new { type = new[] { "string", "null" } },
                    amountToman = new { type = new[] { "integer", "null" } },
                    summary = new { type = "string" },
                }),
                ["required"] = JsonSerializer.SerializeToElement(new[] { "category", "orderCode", "amountToman", "summary" }),
                ["additionalProperties"] = JsonSerializer.SerializeToElement(false),
            },
        },
    },
});

var json = response.Content.Select(b => b.Value).OfType<TextBlock>().First().Text;
var triage = JsonSerializer.Deserialize<Triage>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web))!;

Console.WriteLine($"{triage.Category}: {triage.Summary} (order {triage.OrderCode ?? "-"})");
Console.WriteLine($"stop: {response.StopReason}, tokens in/out: {response.Usage.InputTokens}/{response.Usage.OutputTokens}");

public record Triage(string Category, string? OrderCode, long? AmountToman, string Summary);

خط‌هایی که بار اصلی را می‌کشند:

  • System مهم‌ترین قاعده را می‌گوید: استخراج کن، نساز. دستورهای «چه کاری نکن» جایشان اینجاست، نه در هر پیام.
  • @enum برای دسته‌بندی یعنی switch شما هیچ‌وقت به مقداری نمی‌رسد که نمی‌شناسد.
  • orderCode نال‌پذیر راه درستی به مدل می‌دهد تا بگوید «وجود ندارد». بدون گزینه‌ی null، Schema ممکن است مدل را به پر کردن فیلد با چیزی باورپذیر سوق دهد.
  • additionalProperties: false شیء را دقیقاً هم‌شکل رکورد شما نگه می‌دارد.
  • Usage در هر پاسخ هست. توکن ورودی و خروجی را به تفکیک هر قابلیت لاگ کنید؛ این‌طوری قابلیت گران را پیدا می‌کنید.

چیزهایی که در محیط عملیاتی گاز می‌گیرند

  • max_tokens کم. پاسخ وسط جمله قطع می‌شود و JSON قابل پارس نیست. StopReason را بررسی کنید و جای کافی بدهید.
  • اعتماد کورکورانه به محتوا. Schema شکل را تضمین می‌کند، نه درستی را. قواعد کسب‌وکار (آیا سفارش AB12CD34EF وجود دارد؟) را در کد بررسی کنید.
  • گذاشتن ورودی کاربر در System Prompt. متن غیرقابل‌اعتماد را در پیام کاربر و ترجیحاً بین جداکننده‌های روشن بگذارید تا به‌عنوان داده خوانده شود.
  • نداشتن Timeout و Retry. این یک API بیرونی HTTP است. SDK به‌طور پیش‌فرض خطای محدودیت نرخ و خطای سرور را چند بار تکرار می‌کند؛ تصمیم بگیرید اگر باز هم شکست خورد قابلیت شما چه می‌کند.

مزایا و معایب خروجی ساختاریافته

مزایا

  • کدتان داده‌ی تایپ‌دار می‌گیرد و فراخوانی هوش مصنوعی شبیه هر کلاینت API دیگری می‌شود.
  • Enum و فیلدهای نال‌پذیر پاسخ‌های مجاز را صریح می‌کنند، از جمله «نامعلوم».
  • Retry کمتر به خاطر JSON خراب.

معایب

  • Schema یک قرارداد است که مثل هر API باید نسخه‌بندی شود.
  • فقط شکل را محدود می‌کند؛ واقعیت‌ها همچنان بررسی لازم دارند.
  • Schemaهای خیلی بزرگ یا تودرتو پرامپت را طولانی و کار مدل را سخت می‌کنند؛ کوچک نگهشان دارید و استخراج‌های بزرگ را بشکنید.

قدم بعدی

با خروجی تایپ‌دار از یک فراخوانی، قدم بعد دادن داده‌ی درست به مدل است: RAG در دات‌نت. کل مسیر در نقشه‌ی راه AI Engineering آمده است.


یک قابلیت LLM لازم دارید که بک‌اند دات‌نت شما واقعاً بتواند به آن تکیه کند؟ ببینید چطور می‌توانم کمک کنم.

پرسش‌های متداول

چرا یک پرامپت هر بار جواب متفاوتی می‌دهد؟

مدل‌ها متن را با نمونه‌برداری تولید می‌کنند، پس تفاوت‌های کوچک طبیعی است. تفاوتی را که مهم است با دستورالعمل روشن، خروجی ساختاریافته مطابق Schema و کدی که نتیجه را اعتبارسنجی می‌کند کم کنید، نه با انتظار متن یکسان.

متن فارسی توکن بیشتری از انگلیسی مصرف می‌کند؟

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

خروجی ساختاریافته از خواستن JSON در پرامپت بهتر است؟

بله. خواستن JSON با کلمات بیشتر وقت‌ها جواب می‌دهد، ولی خروجی ساختاریافته پاسخ را به Schema شما محدود می‌کند و کدتان بدون پارس محتاطانه آن را می‌خواند. قواعد کسب‌وکار را همچنان اعتبارسنجی کنید.