首页 / 文章 / 如何在不烧钱的情况下测试AI产品
← 返回
AI技术

如何在不烧钱的情况下测试AI产品

✍️ zhirenhun 📅 2026/7/14 👁 47 阅读 ⏱ 11 分钟
如何在不烧钱的情况下测试AI产品

如何在不烧钱的情况下测试AI产品

原文:https://dev.to/debs_obrien/how-we-test-an-ai-product-without-burning-credit-4c5p


大多数测试都很廉价。点击按钮,断言某些变化,运行一千次也无人察觉。但测试AI产品则不同,因为有趣的行为来自模型,每次触发都需要付费。

我们在AI Platform上构建课程产品时直接遇到了这个问题。我想带您了解我们最终如何使用Playwright对整个聊天流程进行端到端测试。

首先,平台是什么

Zephyr Cloud的AI Platform (TAP) 是一款桌面应用,团队可以在频道中与AI专家协作。可以理解为Slack,但频道中的某些成员是你可以对话、提及和分配任务的AI代理。

创建频道,提及专家,它就会像其他成员一样在对话中回复。这个聊天界面是产品的核心,因此也是我们最需要信任的部分。

Kent的课程是什么

我们与Kent C. Dodds合作,在平台上构建了一个课程试点项目。您可以在此处阅读公告。

该课程是一个产品工程工作坊,存在于平台内一个名为Course Studio的界面中。学习者不是观看视频,而是通过与AI利益相关者聊天来完成真实练习。有一位名为Kody的向导帮助你构建问题框架,还有像产品VP这样的利益相关者专家,你可以通过采访收集证据。完成后,你撰写一份简短备忘录,一个隐藏的AI评估器会读取整个对话并根据评分标准打分。

所以这是一个真正的AI产品,层层叠加在另一个AI产品之上。使用起来很愉快,测试起来有点吓人。

为什么测试很困难

从测试的角度看一个练习。要完成它,学习者需要:

  • 提及向导并获取响应
  • 采访一个或多个利益相关者并获取响应
  • 提交备忘录
  • 触发评估器,读取所有内容并返回分数

每一步都是真实的模型调用。路由消息以决定谁回答是一次调用。每个专家回复是一次调用。评估器是另一次调用,而且是一次大调用,因为它读取整个对话。

现在乘以五个练习,再乘以每次测试套件运行的次数。如果我们以显而易见的方式测试,成本会随着每次运行而攀升,而且随着我们添加更多内容,套件会变得更慢、更不稳定。这不是任何人想要运行的套件。

思路:替换模型,保持其他一切真实

这是我喜欢的地方。我们没有模拟整个应用或存根UI。我们保持所有内容真实,只替换了一个昂贵的部分:模型。

这些测试使用Playwright驱动实际的桌面应用,与学习者运行的应用相同。为了实现可测试性,应用在window上暴露了其聊天提供者。我们的测试工具介入,保留对真实提供者的引用,并包装了原本会与模型通信的三个函数:

  • 决定谁应该回答的函数
  • 发送专家回复的函数
  • 将更新推回UI的事件流

当测试提及专家时,测试工具用脚本化的回复代替调用模型。当评估器运行时,它返回固定的评分结果。其他一切——消息、时间线、头像、评分条——仍然通过真实代码运行,并完全按照学习者看到的方式渲染。

图表:真实聊天UI与测试工具通信,测试工具拦截模型调用并返回脚本化的专家和评估器回复,而消息仍然通过真实API保存

消息仍然通过真实聊天API保存。因此我们正在端到端地测试真实的产品体验。唯一缺少的是账单。

你可能会问为什么我们不直接模拟网络。我们想要真实的routing、真实的消息管道和真实的UI运行,而这些正是网络模拟会跳过的部分。在提供者层面拦截是最小的替换,保留了学习者实际接触的所有内容。

我们实际测试什么

我们运行所有五个练习作为完整完成流程。对于每个练习,测试:

  • 打开一个新的尝试,创建真实的聊天室
  • 提及向导并断言正确的回复到达
  • 采访每个利益相关者并检查他们的响应
  • 提交备忘录
  • 观察真实的评分条填充到100%

这读起来几乎像是对学习者行为的描述,这正是你希望测试看起来的样子。以下是其结构:

test('学习者完成练习,不消耗积分', async ({ app }) => {
  await installDeterministicHarness(app.page, config);
  
  await openExerciseAttempt(app.page, config); // 真实聊天室,真实UI
  
  // 提及专家并断言脚本化回复
  await sendMentionedMessage(app.page, 'Kody', '帮我构建问题框架。');
  await expectAssistantMessage(app.page, config.kodyResponseAnchor, 'Kody');
  
  // 提交备忘录,观察真实评分条填充
  await sendPlainMessage(app.page, config.finalMemo);
  await expect(app.page.getByTestId('course-attempt-eval-chip')).toHaveText(
    /100%/,
  );
  
  // 证明评估器被拦截,未计费
  await expect
    .poll(async () => (await readHarnessCalls(app.page)).evaluatorCallCount)
    .toBeGreaterThan(0);
});

保持诚实的护栏

这种方法的陷阱在于:一个看似免费但悄悄进行真实调用的测试比没有测试更糟糕,因为你信任它,而账单却在悄悄攀升。

因此测试工具是严格的。任何它没有脚本的消息都不会回退到真实调用。它会返回无害的"忽略"代替。非评估器的专家轮次返回阻塞的存根。我们断言评估器确实被拦截,这样测试永远不会默默跳过它本应检查的内容。

我知道这很重要,因为我曾发布过一个标题为以下内容的修复:

test(course-studio): 防止确定性测试使用提供者

一个未匹配的消息曾经悄悄通过到真实提供者。它工作了,测试通过了,但每次运行都在悄悄花钱。修复方法是让测试工具永远拒绝这样做。测试项目甚至以no-credit命名,这样一眼就能看出哪些套件是安全的。

什么有效,什么无效

比我预期效果更好的:保持UI真实。因为我们只替换模型,测试能捕获真实的UI回归。如果时间线停止渲染回复,或评分条停止更新,测试会失败,这是真正的产品bug,而不是模拟不同步。

并非免费获得:

  • 保持脚本与产品同步。脚本化回复必须随着练习变化而保持可信。当内容变动时,fixtures也必须随之变动。
  • 使失败诚实。大部分工作不是伪造happy path,而是确保测试工具不能撒谎。"忽略未匹配消息"规则和评估器断言都存在,因为天真的版本看起来正常,却在做错误的事情。
  • webview边界。这些测试针对真实运行的应用,这对信心很好,但意味着它们不是你在每次按键时运行的快速、隔离的单元测试。它们是独立的层级。

这对你意味着什么

测试过去主要关乎正确性。对于AI产品,它还关乎成本,两者相互矛盾。模拟太多,测试通过而真实产品悄悄崩溃。模拟太少,每次运行都花钱。

对我们来说,解决之道是找到堆栈中最昂贵的单次调用,并在尽可能接近模型的地方拦截它,然后让其他一切真实运行。你保持诚实的端到端覆盖,成本不再随测试数量增长。如果你在模型之上构建,你也会遇到同样的障碍,我认为大多数团队最终会在某个地方画这样一条线。

真正值得厘清的唯一问题是:你能否信任自己划定的界限。那些看似自由却暗藏真实调用的测试最为危险,因为你会因此放松对账单的监控。因此,无论你将界限划在何处,当有事物越界时,务必发出响亮的警报。这才是值得精心构建的部分。

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

AI 测试 Playwright 代理
← 上一篇
提示词为何失败(以及如何修正)
下一篇 →
AI时代的可观测性设计(第二部分):PII防护与自愈

📌 相关推荐

如何阻止Claude说“承重”之类的话
2026/7/15
LogiChat:从问答对到文档RAG的架构重构
2026/7/15
AI时代的可观测性设计(第二部分):PII防护与自愈
2026/7/15
← 返回文章列表