ایجنت‌های هوش مصنوعی و فراخوانی ابزار در دات‌نت: حلقه را دستی بنویسید

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

یک مثال واقعی: «لایسنس من کجاست؟»

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

پس دو ابزار می‌گیرد: get_order_status (فقط‌خواندنی) و draft_reply که به‌جای فرستادن ایمیل، پیش‌نویسی برای یک انسان ذخیره می‌کند. مدل تصمیم می‌گیرد کی هر کدام را صدا بزند؛ کد شما تصمیم می‌گیرد هر کدام اجازه‌ی چه کاری دارد.

فراخوانی ابزار چطور کار می‌کند

  1. گفت‌وگو را همراه فهرست ابزارها می‌فرستید؛ هر کدام با نام، توضیح و JSON Schema ورودی.
  2. اگر مدل ابزاری لازم داشته باشد، پاسخ یک یا چند بلوک tool_use دارد و دلیل توقف tool_use است.
  3. کد شما هر ابزار درخواستی را اجرا می‌کند و همه‌ی نتیجه‌ها را در پیام بعدی کاربر برمی‌گرداند، هر کدام متصل به شناسه‌ی tool_use خودش. خطاها هم با علامت خطا برمی‌گردند تا مدل خودش را اصلاح کند.
  4. تکرار تا وقتی دلیل توقف end_turn شود.

مدل خودش هیچ‌چیز را اجرا نمی‌کند؛ فقط درخواست می‌دهد و کد شما دروازه‌بان است.

کد قابل‌اجرا: حلقه‌ی دستی

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

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

AnthropicClient client = new();

Tool[] tools =
[
    new Tool
    {
        Name = "get_order_status",
        Description = "Returns the status and product of a shop order. Read-only.",
        InputSchema = new()
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["code"] = JsonSerializer.SerializeToElement(new { type = "string", description = "10-character order code" }),
            },
            Required = ["code"],
        },
    },
    new Tool
    {
        Name = "draft_reply",
        Description = "Saves a reply to the customer as a draft for a human to review. Does not send anything.",
        InputSchema = new()
        {
            Properties = new Dictionary<string, JsonElement>
            {
                ["text"] = JsonSerializer.SerializeToElement(new { type = "string" }),
            },
            Required = ["text"],
        },
    },
];

List<MessageParam> messages =
[
    new UserMessageParam { Content = "A customer asks why order GH56JK78MN has no license yet. Check it and draft a short reply." },
];

// The agent loop: call the model, run the tools it asks for, send the results back, repeat until it stops asking.
for (var turn = 0; turn < 10; turn++)
{
    var response = await client.Messages.Create(new MessageCreateParams
    {
        Model = "claude-opus-5",
        MaxTokens = 4096,
        System = "You are a support assistant for a small software shop. Use the tools; never promise refunds.",
        Tools = [.. tools],
        Messages = messages,
    });

    List<ContentBlockParam> assistant = [];
    List<ContentBlockParam> results = [];
    foreach (var block in response.Content)
    {
        if (block.TryPickText(out var text))
        {
            assistant.Add(new TextBlockParam { Text = text.Text });
            Console.WriteLine(text.Text);
        }
        else if (block.TryPickThinking(out var thinking))
        {
            assistant.Add(new ThinkingBlockParam { Thinking = thinking.Thinking, Signature = thinking.Signature });
        }
        else if (block.TryPickToolUse(out var call))
        {
            assistant.Add(new ToolUseBlockParam { ID = call.ID, Name = call.Name, Input = call.Input });
            var (output, failed) = Run(call.Name, call.Input);
            results.Add(new ToolResultBlockParam { ToolUseID = call.ID, Content = output, IsError = failed });
        }
    }

    if (response.StopReason != "tool_use")
    {
        break; // end_turn (done), max_tokens or refusal: stop and let a human look.
    }

    messages.Add(new AssistantMessageParam { Content = assistant });
    messages.Add(new UserMessageParam { Content = results }); // all results of this turn in one message
}

static (string Output, bool Failed) Run(string name, IReadOnlyDictionary<string, JsonElement> input)
{
    try
    {
        return name switch
        {
            "get_order_status" => (input["code"].GetString() == "GH56JK78MN" ? "Pending: payment not matched yet" : "Not found", false),
            "draft_reply" => (SaveDraft(input["text"].GetString()!), false),
            _ => ($"Unknown tool {name}", true),
        };
    }
    catch (Exception ex) when (ex is KeyNotFoundException or InvalidOperationException)
    {
        return ($"Bad input: {ex.Message}", true); // the model reads this and can correct itself
    }
}

static string SaveDraft(string text)
{
    File.AppendAllText("drafts.txt", text + Environment.NewLine + "---" + Environment.NewLine);
    return "Draft saved for review.";
}

جزئیاتی که این کد را درست می‌کند:

  • نوبت دستیار دقیقاً همان‌طور برگردانده می‌شود، با فراخوانی‌های ابزار و هر بلوک Thinking (همراه امضایش)، تا درخواست بعدی کل تاریخچه را داشته باشد.
  • همه‌ی نتیجه‌های یک نوبت در یک پیام کاربر برمی‌گردند. مدل ممکن است هم‌زمان چند ابزار بخواهد؛ همه را با هم جواب دهید.
  • خطا نتیجه است، نه استثنا. IsError = true با پیامی خوانا به مدل اجازه می‌دهد اشتباه خودش را، مثلاً کد سفارش نادرست، اصلاح کند.
  • turn < 10 ترمز است. ایجنت گیج‌شده ممکن است در حلقه بماند؛ سقف، این را به یک خطای قابل‌دیدن تبدیل می‌کند نه یک صورت‌حساب.
  • draft_reply پیش‌نویس می‌کند، نمی‌فرستد. مهم‌ترین تصمیم امنیتی در طراحی ابزار است، نه در پرامپت.

اول Workflow، بعد ایجنت

پیش از ساختن ایجنت بپرسید مراحل معلوم است یا نه. «سفارش را پیدا کن، بعد جواب بنویس» یک توالی ثابت است: دو فراخوانی در کد خودتان (یک Workflow) ارزان‌تر، سریع‌تر و راحت‌تر تست‌پذیر است. ایجنت را وقتی به کار ببرید که مسیر به چیزی بستگی دارد که مدل پیدا می‌کند: کدام ابزار، چند بار، به چه ترتیبی. بسیاری از محصولات خوب Workflowهایی هستند با یک مرحله‌ی ایجنت‌مانند در وسط.

وقتی یک بار این حلقه را دستی نوشتید، همه‌جا می‌شناسیدش: در فریم‌ورک‌ها، در Tool Runner خود SDK و در کلاینت‌های MCP که همین حلقه را روی ابزارهای یک سرور MCP اجرا می‌کنند.

مزایا و معایب ایجنت‌ها

مزایا

  • کارهایی را انجام می‌دهند که مراحلشان از پیش قابل نوشتن نیست.
  • داده و اقدام‌های شما را با استدلال مدل در یک حلقه ترکیب می‌کنند.
  • همان ابزارها به کارهای زیادی خدمت می‌کنند، بدون کد جدید برای هر کار.

معایب

  • فراخوانی‌های بیشتر یعنی هزینه و تأخیر بیشتر از یک Workflow ثابت.
  • تستشان سخت‌تر است: مسیر بین اجراها عوض می‌شود، پس به Eval نیاز دارید نه فقط تست واحد.
  • هر ابزار دامنه‌ی خطا را بزرگ‌تر می‌کند؛ متنی در ایمیل یا صفحه‌ی وب ممکن است سعی کند مدل را هدایت کند، پس ابزار محدود و تأیید انسانی مهم است.

قدم‌های بعد

هر فراخوانی ابزار را با آرگومان و نتیجه‌اش لاگ کنید، مجموعه‌ی کوچکی از کارهای واقعی بسازید که بعد از هر تغییر دوباره اجرا شوند، و فقط بعد از آن ابزار بیشتری اضافه کنید. کل مسیر در نقشه‌ی راه AI Engineering آمده و خروجی تایپ‌دار، که هر نتیجه‌ی ابزاری از آن سود می‌برد، در مبانی API مدل‌های زبانی.


دستیاری می‌خواهید که با سیستم‌های شما کار کند ولی تصمیم‌های پرریسک را به آدم‌ها بسپارد؟ ببینید چطور می‌توانم کمک کنم.

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

تفاوت Workflow و ایجنت چیست؟

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

از فریم‌ورک ایجنت استفاده کنم؟

اول یک بار حلقه را دستی بنویسید؛ کوتاه است. بعد فریم‌ورک یا Tool Runner خود SDK می‌تواند کد تکراری را کم کند و می‌دانید دقیقاً چه می‌کند و تأیید، لاگ و محدودیت را کجا اضافه کنید.

چطور جلوی کار آسیب‌زای ایجنت را بگیرم؟

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