MEAI 中间件使用说明

decade
11
2026-01-30

本文结合现有实现,总结 AI 中间件的设计理念、核心组件和实际使用方式,帮助你在当前项目中稳定扩展能力。

概览

AI 中间件是一层层包裹在 IChatClient 外部的处理管道。每一层只完成一个明确职责,例如日志、内容过滤、限流、监控等。请求从外到内进入,响应从内到外返回,形成清晰的责任链。

核心组件与角色

ChatClientBuilder

ChatClientBuilder 负责将多个中间件按顺序组装成最终的 IChatClient。它强调“组合”,避免在业务代码中散落各种 new 调用,使中间件可以复用并保持顺序可控。

DelegatingChatClient

DelegatingChatClient 是中间件的基础类型。自定义中间件只需要继承它,并在调用 base.GetResponseAsync 或 base.GetStreamingResponseAsync 之前或之后插入逻辑即可。

UseXxx 扩展方法

UseXxx 扩展方法用于把中间件以“搭积木”的方式串联起来。它将实例化逻辑集中到扩展方法中,调用方只关注顺序与组合。

本项目中的实现

获取默认客户端

当前项目通过 OpenAIClient 创建原始 ChatClient,然后转为 IChatClient 作为中间件管道的入口。

public static IChatClient GetDefaultChatClient(string baseUrl, string apiKey,string model)
{
    OpenAIClientOptions clientOptions = new()
    {
        Endpoint = new Uri(baseUrl)
    };
    
    OpenAIClient aiClient = new(new ApiKeyCredential(apiKey), clientOptions);
    var chatClient = aiClient.GetChatClient(model);
    var CanUseChatClient = chatClient.AsIChatClient();
    return CanUseChatClient;
}

自定义中间件:JustKiddingChatClient

该中间件在请求前写入日志,并在响应为空时补充系统消息。

public class JustKidinggChatClient : DelegatingChatClient
{
    public JustKidinggChatClient(IChatClient innerClient)
        : base(innerClient)
    {
    }

    public async override Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        System.Console.WriteLine("JustKidinggChatClient GetResponseAsync");
        var response = await base.GetResponseAsync(messages, options, cancellationToken);
        if (response.Messages.Count == 0)
        {
            response.Messages.Add(new ChatMessage(ChatRole.System, "不赖aaa"));
        }
        return response;
    }
}

内容过滤中间件:ProfanityFilteringChatClient

该中间件负责检查并清洗不当用语,并在检测到不当措辞时向控制台输出提示,同时向响应中追加系统消息。

public class ProfanityFilteringChatClient : DelegatingChatClient
{
    private readonly HashSet<string> _badWords = new(StringComparer.OrdinalIgnoreCase)
    {
        "傻逼","妈的","操","fuck","shit"
    };

    public ProfanityFilteringChatClient(IChatClient innerClient)
        : base(innerClient)
    {
    }

    public override Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        var hasProfanity = ContainsProfanity(messages);
        var cleaned = SanitizeMessages(messages);
        return GetResponseWithWarningAsync(cleaned, options, cancellationToken, hasProfanity);
    }
}

组合使用方式

中间件采用装饰器方式一层层包裹。外层先处理,内层后处理。

var chatClient = GetDefaultChatClient(baseUrl, apiKey, model);

var kidinggClient = new JustKidinggChatClient(chatClient);
var safeClient = new ProfanityFilteringChatClient(kidinggClient);

var result = await safeClient.GetResponseAsync(
    new ChatMessage[] { new ChatMessage(ChatRole.User, "你好,你这个傻逼能不能帮我查下天气?") });
Console.WriteLine(result);

中间件执行顺序

  • 请求进入时,从最外层中间件开始,逐层调用 base.GetResponseAsync。

  • 响应返回时,从最内层开始回溯到最外层。

  • 顺序直接决定逻辑效果,应优先放置全局性处理,再放置控制资源和内容的处理。

流式与非流式的处理差异

非流式

GetResponseAsync 一次性返回完整响应,适合需要在响应内容上做统一加工的场景。

流式

GetStreamingResponseAsync 以流式增量返回,适合边生成边展示的场景。若需要统计或修正整体内容,必须自行累计每个更新片段后再处理。

使用建议

  • 每个中间件只做一件事,保持职责单一。

  • 所有自定义逻辑应围绕 base.GetResponseAsync 前后插入,保证链路可组合。

  • 优先通过扩展方法封装中间件的注册逻辑,使构建方式更统一。