第 15 章 · Faux Provider 与单元测试
本章目标:用 fauxProvider() 编写脚本化 LLM 响应,在 vitest 中对 Agent 循环做确定性测试,摆脱真实 API 依赖。
15.1 为什么 LLM 测试需要 Faux
直接调真实 API 写单测有三个致命问题:慢(秒级响应)、贵(按 token 计费)、不确定(同样输入不同输出,无法断言)。pi-ai 内置的 fauxProvider() 解决了这一切——一个内存中的假 Provider,按你编排的剧本返回响应:
typescript
import {
createModels,
fauxAssistantMessage,
fauxProvider,
fauxText,
fauxThinking,
fauxToolCall,
} from '@earendil-works/pi-ai';
// 创建假 Provider,可指定模拟的输出速度
const faux = fauxProvider({
tokensPerSecond: 50, // 可选:模拟流式速度
});
const models = createModels();
models.setProvider(faux.provider);
// 拿到这个 Provider 附带的模型对象
const model = faux.getModel();15.2 编排脚本化响应
faux.setResponses() 按调用顺序排好一列预设响应,每次请求弹出下一个。可以混合文本、思考块、工具调用任意组合:
typescript
const context = {
messages: [{
role: 'user',
content: 'Summarize package.json and then call echo',
timestamp: Date.now(),
}],
};
// 第一轮:模型先思考,再发起工具调用
faux.setResponses([
fauxAssistantMessage([
fauxThinking('Need to inspect package metadata first.'),
fauxToolCall('echo', { text: 'package.json' }),
], { stopReason: 'toolUse' }), // 关键:声明等待工具结果
]);
const first = await models.complete(model, context, {
sessionId: 'session-1',
cacheRetention: 'short',
});
context.messages.push(first);
// 我们代替真实工具执行,把结果回填进上下文
context.messages.push({
role: 'toolResult',
toolCallId: first.content.find((b) => b.type === 'toolCall')!.id,
toolName: 'echo',
content: [{ type: 'text', text: 'package.json contents here' }],
isError: false,
timestamp: Date.now(),
});15.3 多轮剧本与流式断言
继续编排第二轮——模型"看到"工具结果后给出总结。流式事件同样可以被完整消费和断言:
typescript
// 第二轮:模型基于工具结果总结
faux.setResponses([
fauxAssistantMessage([
fauxThinking('Now I can summarize the tool output.'),
fauxText('Here is the summary.'),
]),
]);
// 流式消费整个循环
const s = models.stream(model, context);
for await (const event of s) {
console.log(event.type); // message_start → delta... → message_end
}剧本机制的价值在于精确复现多轮工具循环:工具调用→结果回填→再次生成,这条 Agent 最核心的路径可以在毫秒级完成验证。
15.4 在 vitest 中测试 Agent 循环
把 Faux Provider 与 Agent 类组合,就能对完整的 Agent 行为写断言:
typescript
// agent.test.ts
import { describe, it, expect } from 'vitest';
import { Agent } from '@earendil-works/pi-agent-core';
import { createModels, fauxAssistantMessage, fauxProvider, fauxText } from '@earendil-works/pi-ai';
describe('coding assistant agent', () => {
it('should answer with scripted response', async () => {
const faux = fauxProvider({ tokensPerSecond: 1000 }); // 加速测试
const models = createModels();
models.setProvider(faux.provider);
// 预设模型回复
faux.setResponses([
fauxAssistantMessage([fauxText('Hello from mock!')]),
]);
const agent = new Agent({
initialState: {
systemPrompt: 'test',
model: faux.getModel(),
tools: [],
messages: [],
},
streamFn: models.streamSimple.bind(models),
});
// 收集事件用于断言
const events: string[] = [];
agent.subscribe((event) => events.push(event.type));
await agent.prompt('Say hi');
expect(events).toContain('agent_start');
expect(events).toContain('agent_end');
});
});测试金字塔建议
用 Faux 覆盖 95% 的逻辑测试(快、免费、确定),只留少量端到端测试打真实 API 验证集成正确性。
本章小结
- 真实 API 测试慢、贵、不确定;Faux Provider 提供内存中的确定性替代;
faux.setResponses()按顺序编排响应,可混合 thinking/text/toolCall 块;- 手动构造 toolResult 回填上下文即可复现完整工具循环;
- Faux + vitest 可对 Agent 类的事件序列做精确断言;
- 策略:Faux 覆盖绝大多数测试,少量 E2E 打真实 API。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. LLM 单元测试使用 Faux Provider 的最主要原因是?
2. faux.setResponses([...]) 设置多个响应时如何工作?
3. 在 Faux 场景中模拟模型发起工具调用后,工具结果应如何处理?
4. 下列哪条是官方推荐的测试策略?
🛠️ 动手实践
- 为第 6 章的文件读取工具编写一套两轮剧本测试:第一轮发起 read 工具调用,第二轮总结内容。
- 用 vitest 断言 Agent 事件的完整顺序:agent_start → turn_start → ... → agent_end。
- 给你的工具函数补充参数校验失败的测试用例(Faux 返回错误格式的参数)。
下一章探索 pi-ai 一个独特能力:对话进行到一半时切换模型。