MAF Middleware中间件入门:从零开始实战教程

2026-06-20阅读 0热度 0
其他

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

聊到 MAF 中间件之前,先回顾一下前面几篇我们搞定了什么:

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

* 让 Agent 能**对话**(`RunAsync`) * 让 Agent 能**调工具**(`tools:`) * 让 Agent 能**记历史**(`AgentSession`) 基础能力齐了,但一旦往生产环境推,你很快就会遇到一堆“每个请求都要做,但又跟业务逻辑无关”的事情。比如每次 Run 都要打日志、记耗时,敏感词得在调用前拦下来,异常得转成用户友好的提示……这些需求,如果直接在业务代码里写,那每个 `RunAsync` 前后都得来一遍 copy-paste,跟 Agent 逻辑搅在一起,维护起来相当痛苦。 这类问题,软件工程里有个专门的名字——**横切关注点(Cross-Cutting Concerns)**。MAF 的 **Middleware(中间件)** 就是为此设计的标准插槽。它让你在 Agent 执行前后(甚至底层模型 HTTP 调用前后)插入逻辑,不用动 Agent 核心代码,就能把日志、治理、鉴权这些能力一层层叠加上去。

MAF 的三层 Middleware

MAF 把扩展点分成了三个层次。今天先重点实现前两层,第三层顺带介绍一下。 | 层级 | 拦截什么 | 注册方式 | |---|---|---| | **Agent Run** | 每次 `AIAgent.RunAsync` / `RunStreamingAsync` | `agent.AsBuilder().Use(runFunc:, runStreamingFunc:)` | | **Function 调用** | 工具(如 `GetWeather`)执行前后 | `agent.AsBuilder().Use(functionCallback)` | | **IChatClient** | 发往推理服务的请求 | `chatClient.AsBuilder().Use(getResponseFunc:, ...)` | 调用链路是这样的:
你的代码: agent.RunAsync("你好")
        │
        ▼  ┌─────────────────────────────────────┐
           │  Agent Run Middleware(业务层)      │  日志、敏感词、异常包装
           └─────────────────┬───────────────────┘
                             ▼
           ┌─────────────────────────────────────┐
           │  ChatClientAgent / 工具循环等        │
           └─────────────────┬───────────────────┘
                             ▼
           ┌─────────────────────────────────────┐
           │  IChatClient Middleware(传输层)    │  HTTP 耗时、原始请求
           └─────────────────┬───────────────────┘
                             ▼
                        百炼 / OpenAI API
Agent Run 层处理业务语义——审计、限流、敏感词、统一异常文案;IChatClient 层处理传输语义——HTTP 耗时、重试、链路 ID、请求体大小。这两层日志可以同时开启:Agent 层看“一次 Run 业务”,IChatClient 层看“一次 GetResponseAsync 网络调用”,视角不同,互不干扰。

Agent Run Middleware:实现

**注册入口:`AIAgentBuilder.Use`** 在已有的 `AIAgent` 上通过 Builder 挂中间件,再 `Build()` 得到带管道的 Agent: ```csharp var agent = coreAgent .AsBuilder() .Use(runFunc: YourMiddleware, runStreamingFunc: null) .Build(); ``` `runFunc` 是一个委托方法,签名如下: ```csharp static async Task YourMiddleware( IEnumerable messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken) { var response = await innerAgent.RunAsync(messages, session, options, cancellationToken); return response; } ``` 参数含义: | 参数 | 含义 | |---|---| | `messages` | 本轮传入的消息(含历史,取决于你怎么调 `RunAsync`) | | `session` | 会话对象;有 `AgentSession` 时可用它区分多用户 | | `options` | 本轮 `AgentRunOptions` | | `innerAgent` | **管道下一环**:可能是下一个 Middleware,或者最内层的核心 Agent | | 返回值 | 可原样返回,也可替换为新的 `AgentResponse` | 执行模型其实很简洁,就两句话: 1. **继续管道** → 调用 `await innerAgent.RunAsync(...)` 2. **短路**(不调大模型)→ 直接 `return new AgentResponse(...)`,**不要**调 `innerAgent` **链式多个 `.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` 先进入这一层)。请求从外往里走,响应从内往外返回。 **关于流式** `Use` 还可以传入 `runStreamingFunc` 来处理 `RunStreamingAsync`。为聚焦非流式场景,demo 中统一写 `runStreamingFunc: null`。生产环境若要用流式,建议为流式单独实现一版,或者使用文档中的 `Use(sharedFunc:)`(适合只改输入、不改输出的场景)。

四个 Middleware 实现详解

文件:`Middleware/AgentRunMiddleware.cs`。四个静态方法,方法名即职责。 **RequestLogging — 请求/响应可观测** 在调用 `innerAgent` 前后打日志:会话标识、消息条数、粗算输入 Token、用户预览、耗时、`Usage`。 ```csharp public static async Task RequestLogging( IEnumerable 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`,便于和账单对照。 **ContentGovernance — 敏感词短路** 默认拦截关键词:`密码`、`身份证号`、`apikey`、`sk-`。命中后**不调用** `innerAgent`,直接返回助手消息。这意味着什么?拦截操作不调用大模型,也就不会产生 Token 费用。 ```csharp public static async Task ContentGovernance( IEnumerable 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` 数组即可。 **ExceptionHandling — 异常包装** 内层 Middleware 或核心 Agent 抛错时,转为 `AgentResponse`,调用方仍然走正常的 `await agent.RunAsync(...)` 流程。但 `OperationCanceledException` 会继续向上抛,不会吞掉取消信号。 ```csharp public static async Task ExceptionHandling( IEnumerable 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})")); } } ``` **FaultInjection — 异常场景演示** 用户输入包含“模拟异常”时抛出 `InvalidOperationException`,用于验证 `ExceptionHandling` 是否生效。仅 demo 使用,生产环境不要保留。 ```csharp public static async Task FaultInjection( IEnumerable 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

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 YourFunctionMiddleware( AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken) { object? result = await next(context, cancellationToken); return result; } ``` 参数含义: | 参数 | 含义 | |---|---| | `context.Function` | 当前被调用的 `AIFunction`(如 `GetWeather`) | | `context.Arguments` | 模型填写的参数 | | `context.Iteration` | 第几轮工具调用循环 | | `next` | 继续执行下一环(最终落到你的 C# 方法) | | 返回值 | 可原样返回,也可**短路**直接 `return "..."` 而不调 `next` | **例子:InvocationLogging — 工具调用日志** 文件 `Middleware/FunctionInvocationMiddleware.cs`: ```csharp public static async ValueTask InvocationLogging( AIAgent agent, FunctionInvocationContext context, Func> 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; } ``` **例子:UnknownProductGuard — 不在调用模型** 对 `GetProductInfo`,若 `productId` 以 `X` 开头,**不调用** `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); ``` **与 Agent Run 层的关系** ``` 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 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 更易观测、更易管控,也更适合往生产环境推进。
免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策