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,但无权限控制

五、关键结论

  1. 过滤器只作用于 InvokeAsync() —— 直接调 IChatCompletionService 会绕过所有安全机制
  2. 模型注册顺序决定默认值 —— 最后注册的 qwen-max 成为默认服务
  3. 流式处理在插件内部实现 —— 测试 4 的 GenerateCode 用流式,用户体验更好
  4. 权限控制依赖过滤器 —— 无 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
    );
}
View Code

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] + "...";
        }
    }
}
View Code

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;
        }
    }
}
View Code

路由配置文件-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"
  }
}
View Code

路由切换策略-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));
        }
    }

}
View Code

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)
                }
            );
        }
    }
}
View Code

项目引用

<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>
View Code

运行结果

测试1

image

 测试2

image

 测试3

image

 测试4

image

 核心代码

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

posted @ 2026-08-10 13:34  天才卧龙  阅读(18)  评论(0)    收藏  举报