DEV Community

Cover image for Building Production-Ready AI Agents with Semantic Kernel
Sebastian Van Rooyen
Sebastian Van Rooyen Subscriber

Posted on

Building Production-Ready AI Agents with Semantic Kernel

If you’re looking to build a production-ready AI agent with Semantic Kernel, I can help. Let’s chat.

Semantic Kernel is one of the most pragmatic ways to build agentic AI in the .NET world. It gives you a first-class abstraction for prompts, tools/functions, memory and orchestration — without tying you to a single monolithic UI or brittle glue code. In this post I’ll explain why it’s powerful, compare it to other approaches, and show a clean, production-ready .NET architecture that follows SOLID principles, separation of concerns, and demonstrates how to wire a WhatsApp webhook → agent flow.

TL;DR: Semantic Kernel = prompts + tool calling + memory + orchestration. Combine that with a properly layered .NET app and you get maintainable, testable, deployable agents.


Why Semantic Kernel? (short & practical)

  • First-class function/tool calling — expose C# functions to the model as “tools” and let the model call them. That makes grounding, accuracy and deterministic side effects easier.
  • Memory primitives — store short/long term memory pieces and retrieve them for context.
  • Composable prompts / semantic functions — encapsulate prompts with parameters and reuse them.
  • Language-model agnostic — works with OpenAI, Azure OpenAI, and others (via connectors).
  • Designed for agents — not just single-prompt Q&A; it supports planners and multi-step workflows.

Compared to other frameworks:

  • Plain SDK usage (raw OpenAI SDK): great for one-off prompts but you must build your own tooling, prompt management and tool integration.
  • RAG libraries (LangChain, LlamaIndex): great for retrieval and pipelines — Semantic Kernel provides more opinionated tooling for function calling and memory especially tailored for .NET.
  • Full agent platforms (closed SaaS): those can be easier for non-devs but often lock you in. SK keeps you in code and composable.

Project structure (what we’ll build)

NexfluenceAgent/ ├─ src/ │ ├─ Nexfluence.Api/ # Web API (webhook) │ ├─ Nexfluence.Core/ # Domain models & interfaces │ ├─ Nexfluence.Services/ # Agent service, prompt service │ └─ Nexfluence.Plugins/ # Business plugin (catalog, FAQs) ├─ tests/ └─ dockerfile 
Enter fullscreen mode Exit fullscreen mode

We’ll show key parts: DI, controller, service, plugin. All code is idiomatic .NET 8 and uses async.


SOLID in practice — design goals

  • Single Responsibility: each class has one reason to change (controller: HTTP; service: agent orchestration; plugin: business facts).
  • Open/Closed: add new tools/skills via plugins without editing core logic.
  • Liskov: services implement interfaces and can be replaced by mocks in tests.
  • Interface Segregation: small focused interfaces (IAgentService, IPromptBuilder).
  • Dependency Inversion: high level modules depend on abstractions (interfaces), not concrete SK types.

Key code snippets

Note: these are compacted for clarity — treat them as copy-pasteable starting points.

Program.cs — wiring DI, Kernel, resilience, and web API

// Program.cs (minimal) using Microsoft.SemanticKernel; using Microsoft.Extensions.Caching.Memory; using Nexfluence.Core; using Nexfluence.Services; using Nexfluence.Plugins; var builder = WebApplication.CreateBuilder(args); // Configuration & secrets var openAiKey = builder.Configuration["OpenAI:ApiKey"] ?? throw new InvalidOperationException("Missing OpenAI key"); // 1) Create Kernel (DI-friendly) var kernel = Kernel.Builder .WithOpenAIChatCompletionService(modelId: "gpt-4o-mini", apiKey: openAiKey) .Build(); kernel.Plugins.AddFromType<BusinessPlugin>("business"); builder.Services.AddSingleton<IKernel>(kernel); // 2) App services (DI) builder.Services.AddMemoryCache(); builder.Services.AddScoped<IAgentService, SemanticKernelAgentService>(); builder.Services.AddScoped<IPromptBuilder, PromptBuilder>(); // 3) Observability & resiliency recommendations // - Use ILogger<T> everywhere (provided by builder) // - Consider Polly policies for outbound requests (e.g. OpenAI, Twilio) // - Add health checks / metrics builder.Services.AddControllers(); var app = builder.Build(); app.MapControllers(); app.Run(); 
Enter fullscreen mode Exit fullscreen mode

Domain interfaces (Nexfluence.Core)

// IAgentService.cs public interface IAgentService { Task<string> HandleMessageAsync(string sessionId, string userMessage, CancellationToken ct = default); } // IPromptBuilder.cs public interface IPromptBuilder { string BuildSystemPrompt(); string BuildUserPrompt(string userMessage); } 
Enter fullscreen mode Exit fullscreen mode

Agent service — orchestrates SK calls (Single Responsibility + DIP)

// SemanticKernelAgentService.cs using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Orchestration; public class SemanticKernelAgentService : IAgentService { private readonly IKernel _kernel; private readonly IPromptBuilder _promptBuilder; private readonly ILogger<SemanticKernelAgentService> _log; private readonly IMemoryCache _cache; public SemanticKernelAgentService(IKernel kernel, IPromptBuilder promptBuilder, ILogger<SemanticKernelAgentService> log, IMemoryCache cache) { _kernel = kernel; _promptBuilder = promptBuilder; _log = log; _cache = cache; } public async Task<string> HandleMessageAsync(string sessionId, string userMessage, CancellationToken ct = default) { // Simple per-session memory example (could be replaced with persistent store) var memoryKey = $"session:{sessionId}:history"; var history = _cache.GetOrCreate(memoryKey, entry => { entry.SlidingExpiration = TimeSpan.FromMinutes(30); return new List<string>(); }); history.Add(userMessage); _cache.Set(memoryKey, history); // Build system + user prompt var system = _promptBuilder.BuildSystemPrompt(); var userPrompt = _promptBuilder.BuildUserPrompt(userMessage); // Create semantic function (encapsulated prompt) var prompt = $"{system}\n\nUser: {userPrompt}"; // Use the Kernel chat completion service var chat = _kernel.GetService<IChatCompletion>(); var result = await chat.GetChatMessageAsync(prompt, ct: ct); // If SK supports tool/function calling, prefer that path by configuring execution settings var reply = result?.Content ?? "Sorry, I couldn't process that right now."; _log.LogInformation("Agent reply: {Reply}", reply); // Store assistant reply in history history.Add(reply); _cache.Set(memoryKey, history); return reply; } } 
Enter fullscreen mode Exit fullscreen mode

Note: GetChatMessageAsync is illustrative — adapt to the exact SK API in your version (e.g., IChatCompletionService.GetChatMessageContentAsync with ChatHistory if available).

Prompt builder — interface segregation & single responsibility

// PromptBuilder.cs public class PromptBuilder : IPromptBuilder { public string BuildSystemPrompt() => "You are Nexfluence's WhatsApp assistant. Be helpful, concise, and prefer business plugin for factual answers."; public string BuildUserPrompt(string userMessage) => userMessage.Trim(); } 
Enter fullscreen mode Exit fullscreen mode

Business plugin (expose facts as functions) — Open/Closed: add more functions without changing core

// BusinessPlugin.cs using Microsoft.SemanticKernel; using System.ComponentModel; public class BusinessPlugin { private static readonly List<Product> Catalog = new() { new("Blue Hoodie","S",29.99m), new("Blue Hoodie","M",29.99m), new("Black T-Shirt","M",14.99m), }; [KernelFunction("list_products")] [Description("List product names.")] public Task<List<string>> ListProductsAsync() => Task.FromResult(Catalog.Select(p => p.Name).Distinct().ToList()); [KernelFunction("get_price")] [Description("Get price by name and size.")] public Task<decimal?> GetPriceAsync(string name, string size) { var item = Catalog.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && p.Size.Equals(size, StringComparison.OrdinalIgnoreCase)); return Task.FromResult(item?.Price); } public record Product(string Name, string Size, decimal Price); } 
Enter fullscreen mode Exit fullscreen mode

Controller — contract, validation, and Twilio-compatible webhook (SRP + testable)

// WhatsAppWebhookController.cs using Microsoft.AspNetCore.Mvc; using Twilio.TwiML; using Nexfluence.Core; [ApiController] [Route("api/[controller]")] public class WhatsAppWebhookController : ControllerBase { private readonly IAgentService _agentService; private readonly ILogger<WhatsAppWebhookController> _log; public WhatsAppWebhookController(IAgentService agentService, ILogger<WhatsAppWebhookController> log) { _agentService = agentService; _log = log; } [HttpPost] public async Task<IActionResult> Receive([FromForm] string Body, [FromForm] string From, CancellationToken ct) { if (string.IsNullOrWhiteSpace(Body) || string.IsNullOrWhiteSpace(From)) return BadRequest(new { error = "Body and From are required" }); var sessionId = From; // simple session mapping var reply = await _agentService.HandleMessageAsync(sessionId, Body, ct); var twiml = new MessagingResponse(); twiml.Message(reply); return Content(twiml.ToString(), "application/xml"); } } 
Enter fullscreen mode Exit fullscreen mode

Production readiness checklist

Below are recommended (and mostly simple) steps to make this production-ready:

  1. Secure secrets
  • Use managed secret stores: Azure Key Vault / AWS Secrets Manager. Don’t store API keys in appsettings.json in prod.
  1. Request validation & input sanitization
  • Validate incoming webhook (Twilio signature validation). Reject unknown callers.
  1. Resiliency
  • Wrap outbound calls to language models with Polly policies: retry + exponential backoff + circuit breaker.
  1. Observability
  • Use ILogger everywhere. Add structured logging. Add metrics (Prometheus, App Insights).
  • Log prompts and responses selectively (avoid logging PII).
  1. Rate limiting & quotas
  • Prevent abuse: per-session and per-phone number throttling.
  1. Cost control
  • Limit token usage per user, cache frequent answers, and use shorter models for basic FAQs, switching to larger ones only when needed.
  1. Testing
  • Unit test IAgentService by mocking IKernel or by using a test double.
  • Integration tests for the plugin functions and webhook endpoints.
  1. CI/CD & containerization
  • Dockerfile + GitHub Actions / Azure Pipelines.
  • Deploy behind an API gateway; use HTTPS and validate TLS.
  1. Data persistence
  • Replace IMemoryCache session store with durable storage (Redis, Cosmos DB, Postgres) if you want long-lived memory across restarts.
  1. Privacy & Compliance
* Add consent flows if you store customer data. Provide data deletion APIs. 
Enter fullscreen mode Exit fullscreen mode

Example: Twilio signature validation (security)

// Validate Twilio signature before processing webhook (pseudo) public bool ValidateTwilioRequest(HttpRequest request, string twilioAuthToken) { var signature = request.Headers["X-Twilio-Signature"].FirstOrDefault(); var url = $"{request.Scheme}://{request.Host}{request.Path}"; var form = request.HasFormContentType ? request.Form.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()) : new Dictionary<string,string>(); return Twilio.Security.RequestValidator.Validate(url, form, signature, twilioAuthToken); } 
Enter fullscreen mode Exit fullscreen mode

Small note on costs & model selection

For FAQ and short replies use cheaper models or instruction-tuned smaller models. For multi-step planners or creative tasks, escalate to a larger model. You can implement model routing in IAgentService based on intent.


Final words — how to extend

  • Add cart and order plugins (tools that perform DB writes and trigger downstream workflows).
  • Add a human handoff skill (if confidence low, forward to human agent).
  • Add analytics: conversation funnels, deflection rate, average response time.

Summary

Semantic Kernel + clean .NET architecture gives you:

  • composable AI agents,
  • clear separation of concerns,
  • testable services,
  • and a path to production with standard engineering practices.

Top comments (0)