在开发 AI 应用时,我们经常面临一个巨大的挑战:LLM(大语言模型)是非确定性的。
同一个 Prompt,今天输出 "Hello",明天可能输出 "Hi"。而且,每次运行测试都要消耗 Token,不仅速度慢,还要花钱(尤其是 GPT-4 这种昂贵的模型)。
Vercel AI SDK Core 提供了一套强大的测试工具 (Test Helpers) ,允许我们构建 Mock Provider,模拟 AI 的各种行为(包括流式输出、延迟、结构化数据)。这样,我们就可以编写稳定、快速且免费的单元测试。
本文将带你掌握 MockLanguageModelV3 和 simulateReadableStream 的核心用法。
🛠️ 核心工具准备
所有测试工具都可以从 ai/test 和 ai 中直接导入:
TypeScript
import { generateText, streamText } from 'ai';
// 核心 Mock 模型
import { MockLanguageModelV3 } from 'ai/test';
// 用于模拟流式传输的工具
import { simulateReadableStream } from 'ai';
场景一:测试文本生成 (generateText)
这是最基础的测试场景。我们不需要真正去调 OpenAI 或通义千问的 API,而是告诉 SDK:“当你可以生成文本时,直接返回这个结果。”
示例代码
TypeScript
import { generateText } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';
// 模拟一个总是返回 "Hello, Juejin!" 的模型
const model = new MockLanguageModelV3({
doGenerate: async () => ({
// 模拟输出内容
content: [{ type: 'text', text: 'Hello, Juejin!' }],
// 模拟结束原因
finishReason: { unified: 'stop', raw: undefined },
// 模拟 Token 消耗(可选,用于测试计费逻辑)
usage: {
inputTokens: 10,
outputTokens: 20,
},
}),
});
async function main() {
const result = await generateText({
model,
prompt: 'Write a greeting',
});
console.log(result.text); // 输出: "Hello, Juejin!"
}
main();
🔍 关键点: doGenerate 是一个异步函数,必须返回符合 AI SDK 规范的对象结构。
场景二:测试流式输出 (streamText)
流式传输 (Streaming) 是 AI 应用体验的核心。在测试中,我们需要模拟数据块 (Chunks) 一个接一个到达的过程,甚至可以模拟网络延迟。
simulateReadableStream 是实现这一点的神器。
示例代码
TypeScript
import { streamText, simulateReadableStream } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';
const model = new MockLanguageModelV3({
doStream: async () => ({
stream: simulateReadableStream({
// 定义流式输出的每一个块
chunks: [
{ type: 'text-start', id: 'chunk-1' },
{ type: 'text-delta', id: 'chunk-1', delta: '正在' },
{ type: 'text-delta', id: 'chunk-1', delta: '生成' },
{ type: 'text-delta', id: 'chunk-1', delta: '内容...' },
{ type: 'text-end', id: 'chunk-1' },
{
type: 'finish',
finishReason: { unified: 'stop', raw: undefined },
usage: { inputTokens: 5, outputTokens: 10 },
},
],
// 可选:模拟每个块之间的延迟(毫秒),测试前端打字机效果
chunkDelayInMs: 100,
}),
}),
});
async function main() {
const result = streamText({
model,
prompt: 'Show me streaming',
});
for await (const textPart of result.textStream) {
process.stdout.write(textPart); // 依次输出:正在 -> 生成 -> 内容...
}
}
main();
场景三:测试结构化数据 (generateObject)
当你使用 generateObject 或 streamObject 生成 JSON 数据时(例如生成前端组件 Props 或提取信息),测试需要确保返回的是合法的 JSON 结构。
示例代码
TypeScript
import { generateObject } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';
import { z } from 'zod';
const model = new MockLanguageModelV3({
doGenerate: async () => ({
// 注意:这里 text 必须是 JSON 字符串
content: [{ type: 'text', text: '{"recipe": "番茄炒蛋", "calories": 200}' }],
finishReason: { unified: 'stop', raw: undefined },
usage: { inputTokens: 10, outputTokens: 20 },
}),
});
async function main() {
const result = await generateObject({
model,
schema: z.object({
recipe: z.string(),
calories: z.number(),
}),
prompt: 'Recommend a dish',
});
console.log(result.object);
// 输出: { recipe: '番茄炒蛋', calories: 200 }
}
main();
💡 进阶技巧:在 CI/CD 中集成
使用 Mock 模型的最大优势在于解耦。
- 本地开发:你可以使用真实的 API Key(OpenAI / 通义千问)来验证效果。
- CI/CD 流水线:在 GitHub Actions 或 GitLab CI 中,你完全不需要配置
OPENAI_API_KEY。直接在测试代码中注入MockLanguageModelV3,既安全又无需担心触发 API 速率限制。
配合 Vitest 单元测试示例
TypeScript
import { describe, it, expect } from 'vitest';
import { generateText } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';
describe('AI Service', () => {
it('should return fixed response', async () => {
const mockModel = new MockLanguageModelV3({
doGenerate: async () => ({
content: [{ type: 'text', text: 'MOCKED_RESPONSE' }],
finishReason: { unified: 'stop', raw: undefined },
usage: { inputTokens: 0, outputTokens: 0 },
}),
});
const { text } = await generateText({
model: mockModel,
prompt: 'test',
});
expect(text).toBe('MOCKED_RESPONSE');
});
});
总结
MockLanguageModelV3 是 Vercel AI SDK 中被低估的神器。通过它,我们可以:
- 省钱:测试不消耗 API 额度。
- 提速:Mock 是毫秒级返回,而真实 LLM 可能需要几秒。
- 稳定:消除了 AI 的随机性,让测试结果 100% 可复现。
Happy Coding! 🚀