Building a Production RAG Chatbot in C# with Semantic Kernel
- Authors

- Name
- Nino
- Occupation
- Senior Tech Editor
Most RAG tutorials end after a 30-minute demonstration. You build a clean prototype using local sample text, verify that it answers basic questions, and consider the project finished. However, when deployed to production, real-world complexity takes over: tenant data leaks across boundaries because security trimming is absent, hallucination rates spike to 22% due to ungrounded prompts, and response latency degrades beyond 4 seconds because document chunking was improperly calibrated.
Building a enterprise-grade Retrieval-Augmented Generation (RAG) assistant in C# requires solving critical operational challenges: multi-tenant access control, hybrid retrieval precision, prompt grounding, streaming performance, and low-latency response caching.
This guide breaks down the full production architecture of a enterprise help center assistant built on .NET 9 and Angular 19. Serving over 8,400 monthly active users across 3,200 indexed documents (~24,000 vector chunks), this system maintains a 4% hallucination rate, a p95 retrieval latency of 95 ms, and an average operational cost of $0.004 per query. Developers looking to benchmark models or access multi-provider enterprise endpoints can leverage aggregators such as n1n.ai for reliable API routing.
The Production RAG Stack
The following table highlights the core architectural choices that separate a production-ready RAG application from a simple demo project:
| Layer | Production Choice | Technical Rationale |
|---|---|---|
| Orchestrator | Microsoft Semantic Kernel 1.x | First-class .NET support, native dependency injection, OpenTelemetry tracing. |
| Vector Index | Azure AI Search | Native hybrid search (BM25 + vector), OData security trimming, semantic reranker. |
| LLM Provider | Azure OpenAI / n1n.ai | Enterprise SLA, managed identity support, high-throughput model routing. |
| Embeddings | text-embedding-3-small | 1,536 dimensions; optimal balance between accuracy, storage cost, and speed. |
| Chunking | 400–600 tokens / 80 overlap | Prevents context loss across boundaries without diluting vector relevance. |
| Retrieval | Hybrid Search + Semantic Reranker | Combines exact keyword matching with dense vector recall and L2 cross-encoder reranking. |
| Grounding | Enforced Prompt Rules + Citations | Eliminates external memory confabulation; drops hallucination rates from 22% to 4%. |
| Streaming | Server-Sent Events (SSE) | Delivers a Time-To-First-Token (TTFT) < 500 ms for superior user experience. |
| Caching | Redis (Tenant Key Hash, 60s TTL) | Achieves a 34% cache hit rate, cutting latency and LLM token expenditures. |
| Evaluation | 80-Question Golden Set | Nightly automated CI runs catch regression in recall and grounding accuracy. |
Architectural Deep Dive
1. Security Trimming and Multi-Tenant Index Schema
In a multi-tenant SaaS application, preventing data leakage across tenant boundaries is mandatory. Security trimming must occur at the vector database level during query execution rather than as a post-retrieval application filter.
In Azure AI Search, every document chunk indexed contains tenant filtering metadata. The index definition must mark partnerId as a filterable field:
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
public class KnowledgeChunkDocument
{
[SimpleField(IsKey = true, IsFilterable = true)]
public string Id { get; set; } = default!;
[SimpleField(IsFilterable = true, IsFacetable = true)]
public string PartnerId { get; set; } = default!;
[SearchableField(IsFilterable = true)]
public string Title { get; set; } = default!;
[SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnLucene)]
public string Content { get; set; } = default!;
[VectorSearchField(VectorSearchDimensions = 1536, VectorSearchProfileName = "my-hnsw-profile")]
public ReadOnlyMemory<float>? ContentVector { get; set; }
}
When executing queries via Semantic Kernel, security filters are passed down to the underlying AzureAISearchVectorStore query builder. An explicit OData filter guarantees that tenant scoped documents cannot be retrieved by unauthorized parties:
public SearchOptions BuildTenantSearchOptions(string partnerId, int topK = 5)
\{
var options = new SearchOptions
\{
Top = topK,
// Enforce server-side security trimming: global docs OR specific partner docs
Filter = $"partnerId eq '*' or partnerId eq '\{partnerId\}'