在构建 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 接口。你可以实现以下三个方法中的任意一个或全部:
transformParams: 在调用模型前修改参数(适用于 RAG、Prompt 注入)。wrapGenerate: 拦截非流式调用 (generateText,generateObject)。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, // 最内层:过滤模型出来的原始脏话
]
});
在这个例子中:
- 请求进来 ->
loggingMiddleware记录 ->ragMiddleware修改 Prompt ->safetyMiddleware透传 -> 模型执行。 - 响应回来 -> 模型返回 ->
safetyMiddleware过滤 ->ragMiddleware透传 ->loggingMiddleware记录结果。
💡 最佳实践
-
保持轻量:中间件会运行在每一次 AI 调用中,避免在其中进行耗时过长的同步操作。
-
流式处理要注意:
wrapStream比较复杂,因为你不能简单地修改text字符串(它是流)。如果你需要修改流内容,通常需要使用TransformStream对二进制流进行实时处理。 -
调试元数据:你可以通过
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! 🚀