.NET AI Essentials – Giải thích các khối xây dựng cốt lõi

Trí tuệ nhân tạo (AI) đang thay đổi cách chúng ta xây dựng ứng dụng. Đội ngũ .NET đã ưu tiên bắt kịp những thay đổi nhanh chóng trong lĩnh vực AI tạo sinh (generative AI) và tiếp tục cung cấp các công cụ, thư viện cũng như hướng dẫn cho các nhà phát triển .NET muốn xây dựng ứng dụng thông minh. Với .NET, các nhà phát triển có một hệ sinh thái mạnh mẽ để tích hợp AI một cách liền mạch vào ứng dụng của mình. Bài viết này giới thiệu các khối xây dựng (building blocks) cho AI trong .NET.

Bao gồm:

  • Microsoft.Extensions.AI – truy cập LLM thống nhất
  • Microsoft.Extensions.VectorData – tìm kiếm ngữ nghĩa và lưu trữ embeddings
  • Microsoft Agent Framework – quy trình làm việc theo mô hình agent
  • Model Context Protocol (MCP) – khả năng tương tác (interoperability)

Chúng ta sẽ khám phá từng thư viện với các ví dụ thực tế và mẹo để bắt đầu. Đây là bài đầu tiên trong chuỗi bốn bài viết, mỗi bài sẽ đề cập đến một trong bốn lĩnh vực nêu trên.

Giới thiệu Microsoft.Extensions.AI: một API, nhiều nhà cung cấp

Thư viện nền tảng để kết nối với AI tạo sinh trong .NET là Microsoft Extensions for AI, thường được viết tắt là “MEAI”. Nếu bạn đã quen thuộc với Semantic Kernel, thư viện này sẽ thay thế các nguyên gốc (primitives), tính năng phổ quát và API mà đội ngũ Semantic Kernel từng giới thiệu. Nó cũng tích hợp các mô hình và phương pháp hay đã thành công như dependency injection, middleware và builder pattern mà các nhà phát triển đã quen thuộc trong các công nghệ web như ASP.NET, minimal APIs và Blazor.

Một API thống nhất cho nhiều nhà cung cấp

Thay vì phải xoay xở với nhiều SDK khác nhau, bạn có thể dùng một abstraction duy nhất cho OpenAI, OllamaSharp, Azure OpenAI và nhiều dịch vụ khác. Hãy cùng xem một vài ví dụ đơn giản. Dưới đây là các bước “bắt đầu” với OllamaSharp:

var uri = new Uri("http://localhost:11434");
var ollama = new OllamaApiClient(uri)
{
    SelectedModel = "mistral:latest"
};
await foreach (var stream in ollama.GenerateAsync("How are you today?"))
{
    Console.Write(stream.Response);
}

Nếu bạn dùng OpenAI, đoạn mã sẽ trông như thế này:

OpenAIResponseClient client = new("o3-mini", Environment.GetEnvironmentVariable("OPENAI_API_KEY"));

OpenAIResponse response = await client.CreateResponseAsync(
            [ResponseItem.CreateUserMessageItem("How are you today?")]
  );
foreach (ResponseItem outputItem in response.OutputItems)
{
    if (outputItem is MessageResponseItem message)
    {

        Console.WriteLine($"{message.Content.FirstOrDefault()?.Text}");
    }
}

Để sử dụng các API phổ quát, bạn cấu hình client theo cùng cách, nhưng sau đó bạn có một API chung để gửi yêu cầu. Chat client của OllamaSharp đã hỗ trợ interface phổ quát IChatClient. Còn OpenAI client thì chưa, nên bạn có thể dùng extension method tiện lợi có sẵn trong Open AI adapter.

IChatClient client =
    new OpenAIClient(key).GetChatClient("o3-mini").AsIChatClient();

Giờ bạn có thể dùng cùng một API để nhận phản hồi, bất kể bạn sử dụng nhà cung cấp nào.

await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync("How are you today?"))
{
    Console.Write(update);
}

Không chỉ tiện lợi: điều gì xảy ra đằng sau hậu trường

Bên cạnh việc xử lý ủy quyền (delegation) đến và đi từ nhà cung cấp, các extension phổ quát còn quản lý số lần thử lại (retries), giới hạn token, tích hợp với dependency injection và hỗ trợ middleware. Tôi sẽ đề cập đến middleware ở phần sau của bài viết. Hãy cùng xem một ví dụ về cách các extension này hoạt động để giúp bạn đơn giản hóa mã cần viết.

Structured output một cách cực kỳ dễ dàng

Structured output cho phép bạn chỉ định một schema cho dữ liệu trả về. Điều này không chỉ giúp bạn xử lý đầu ra mà không cần phải tự parse phản hồi, mà còn cung cấp thêm ngữ cảnh về đầu ra mong đợi cho mô hình. Dưới đây là một ví dụ sử dụng Open AI SDK:

class Family
{
    public List<Person> Parents { get; set; }
    public List<Person>? Children { get; set; }

    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

ChatCompletionOptions options = new()
{
    ResponseFormat = StructuredOutputsExtensions.CreateJsonSchemaFormat<Family>("family", jsonSchemaIsStrict: true),
    MaxOutputTokenCount = 4096,
    Temperature = 0.1f,
    TopP = 0.1f
};

List<ChatMessage> messages =
[
    new SystemChatMessage("You are an AI assistant that creates families."),
    new UserChatMessage("Create a family with 2 parents and 2 children.")
];

ParsedChatCompletion<Family?> completion = chatClient.CompleteChat(messages, options);
Family? family = completion.Parsed;

Giờ hãy làm điều tương tự, nhưng lần này sử dụng các extension cho AI:

class Family
{
    public List<Person> Parents { get; set; }
    public List<Person>? Children { get; set; }

    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

var family = await client.GetResponseAsync<Family>(
    [
        new ChatMessage(
            ChatRole.System,
            "You are an AI assistant that creates families."),
        new ChatMessage(
            ChatRole.User,
            "Create a family with 2 parents and 2 children."
        )]);

Extension method có kiểu (typed) sử dụng các adapter để cung cấp schema phù hợp và thậm chí tự động parse cũng như deserialize phản hồi cho bạn.

Yêu cầu và phản hồi được chuẩn hóa

Bạn đã bao giờ muốn thay đổi temperature của một mô hình chưa? Một số bạn có thể hỏi: “Temperature là gì?” Trong “thế giới thực”, temperature là thước đo năng lượng của một hệ thống. Nếu các hạt đứng yên, hay bị đóng băng tại chỗ, thì đó là trạng thái lạnh. Nhiệt nghĩa là có rất nhiều năng lượng và chuyển động, ít nhất là ở cấp độ vi mô. Nhiệt độ ảnh hưởng đến entropy – thước đo của sự ngẫu nhiên. Khái niệm tương tự cũng áp dụng cho các mô hình.

Nếu bạn còn nhớ từ bài giới thiệu, các mô hình về cơ bản là những cỗ máy xác suất khổng lồ tung xúc xắc – đại loại vậy. Nếu bạn đặt temperature thấp, mô hình sẽ tạo ra các phản hồi dễ đoán hơn (và thường chính xác hơn), trong khi temperature cao hơn sẽ đưa thêm sự ngẫu nhiên vào phản hồi. Bạn có thể hình dung điều này giống như việc cho phép các phản hồi có xác suất thấp hơn có tiếng nói lớn hơn trong phương trình, điều này có thể dẫn đến các phản hồi không có căn cứ (khi mô hình đưa ra thông tin không chính xác hoặc lệch ngữ cảnh) nhưng cũng có thể tạo ra các phản hồi trông “sáng tạo” hơn.

Các tác vụ mang tính tất định (deterministic) hơn như phân loại và tóm tắt có lẽ sẽ được hưởng lợi từ temperature thấp hơn, trong khi việc brainstorm ý tưởng cho một chiến dịch marketing có thể được hưởng lợi từ temperature cao hơn. Vấn đề cốt lõi là: các mô hình cho phép bạn tinh chỉnh mọi thứ từ temperature đến số token tối đa, và tất cả đều được chuẩn hóa trong class ChatOptions.

Ở chiều ngược lại, khi bạn nhận được một phản hồi, phản hồi đó chứa một instance UsageDetails. Hãy dùng nó để theo dõi số lượng token của bạn.

Middleware ở đâu?

Các nhà phát triển web .NET đã quen thuộc với sức mạnh của middleware. Hãy nghĩ về nó như một mô hình plugin cho quy trình làm việc của bạn. Trong trường hợp này, quy trình làm việc hay pipeline chính là tương tác với mô hình. Middleware cho phép bạn can thiệp vào pipeline và thực hiện những việc như:

  • Chặn nội dung độc hại trước khi đến mô hình
  • Chặn hoặc giới hạn (throttle) các yêu cầu
  • Cung cấp các dịch vụ như telemetry và tracing

MEAI cung cấp sẵn middleware cho telemetry và tracing. Bạn có thể sử dụng builder pattern quen thuộc để áp dụng middleware vào một chat client hiện có. Dưới đây là một phương thức ví dụ thêm middleware vào bất kỳ client hiện có nào – bất kể nhà cung cấp – để xử lý cả việc ghi log và tạo sự kiện Open Telemetry (OTEL).

public IChatClient BuildEnhancedChatClient(
            IChatClient innerClient,
            ILoggerFactory? loggerFactory = null)
        {
            var builder = new ChatClientBuilder(innerClient);

            if (loggerFactory is not null)
            {
                builder.UseLogging(loggerFactory);
            }

            var sensitiveData = false; // true for debugging

            builder.UseOpenTelemetry(
                configure: options =
                    options.EnableSensitiveData = sensitiveData);
            return builder.Build();
        }

Các sự kiện OTEL có thể được gửi đến một dịch vụ đám mây như Application Insights hoặc, nếu bạn dùng Aspire, tới Aspire dashboard của bạn. Aspire đã được cập nhật để cung cấp thêm ngữ cảnh cho các ứng dụng thông minh. Hãy tìm các biểu tượng “sparkles” (hình ngôi sao lấp lánh) trong dashboard để xem các trace liên quan đến tương tác với mô hình.

DataContent cho các cuộc hội thoại đa phương thức (Multi-Modal)

Các mô hình ngày nay không chỉ đơn thuần trao đổi văn bản qua lại. Ngày càng có nhiều mô hình đa phương thức được phát hành, có thể tiếp nhận dữ liệu ở nhiều định dạng khác nhau, bao gồm hình ảnh và âm thanh, đồng thời trả về các dữ liệu tương tự. Mặc dù hầu hết các ví dụ về MEAI tập trung vào tương tác dạng văn bản – được biểu diễn bằng các instance TextContent – vẫn có một số loại nội dung tích hợp sẵn, tất cả đều dựa trên AIContent:

  • ErrorContent – thông tin lỗi chi tiết kèm mã lỗi
  • UserInputRequestContent – yêu cầu người dùng nhập liệu (bao gồm FunctionApprovalRequestContent và FunctionApprovalResponseContent)
  • FunctionCallContent – biểu diễn một yêu cầu gọi công cụ (tool request)
  • HostedFileContent – tham chiếu dữ liệu được lưu trữ bởi một dịch vụ AI chuyên biệt
  • UriContent – tham chiếu web

Đây không phải là danh sách đầy đủ, nhưng bạn đã hình dung được ý tưởng. Loại bạn có khả năng sử dụng nhiều nhất, tuy nhiên, là DataContent – loại có thể biểu diễn hầu hết mọi loại media. Nó chỉ đơn giản là một mảng byte kèm theo loại media (media type).

Ví dụ, giả sử tôi muốn gửi bức ảnh của mình cho một mô hình kèm theo chỉ dẫn mô tả ảnh và cung cấp danh sách các tag. Giả sử ảnh của tôi được lưu tại c:\photo.jpg, tôi có thể làm như sau:

var instructions = "You are a photo analyst able to extract the utmost detail from a photograph and provide a description so thorough and accurate that another LLM could generate almost the same image just from your description.";

var prompt = new TextContent("What's this photo all about? Please provide a detailed description along with tags.");

var image = new DataContent(File.ReadAllBytes(@"c:\photo.jpg"), "image/jpeg");

var messages = new List<ChatMessage>
            {
                new(ChatRole.System, instructions),
                new(ChatRole.User, [prompt, image])
            };

record ImageAnalysis(string Description, string[] tags);

var analysis = await chatClient.GetResponseAsync<ImageAnalysis>(messages);

Một số điểm nổi bật khác

Mặc dù nằm ngoài phạm vi bài viết này, vẫn có nhiều dịch vụ khác mà các extension nền tảng cung cấp. Ví dụ bao gồm:

  • Cancellation tokens cho các ứng dụng phản hồi nhanh
  • Xử lý lỗi và khả năng phục hồi (resilience) tích hợp sẵn
  • Các nguyên gốc (primitives) để xử lý vector và embeddings
  • Tạo hình ảnh (image generation)

Tổng kết

Trong bài viết này, chúng ta đã khám phá khối xây dựng nền tảng cho các ứng dụng thông minh trong .NET: Microsoft Extensions for AI. Trong bài viết tiếp theo, tôi sẽ hướng dẫn bạn về các extension liên quan đến vector và giải thích vì sao chúng không nằm trong mô hình cốt lõi, sau đó chúng ta sẽ tiếp tục với agent framework và MCP.

Trong lúc chờ đợi, tôi có một vài lựa chọn để bạn tìm hiểu thêm và bắt đầu xây dựng các ứng dụng thông minh của mình.

Chúc bạn lập trình vui vẻ!