Skip to content

第 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. 下列哪条是官方推荐的测试策略?

🛠️ 动手实践

  1. 为第 6 章的文件读取工具编写一套两轮剧本测试:第一轮发起 read 工具调用,第二轮总结内容。
  2. 用 vitest 断言 Agent 事件的完整顺序:agent_start → turn_start → ... → agent_end。
  3. 给你的工具函数补充参数校验失败的测试用例(Faux 返回错误格式的参数)。

下一章探索 pi-ai 一个独特能力:对话进行到一半时切换模型。