LLM Gateway - 路由模块 -基于Token自动切换大模型 -实时消耗token统计
📋 执行流程总结概要
一、初始化阶段(6 步)
| 步骤 | 操作 | 关键内容 |
|---|---|---|
| ① | 程序启动 | 打印 Banner |
| ② | 注册 3 个模型 | qwen-turbo / qwen-plus / qwen-max |
| ③ | 加载路由配置 | 读取 gateway.config.json |
| ④ | 创建 Router | RuleBasedModelRouter |
| ⑤ | 注册过滤器链 | Authentication → Router → Logging |
| ⑥ | Kernel 构建完成 | 准备就绪 |
| ⑦ | 注册 3 个插件 | ChatPlugin / TranslatePlugin / GenerateCodePlugin |
二、测试阶段(4 个测试)
测试 1:企业用户 - 聊天插件
| 项目 | 值 |
|---|---|
| 用户等级 | enterprise |
| 调用方式 | kernel.InvokeAsync() |
| 过滤器 | ✅ 触发 |
| 路由决策 | chat → Pro 级 → qwen-max |
| 权限 | 全部插件可用 |
测试 2:无 Token - 直接调用
| 项目 | 值 |
|---|---|
| 用户等级 | 无 |
| 调用方式 | IChatCompletionService.GetChatMessageContentAsync() |
| 过滤器 | ❌ 不触发 |
| 路由决策 | 无 → 默认最后注册的 qwen-max |
| 权限 | 无限制(绕过认证) |
测试 3:付费用户 - 翻译插件
| 项目 | 值 |
|---|---|
| 用户等级 | paid |
| 调用方式 | kernel.InvokeAsync() |
| 过滤器 | ✅ 触发 |
| 路由决策 | translate → Balanced 级 → qwen-plus |
| 权限 | Chat + Translate |
测试 4:企业用户 - 代码生成插件
| 项目 | 值 |
|---|---|
| 用户等级 | enterprise |
| 调用方式 | kernel.InvokeAsync() |
| 过滤器 | ✅ 触发 |
| 路由决策 | code → Pro 级 → qwen-max |
| 流式处理 | ✅ GetStreamingChatMessageContentsAsync |
| 权限 | 全部插件可用 |
三、核心机制对比
┌─────────────────────────────────────────────────────────────────┐
│ 两种调用方式对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
│ │ kernel.InvokeAsync() │ │ IChatCompletionService │ │
│ │ (推荐✅) │ │ (不推荐❌) │ │
│ ├─────────────────────────┤ ├─────────────────────────────┤ │
│ │ ✅ 触发过滤器链 │ │ ❌ 绕过过滤器链 │ │
│ │ ✅ 认证鉴权生效 │ │ ❌ 无认证鉴权 │ │
│ │ ✅ 动态路由模型 │ │ ❌ 固定用默认模型 │ │
│ │ ✅ 日志记录完整 │ │ ❌ 无日志 │ │
│ │ ✅ 权限控制生效 │ │ ❌ 无权限控制 │ │
│ └─────────────────────────┘ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
四、用户等级 - 模型映射
┌──────────────┬──────────────┬──────────────┬──────────────┐ │ 用户等级 │ 模型层级 │ 实际模型 │ 可用插件 │ ├──────────────┼──────────────┼──────────────┼──────────────┤ │ enterprise │ Pro │ qwen-max │ 全部 3 个 │ │ paid │ Balanced │ qwen-plus │ Chat+Trans │ │ free │ Fast │ qwen-turbo │ 仅 Chat │ │ 无 Token │ 默认 │ qwen-max* │ 无限制* │ └──────────────┴──────────────┴──────────────┴──────────────┘ * 测试 2 因绕过过滤器,实际用最后注册的 qwen-max,但无权限控制
五、关键结论
- 过滤器只作用于
InvokeAsync()—— 直接调IChatCompletionService会绕过所有安全机制 - 模型注册顺序决定默认值 —— 最后注册的
qwen-max成为默认服务 - 流式处理在插件内部实现 —— 测试 4 的
GenerateCode用流式,用户体验更好 - 权限控制依赖过滤器 —— 无 Token 时可绕过权限(生产环境需加强)
代码部分
Models
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Text.Json.Serialization; using System.Threading.Tasks; namespace ConsoleApp6.Models { /// <summary> /// 模型层级(不绑定具体模型名) /// 用于路由决策和成本优化 /// JSON 序列化时使用字符串(如:"Balanced")而非数字 /// </summary> [JsonConverter(typeof(JsonStringEnumConverter))] public enum ModelTier { /// <summary> /// 纳米级 - 最便宜,适合简单任务 /// 示例:llama3-70b(本地部署,免费) /// </summary> [Description("纳米级 - 最便宜,简单任务")] Nano = 0, /// <summary> /// 快速级 - 低成本,低延迟 /// 示例:qwen-turbo(¥0.0005/百万 Token) /// </summary> [Description("快速级 - 低成本,低延迟")] Fast = 1, /// <summary> /// 平衡级 - 性价比高 /// 示例:qwen-plus(¥0.0008/百万 Token) /// </summary> [Description("平衡级 - 性价比")] Balanced = 2, /// <summary> /// 专业级 - 高质量,适合复杂任务 /// 示例:qwen-max(¥0.002/百万 Token) /// </summary> [Description("专业级 - 高质量")] Pro = 3, /// <summary> /// 旗舰级 - 最强,最贵 /// 示例:gpt-4-turbo, claude-3-opus /// </summary> [Description("旗舰级 - 最强,最贵")] Flagship = 4, /// <summary> /// 合规级 - 通过安全认证,适合敏感场景 /// 示例:通过等保认证的模型 /// </summary> [Description("合规级 - 通过安全认证")] Compliance = 5 } /// <summary> /// 路由决策结果 /// </summary> public record ModelRoute( string ModelId, // 实际模型 ID(如 gpt-4-turbo) string Provider, // 供应商(如 openai, azure, dashscope) ModelTier Tier, // 模型层级 string Reason // 路由原因 ); /// <summary> /// Token 预算 /// </summary> public record TokenBudget( int EstimatedInputTokens, int ReservedOutputTokens, int TotalReservedTokens ) { /// <summary> /// 估算 Token(简化:4 字符≈1 Token 中文) /// </summary> public static TokenBudget Estimate(string prompt, int maxOutput = 1024) { var inputTokens = (int)(prompt.Length / 4); return new TokenBudget(inputTokens, maxOutput, inputTokens + maxOutput); } } /// <summary> /// Token 用量 /// </summary> public record TokenUsage(int InputTokens, int OutputTokens); /// <summary> /// 租户信息 /// 用于多租户场景下的认证、授权和配额管理 /// 每次请求认证通过后,将租户信息注入到 Kernel.Data 中 /// 供后续过滤器(限流、路由、日志)使用 /// </summary> public class TenantInfo { /// <summary> /// 租户唯一标识 /// </summary> public string TenantId { get; set; } /// <summary> /// 用户唯一标识 /// 用于细粒度成本统计和审计追踪 /// 示例:"user_demo", "user_zhangsan", "u_123456" /// </summary> public string UserId { get; set; } /// <summary> /// 租户等级 - 类似 RoleCode /// </summary> public TierEnum Tier { get; set; } /// <summary> /// 每日 Token 配额 /// </summary> public int DailyTokenQuota { get; set; } /// <summary> /// 允许调用的Kernel插件 /// </summary> public List<string> Permissions { get; set; } = new List<string>(); } public enum TierEnum { /// - "free": 免费用户,只能用 Nano/Fast 层级,有严格配额限制 /// - "paid": 付费用户,可用 Nano~Pro 层级,配额较宽松 /// - "enterprise": 企业用户,可用全部层级,配额高或无限制 free, paid, enterprise } /// <summary> /// 路由请求 /// </summary> public record RouteRequest( string TenantId, // 租户 ID string UserId, // 用户 ID string UserTier, // 用户等级(free/paid/enterprise) string Scene, // 业务场景 string Prompt // 提示词 ); public record RouteConfig( List<RoutePolicy> Routes, Dictionary<string, string> UserTierMapping ); public record RoutePolicy( string Scene, List<ModelCandidate> Candidates ); public record ModelCandidate( string ModelId, ModelTier Tier ); }
Filters
using ConsoleApp6.Models; using ConsoleApp6.Routers; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Polly.Retry; using Qdrant.Client.Grpc; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace ConsoleApp6.Filters { /// <summary> /// 认证过滤器 /// 基于你的博客中的 AuthenticationFilter 扩展 /// /// 作用时机:函数执行前 /// 核心职责: /// - 从上下文提取 Token /// - 调用用户服务验证 Token /// - 注入租户信息到 Kernel.Data /// - 权限校验(可选) /// </summary> public class AuthenticationFilter : IFunctionInvocationFilter { private readonly Func<string, TenantInfo> _getTenantInfo; /// <summary> /// 创建认证过滤器 /// </summary> /// <param name="getTenantInfo">验证 Token 并获取租户信息的委托</param> public AuthenticationFilter(Func<string, TenantInfo> getTenantInfo) { _getTenantInfo = getTenantInfo; } /// <summary> /// 函数执行前的拦截逻辑 /// </summary> public async Task OnFunctionInvocationAsync( FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next) { // 1️⃣ 从上下文获取 Token var token = GetTokenFromContext(context); if (string.IsNullOrEmpty(token)) { throw new UnauthorizedAccessException("❌ 缺少认证 Token"); } // 2️⃣ 验证 Token 并获取租户信息 var tenantInfo = _getTenantInfo(token); var pluginName = context.Function.PluginName; if (tenantInfo.Permissions != null && tenantInfo.Permissions.Count > 0) { // 如果 Permissions 不为空,检查是否有权限 if (!tenantInfo.Permissions.Any(A=>A== pluginName)) { Console.WriteLine( $"❌ 用户没有权限调用插件:{pluginName}," + $"可用权限:[{string.Join(", ", tenantInfo.Permissions)}]"); return; } } else { Console.WriteLine($"❌ 用户无任何权限"); return; } // 3️⃣ 注入租户信息到上下文(供后续过滤器使用) context.Kernel.Data["tenant_id"] = tenantInfo.TenantId; context.Kernel.Data["user_id"] = tenantInfo.UserId; context.Kernel.Data["user_tier"] = tenantInfo.Tier; context.Kernel.Data["auth_token"] = token; context.Kernel.Data["permissions"] = tenantInfo.Permissions; context.Kernel.Data["daily_quota"] = tenantInfo.DailyTokenQuota; Console.WriteLine($"🔐 认证通过:租户={tenantInfo.TenantId}, 用户={tenantInfo.UserId}, 等级={tenantInfo.Tier}"); // 4️⃣ 继续执行后续过滤器和函数 await next(context); // 5️⃣ 执行后:记录审计日志(可选) Console.WriteLine($"✅ 函数完成:{context.Function.PluginName}.{context.Function.Name}"); } /// <summary> /// 从上下文提取 Token /// 优先从 Kernel.Data 中获取,其次从 Arguments 中获取 /// </summary> private string GetTokenFromContext(FunctionInvocationContext context) { // 方式 1:从 Kernel.Data 获取(推荐) if (context.Kernel.Data.TryGetValue("auth_token", out var token)) { return token?.ToString() ?? ""; } // 方式 2:从 Arguments 获取 if (context.Arguments.TryGetValue("auth_token", out var argToken)) { return argToken?.ToString() ?? ""; } return ""; } } /// <summary> /// 模型路由过滤器(核心) /// ⭐ 根据用户 Token 等级 + 场景 自动选择模型 /// </summary> public class RouterFilter : IFunctionInvocationFilter { private readonly IModelRouter _router; public RouterFilter(IModelRouter router) { _router = router; } public async Task OnFunctionInvocationAsync( FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next) { // 1️⃣ 从认证过滤器注入的数据中获取用户信息 var tenantId = context.Kernel.Data["tenant_id"]?.ToString() ?? "default"; var userId = context.Kernel.Data["user_id"]?.ToString() ?? "anonymous"; var userTier = context.Kernel.Data["user_tier"]?.ToString() ?? "free"; // ⭐ 关键:enterprise/paid/free // 2️⃣ 推断场景(从插件名/函数名) var scene = GetSceneFromContext(context); // 3️⃣ 获取 Prompt var prompt = GetPromptFromContext(context); Console.WriteLine($"📥 路由请求:用户等级={userTier}, 场景={scene}"); // 4️⃣ ⭐ 执行路由决策(核心逻辑在路由器中) var route = await _router.RouteAsync(new RouteRequest( TenantId: tenantId, UserId: userId, UserTier: userTier, // ⭐ 传给路由器 Scene: scene, Prompt: prompt )); // 5️⃣ ⭐ 注入路由结果到 Kernel.Data(供 Plugin 使用) context.Kernel.Data["route_model"] = route.ModelId; context.Kernel.Data["route_provider"] = route.Provider; context.Kernel.Data["route_tier"] = route.Tier.ToString(); context.Kernel.Data["route_reason"] = route.Reason; Console.WriteLine( $"🎯 路由决策:场景={scene}, 用户等级={userTier}, " + $"模型={route.ModelId}, 层级={route.Tier}, " + $"原因={route.Reason}"); // 6️⃣ 继续执行 Plugin await next(context); } /// <summary> /// 从函数名/插件名推断场景 /// </summary> private string GetSceneFromContext(FunctionInvocationContext context) { var pluginName = context.Function.PluginName.ToLower(); var functionName = context.Function.Name.ToLower(); if (pluginName.Contains("translate") || functionName.Contains("translate")) return "translation"; if (pluginName.Contains("summarize") || functionName.Contains("summarize")) return "summary"; if (pluginName.Contains("code") || functionName.Contains("code")) return "code"; if (pluginName.Contains("chat") || functionName.Contains("chat")) return "chat"; return "default"; } /// <summary> /// 从上下文提取 Prompt 文本 /// </summary> private string GetPromptFromContext(FunctionInvocationContext context) { return string.Join(" ", context.Arguments.Values.Select(v => v?.ToString() ?? "")); } } public class LoggingFilter : IFunctionInvocationFilter { private readonly ILogger<LoggingFilter> _logger; private readonly Stopwatch _stopwatch; public LoggingFilter(ILogger<LoggingFilter> logger) { _logger = logger; _stopwatch = new Stopwatch(); } public async Task OnFunctionInvocationAsync( FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next) { var functionName = $"{context.Function.PluginName}.{context.Function.Name}"; var arguments = string.Join(", ", context.Arguments.Select(kvp => $"{kvp.Key}={kvp.Value}")); // 📝 开始执行函数 - 彩色输出 _logger.LogInformation("📝 开始执行函数:{FunctionName}({Arguments})", functionName, arguments); Console.ForegroundColor = ConsoleColor.Cyan; Console.Write($"📝 开始执行函数:{functionName}("); Console.ForegroundColor = ConsoleColor.White; Console.Write($"{arguments}"); Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine(")"); Console.ResetColor(); _stopwatch.Restart(); try { // 执行函数 await next(context); _stopwatch.Stop(); var result = context.Result?.GetValue<object>()?.ToString() ?? "null"; // ⭐ 获取 Token 消耗量(从 Kernel.Data 中读取) var inputTokens = context.Kernel.Data.TryGetValue("input_tokens", out var inputObj) ? (int)inputObj : 0; var outputTokens = context.Kernel.Data.TryGetValue("output_tokens", out var outputObj) ? (int)outputObj : 0; var totalTokens = inputTokens + outputTokens; // ✅ 函数执行成功 - 彩色输出 _logger.LogInformation( "✅ 函数执行成功:{FunctionName},耗时 {ElapsedMs}ms,Token 消耗:输入={InputTokens}, 输出={OutputTokens}, 总计={TotalTokens},结果:{Result}", functionName, _stopwatch.ElapsedMilliseconds, inputTokens, outputTokens, totalTokens, Truncate(result, 100)); Console.ForegroundColor = ConsoleColor.Green; Console.Write($"✅ 函数执行成功:{functionName},"); Console.ForegroundColor = ConsoleColor.DarkGreen; Console.Write($"耗时 {_stopwatch.ElapsedMilliseconds}ms,"); // ⭐ Token 消耗量 - 单独一行彩色输出 Console.ForegroundColor = ConsoleColor.Yellow; Console.Write($"📊 Token 消耗:"); Console.ForegroundColor = ConsoleColor.White; Console.Write($"输入={inputTokens}, "); Console.ForegroundColor = ConsoleColor.White; Console.Write($"输出={outputTokens}, "); Console.ForegroundColor = ConsoleColor.Cyan; Console.Write($"总计={totalTokens}"); Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine($", 结果:{Truncate(result, 100)}"); Console.ResetColor(); } catch (Exception ex) { _stopwatch.Stop(); // ❌ 函数执行失败 - 彩色输出 _logger.LogError( ex, "❌ 函数执行失败:{FunctionName},耗时 {ElapsedMs}ms,错误:{Error}", functionName, _stopwatch.ElapsedMilliseconds, ex.Message); Console.ForegroundColor = ConsoleColor.Red; Console.Write($"❌ 函数执行失败:{functionName},"); Console.ForegroundColor = ConsoleColor.DarkRed; Console.Write($"耗时 {_stopwatch.ElapsedMilliseconds}ms,"); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"错误:{ex.Message}"); Console.ResetColor(); throw; } } private string Truncate(string text, int maxLength) { if (string.IsNullOrEmpty(text) || text.Length <= maxLength) return text; return text[..maxLength] + "..."; } } }
Plugins
using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Qdrant.Client.Grpc; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp6.Plugins { /// <summary> /// 演示插件(通用场景) /// 用于测试 LLM Gateway 的路由、限流、缓存、成本统计等功能 /// </summary> public class ChatPlugin { private readonly Kernel _kernel; public ChatPlugin(Kernel kernel) { _kernel = kernel; } /// <summary> /// 通用对话 - 真正调用 LLM /// </summary> [KernelFunction("Chat")] [Description("通用对话 - 日常聊天、问答、咨询")] public async Task<string> ChatMessage( [Description("用户消息")] string message, [Description("对话历史(可选)")] string? history = null) { // ✅ 获取当前路由的模型 var modelId = _kernel.Data.TryGetValue("route_model", out var modelObj) ? modelObj?.ToString() ?? "qwen-plus" : "qwen-plus"; // 2️⃣ ⭐ 根据 modelId 构建 serviceId,获取对应的服务--参考:builder.AddOpenAIChatCompletion( serviceId: "dashscope_qwen-turbo", modelId: "qwen-turbo", var serviceId = $"dashscope_{modelId}"; // 如:dashscope_qwen-max var chatService = _kernel.GetRequiredService<IChatCompletionService>(serviceId); // ✅ 获取 ChatCompletionService // ✅ 构建对话 var chatHistory = new ChatHistory(); if (!string.IsNullOrEmpty(history)) { chatHistory.AddUserMessage(history); } chatHistory.AddUserMessage(message); // ✅ 真正调用 LLM var response = await chatService.GetChatMessageContentAsync( chatHistory, cancellationToken: default); if (response.Metadata != null&& response.Metadata["Usage"]!=null) { var Usage = (OpenAI.Chat.ChatTokenUsage)response.Metadata["Usage"]; var InputTokenCount = Usage.InputTokenCount; var OutputTokenCount = Usage.OutputTokenCount; var TotalTokenCount = Usage.TotalTokenCount; _kernel.Data["input_tokens"] = Usage.InputTokenCount; _kernel.Data["output_tokens"] = Usage.OutputTokenCount; _kernel.Data["total_tokens"] = Usage.TotalTokenCount; } else { _kernel.Data["input_tokens"] = 0; _kernel.Data["output_tokens"] = 0; _kernel.Data["total_tokens"] = 0; } return response.Content ?? "无回复"; } } /// <summary> /// 翻译插件 /// </summary> public class TranslatePlugin { private readonly Kernel _kernel; public TranslatePlugin(Kernel kernel) { _kernel = kernel; } /// <summary> /// 翻译 - 真正调用 LLM /// </summary> [KernelFunction("Translate")] [Description("文本翻译 - 支持多语言互译")] public async Task<string> Translate( [Description("原文")] string text, [Description("目标语言")] string targetLanguage, [Description("源语言(可选,自动检测)")] string? sourceLanguage = null) { // ✅ 获取当前路由的模型 var modelId = _kernel.Data.TryGetValue("route_model", out var modelObj) ? modelObj?.ToString() ?? "qwen-plus" : "qwen-plus"; // 2️⃣ ⭐ 根据 modelId 构建 serviceId,获取对应的服务--参考:builder.AddOpenAIChatCompletion( serviceId: "dashscope_qwen-turbo", modelId: "qwen-turbo", var serviceId = $"dashscope_{modelId}"; // 如:dashscope_qwen-max //重要 var chatService = _kernel.GetRequiredService<IChatCompletionService>(serviceId); var prompt = $"请将以下文本从{(string.IsNullOrEmpty(sourceLanguage) ? "自动检测" : sourceLanguage)}翻译成{targetLanguage}:\n\n{text}"; var chatHistory = new ChatHistory(); chatHistory.AddUserMessage(prompt); var response = await chatService.GetChatMessageContentAsync( chatHistory, cancellationToken: default); if (response.Metadata != null && response.Metadata["Usage"] != null) { var Usage = (OpenAI.Chat.ChatTokenUsage)response.Metadata["Usage"]; var InputTokenCount = Usage.InputTokenCount; var OutputTokenCount = Usage.OutputTokenCount; var TotalTokenCount = Usage.TotalTokenCount; _kernel.Data["input_tokens"] = Usage.InputTokenCount; _kernel.Data["output_tokens"] = Usage.OutputTokenCount; _kernel.Data["total_tokens"] = Usage.TotalTokenCount; } else { _kernel.Data["input_tokens"] = 0; _kernel.Data["output_tokens"] = 0; _kernel.Data["total_tokens"] = 0; } return response.Content ?? "无回复"; } } /// <summary> /// 工具插件 - 代码生成 /// </summary> public class GenerateCodePlugin { private readonly Kernel _kernel; private readonly ILogger? _logger; public GenerateCodePlugin(Kernel kernel, ILogger<GenerateCodePlugin>? logger = null) { _kernel = kernel; _logger = logger; } [KernelFunction("GenerateCode")] [Description("代码生成 - 根据需求描述生成代码")] public async Task<string> GenerateCode( [Description("需求描述")] string requirement, [Description("编程语言")] string language, [Description("代码风格(可选)")] string? style = null, [Description("是否包含注释")] bool includeComments = true) { // ✅ 获取路由决策的模型 var modelId = _kernel.Data.TryGetValue("route_model", out var modelObj) ? modelObj?.ToString() ?? "qwen-plus" : "qwen-plus"; var serviceId = $"dashscope_{modelId}"; var chatService = _kernel.GetRequiredService<IChatCompletionService>(serviceId); // 构建 Prompt var prompt = $"请用{language}编写代码,需求:{requirement}"; if (!string.IsNullOrEmpty(style)) { prompt += $"\n代码风格:{style}"; } prompt += $"\n{(includeComments ? "请包含详细注释" : "不需要注释")}"; var chatHistory = new ChatHistory(); chatHistory.AddUserMessage(prompt); // ✅ 流式处理 + 实时打印 var responseBuilder = new StringBuilder(); int inputTokenCount = 0; int outputTokenCount = 0; bool hasContent = false; _logger?.LogInformation("🚀 开始流式生成代码..."); try { await foreach (var chunk in chatService.GetStreamingChatMessageContentsAsync( chatHistory, cancellationToken: default)) { if (chunk.Content is string chunkContent && !string.IsNullOrEmpty(chunkContent)) { responseBuilder.Append(chunkContent); hasContent = true; // 🔥 实时打印(控制台可见流式效果) Console.Write(chunkContent); // ⭐ 关键:逐块打印,不换行 Console.Out.Flush(); // ⭐ 关键:立即刷新缓冲区 } // 累积 Token 统计(通常在最后一个 chunk) if (chunk.Metadata?.TryGetValue("Usage", out var usageObj) == true) { var usage = (OpenAI.Chat.ChatTokenUsage)usageObj; if (usage != null) { inputTokenCount = usage.InputTokenCount; outputTokenCount = usage.OutputTokenCount; } } } } catch (OperationCanceledException) { _logger?.LogWarning("⚠️ 流式生成被取消"); throw; } // 换行(流式输出结束后) if (hasContent) { Console.WriteLine(); } var finalResponse = responseBuilder.ToString(); // 保存 Token 统计 _kernel.Data["input_tokens"] = inputTokenCount; _kernel.Data["output_tokens"] = outputTokenCount; _kernel.Data["total_tokens"] = inputTokenCount + outputTokenCount; _logger?.LogInformation("✅ 代码生成完成,总 Token: {Total}", inputTokenCount + outputTokenCount); return finalResponse; } } }
路由配置文件-gateway.config.json
{ "routes": [ { "scene": "chat", "candidates": [ { "modelId": "qwen-max", "tier": "Pro" }, { "modelId": "qwen-plus", "tier": "Balanced" }, { "modelId": "qwen-turbo", "tier": "Fast" } ] } ], "userTierMapping": { "enterprise": "Pro", "paid": "Balanced", "free": "Fast" } }
路由切换策略-Routers
using ConsoleApp6.Models; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp6.Routers { /// <summary> /// 模型路由接口 /// </summary> public interface IModelRouter { /// <summary> /// 根据请求信息路由到最佳模型 /// </summary> Task<ModelRoute> RouteAsync(RouteRequest request); } /// <summary> /// 基于规则的路由器(同步版本) /// ⭐ 核心:根据用户等级直接选择模型 /// </summary> public class RuleBasedModelRouter : IModelRouter { private readonly RouteConfig _config; public RuleBasedModelRouter(RouteConfig config) { _config = config; } public ModelRoute Route(RouteRequest request) { Console.WriteLine($"🔍 开始路由:场景={request.Scene}, 用户等级={request.UserTier}"); // 1️⃣ 根据用户等级查找目标模型层级 var targetTier = "Fast"; // 默认用最便宜的 if (request.UserTier == "enterprise") { targetTier = "Pro"; } else if (request.UserTier == "paid") { targetTier = "Balanced"; } else if (request.UserTier == "free") { targetTier = "Fast"; } Console.WriteLine($"📊 目标模型层级:{targetTier}"); // 2️⃣ 查找场景策略 var policy = _config.Routes.FirstOrDefault(r => r.Scene == request.Scene); if (policy == null) { policy = _config.Routes.First(); Console.WriteLine($"⚠️ 未找到场景策略,使用默认:{policy.Scene}"); } Console.WriteLine($"📋 场景策略:{policy.Scene}, 候选模型数={policy.Candidates.Count}"); // 3️⃣ 根据目标层级选择模型 ModelCandidate candidate = null; foreach (var c in policy.Candidates) { if (c.Tier.ToString() == targetTier) { candidate = c; break; } } // 兜底:用最后一个(最便宜的) if (candidate == null) { candidate = policy.Candidates.Last(); Console.WriteLine($"⚠️ 未找到匹配层级的模型,使用兜底:{candidate.ModelId}"); } Console.WriteLine($"✅ 选中模型:{candidate.ModelId} (Tier={candidate.Tier})"); // 4️⃣ 返回路由结果 return new ModelRoute( ModelId: candidate.ModelId, Provider: "dashscope", Tier: candidate.Tier, Reason: $"user_tier={request.UserTier}" ); } // ⭐ 实现接口(包装同步方法) public Task<ModelRoute> RouteAsync(RouteRequest request) { return Task.FromResult(Route(request)); } } }
main入口
using ConsoleApp1; using ConsoleApp6.Filters; using ConsoleApp6.Models; using ConsoleApp6.Plugins; using ConsoleApp6.Routers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Newtonsoft.Json; using System.Timers; namespace ConsoleApp5 { class Program { static string ChatPluginName = "ChatPlugin"; //聊天插件 static string TranslatePluginName = "TranslatePlugin"; //翻译插件 static string GenerateCodePluginName = "GenerateCodePlugin"; // 代码生成插件 static async Task Main(string[] args) { Console.WriteLine("╔════════════════════════════════════════╗"); Console.WriteLine("║ LLM Gateway - 路由模块学习版 ║"); Console.WriteLine("║ 基于用户 Token 等级自动切换模型 ║"); Console.WriteLine("╚════════════════════════════════════════╝\n"); var kernel = CreateGatewayKernel(); // 注册插件-聊天插件 kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new ChatPlugin(kernel), ChatPluginName)); // 注册插件-翻译插件 kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new TranslatePlugin(kernel), TranslatePluginName)); // 注册插件-代码生成插件 kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new GenerateCodePlugin(kernel), GenerateCodePluginName)); // ========== 测试不同等级的 Token ========== // 测试 1: 企业用户(enterprise) Console.WriteLine("\n═══════════════════════════════════════"); Console.WriteLine("【测试 1】企业用户 Token(enterprise)"); Console.WriteLine("═══════════════════════════════════════"); var enterpriseUser = new TenantInfo() { TenantId= Guid.NewGuid().ToString("N"), UserId = Guid.NewGuid().ToString("N"), Tier = TierEnum.enterprise, // ⭐ 这个最重要,决定能用啥模型 DailyTokenQuota = 100 * 10000,//--企业级用户 Token配额 100万 Permissions = new List<string>() }; kernel.Data["auth_token"] = JsonConvert.SerializeObject(enterpriseUser); var Chat_funcName = "Chat";//插件函数 var result1 = await kernel.InvokeAsync(ChatPluginName, Chat_funcName, new KernelArguments { ["message"] = "你好" }); Console.WriteLine($"✅ 结果:{result1}\n"); // 测试 2:无Token用户,使用chatService.GetChatMessageContentAsync,不触发过滤器。 //因为FunctionInvocationFilters 只作用于: kernel.InvokeAsync() / kernel.InvokePromptAsync() —— 走 Function 不作用于 IChatCompletionService.GetChatMessageContentAsync() —— 这是直接调用聊天服务 kernel.Data["auth_token"] = ""; var chatService = kernel.GetRequiredService<IChatCompletionService>(); var history = new ChatHistory(@"你是中英文翻译专家,输入中文自动翻译为英文,输入英文自动翻译为中文。"); var settings = new OpenAIPromptExecutionSettings { Temperature = 0.7, MaxTokens = 1000 }; history.AddUserMessage("你好,我的名字是可爱的兔子"); var response = await chatService.GetChatMessageContentAsync( history, settings, kernel ); history.AddAssistantMessage(response.Content!); Console.WriteLine($"\n🤖 助手:{response.Content}\n"); //测试3 翻译专家-插件模式 - Translate var paidUser = new TenantInfo() { TenantId = Guid.NewGuid().ToString("N"), UserId = Guid.NewGuid().ToString("N"), Tier = TierEnum.paid, // ⭐ 这个最重要,决定能用啥模型 DailyTokenQuota = 100 * 1000,//--付费用户 Token配额 10万 Permissions = new List<string>() }; kernel.Data["auth_token"] = JsonConvert.SerializeObject(paidUser); var Translate_funcName = "Translate"; var result_Translate = await kernel.InvokeAsync(TranslatePluginName, Translate_funcName, new KernelArguments { ["text"] = "爽啊,加油,用力", ["targetLanguage"] = "日语", ["sourceLanguage"] = "中文" }); Console.WriteLine($"✅ 结果:{result_Translate}\n"); //测试4 代码生成-插件模式 - GenerateCodePlugin -- 流式处理器[详见插件] kernel.Data["auth_token"] = JsonConvert.SerializeObject(enterpriseUser); var GenerateCode_funcName = "GenerateCode"; var result_GenerateCode = await kernel.InvokeAsync(GenerateCodePluginName, GenerateCode_funcName, new KernelArguments { ["requirement"] = "生成冒泡排序", ["language"] = "C#", ["style"] = "详细", ["includeComments"] = true }); Console.Read(); } static Kernel CreateGatewayKernel() { var builder = Kernel.CreateBuilder(); // ========== 1. 注册模型服务(3 个模型) ========== var dashScopeApiKey = ConstParm.apiKey; var dashScopeEndpoint = ConstParm.endpoint; builder.AddOpenAIChatCompletion( serviceId: "dashscope_qwen-turbo", // Fast 级 modelId: "qwen-turbo", apiKey: dashScopeApiKey, endpoint: new Uri(dashScopeEndpoint)); builder.AddOpenAIChatCompletion( serviceId: "dashscope_qwen-plus", // Balanced 级 modelId: "qwen-plus", apiKey: dashScopeApiKey, endpoint: new Uri(dashScopeEndpoint)); builder.AddOpenAIChatCompletion( serviceId: "dashscope_qwen-max", // Pro 级 modelId: "qwen-max", apiKey: dashScopeApiKey, endpoint: new Uri(dashScopeEndpoint)); // ========== 2. 加载路由配置 ========== var configPath = Path.Combine(AppContext.BaseDirectory, "gateway.config.json"); var config = LoadRouteConfig(configPath); // ========== 3. 创建路由依赖服务 ========== var router = new RuleBasedModelRouter(config); // 注册服务 builder.Services.AddSingleton(config); builder.Services.AddSingleton<IModelRouter>(router); builder.Services.AddLogging(b => b .AddConsole() .SetMinimumLevel(LogLevel.Information)); var kernel = builder.Build(); // ========== 4. 注册过滤器链(只保留核心) ========== // 认证过滤器:解析 Token,注入 user_tier kernel.FunctionInvocationFilters.Add(new AuthenticationFilter(GetTenantInfoAsync)); // 路由过滤器:根据 user_tier + scene 选择模型 kernel.FunctionInvocationFilters.Add(new RouterFilter(router)); var logger = kernel.Services.GetRequiredService<ILogger<LoggingFilter>>(); kernel.FunctionInvocationFilters.Add(new LoggingFilter(logger)); return kernel; } /// <summary> /// 根据 Token 解析租户信息(核心:决定用户等级) /// </summary> static TenantInfo GetTenantInfoAsync(string token) { var user = JsonConvert.DeserializeObject<TenantInfo>(token); if (user != null) { switch (user.Tier) { case TierEnum.enterprise: user.Permissions = new List<string>() { ChatPluginName, TranslatePluginName, GenerateCodePluginName}; break; //企业级用户可以使用聊天、翻译、代码生成全部插件 case TierEnum.paid: user.Permissions = new List<string>() { ChatPluginName, TranslatePluginName }; break;//付费用户可以使用聊天、翻译2个插件 case TierEnum.free: user.Permissions = new List<string>() { ChatPluginName }; break;//免费用户只允许聊天插件 } return user; } else { return new TenantInfo() { }; } } static RouteConfig LoadRouteConfig(string path) { if (!File.Exists(path)) { Console.WriteLine($"⚠️ 配置文件不存在:{path}, 使用默认配置"); return CreateDefaultConfig(); } try { var json = File.ReadAllText(path); var config = JsonConvert.DeserializeObject<RouteConfig>(json); Console.WriteLine($"✅ 加载配置文件:{path}"); return config ?? CreateDefaultConfig(); } catch (Exception ex) { Console.WriteLine($"⚠️ 加载配置文件失败:{ex.Message}, 使用默认配置"); return CreateDefaultConfig(); } } static RouteConfig CreateDefaultConfig() { return new RouteConfig( Routes: new List<RoutePolicy> { // chat 场景:三个候选模型(按优先级排序) new RoutePolicy( Scene: "chat", Candidates: new List<ModelCandidate> { new ModelCandidate("qwen-max", ModelTier.Pro), new ModelCandidate("qwen-plus", ModelTier.Balanced), new ModelCandidate("qwen-turbo", ModelTier.Fast) }) }, // 用户等级映射:决定用什么层级的模型 UserTierMapping: new Dictionary<string, string> { { "enterprise", "Pro" }, // 企业用户 → Pro 级 (qwen-max) { "paid", "Balanced" }, // 付费用户 → 平衡级 (qwen-plus) { "free", "Fast" } // 免费用户 → 快速级 (qwen-turbo) } ); } } }
项目引用
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <NoWarn>$(NoWarn);NU5104</NoWarn> </PropertyGroup> <ItemGroup> <Compile Remove="Registry\**" /> <EmbeddedResource Remove="Registry\**" /> <None Remove="Registry\**" /> </ItemGroup> <ItemGroup> <Compile Remove="Program - 复制.cs" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.10" /> <PackageReference Include="Microsoft.SemanticKernel" Version="1.78.0" /> <PackageReference Include="Microsoft.SemanticKernel.Connectors.Sqlite" Version="1.51.0-preview" /> <PackageReference Include="Microsoft.SemanticKernel.Plugins.Core" Version="1.78.0-preview" /> <PackageReference Include="Microsoft.SemanticKernel.Plugins.Memory" Version="1.78.0-alpha" /> <PackageReference Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.78.0" /> <PackageReference Include="Microsoft.SemanticKernel.PromptTemplates.Handlebars" Version="1.78.0" /> <PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="1.78.0-alpha" /> <PackageReference Include="Qdrant.Client" Version="1.18.1" /> <PackageReference Include="Sdcb.DashScope" Version="2.0.0" /> <!-- ✅ 新增:限流需要 --> <PackageReference Include="System.Threading.RateLimiting" Version="8.0.0" /> <!-- ✅ 新增:重试需要 --> <PackageReference Include="Polly" Version="8.4.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Services\Services.csproj" /> </ItemGroup> <ItemGroup> <None Update="gateway.config.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup> </Project>
运行结果
测试1

测试2

测试3

测试4

核心代码
三个过滤器-,在执行InvokeAsync时依次触发鉴权过滤器-路由过滤器-日志过滤器,三个过滤器通过 context.Kernel.Data 实现数据互传,通过传递的数据,依次判断是否合法用户->传递路由过滤器->路由过滤器获取用户类型,根据用户类型获取route_model,插件根据route_model获取可使用的模型!

浙公网安备 33010602011771号