本文结合现有实现,总结 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 前后插入,保证链路可组合。
优先通过扩展方法封装中间件的注册逻辑,使构建方式更统一。