Microsoft Agent Framework to open source’owa biblioteka Microsoftu do budowania agentów AI w .NET i Pythonie. Łączy dwa wcześniejsze projekty firmy: Semantic Kernel, z którego przejęła integrację z aplikacjami i gotowość do użycia produkcyjnego, oraz AutoGen, z którego pochodzą abstrakcje agentów i współpracy wielu agentów. Oba projekty są dziś w trybie utrzymania, a nowe funkcje trafiają już tylko do Agent Framework.
W .NET framework opiera się na Microsoft.Extensions.AI, czyli na tych samych abstrakcjach (IChatClient, AIFunction), których używa reszta ekosystemu. Nie narzuca więc konkretnego dostawcy modeli. Agent może korzystać z Azure OpenAI, OpenAI, Claude czy modelu uruchomionego lokalnie, o ile istnieje dla niego implementacja IChatClient.
Framework obejmuje pojedynczych agentów z narzędziami i historią rozmowy, workflowy łączące wielu agentów w jeden proces oraz hosting w ASP.NET Core. Do tego dochodzą paczki pomocnicze, m.in. DevUI, czyli gotowy czat w przeglądarce do testowania agentów podczas developmentu.
Spis treści
Pokaż spis treści
Czym agent różni się od zwykłego zapytania do modelu
Zwykłe zapytanie do modelu językowego jest proste: wysyła się wiadomość, wraca odpowiedź. Model zna tylko to, co widział podczas treningu, co dostał w zapytaniu, i nie może nic sprawdzić ani nic zrobić poza wygenerowaniem tokenów.
Agent dostaje dodatkowo listę narzędzi (tools). Narzędzie to funkcja po stronie aplikacji, opisana nazwą, opisem słownym i schematem parametrów. Ważne jest to, że model nigdy sam niczego nie wykonuje. Nie ma dostępu do kodu aplikacji, do bazy danych ani do internetu. Potrafi tylko wygenerować odpowiedź, a w tej odpowiedzi może się znaleźć specjalny blok z prośbą o wywołanie narzędzia. Całą resztę robi aplikacja, która rozmawia z modelem, często nazywana harnessem.
Jak wygląda wywołanie narzędzia w API Claude
Dobrze to widać na przykładzie. Niech aplikacja udostępnia modelowi narzędzie fireball, czyli zaklęcie kuli ognia rzucane we wskazany cel. W zapytaniu do Messages API, obok treści wiadomości, trafia definicja narzędzia:
{
"model": "claude-haiku-4-5-20251001",
"max_tokens": 1024,
"tools": [
{
"name": "fireball",
"description": "Cast Fireball at a target. The novice has no control over the outcome.",
"input_schema": {
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "What to aim the fireball at"
}
},
"required": ["target"]
}
}
],
"messages": [
{
"role": "user",
"content": "Cast a fireball at this training dummy"
}
]
}
Model nie potrafi sam rzucić zaklęcia, ale z opisu wie, że służy do tego narzędzie fireball. Dlatego zamiast zwykłego tekstu odsyła odpowiedź z blokiem typu tool_use, a pole stop_reason ma wartość tool_use:
{
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "I will try to hit the training dummy." },
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "fireball",
"input": { "target": "training dummy" }
}
]
}
Blok tool_use zawiera nazwę narzędzia, argumenty zgodne ze schematem i unikalny identyfikator wywołania. Na tym rola modelu w tym kroku się kończy. To tylko dane w odpowiedzi HTTP, nic jeszcze się nie wykonało.
Teraz ruch należy do harnessu. Aplikacja odczytuje blok tool_use, znajduje u siebie funkcję o nazwie fireball, wywołuje ją z argumentem "training dummy" i dostaje wynik, czyli opis tego, jak poszło zaklęcie. Ten wynik musi wrócić do modelu, więc aplikacja wysyła kolejne zapytanie. Zawiera ono całą dotychczasową rozmowę, łącznie z odpowiedzią modelu z blokiem tool_use, oraz nową wiadomość z blokiem tool_result:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "You aim at training dummy. You miss. The drapes are on fire now."
}
]
}
Pole tool_use_id łączy wynik z konkretnym wywołaniem. Ma to znaczenie, gdy model poprosi o kilka narzędzi naraz. Dopiero z tym wynikiem model pisze właściwą odpowiedź dla użytkownika, np. przeprosiny za podpalone zasłony. Gdyby potrzebował jeszcze jednego narzędzia, znowu odesłałby tool_use i cykl by się powtórzył.
Agentic loop
Ten cykl nazywa się agentic loop. W skrócie wygląda tak:
- Harness wysyła do modelu historię rozmowy, system prompt i definicje narzędzi.
- Model odpowiada zwykłym tekstem albo blokiem
tool_use. - Jeśli przyszedł
tool_use, harness wywołuje wskazaną funkcję i odsyła wynik w blokutool_result. - Model dostaje wynik i wraca do kroku 2, aż zwróci odpowiedź bez
tool_use.
Bez frameworka całą tę pętlę trzeba napisać ręcznie, zwykle w while: wywołać model, sprawdzić stop_reason, znaleźć właściwą metodę po nazwie, zdeserializować argumenty, wykonać ją, zserializować wynik, dopisać obie wiadomości do historii i wywołać model jeszcze raz. Do tego dochodzi przechowywanie historii rozmowy między kolejnymi pytaniami użytkownika.
Co robi Agent Framework
Agent Framework pełni rolę harnessu i przejmuje całą tę pętlę. Wystarczy zarejestrować agenta przez AddAIAgent(...) i podpiąć mu narzędzia przez WithAITool(...). Framework sam buduje definicje narzędzi z metod C#, rozpoznaje w odpowiedzi blok tool_use, wywołuje właściwą metodę, odsyła wynik jako tool_result i przechowuje historię rozmowy.
Anthropic nie udostępnia do tego żadnego osobnego „agentowego” API. Pod spodem idą te same zwykłe zapytania request-response do Messages API, co przy pojedynczym pytaniu do Claude, z tą samą strukturą bloków co w przykładzie powyżej. Różnica polega na tym, że framework wysyła ich kilka i łączy wyniki. Agent jest więc sposobem korzystania z API, a nie jego nowym rodzajem. Ma to też konsekwencję praktyczną: każdy obrót pętli to osobne wywołanie modelu i osobno liczone tokeny.
Wspomniana na początku abstrakcja IChatClient działa podobnie jak ILogger przy logowaniu: kod aplikacji zna tylko interfejs, a konkretny dostawca (Claude, OpenAI, model lokalny) jest podpinany w jednym miejscu podczas konfiguracji. Zmiana modelu nie wymaga więc ruszania logiki agenta ani narzędzi.
Czego potrzeba
Do zbudowania pierwszego projektu potrzebne są:
- Klucz API do Claude. Należy założyć konto na platform.claude.com, przejść do sekcji API Keys w ustawieniach i utworzyć nowy klucz. Wywołania API są płatne według zużytych tokenów, dlatego do eksperymentów dobrze sprawdza się mały i tani model, taki jak Claude Haiku 4.5.
- Świadomość, że paczki Agent Framework są jeszcze w wersjach preview i alpha. Projekt używa
1.22.0-preview.260918.1dlaMicrosoft.Agents.AI.HostingiMicrosoft.Agents.AI.DevUIoraz1.22.0-alpha.260918.1dlaMicrosoft.Agents.AI.Hosting.OpenAI. Nazwy metod mogą się zmieniać między wersjami, więc przy błędach kompilacji najpierw warto porównać numery paczek.
Założenie projektu
Punktem wyjścia jest pusta aplikacja ASP.NET Core:
dotnet new web -n AgentFrameworkClaudeDemo
cd AgentFrameworkClaudeDemo
Szablon web tworzy projekt na Microsoft.NET.Sdk.Web z minimalnym Program.cs. Aplikacja konsolowa by tu nie wystarczyła, bo DevUI to strona internetowa i potrzebuje serwera HTTP. Ten sam serwer wystawia agenta jako endpoint, więc później można się do niego podłączyć z dowolnego innego klienta, nie tylko z przeglądarki.
Instalacja paczek
Projekt potrzebuje czterech paczek:
dotnet add package Anthropic --version 12.50.0
dotnet add package Microsoft.Agents.AI.Hosting --version 1.22.0-preview.260918.1
dotnet add package Microsoft.Agents.AI.Hosting.OpenAI --version 1.22.0-alpha.260918.1
dotnet add package Microsoft.Agents.AI.DevUI --version 1.22.0-preview.260918.1
Anthropic to oficjalne SDK od Anthropic. Oprócz własnego klienta zawiera adapter do Microsoft.Extensions.AI (metodę AsIChatClient), więc nie jest potrzebna żadna dodatkowa paczka. To jedyne miejsce w projekcie, które wie cokolwiek o Claude.
Microsoft.Agents.AI.Hosting łączy Agent Framework z ASP.NET Core. Daje metodę AddAIAgent, która rejestruje agenta w kontenerze DI tak samo jak każdy inny serwis.
Microsoft.Agents.AI.Hosting.OpenAI wystawia zarejestrowanych agentów przez HTTP w formacie OpenAI Responses i Conversations API. Format pochodzi od OpenAI, ale jest tu tylko protokołem komunikacji z serwerem i nie ma związku z tym, jaki model odpowiada. Dzięki temu do agenta opartego na Claude można się podłączyć każdym klientem, który zna ten format.
Microsoft.Agents.AI.DevUI dostarcza gotowy interfejs w przeglądarce: czat, podgląd zdarzeń i listę narzędzi agenta. Służy do testowania podczas developmentu.
Klucz API poza kodem
Klucza API nie należy wpisywać do appsettings.json ani do kodu, szczególnie gdy repozytorium jest publiczne. Do lokalnego developmentu w .NET służą user secrets:
dotnet user-secrets init
dotnet user-secrets set "Anthropic:ApiKey" "sk-ant-..."
Pierwszy command dopisuje do pliku projektu UserSecretsId. Drugi zapisuje klucz w pliku secrets.json w profilu użytkownika (na Windows w %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\), czyli poza katalogiem repozytorium. WebApplication.CreateBuilder w środowisku Development sam dołącza te wartości do konfiguracji, więc w kodzie odczytuje się je tak samo jak każde inne ustawienie.
Opcjonalnie można ustawić też Anthropic:Model. Gdy tego ustawienia brakuje, projekt używa claude-haiku-4-5-20251001.
Konfiguracja agenta w Program.cs
Cała konfiguracja mieści się w jednym pliku Program.cs, liczącym około 60 linii.
using System.ComponentModel;
using Anthropic;
using Anthropic.Core;
using Microsoft.Agents.AI.DevUI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Extensions.AI;
System.ComponentModel jest potrzebne do atrybutu [Description], który pojawi się przy narzędziu. Pozostałe przestrzenie nazw pochodzą z czterech paczek zainstalowanych wcześniej.
Odczyt konfiguracji
var builder = WebApplication.CreateBuilder(args);
var apiKey = builder.Configuration["Anthropic:ApiKey"]
?? throw new InvalidOperationException(
"No API key. Set it by: dotnet user-secrets set \"Anthropic:ApiKey\" \"<your-key>\"");
var model = builder.Configuration["Anthropic:Model"] ?? "claude-haiku-4-5-20251001";
Ten fragment odpowiada za pobranie klucza API i nazwy modelu. WebApplication.CreateBuilder składa konfigurację z kilku źródeł (plików appsettings, zmiennych środowiskowych, a w Development także user secrets), więc klucz zapisany wcześniej przez dotnet user-secrets set jest tu dostępny bez dodatkowego kodu.
Brak klucza kończy się wyjątkiem już przy starcie aplikacji, z komunikatem podającym dokładny command do wpisania. To wygodniejsze niż błąd 401 z Anthropic API przy pierwszym pytaniu do agenta. Model ma wartość domyślną, więc jego ustawienie jest opcjonalne.
Klient modelu
IChatClient chatClient = new AnthropicClient(new ClientOptions { ApiKey = apiKey })
.AsIChatClient(model);
builder.Services.AddChatClient(chatClient);
To jedyne miejsce w projekcie, które wie, że po drugiej stronie jest Claude. AnthropicClient to klient z oficjalnego SDK. Sam w sobie ma API specyficzne dla Anthropic, dlatego AsIChatClient(model) od razu opakowuje go w standardowy interfejs IChatClient z Microsoft.Extensions.AI. Parametr model ustala, który model Claude obsłuży wszystkie zapytania tego klienta.
AddChatClient rejestruje klienta w kontenerze DI. Agent utworzony kilka linijek niżej nie dostaje klienta wprost, tylko pobiera go z DI. Dzięki temu podmiana Claude na inny model sprowadza się do zmiany tego jednego fragmentu, a definicja agenta i narzędzi się nie zmienia.
System prompt
const string systemPrompt = """
You are a novice mage, a young apprentice assistant. You are helpful, polite,
and a little nervous. Always answer using simple, plain English. Use short
sentences and easy words. Avoid fancy or complex language.
""";
System prompt to instrukcja, którą model dostaje przy każdym zapytaniu, niezależnie od tego, o co pyta użytkownik. Określa rolę agenta i styl odpowiedzi. W tym projekcie agent dostaje personę nerwowego ucznia maga. Taki przykład dobrze pokazuje, jak mocno system prompt wpływa na odpowiedzi. W prawdziwej aplikacji w tym miejscu opisuje się zadanie agenta, jego ograniczenia i oczekiwany format odpowiedzi.
System prompt jest wysyłany do modelu przy każdym wywołaniu, więc jego długość przekłada się bezpośrednio na liczbę tokenów wejściowych, a tym samym na koszt.
Narzędzie fireball
[Description("Cast Fireball at a target. The novice has no control over the outcome.")]
static string Fireball([Description("What to aim the fireball at")] string target)
{
string[] outcomes =
[
$"You aim at {target}. You miss. The drapes are on fire now.",
$"You aim at {target}. You miss. The bread on the windowsill is toasted now.",
$"You aim at {target}. It was the Archmage's coat. It is on fire now.",
$"You aim at {target}. You panic and hit your own reflection instead. Your eyebrows are not fine.",
$"You aim at {target}. You hit it. Everyone is shocked, including you.",
];
return outcomes[Random.Shared.Next(outcomes.Length)];
}
To jest narzędzie, które agent może wywołać. Z punktu widzenia C# to zwykła statyczna metoda: przyjmuje cel i losuje jeden z pięciu wyników. Wybór losowej funkcji jest celowy. Model nie ma jak zgadnąć rezultatu, więc żeby odpowiedzieć, musi faktycznie poprosić o wywołanie narzędzia, a treść jego odpowiedzi zależy od tego, co metoda zwróciła.
Atrybuty [Description] są tu ważniejsze, niż wyglądają. To z nich powstaje pole description w definicji narzędzia, a opis parametru trafia do input_schema, czyli do tych samych pól, które w przykładzie z początku wpisu były wpisane ręcznie w JSON-ie. Model nie widzi kodu metody, tylko te opisy. Na ich podstawie decyduje, kiedy użyć narzędzia i co przekazać w target. Nieprecyzyjny opis sprawia, że model wywołuje narzędzie w złych momentach albo wcale. Zdanie „The novice has no control over the outcome” dodatkowo podpowiada modelowi, że wynik może być zaskakujący.
Rejestracja agenta
builder.AddAIAgent("NoviceMage", systemPrompt)
.WithAITool(AIFunctionFactory.Create(Fireball, name: "fireball"));
Te dwie linijki tworzą agenta i łączą wszystkie poprzednie elementy. AddAIAgent rejestruje w aplikacji agenta o nazwie NoviceMage z podanym system promptem. Agent korzysta z IChatClient zarejestrowanego wcześniej w DI. Nazwa jest identyfikatorem agenta: pojawia się w DevUI i służy do wskazania go w zapytaniach HTTP.
AIFunctionFactory.Create zamienia metodę Fireball na obiekt AIFunction. Przez refleksję odczytuje jej parametry i atrybuty [Description] i buduje z nich definicję narzędzia ze schematem w JSON Schema. Parametr name nadaje narzędziu nazwę fireball, którą model zobaczy i którą poda w bloku tool_use. WithAITool podpina tak przygotowane narzędzie do agenta.
Od tej chwili framework działa jako harness. Dołącza definicję fireball do każdego zapytania, a gdy w odpowiedzi Claude pojawi się blok tool_use z tą nazwą, sam deserializuje argumenty, wywołuje metodę Fireball i odsyła wynik w bloku tool_result.
Endpointy HTTP i DevUI
builder.Services.AddOpenAIResponses();
builder.Services.AddOpenAIConversations();
builder.Services.AddDevUI();
var app = builder.Build();
app.MapOpenAIResponses();
app.MapOpenAIConversations();
if (app.Environment.IsDevelopment())
{
app.MapDevUI();
}
app.Run();
Ostatni fragment odpowiada za wystawienie agenta na zewnątrz. Każda funkcjonalność składa się z dwóch części, jak zwykle w ASP.NET Core: Add... rejestruje usługi w DI, a Map... podpina endpointy do routingu.
AddOpenAIResponses() i MapOpenAIResponses() udostępniają endpoint POST /v1/responses, przez który wywołuje się agenta. AddOpenAIConversations() i MapOpenAIConversations() dodają przechowywanie rozmów po stronie serwera. Każda odpowiedź dostaje identyfikator rozmowy (conversation.id), dzięki czemu kolejne pytania mogą się do niej odnosić bez ponownego wysyłania całej historii przez klienta.
AddDevUI() i MapDevUI() dokładają interfejs w przeglądarce. MapDevUI() jest wywoływane tylko w środowisku Development. DevUI nie ma żadnego uwierzytelniania i pokazuje wszystko, co dzieje się w agencie, więc nie powinien trafić na produkcję.
W całym pliku nie ma pętli agenta ani kodu, który rozpoznaje bloki tool_use, wywołuje metody czy przechowuje historię rozmowy. Tym wszystkim zajmuje się framework.
Uruchomienie i pierwsza rozmowa w DevUI
Aplikację uruchamia się standardowo:
dotnet run
W Properties/launchSettings.json ustawione są "launchBrowser": true i "launchUrl": "devui", a applicationUrl ma wartość https://localhost:7139;http://localhost:5139. Po starcie przeglądarka sama otwiera więc https://localhost:7139/devui/. Przy ręcznym wpisywaniu adresu trzeba pamiętać o końcowym ukośniku, bo /devui bez niego zwraca przekierowanie 301.
Na górze ekranu wybiera się agenta z listy (w tym projekcie jest tylko NoviceMage), a na dole wpisuje wiadomość. W rozmowie poniżej padły dwa pytania: „Who are you?” i „Cast a fireball at this training dummy”.

Lewa część to czat. Odpowiedzi pojawiają się fragment po fragmencie, bo DevUI korzysta ze streamingu. Prawa część, zakładka Events, pokazuje, co działo się pod spodem.
Na pierwsze pytanie model odpowiedział od razu, bo nie potrzebował do tego żadnego narzędzia. Przy drugim zachował się inaczej. Odesłał blok tool_use z nazwą fireball i argumentem {"target":"training dummy"}, dokładnie tak jak w przykładzie z początku wpisu. W panelu Events widać go jako zdarzenie function_call.complete. Identyfikator wywołania zaczyna się od toolu_, bo nadaje go Claude w bloku tool_use. Następnie framework, jako harness, wywołał metodę Fireball w C#. Metoda wylosowała wynik o trafieniu we własne odbicie, a framework odesłał go do Claude w bloku tool_result. Dopiero wtedy model napisał odpowiedź o spalonych brwiach i przepraszał za zamieszanie. Tak wygląda jeden pełny obrót agentic loop.
Pod każdą odpowiedzią DevUI pokazuje zużycie tokenów. Pierwsza odpowiedź kosztowała 641 tokenów wejściowych, choć pytanie miało trzy słowa. Większość to system prompt i definicja narzędzia, które idą do modelu przy każdym wywołaniu. Druga odpowiedź to już 1616 tokenów wejściowych, bo obejmuje historię rozmowy i dwa wywołania modelu: przed użyciem narzędzia i po nim. Ikonka klucza z jedynką oznacza jedno wywołanie narzędzia.
Zakładka Tools pokazuje narzędzia dostępne dla agenta, a Traces szczegóły pojedynczych wywołań. Cały ten interfejs przychodzi z paczką DevUI po dodaniu AddDevUI() i MapDevUI(), bez jednej linijki frontendu.
Sync vs streaming na żywym przykładzie
DevUI to tylko jeden z klientów. Ten sam agent jest dostępny jako zwykły endpoint HTTP POST /v1/responses, zgodny z OpenAI Responses API. Warto go wywołać bezpośrednio, żeby zobaczyć różnicę między odpowiedzią synchroniczną a strumieniowaną.
Agenta wskazuje się w body przez "agent": {"name": "NoviceMage"}, czyli nazwą podaną w builder.AddAIAgent("NoviceMage", ...). Aplikacja może rejestrować wielu agentów, a ten parametr decyduje, który z nich odpowie. Bez niego endpoint zwraca 400:
{
"error": {
"message": "No 'agent.name' or 'metadata[\"entity_id\"]' specified in the request.",
"type": "invalid_request_error",
"code": "missing_required_parameter",
"param": null
}
}
Przykłady poniżej łączą się z http://localhost:5139. HTTPS na porcie 7139 też działa, o ile certyfikat jest zaufany (dotnet dev-certs https --trust).
Wersja synchroniczna
Przy "stream": false serwer czeka, aż agent skończy całą pracę, łącznie z ewentualnymi wywołaniami narzędzi, i dopiero wtedy odsyła odpowiedź. Wersja dla basha:
curl -s http://localhost:5139/v1/responses \
-H "Content-Type: application/json" \
-d '{"agent":{"name":"NoviceMage"},"input":"Who are you? Answer in one sentence.","stream":false}'
PowerShell 7 łamie linie backtickiem, a nie \. Z backslashem potraktowałby -H i -d jako osobne polecenia:
curl -s http://localhost:5139/v1/responses `
-H "Content-Type: application/json" `
-d '{"agent":{"name":"NoviceMage"},"input":"Who are you? Answer in one sentence.","stream":false}'
Odpowiedź to jeden obiekt JSON. Po skróceniu wygląda tak:
{
"object": "response",
"status": "completed",
"agent": { "name": "NoviceMage", "version": "latest" },
"output": [
{
"role": "assistant",
"type": "message",
"content": [
{
"type": "output_text",
"text": "I'm a nervous young apprentice mage who is learning magic and here to help you!"
}
]
}
],
"usage": { "input_tokens": 646, "output_tokens": 22, "total_tokens": 668 }
}
Tekst odpowiedzi znajduje się w output[0].content[0].text. Pole usage pokazuje ten sam efekt co w DevUI: 646 tokenów na wejściu przy jednym krótkim pytaniu, głównie przez system prompt i definicję narzędzia.
Wersja streaming
Wersja strumieniowana różni się tylko wartością "stream": true. Flaga -N wyłącza buforowanie w curlu, dzięki czemu fragmenty pojawiają się w terminalu od razu, a nie dopiero na końcu:
curl -N http://localhost:5139/v1/responses \
-H "Content-Type: application/json" \
-d '{"agent":{"name":"NoviceMage"},"input":"Who are you? Answer in one sentence.","stream":true}'
curl -N http://localhost:5139/v1/responses `
-H "Content-Type: application/json" `
-d '{"agent":{"name":"NoviceMage"},"input":"Who are you? Answer in one sentence.","stream":true}'
Tym razem serwer odsyła strumień zdarzeń w formacie Server-Sent Events. Każde zdarzenie to linia event: z typem i linia data: z JSON-em. Kolejność jest stała: response.created, response.in_progress, response.output_item.added, seria response.output_text.delta z kolejnymi fragmentami tekstu, a na końcu response.completed. Kilka zdarzeń z prawdziwego przebiegu (pola skrócone):
event: response.output_text.delta
data: {"type":"response.output_text.delta","output_index":0,"content_index":0,"delta":"I am a young","sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","output_index":0,"content_index":0,"delta":" and","sequence_number":6}
event: response.output_text.delta
data: {"type":"response.output_text.delta","output_index":0,"content_index":0,"delta":" nervous","sequence_number":7}
Tekst przychodzi w polu delta, zwykle po kilka słów, a sequence_number pozwala ułożyć zdarzenia we właściwej kolejności. Zdarzenie response.output_text.done niesie całe zdanie, a response.completed pełny obiekt odpowiedzi, taki sam jak w wersji synchronicznej.
Kiedy który wariant
Streaming ma sens wszędzie tam, gdzie odpowiedź czyta człowiek. Pierwsze słowa pojawiają się po ułamku sekundy, zamiast po kilku sekundach, gdy model skończy generować całość. Dlatego DevUI zawsze streamuje. Przy wywołaniach narzędzi różnica jest jeszcze większa, bo agent potrzebuje kilku obrotów pętli, zanim zacznie pisać odpowiedź.
Wersja synchroniczna jest prostsza w obsłudze i lepiej pasuje do integracji, w których liczy się tylko finalny wynik: zadań w tle, przetwarzania danych, wywołań z innego serwisu. Jeden obiekt JSON łatwiej zdeserializować i zapisać niż strumień zdarzeń. W obu przypadkach to ten sam endpoint i ten sam agent, a różnica sprowadza się do jednego pola w zapytaniu.
Podsumowanie
Z pustego projektu powstał agent na Claude z jednym narzędziem. Testuje się go w DevUI, a z zewnątrz jest dostępny przez endpoint zgodny z OpenAI Responses API, pod który można podpiąć dowolny frontend albo inny serwis. Wszystko mieści się w jednym Program.cs na 60 linii. Pętlę agenta, obsługę narzędzi, historię rozmów i interfejs czatu zapewnia framework.
Pełny kod: github.com/nowakdawid-blog/AgentFrameworkClaudeDemo.