入门(4):使用 MAF Middleware 中间件

MAF 入门(4):使用 MAF Middleware 中间件

一、为什么需要 Middleware?

前面几篇,我们学会了:

  • 让 Agent 对话RunAsync
  • 让 Agent 调工具tools:
  • 让 Agent 记历史AgentSession

一旦要上生产,你还会反复遇到这些需求:

需求 若写在业务里 问题
每次 Run 打日志、记耗时 每个 RunAsync 前后 copy 一遍 重复、易漏
敏感词拦截 在 Controller 里 if 判断 和 Agent 逻辑缠在一起
统一异常转友好文案 try/catch 包每个调用点 维护成本高

这些叫 横切关注点(Cross-Cutting Concerns) :跟「这一轮对话的业务」无关,但每一轮 Run 都要做

Middleware(中间件) 是 MAF 提供的标准插槽:在 Agent 执行前后(或更底层的模型 HTTP 调用前后)插入逻辑,不改核心 Agent 代码,就能叠加日志、治理、鉴权等能力。


二、MAF 的三层 Middleware

Microsoft Agent Framework 把扩展点分成三层(本文实现第 1、2 层,顺带介绍第 3 层):

层级 拦截什么 注册方式
Agent Run 每次 AIAgent.RunAsync / RunStreamingAsync agent.AsBuilder().Use(runFunc:, runStreamingFunc:)
Function 调用 GetWeather / GetProductInfo 等工具执行前后 agent.AsBuilder().Use(functionCallback)
IChatClient 发往推理服务的请求 chatClient.AsBuilder().Use(getResponseFunc:, ...)

调用链示意:

text 复制代码
你的代码: agent.RunAsync("你好")
        │
        ▼  ┌─────────────────────────────────────┐
           │  Agent Run Middleware(业务层)      │  日志、敏感词、异常包装
           └─────────────────┬───────────────────┘
                             ▼
           ┌─────────────────────────────────────┐
           │  ChatClientAgent / 工具循环等        │
           └─────────────────┬───────────────────┘
                             ▼
           ┌─────────────────────────────────────┐
           │  IChatClient Middleware(传输层)    │  HTTP 耗时、原始请求
           └─────────────────┬───────────────────┘
                             ▼
                        百炼 / OpenAI API

Agent Run 层 适合业务语义:审计、限流、敏感词、统一异常文案。

IChatClient 层适合传输语义:HTTP 耗时、重试、链路 ID、请求体大小。

两层日志可以同时开:Agent 层看「一次 Run 业务」;IChatClient 层看「一次 GetResponseAsync 网络调用」。


三、Agent Run Middleware:实现

3.1 注册入口:AIAgentBuilder.Use

在已有 AIAgent 上通过 Builder 挂中间件,再 Build() 得到带管道的 Agent:

csharp 复制代码
var agent = coreAgent
    .AsBuilder()
    .Use(runFunc: YourMiddleware, runStreamingFunc: null)
    .Build();

runFunc 是一个委托方法:

csharp 复制代码
static async Task<AgentResponse> YourMiddleware(
    IEnumerable<ChatMessage> messages,
    AgentSession? session,
    AgentRunOptions? options,
    AIAgent innerAgent,
    CancellationToken cancellationToken)
{
    var response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
    return response;
}
参数 含义
messages 本轮传入的消息(含历史,取决于你怎么调 RunAsync
session 会话对象;有 AgentSession 时可从 session 区分多用户
options 本轮 AgentRunOptions
innerAgent 管道下一环:下一个 Middleware,或最内层的核心 Agent
返回值 可原样返回,也可替换为新的 AgentResponse

执行模型可以记成两句话:

  1. 继续管道 → 调用 await innerAgent.RunAsync(...)
  2. 短路 (不调大模型)→ 直接 return new AgentResponse(...)不要innerAgent

3.2 链式多个 .Use()

多个中间件通过连续 .Use() 组成管道:

csharp 复制代码
return core.AsBuilder()
    .Use(runFunc: AgentRunMiddleware.FaultInjection, runStreamingFunc: null)
    .Use(runFunc: AgentRunMiddleware.ContentGovernance, runStreamingFunc: null)
    .Use(runFunc: AgentRunMiddleware.ExceptionHandling, runStreamingFunc: null)
    .Use(runFunc: AgentRunMiddleware.RequestLogging, runStreamingFunc: null)
    .Build();

顺序(与 ASP.NET Core 中间件类似):

  • .Use()内层(离核心 Agent 近)
  • .Use()外层 (你的 RunAsync 先进入这一层)
ini 复制代码
请求从外往里,响应从内往外返回。

### 3.3 流式说明

`Use` 还可传入 `runStreamingFunc` 处理 `RunStreamingAsync`。Demo 为聚焦非流式场景,统一写 `runStreamingFunc: null`。  
生产若要用流式,应为流式单独实现一版,或使用文档中的 `Use(sharedFunc:)`(适合只改输入、不改输出的场景)。

---

## 四、四个 Middleware 实现详解

文件:`Middleware/AgentRunMiddleware.cs`。四个 **静态方法**,方法名即职责,签名与上一节相同。

### 4.1 RequestLogging --- 请求/响应可观测

在调用 `innerAgent` 前后打日志:会话标识、消息条数、粗算输入 Token、用户预览、耗时、`Usage`。

```csharp
public static async Task<AgentResponse> RequestLogging(
    IEnumerable<ChatMessage> messages,
    AgentSession? session,
    AgentRunOptions? options,
    AIAgent innerAgent,
    CancellationToken cancellationToken)
{
    var sw = Stopwatch.StartNew();
    string sessionHint = session?.GetHashCode().ToString("X") ?? "none";
    int estimatedTokens = AgentMiddlewareHelpers.EstimateInputTokens(messages);
    string userPreview = GetLastUserText(messages);

    Console.WriteLine(
        $"[MAF.RequestLog] ▶ 请求 | session={sessionHint} | msgs={messages.Count()} " +
        $"| estTokens≈{estimatedTokens} | user=\"{AgentMiddlewareHelpers.Truncate(userPreview, 40)}\"");

    AgentResponse response = await innerAgent.RunAsync(messages, session, options, cancellationToken);

    sw.Stop();
    Console.WriteLine(
        $"[MAF.RequestLog] ◀ 响应 | {sw.ElapsedMilliseconds}ms | replyLen={response.Text?.Length ?? 0} " +
        $"| usage={AgentMiddlewareHelpers.FormatUsage(response.Usage)}");

    return response;
}

AgentMiddlewareHelpers.EstimateInputTokens 用「字符数 / 2」粗算 Token,适合 Demo;生产可换成 tiktoken 等库。

FormatUsage 输出 InputTokenCount / OutputTokenCount,便于和账单对照。

4.2 ContentGovernance --- 敏感词短路

默认拦截关键词:密码身份证号apikeysk-。命中后不调用 innerAgent,直接返回助手消息,不产生模型 Token 费用

csharp 复制代码
public static async Task<AgentResponse> ContentGovernance(
    IEnumerable<ChatMessage> messages,
    AgentSession? session,
    AgentRunOptions? options,
    AIAgent innerAgent,
    CancellationToken cancellationToken)
{
    string? hit = FindBlockedKeyword(messages);
    if (hit is not null)
    {
        Console.WriteLine($"[MAF.Governance] ⛔ 拦截 | keyword=\"{hit}\"");
        return new AgentResponse(new ChatMessage(
            ChatRole.Assistant,
            $"【治理拦截】检测到敏感内容({hit}),请求未发送到大模型。"));
    }

    return await innerAgent.RunAsync(messages, session, options, cancellationToken);
}

只扫描 ChatRole.User 的消息文本。扩展关键词改 BlockedKeywords 数组即可。

4.3 ExceptionHandling --- 异常包装

内层 Middleware 或核心 Agent 抛错时,转为 AgentResponse,调用方仍走正常的 await agent.RunAsync(...) 流程。OperationCanceledException 继续向上抛,不吞掉取消。

csharp 复制代码
public static async Task<AgentResponse> ExceptionHandling(
    IEnumerable<ChatMessage> messages,
    AgentSession? session,
    AgentRunOptions? options,
    AIAgent innerAgent,
    CancellationToken cancellationToken)
{
    try
    {
        return await innerAgent.RunAsync(messages, session, options, cancellationToken);
    }
    catch (OperationCanceledException)
    {
        throw;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[MAF.Exception] ✖ {ex.GetType().Name}: {ex.Message}");
        return new AgentResponse(new ChatMessage(
            ChatRole.Assistant,
            $"【系统异常】请求处理失败,请稍后重试。({ex.GetType().Name})"));
    }
}

4.4 FaultInjection --- 异常场景演示

用户输入包含「模拟异常」时抛出 InvalidOperationException,用于验证 ExceptionHandling 是否生效。仅 Demo 使用,生产环境不要保留。

csharp 复制代码
public static async Task<AgentResponse> FaultInjection(
    IEnumerable<ChatMessage> messages,
    AgentSession? session,
    AgentRunOptions? options,
    AIAgent innerAgent,
    CancellationToken cancellationToken)
{
    if (GetLastUserText(messages).Contains("模拟异常", StringComparison.Ordinal))
    {
        throw new InvalidOperationException("FaultInjection:模拟 Middleware 管道内异常");
    }

    return await innerAgent.RunAsync(messages, session, options, cancellationToken);
}

五、Function 调用 Middleware

5.1 何时生效?

Agent 通过 AsAIAgent(..., tools: tools) 注册工具后,MAF 会在内部使用 FunctionInvokingChatClient 跑 Tool Calling 循环。此时可对每一次工具执行再挂 Middleware。

注册方式(与 Agent Run 的 runFunc 不同,是另一个 Use 重载):

csharp 复制代码
core.AsBuilder()
    .Use(runFunc: AgentRunMiddleware.RequestLogging, runStreamingFunc: null)
    // ... 其他 Agent Run 中间件 ...
    .Use(FunctionInvocationMiddleware.UnknownProductGuard)
    .Use(FunctionInvocationMiddleware.InvocationLogging)
    .Build();

方法签名:

csharp 复制代码
static async ValueTask<object?> YourFunctionMiddleware(
    AIAgent agent,
    FunctionInvocationContext context,
    Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,
    CancellationToken cancellationToken)
{
    object? result = await next(context, cancellationToken);
    return result;
}
参数 含义
context.Function 当前被调用的 AIFunction(如 GetWeather
context.Arguments 模型填写的参数
context.Iteration 第几轮工具调用循环
next 继续执行下一环(最终落到你的 C# 方法)
返回值 可原样返回,也可短路 直接 return "..." 而不调 next

5.2 例子:InvocationLogging --- 工具调用日志

Middleware/FunctionInvocationMiddleware.cs

csharp 复制代码
public static async ValueTask<object?> InvocationLogging(
    AIAgent agent,
    FunctionInvocationContext context,
    Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,
    CancellationToken cancellationToken)
{
    Console.WriteLine($"[MAF.Function] ▶ {context.Function.Name}(...) | iter={context.Iteration}");

    object? result = await next(context, cancellationToken);

    Console.WriteLine($"[MAF.Function] ◀ {context.Function.Name} => ...");
    return result;
}

5.3 例子:UnknownProductGuard 不在调用模型

GetProductInfo,若 productIdX 开头,不调用 next,直接返回拦截文案(模型会基于该结果组织回复):

csharp 复制代码
if (IsGetProductInfo(context.Function.Name)
    && TryGetArgument(context.Arguments, "productId", out string? productId)
    && productId.StartsWith('X'))
{
    return $"【工具拦截】产品编号 {productId} 不允许查询,请使用 P001 / P002 / P003。";
}
return await next(context, cancellationToken);

5.4 与 Agent Run 层的关系

text 复制代码
agent.RunAsync("查 P002")
  → Agent Run Middleware(RequestLog / Governance ...)
    → 核心 Agent + Tool 循环
         → 模型返回 tool_call: GetProductInfo
         → Function Middleware(InvocationLogging → UnknownProductGuard → AgentTools.GetProductInfo)
         → 模型根据工具结果生成最终回复
  ← AgentResponse

两层正交:Agent Run 包「整次对话」;Function 包「每一次工具执行」。


六、中间件链式注册、调用

CreateWithMiddlewarePipeline 根据是否传入 tools 决定是否注册 Function 层:

csharp 复制代码
public static IList<AITool> CreateDemoTools() =>
[
    AIFunctionFactory.Create(AgentTools.GetWeather),
    AIFunctionFactory.Create(AgentTools.GetProductInfo),
];

AIAgent core = tools is null
    ? client.AsAIAgent(instructions: instructions, name: name)
    : client.AsAIAgent(instructions: instructions, name: name, tools: tools);

AIAgentBuilder builder = core.AsBuilder()
    .Use(runFunc: AgentRunMiddleware.FaultInjection, runStreamingFunc: null)
    .Use(runFunc: AgentRunMiddleware.ContentGovernance, runStreamingFunc: null)
    .Use(runFunc: AgentRunMiddleware.ExceptionHandling, runStreamingFunc: null)
    .Use(runFunc: AgentRunMiddleware.RequestLogging, runStreamingFunc: null);

if (tools is not null)
{
    builder = builder
        .Use(FunctionInvocationMiddleware.UnknownProductGuard)
        .Use(FunctionInvocationMiddleware.InvocationLogging);
}

return builder.Build();

无工具时只走 Agent Run 管道;有工具时两层一起启用。


七、IChatClient 层 Middleware(可选)

Middleware/ChatClientLoggingMiddleware.cs模型 HTTP 调用 前后记日志:

csharp 复制代码
public static ChatClientBuilder UseRequestLogging(this ChatClientBuilder builder, string tag = "MAF.ChatClient")
{
    return builder.Use(
        getResponseFunc: async (messages, options, innerClient, cancellationToken) =>
        {
            var sw = Stopwatch.StartNew();
            Console.WriteLine($"[{tag}] ▶ GetResponseAsync | msgs={messages.Count()}");

            ChatResponse response = await innerClient.GetResponseAsync(messages, options, cancellationToken);

            sw.Stop();
            Console.WriteLine($"[{tag}] ◀ GetResponseAsync {sw.ElapsedMilliseconds}ms");

            return response;
        },
        getStreamingResponseFunc: null);
}

与 Agent Run 层的对比:

Agent Run(RequestLogging IChatClient(UseRequestLogging
拦截点 一次 agent.RunAsync 一次 GetResponseAsync
典型用途 业务审计、敏感词、异常文案 HTTP 耗时、重试、链路追踪
工具调用时 一次 Run 可能触发多轮模型调用 每一轮 HTTP 都会经过


八、小结

前几篇让 Agent 能说、能做、能记 ;本篇 AsBuilder().Use(...)对话入口工具入口 两处叠加横切逻辑,让 Agent 更易观测、更易管控,更适合往生产环境推进。


相关推荐
阿里云云原生2 小时前
从追踪到治理:LoongSuite 如何通过 OTel 扩展规范填补 AI Agent 可观测体系的语义空白?
agent
louisliao_19813 小时前
Hermes学习收集
agent
canonical_entropy3 小时前
自进化的两个尺度:RMSP Agent 与 AGE 方法论的深层结构对应
aigc·agent·ai编程
幺风4 小时前
A2UI 技术详解:让 AI Agent 学会“说界面”
agent
米小虾4 小时前
2026 年 AI Agent 开发现状:从概念到产线,这些开源项目正在重新定义自动化
人工智能·agent
zandy10114 小时前
Hermes Agent 2026年6月最新安装教程
docker·github·agent
夜焱辰4 小时前
我花了3个月,把一个终端 AI Agent 搬进了浏览器——踩坑全记录
前端·agent
tech_zjf4 小时前
我如何把「会聊天的 AI」做成「会行动、会记忆、会成长」的社交 Agent
agent·全栈
Agilex松灵机器人4 小时前
松灵技术生态|IsaacLab中实现松灵PIPER机械臂键盘遥操作与数据采集教程
agent·强化学习·仿真·具身智能·skill·松灵机器人