Към съдържанието

Да създадем първия си Agent с .NET

В предишната секция написахме Agent на ръка и видяхме колко ръчна работа е това. Сега ще изградим същото с библиотека, която поема тежката работа вместо нас.

В този урок ще създадеш първия си истински Agent: Alfred, който работи изцяло на твоя компютър и може да извиква C# методи, за да отговаря на въпроси.

Какво е Microsoft.Extensions.AI?

Секция „Какво е Microsoft.Extensions.AI?“

Microsoft.Extensions.AI е библиотеката на Microsoft за работа с LLMs в .NET. Тя е малка, но покрива точно нещата, които в предишния урок писахме сами:

В Dummy Agent го писахме сами С Microsoft.Extensions.AI
Ollama.cs: заявки към модела с HttpClient IChatClient: един интерфейс за всеки доставчик на LLM
ChatMessage.cs: наш record за един message ChatMessage: готов тип със същото име
Описание на tool-а, написано на ръка в system prompt-а AIFunctionFactory: прави tool от обикновен C# метод
Tools.Run и while цикълът UseFunctionInvocation(): изпълнява целия цикъл автоматично

Има и една разлика под повърхността. Нашият dummy Agent беше JSON Agent: молехме модела да пише Action: {...} в текста и сами го парсвахме. Библиотеката използва function calling (виж Actions): моделът връща извикването на tool като структурирани данни, така че няма формат, който да се обърка.

Ще ни трябват Ollama и моделът от предишния урок. Увери се, че Ollama работи, преди да започнеш.

Отвори терминал и изпълни:

Терминал
dotnet new console -n FirstAgent
cd FirstAgent
dotnet add package Microsoft.Extensions.AI
dotnet add package OllamaSharp

Първите две команди създават проекта, а другите две добавят двата NuGet пакета. OllamaSharp е клиентът за Ollama. Той реализира интерфейса IChatClient, така че останалият код не зависи от Ollama. Ако някой ден смениш доставчика на модела, променяш един ред.

Отвори папката FirstAgent в редактора си. Ще работим с три файла.

Създай файл AlfredTools.cs. В него е единственият tool на Alfred: метод, който връща текущия час в дадена часова зона.

AlfredTools.cs
using System.ComponentModel;
public static class AlfredTools
{
[Description("Gets the current local time in a given timezone.")]
public static string GetCurrentTime(
[Description("An IANA timezone name, for example 'Europe/Sofia'.")]
string timezone)
{
try
{
TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(timezone);
DateTime now = TimeZoneInfo.ConvertTime(DateTime.UtcNow, zone);
return $"The current time in {timezone} is {now:yyyy-MM-dd HH:mm:ss}";
}
catch (TimeZoneNotFoundException)
{
return $"Unknown timezone '{timezone}'.";
}
}
}

Три неща правят един метод добър tool:

  • Атрибут [Description] върху метода и върху всеки параметър. Това е всичко, което моделът знае за tool-а (виж Tools).
  • Ясни типове на параметрите и на резултата.
  • Грешките се връщат като текст. Ако моделът подаде невалидна часова зона, той ще прочете съобщението като Observation и може да опита отново.

Отвори Program.cs, изтрий съдържанието му и сложи следното:

Program.cs
using Microsoft.Extensions.AI;
using OllamaSharp;
const string Model = "qwen3.5:9b";
const string SystemPrompt =
"You are Alfred, a helpful and polite AI assistant. " +
"Use your tools when they can help and never invent their results.";
IChatClient model = new OllamaApiClient(new Uri("http://localhost:11434"), Model);
IChatClient agent = new ChatClientBuilder(model)
.UseFunctionInvocation()
.Build();
var options = new ChatOptions
{
Temperature = 0,
Tools = [AIFunctionFactory.Create(AlfredTools.GetCurrentTime)],
};
List<ChatMessage> history =
[
new(ChatRole.System, SystemPrompt),
new(ChatRole.User, "What time is it in Tokyo right now?"),
];
ChatResponse response = await agent.GetResponseAsync(history, options);
AgentLog.PrintSteps(response);
Console.WriteLine($"Alfred: {response.Text}");

Кодът има четири части:

  1. Моделът. OllamaApiClient е връзката с локалния модел, достъпна през интерфейса IChatClient. Той заема мястото на нашия Ollama.cs.
  2. Agent-ът. UseFunctionInvocation() обвива модела и добавя цикъла Thought → Action → Observation. Когато моделът поиска tool, библиотеката го извиква, връща резултата и пита модела отново, докато той не даде финален отговор. За да не се върти безкрайно, цикълът има лимит от 40 обиколки на заявка.
  3. Tools. AIFunctionFactory.Create чете името на метода, атрибутите [Description] и типовете на параметрите и ги подава на модела.
  4. Разговорът. history е списъкът от messages: system prompt-ът и един въпрос. GetResponseAsync го изпраща на Agent-а и връща отговора.

Редът AgentLog.PrintSteps(response) още не се компилира, защото този метод не съществува. Ще го напишем в следващата стъпка.

4. Покажи стъпките на цикъла

Секция „4. Покажи стъпките на цикъла“

Библиотеката изпълнява целия цикъл вътре в GetResponseAsync. За да видим какво се е случило, ще отпечатаме стъпките, през които Agent-ът е минал. Създай файл AgentLog.cs:

AgentLog.cs
using System.Text.Json;
using Microsoft.Extensions.AI;
public static class AgentLog
{
public static void PrintSteps(ChatResponse response)
{
foreach (AIContent content in response.Messages.SelectMany(m => m.Contents))
{
switch (content)
{
case TextReasoningContent thought:
Console.WriteLine($" Thought: {thought.Text}");
break;
case FunctionCallContent call:
string arguments = JsonSerializer.Serialize(call.Arguments);
Console.WriteLine($" Action: {call.Name}({arguments})");
break;
case FunctionResultContent result:
Console.WriteLine($" Observation: {result.Result}");
break;
}
}
}
}

response.Messages съдържа всичко, което Agent-ът е произвел по пътя към отговора. Всяка стъпка от цикъла идва като отделен .NET тип:

Стъпка Тип в Microsoft.Extensions.AI
Thought TextReasoningContent
Action FunctionCallContent
Observation FunctionResultContent
Финален отговор TextContent (достъпен и през response.Text)

5. Стартирай програмата

Секция „5. Стартирай програмата“

В терминала, в папката FirstAgent, изпълни:

Терминал
dotnet run

изход:

Thought: The user is asking for the current time in Tokyo. I need to use the GetCurrentTime function with the appropriate timezone for Tokyo. Tokyo is in the Asia/Tokyo timezone.
Action: GetCurrentTime({"timezone":"Asia/Tokyo"})
Observation: The current time in Asia/Tokyo is 2026-11-12 20:32:31
Thought: The function returned the current time in Tokyo as 2026-11-12 20:32:31. I should present this information clearly to the user.
Alfred: The current time in Tokyo is November 12, 2026 at 8:32:31 PM (20:32:31).

При теб часът и точните думи ще са различни, но стъпките ще са същите. Ето какво се случи вътре в GetResponseAsync:

  1. Библиотеката изпрати на модела разговора заедно с описанието на GetCurrentTime.
  2. Моделът поиска tool-а с аргумент Asia/Tokyo. Това е редът Action.
  3. Библиотеката извика истинския метод AlfredTools.GetCurrentTime и върна резултата на модела. Това е редът Observation.
  4. Моделът прочете резултата и написа финалния отговор.

Това е същият цикъл от Dummy Agent Library, но в нашия код няма нито stop, нито парсване, нито while.

6. Превърни го в разговор

Секция „6. Превърни го в разговор“

Засега Alfred отговаря на един въпрос, записан в кода. В Program.cs замени всичко от реда List<ChatMessage> history надолу със следното:

List<ChatMessage> history = [new(ChatRole.System, SystemPrompt)];
Console.WriteLine("Alfred is ready. Ask a question (or press Enter to exit).");
while (true)
{
Console.Write("\nYou: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
break;
}
history.Add(new ChatMessage(ChatRole.User, input));
ChatResponse response = await agent.GetResponseAsync(history, options);
history.AddMessages(response);
AgentLog.PrintSteps(response);
Console.WriteLine($"Alfred: {response.Text}");
}

Този while не е цикълът на Agent-а. Той само чете въпроси от конзолата. Важните редове са двата с history: моделът не помни нищо между заявките, затова добавяме в списъка всеки въпрос и всичко, което Agent-ът е отговорил.

Изпълни dotnet run и задай на Alfred три въпроса:

Alfred is ready. Ask a question (or press Enter to exit).
You: What time is it in Tokyo right now?
Thought: The user is asking for the current time in Tokyo. I need to use the GetCurrentTime function with the appropriate timezone for Tokyo. Tokyo is in the Asia/Tokyo timezone.
Action: GetCurrentTime({"timezone":"Asia/Tokyo"})
Observation: The current time in Asia/Tokyo is 2026-11-12 20:33:16
Thought: The function returned the current time in Tokyo. I should present this information clearly to the user.
Alfred: The current time in Tokyo is 8:33 PM on November 12, 2026 (20:33:16).
You: And in Sofia?
Thought: The user is asking for the current time in Sofia. I need to use the GetCurrentTime function with the timezone "Europe/Sofia" since Sofia is in Bulgaria which uses the Europe/Sofia timezone.
Action: GetCurrentTime({"timezone":"Europe/Sofia"})
Observation: The current time in Europe/Sofia is 2026-11-12 13:33:43
Thought: The function returned the current time in Sofia as 2026-11-12 13:33:43. I should present this information clearly to the user, converting it to a more readable format if needed.
Alfred: The current time in Sofia is 1:33 PM on November 12, 2026 (13:33:43).
You: Hello, who are you?
Thought: The user is asking me who I am. I should introduce myself as Alfred, a helpful and polite AI assistant. This doesn't require any tool calls - it's just a conversational response.
Alfred: Hello! I'm Alfred, your helpful and polite AI assistant. How can I help you today?

Обърни внимание на две неща:

  • Вторият въпрос е само „And in Sofia?“. Alfred го разбира, защото заедно с него получава целия разговор от history.
  • За третия въпрос не е нужен tool, затова Alfred отговаря директно, без Action. Моделът сам преценява кога да използва tools.

Alfred има само един tool. Най-добрият начин да учиш е да му дадеш още. За начало добави в класа AlfredTools метода Calculator от секцията за Tools:

[Description("Multiply two integers.")]
public static int Calculator(int a, int b) => a * b;

и го регистрирай в Program.cs:

Tools =
[
AIFunctionFactory.Create(AlfredTools.GetCurrentTime),
AIFunctionFactory.Create(AlfredTools.Calculator),
],

Попитай „What is 123 times 456?“ и ще видиш Action: Calculator({"a":123,"b":456}), последван от Observation: 56088.

След това опитай със свои идеи: tool, който чете файл, търси в интернет с HttpClient или проверява времето през публичен API.


Поздравления, изгради първия си Agent! 🎉

Нормално е в началото той да е малко бавен или да греши. В следващите Units ще научим как да изграждаме по-добри Agents.

В следващата секция те очаква финалният quiz за Unit 1.