MAF Middleware中间件入门:从零开始实战教程
2026-06-20阅读 0热度 0
其他
MAF 入门(4):使用 MAF Middleware 中间件
聊到 MAF 中间件之前,先回顾一下前面几篇我们搞定了什么:

* 让 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