🚀 Vercel AI SDK 使用指南:中间件 (Middleware) 深度解析

7 阅读5分钟

在构建 AI 应用时,我们经常需要处理一些与“核心业务逻辑”无关但又至关重要的功能,比如日志记录错误重试内容审查 (Guardrails)缓存 以及 RAG (检索增强生成) 的上下文注入。

如果把这些逻辑都写在每一次模型调用里,代码会变得非常臃肿且难以维护。Vercel AI SDK Core 引入了 中间件 (Middleware) 机制,让你能以优雅的“洋葱模型”方式拦截、修改和增强语言模型的行为。

本文将带你深入了解 Vercel AI SDK 的中间件机制,并手把手教你编写实用的自定义中间件。

什么是中间件?

简单来说,中间件是一个包装器(Wrapper)。它允许你在请求发送给 LLM 之前 修改参数,或者在 LLM 返回响应 之后(但在返回给你的应用之前)拦截并修改结果。

它的核心优势在于 解耦复用。你编写的中间件可以应用在任何兼容 Vercel AI SDK 的模型上(无论是 OpenAI、Anthropic 还是 DeepSeek)。

📦 快速上手:使用 wrapLanguageModel

要使用中间件,你需要从 ai 包中导入 wrapLanguageModel

TypeScript

import { wrapLanguageModel, streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

// 1. 定义或导入你的中间件
const myMiddleware = {
  wrapGenerate: async ({ doGenerate, params }) => {
    console.log('🔒 调用模型前:', params.prompt);
    const result = await doGenerate();
    console.log('🔓 模型响应后:', result.text);
    return result;
  },
};

// 2. 包装原始模型
const wrappedModel = wrapLanguageModel({
  model: openai('gpt-4o'),
  middleware: myMiddleware, // 支持单个对象或数组 [m1, m2]
});

// 3. 像平常一样使用包装后的模型
const result = await streamText({
  model: wrappedModel,
  prompt: '为什么天空是蓝色的?',
});

🛠️ 内置中间件 (Built-in Middleware)

Vercel AI SDK 自带了一些非常实用的中间件,开箱即用:

1. extractReasoningMiddleware (DeepSeek R1 神器)

对于像 DeepSeek R1 这样会输出思维链(CoT)的模型,它们通常把思考过程放在 <think> 标签里。这个中间件可以自动提取这部分内容,让你的结构化输出更干净。

TypeScript

import { wrapLanguageModel, extractReasoningMiddleware } from 'ai';

const model = wrapLanguageModel({
  model: yourDeepSeekModel,
  middleware: extractReasoningMiddleware({ tagName: 'think' }),
});

2. extractJsonMiddleware

有些模型(尤其是小模型)在被要求返回 JSON 时,喜欢自作聪明地加上 Markdown 代码块(json ... )。这个中间件会自动剥离这些外壳,确保 generateObject 能稳定解析。

TypeScript

import { wrapLanguageModel, extractJsonMiddleware } from 'ai';

const model = wrapLanguageModel({
  model: yourModel,
  middleware: extractJsonMiddleware(),
});

💻 实战:编写自定义中间件

自定义中间件本质上是一个对象,实现了 LanguageModelV3Middleware 接口。你可以实现以下三个方法中的任意一个或全部:

  1. transformParams: 在调用模型前修改参数(适用于 RAG、Prompt 注入)。
  2. wrapGenerate: 拦截非流式调用 (generateText, generateObject)。
  3. wrapStream: 拦截流式调用 (streamText)。

实战一:📝 全局日志记录器 (Logger)

这个中间件会记录每次调用的 Prompt 和 Token 使用情况,非常适合调试。

TypeScript

import type { LanguageModelV3Middleware } from '@ai-sdk/provider';

export const loggingMiddleware: LanguageModelV3Middleware = {
  // 拦截非流式生成
  wrapGenerate: async ({ doGenerate, params }) => {
    console.log('[Generate] Request:', JSON.stringify(params.prompt, null, 2));
    
    const result = await doGenerate();
    
    console.log('[Generate] Response:', result.text);
    console.log('[Generate] Usage:', result.usage);
    
    return result;
  },

  // 拦截流式生成
  wrapStream: async ({ doStream, params }) => {
    console.log('[Stream] Request Start');
    const { stream, ...rest } = await doStream();
    
    // 注意:流式拦截比较复杂,通常我们只记录“开始”和“参数”
    // 如果要记录完整的流内容,需要创建一个 TransformStream 来窃听数据
    
    return { stream, ...rest };
  }
};

实战二:🛡️ 安全护栏 (Guardrails) - 敏感词过滤

在企业级应用中,我们可能需要防止模型输出特定的敏感词。

TypeScript

import type { LanguageModelV3Middleware } from '@ai-sdk/provider';

export const safetyMiddleware: LanguageModelV3Middleware = {
  wrapGenerate: async ({ doGenerate }) => {
    // 1. 获取原始结果
    const result = await doGenerate();
    
    // 2. 检查并替换敏感词(这里仅作简单演示,生产环境可用更复杂的正则或专门的模型)
    const sensitiveWords = ['机密', '密码', '内部数据'];
    let safeText = result.text || '';
    
    sensitiveWords.forEach(word => {
      safeText = safeText.replace(new RegExp(word, 'g'), '***');
    });

    // 3. 返回修改后的结果,保留其他元数据(如 usage)
    return {
      ...result,
      text: safeText,
    };
  }
};

实战三:🧠 简易 RAG (上下文注入)

利用 transformParams,我们可以悄悄地把从向量数据库查到的知识注入到 System Prompt 中,而不需要在业务代码里显式拼接。

TypeScript

import type { LanguageModelV3Middleware } from '@ai-sdk/provider';

// 模拟从数据库获取上下文的函数
async function getContextFromDB(query: string) {
  return "知识库补充:Vercel AI SDK 是一个用于构建 AI 应用的 TypeScript 库。";
}

export const ragMiddleware: LanguageModelV3Middleware = {
  transformParams: async ({ params }) => {
    // 假设我们只处理最后一条用户消息
    const lastMessage = params.prompt[params.prompt.length - 1];
    
    if (lastMessage?.role === 'user') {
      const context = await getContextFromDB(lastMessage.content as string);
      
      // 创建一个新的 System Message 注入到 Prompt 头部
      return {
        ...params,
        prompt: [
          { role: 'system', content: `使用以下上下文回答问题: ${context}` },
          ...params.prompt,
        ],
      };
    }
    
    return params;
  },
};

实战四:⚡ 简单缓存 (Caching)

为了省钱和提高速度,对于完全相同的请求,我们可以直接返回缓存结果。

TypeScript

import type { LanguageModelV3Middleware } from '@ai-sdk/provider';

const simpleCache = new Map<string, any>();

export const cacheMiddleware: LanguageModelV3Middleware = {
  wrapGenerate: async ({ doGenerate, params }) => {
    // 1. 生成缓存 Key (简单示例,生产环境请使用更严谨的 Hash)
    const key = JSON.stringify(params.prompt);
    
    // 2. 命中缓存直接返回
    if (simpleCache.has(key)) {
      console.log('⚡ 命中缓存');
      return simpleCache.get(key);
    }
    
    // 3. 未命中,调用模型
    const result = await doGenerate();
    
    // 4. 存入缓存
    simpleCache.set(key, result);
    
    return result;
  }
};

🔗 链式调用 (Chaining)

中间件的强大之处在于可以组合使用。执行顺序就像洋葱:先注册的先执行

TypeScript

const robustModel = wrapLanguageModel({
  model: openai('gpt-4o'),
  middleware: [
    loggingMiddleware, // 最外层:先记录请求,最后记录响应
    ragMiddleware,     // 中间层:注入上下文
    safetyMiddleware,  // 最内层:过滤模型出来的原始脏话
  ]
});

在这个例子中:

  1. 请求进来 -> loggingMiddleware 记录 -> ragMiddleware 修改 Prompt -> safetyMiddleware 透传 -> 模型执行
  2. 响应回来 -> 模型返回 -> safetyMiddleware 过滤 -> ragMiddleware 透传 -> loggingMiddleware 记录结果。

💡 最佳实践

  1. 保持轻量:中间件会运行在每一次 AI 调用中,避免在其中进行耗时过长的同步操作。

  2. 流式处理要注意wrapStream 比较复杂,因为你不能简单地修改 text 字符串(它是流)。如果你需要修改流内容,通常需要使用 TransformStream 对二进制流进行实时处理。

  3. 调试元数据:你可以通过 providerOptions 在请求时动态传递数据给中间件,例如 User ID:

    TypeScript

    await generateText({
      model: wrappedModel,
      prompt: '...',
      providerOptions: {
        myMiddleware: { userId: '12345' } // 中间件内可通过 params.providerMetadata 获取
      }
    });
    

总结

Vercel AI SDK 的中间件机制为开发者提供了极大的灵活性。通过标准化 wrapLanguageModel 接口,我们可以把通用的 AI 工程化逻辑(Log、Cache、Guardrails)从业务代码中剥离出来,写出更干净、更健壮的 AI 应用。

希望这篇指南能帮到你!如果你有任何问题,欢迎在评论区讨论。 Happy Coding! 🚀