如何用 AI SDK 的 wrapLanguageModel 和 Language Model Middleware 拦截修改模型调用
【免费下载链接】aiThe AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai
在用 AI SDK 调用语言模型时,generateText和streamText的参数在每次调用处都要手动处理,想加日志、缓存、RAG 注入或结果过滤时只能散落在各个调用点。AI SDK 提供的 Language Model Middleware 机制可以把这类逻辑集中在模型层:用wrapLanguageModel函数把一个模型和一个(或多个)middleware 组合成一个新的语言模型,所有后续调用都会经过 middleware 的拦截。本文按「实现 middleware → 应用到模型 → 验证拦截生效」的顺序演示这条路径,内容基于 AI SDK 的 Language Model Middleware 文档。
准备条件:
- 一个 TypeScript 项目,代码从
ai包导入wrapLanguageModel等函数; - 一个 provider 包用于创建模型实例,下文示例使用
@ai-sdk/openai,请替换为你实际使用的 provider 和模型; - 对应 provider 的 API key(仓库示例通过
dotenv/config加载环境变量,可参考 Local Caching Middleware 示例)。
middleware 的三个拦截点
自定义 middleware 是一个LanguageModelV4Middleware类型的对象(从@ai-sdk/provider导入),可以按需实现以下三个函数中的任意一个,类型定义见 language-model-v4-middleware.ts:
| 函数 | 作用 |
|---|---|
transformParams | 在参数传给模型前转换它们,对doGenerate和doStream都生效;接收{ type: 'generate' \| 'stream', params, model },返回转换后的参数 |
wrapGenerate | 包装doGenerate:可以修改参数、调用模型、再修改结果 |
wrapStream | 包装doStream:与wrapGenerate对应,处理流式调用的参数、调用和返回的 stream |
middleware 对象按当前规范声明时带有specificationVersion: 'v4'字段(仓库中的 本地缓存 middleware 示例展示了带该字段的写法)。文档提醒:实现自定义 middleware 属于进阶功能,需要熟悉 LanguageModelV4 规范。
用 wrapLanguageModel 把 middleware 应用到模型
wrapLanguageModel接收一个模型和 middleware,返回一个集成了 middleware 的新语言模型,用法和普通模型完全一致(见 API 参考):
import { openai } from '@ai-sdk/openai'; import { wrapLanguageModel, streamText } from 'ai'; const wrappedLanguageModel = wrapLanguageModel({ model: openai('gpt-4o'), // 文档中写作 yourModel,替换为你的 provider 模型实例 middleware: yourLanguageModelMiddleware, }); const result = streamText({ model: wrappedLanguageModel, prompt: 'What cities are in the United States?', });middleware参数也可以是数组。多个 middleware 按提供顺序应用:第一个先转换输入,最后一个直接包裹在模型外层:
const wrappedLanguageModel = wrapLanguageModel({ model: yourModel, middleware: [firstMiddleware, secondMiddleware], }); // applied as: firstMiddleware(secondMiddleware(yourModel))此外还有两个可选参数:modelId和providerId,用于覆盖原模型的 ID 和 provider 标识。
验证拦截:一个完整的日志 middleware
文档给出的 Logging 示例同时实现了wrapGenerate和wrapStream,是最直接的验证方式:如果 middleware 生效,控制台会按调用过程输出参数和生成文本。
import type { LanguageModelV4Middleware, LanguageModelV4StreamPart, } from '@ai-sdk/provider'; export const yourLogMiddleware: LanguageModelV4Middleware = { wrapGenerate: async ({ doGenerate, params }) => { console.log('doGenerate called'); console.log(`params: ${JSON.stringify(params, null, 2)}`); const result = await doGenerate(); const generatedText = result.content .filter(part => part.type === 'text') .map(part => part.text) .join(''); console.log('doGenerate finished'); console.log(`generated text: ${generatedText}`); return result; }, wrapStream: async ({ doStream, params }) => { console.log('doStream called'); console.log(`params: ${JSON.stringify(params, null, 2)}`); const { stream, ...rest } = await doStream(); let generatedText = ''; const textBlocks = new Map<string, string>(); const transformStream = new TransformStream< LanguageModelV4StreamPart, LanguageModelV4StreamPart >({ transform(chunk, controller) { switch (chunk.type) { case 'text-start': { textBlocks.set(chunk.id, ''); break; } case 'text-delta': { const existing = textBlocks.get(chunk.id) || ''; textBlocks.set(chunk.id, existing + chunk.delta); generatedText += chunk.delta; break; } case 'text-end': { console.log( `Text block ${chunk.id} completed:`, textBlocks.get(chunk.id), ); break; } } controller.enqueue(chunk); }, flush() { console.log('doStream finished'); console.log(`generated text: ${generatedText}`); }, }); return { stream: stream.pipeThrough(transformStream), ...rest, }; }, };把它应用到模型上并发起调用:
import { openai } from '@ai-sdk/openai'; import { generateText, wrapLanguageModel } from 'ai'; import { yourLogMiddleware } from './your-log-middleware'; const result = await generateText({ model: wrapLanguageModel({ model: openai('gpt-4o'), middleware: yourLogMiddleware, }), prompt: 'What cities are in the United States?', });如何判断生效:generateText走wrapGenerate分支,日志依次出现doGenerate called、格式化的请求参数、doGenerate finished和完整生成文本;改用streamText则走wrapStream分支,每完成一个文本块打印一条Text block ... completed:,流结束时在flush中打印doStream finished和汇总文本。注意wrapStream里必须用controller.enqueue(chunk)把 chunk 原样转发,否则流会被中断。
拦截修改请求参数:transformParams 与按请求传元数据
transformParams适合在参数到达模型前做注入或改写。一个实用场景是通过providerOptions为每次调用附带自定义元数据(如用户 ID、时间戳),供日志类 middleware 读取:
import { openai } from '@ai-sdk/openai'; import { generateText, wrapLanguageModel } from 'ai'; import type { LanguageModelV4Middleware } from '@ai-sdk/provider'; export const yourLogMiddleware: LanguageModelV4Middleware = { wrapGenerate: async ({ doGenerate, params }) => { console.log('METADATA', params?.providerMetadata?.yourLogMiddleware); const result = await doGenerate(); return result; }, }; const { text } = await generateText({ model: wrapLanguageModel({ model: openai('gpt-4o'), middleware: yourLogMiddleware, }), prompt: 'Invent a new holiday and describe its traditions.', providerOptions: { yourLogMiddleware: { hello: 'world', }, }, }); console.log(text);文档示例中的 provider 导入和模型占位符在这里以openai('gpt-4o')展开,替换成你自己的模型即可。验证方式:控制台会打印METADATA后跟该次调用传入的元数据对象,text照常返回生成结果。
拦截修改响应:guardrails 示例
反过来,middleware 也可以在模型返回后修改结果。文档给出的 guardrails 示例在wrapGenerate中对文本内容做过滤(此处为示例逻辑,把敏感词替换为<REDACTED>):
import type { LanguageModelV4Middleware } from '@ai-sdk/provider'; export const yourGuardrailMiddleware: LanguageModelV4Middleware = { wrapGenerate: async ({ doGenerate }) => { const result = await doGenerate(); // filtering approach, e.g. for PII or other sensitive information: const content = result.content.map(part => part.type === 'text' ? { ...part, text: part.text.replace(/badword/g, '<REDACTED>') } : part, ); return { ...result, content }; }, };缓存是文档给出的另一个完整示例:在wrapGenerate中用JSON.stringify(params)作为缓存键,命中则直接返回缓存的LanguageModelV4GenerateResult,未命中则调用doGenerate()并把结果写入缓存。验证方式是同一组参数第二次调用直接命中缓存、不再触发模型请求;文档示例只实现了wrapGenerate,流式缓存需要自行实现。如果需要基于文件、且同时支持流式的本地缓存,仓库提供了完整的 Local Caching Middleware cookbook,它通过wrapStream记录每个 chunk,命中缓存时用simulateReadableStream以每 10ms 一个 chunk 的速度重放流。
内置 middleware:无需自行实现的拦截能力
如果目标行为已有内置实现,可以直接传入wrapLanguageModel,不需要自己写函数。内置 middleware 包括:
extractReasoningMiddleware:从生成文本中提取推理信息并暴露为结果的reasoning属性,选项tagName指定标签名;startWithReasoning: true时会在生成文本前补上推理标签,适用于回复开头不含推理标签的模型(更多细节见 DeepSeek R1 指南);extractJsonMiddleware:剥离模型把 JSON 包在 markdown 代码围栏里的输出,使其兼容Output.object(),可用transform传入自定义转换函数处理其他格式;simulateStreamingMiddleware:对只返回完整响应的模型模拟流式行为;defaultSettingsMiddleware:为模型应用默认设置,如temperature: 0.5、maxOutputTokens: 800、providerOptions;defaultInstructionsMiddleware:为不含 system message 的调用应用默认指令,调用上直接提供的指令优先于默认值;instructions也可传SystemModelMessage(或数组);addToolInputExamplesMiddleware:把工具的inputExamples序列化进工具描述,供不支持inputExamples属性的 provider 使用;选项有prefix(默认'Input Examples:')、format(默认JSON.stringify(example.input))、remove(默认true,添加后移除原属性)。
限制与注意事项
- 文档明确说明 Logging、Caching、RAG、Guardrails 等自定义示例仅用于展示写法,不是生产实现;其中 RAG 示例用到的
getLastUserMessageText、findSources等辅助函数不属于 AI SDK。 - 流式 guardrails 难以实现:在流结束前无法知道完整内容,文档示例因此只实现了
wrapGenerate侧。 defaultInstructionsMiddleware把归一化 prompt 中已有的任意 system message 视为调用级指令,不会追加默认值;allowSystemInMessages只对受信任的消息历史开启,因为历史中的 system message 会覆盖这些默认值。- 文件缓存方案(cookbook)只面向本地开发而非生产环境;想要新响应时删除缓存文件即可失效缓存,且缓存路径应加入
.gitignore。使用stopWhen的多步流程中,缓存发生在单个模型响应级别:工具调用不会被缓存,每次都会执行。
下一步
wrapLanguageModel完整参数:API 参考;- 各内置 middleware 的参数细节:simulateStreamingMiddleware、defaultInstructionsMiddleware、addToolInputExamplesMiddleware、extractJsonMiddleware;
- middleware 机制总览:Language Model Middleware。
【免费下载链接】aiThe AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考