移动 AI 客户端的真实故障场景(权限撤销、进程挂起、429 超限)无法在 happy path 测试中发现;影子模式与真实流程并行记录,可暴露生产隐患。
你开发了一款习惯追踪应用。用户拒绝了通知权限,你的应用会显示一行解释文字。这行文字目前是静态的。模型可以写出一个更好的句子。但每次 API 调用在手机上都要花钱,所以你写了一条 fallback 路径。Fallbck 在网络失败时触发。你的测试在 Wi-Fi 下通过了。模拟器显示了 fallback。审核人员通过了。真实用户看到的只是你的猜测。
那个悄悄坏掉的 fallback 假设
你的 fallback 只在网络错误时触发。大多数移动 AI 客户端都是这样写的。但真实移动用户的失败方式完全不同。操作系统在请求中途挂起了你的应用。用户在 prompt 还在传输过程中撤销了权限。当配额用尽时,端点返回 429。手机在重试和响应之间断开了。
这些都不会出现在正常流程的演示里。但这些都会出现在生产环境的支持工单里。影子测试能让你在用户发现问题之前先找到它。
什么是影子测试
影子模式让模型与真实产品流程并行运行。它永远不会将模型输出呈现给用户。产品路径仍然走你确定性的 fallback。影子路径记录模型本会说的话、耗时多少、以及消耗了多少 token。你之后在日志文件里对比这些记录。Fallback 持续运行,直到模型赢得信任。
免费模型和 MonkeyCode 提供的免费服务器正好支持这种模式:一个端点输出模型结果,一个地方存放日志。披露:本文是 MonkeyCode 产品推广的一部分。
把试用配额当作预发环境预算,而不是生产 SLA。让生产环境的客户端仍然指向你自己的端点。只有影子运行器单独指向免费端点。
一个可以复制的影子运行器
这个脚本通过一个 OpenAI 兼容的端点发送一个真实 prompt,记录测量值,并将输出与你的 fallback 字符串对比。它捕获了你的 fallback 逻辑关心的确切条件:HTTP 状态码、延迟、token 用量和输出长度。没有模型输出会被写回应用。
// shadow-runner.mjs — one prompt, one free model, one trace
// Usage:
// SHADOW_BASE_URL=https://... \
// SHADOW_API_KEY=test-key \
// SHADOW_MODEL=free-model \
// node shadow-runner.mjs ./prompt.txt
import { readFile } from "node:fs/promises";
const prompt = await readFile(process.argv[2] ?? "./prompt.txt", "utf8");
const fallback = "We need notifications to remind you about your streaks.";
const t0 = performance.now();
const res = await fetch(`${process.env.SHADOW_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.SHADOW_API_KEY}`,
},
body: JSON.stringify({
model: process.env.SHADOW_MODEL,
messages: [{ role: "user", content: prompt }],
temperature: 0.3,
max_tokens: 200,
}),
signal: AbortSignal.timeout(12_000),
});
const latencyMs = Math.round(performance.now() - t0);
const data = await res.json();
const modelText = data.choices?.[0]?.message?.content ?? "";
const fallbackWins =
res.status >= 400 || modelText.trim().length < 10;
console.log(JSON.stringify({
httpStatus: res.status,
latencyMs,
promptTokens: data.usage?.prompt_tokens ?? null,
completionTokens: data.usage?.completion_tokens ?? null,
modelText: modelText.slice(0, 280),
fallbackWins,
}, null, 2));
Node 18 或更高版本就够了。从本地 shell 导出这三个环境变量,不要从应用二进制文件导出。实验期间,prompt 文件应该只包含合成内容或已脱敏的文案。
一个 trace 示例如下。它展示的是记录的形态,而不是基准结果。
{
"httpStatus": 200,
"latencyMs": 1841,
"promptTokens": 86,
"completionTokens": 17,
"modelText": "We need notifications to remind you about your streaks. Want to keep them?",
"fallbackWins": false
}
fallbackWins 是一个长度启发式规则,不是质量判断。在这种规则下,一个长的幻觉输出仍然可能获胜。用 trace 来决定哪些场景值得人工 review。
为你的 fallback 设计的故障演练
仅靠运行器无法证明端点正常工作。它无法证明生命周期能存活下来。在真实设备上用这六种场景运行同一请求,并记录每个结果。
Wi-Fi 基线。运行运行器三次,记录延迟分布。这成为你超时预算的参考。
飞行模式中途。在请求开始后、在它稳定之前切换飞行模式。检查 fallback 是否渲染,还是转圈一直挂起。
后台杀死。开始请求后,将应用切入后台,强制退出,然后重新启动。检查重试队列状态。
权限撤销。当请求还在传输中时,撤销通知权限。功能必须自行禁用,绝不发送空白 prompt。
端点故障。将 SHADOW_BASE_URL 重定向到返回 429 的桩。确认 fallback 触发,重试循环退避。
重试风暴。连续让端点失败五次。从平台能量监控器测量电池影响和数据用量。
每个场景对应一条日志记录。每条日志记录要么证实你的 fallback 设计,要么与之矛盾。这种矛盾就是 review 意见,而不是猜测。
何时应该让 fallback 胜出
阅读 trace 时使用这个决策表。它保持判断在设备和审核人员之间的一致性。
这个表不能替代产品判断。它防止最常见的错误:在模型最容易失败的场景下信任模型。
谁应该跳过这个工作流
影子测试假设你的功能已经有了一条确定性 fallback 和通往模型的网路径。这个前提并不适合所有人。
端侧模型团队。没有端点配额,也没有网络故障需要模拟。你需要的是离线评估集。
内容关键表面。医疗、法律或安全相关的文案不应该通过影子测试进入生产环境。使用人工 review 循环。
生产级 24/7 服务。免费预发环境是测试预算,不是高可用层。
有严格数据驻留要求的团队。来自开发者笔记本的影子运行可能会将 prompt 路由到你无法控制的区域之外。先验证端点位置。
同时保持影子端点远离真实用户消息。合成 prompt 和脱敏文案保持实验的诚实性,并缩短隐私审查。
下次写 fallback 时
你的 fallback 是一个产品决策,不是占位符。它应该和屏幕设计或网络超时一样享有同等的证据。免费模型让这种证据变得便宜。免费服务器给你一个收集证据的地方。
下次你的 PR 描述写"添加了 fallback"时,加一行:在预发环境中针对免费模型做了影子测试。你的审核人员可能还是会问问题。但一条 trace 能在几毫秒内回答他们。