1. 为什么要衡量 workflow
简而言之:没有分析,你活在“我觉得”的模式里,而不是“我知道”的模式里。
在本模块之前的课程中,我们已经把场景拆成步骤,给 GPT、小部件和 MCP 分配了角色,讨论了 tool‑gating 以及跨步骤的状态存储。现在换个视角,从分析的角度看这套结构:它是否按预期工作?用户到底卡在了哪里?
在传统 Web 里大家早已习惯转化漏斗:多少人到了着陆页、多少人把商品加入购物车、多少人完成付款。ChatGPT App 里也是同理,只不过页面换成了 workflow 的步骤,而“点击购买按钮”换成了用户话语、工具调用与小部件交互的组合。
当你在没有指标的情况下构建复杂场景时,你看不到:
- 用户最常在哪个步骤“流失”;
- 他们哪里会停留很久阅读(或者只是去倒了杯茶就没回来);
- 哪个步骤完全没价值,甚至只让人反感;
- 提示词或 tool gating 的修改如何影响行为。
按步骤做分析的目标很简单:学会提高完成场景的人群比例、缩短到达结果的时间,并减少错误与客服求助。
从这一刻起,workflow 不再只是架构对象或 UX 闯关,它还是一套可量化的东西。
2. ChatGPT App 中的场景漏斗
在经典 Web 中,漏斗是线性的: Landing → Product → Cart → Checkout。 在 ChatGPT App 里画面会更“活泼”些:用户可能一句话就“跳过”某个步骤,模型有时也会略过步骤,而小部件与对话文本还可能不同步。
尽管如此,基本思路一致:我们有一串步骤,在每一步都会有人继续向前,也会有人离开。
以我们的 GiftGenius 为例:
- collect_recipient — ChatGPT 与小部件收集收件人的基础信息(性别、年龄、关系、兴趣)。
- collect_budget — 询问预算与货币。
- suggest_ideas — MCP/代理挑选创意,并把礼物卡片返回到小部件。
- review_selection — 用户点赞/隐藏创意,并选出 1–2 个候选。
- checkout — 创建 commerce intent,完成下单。
以漏斗形式可以这样画:
flowchart TD
A[Workflow 开始] --> B["1\. 收件人"]
B --> C["2\. 预算"]
C --> D["3\. 礼物创意"]
D --> E["4\. 选择礼物"]
E --> F["5\. Checkout"]
但别忘了:用户可能会在聊天里说“直接去付款吧”或“先给我看贵一点的选项”,模型也可能决定跳过部分步骤。因此,ChatGPT App 的按步骤分析不仅仅关乎 UI 屏幕,还关乎模型行为:哪些步骤被实际走过、顺序如何、是谁发起了跳转——用户、小部件还是 GPT。
3. 按步骤的基础指标
先从经典的产品分析开始,再稍作适配到 ChatGPT App。
每个 workflow 至少需要四个基础指标。
为方便起见,我们把它们汇总成表:
| 指标 | 含义 | 典型问题 |
|---|---|---|
| Start rate | 有多少用户实际启动了该场景 | 我们的 App 是否真的展示给了用户? |
| Completion rate | 有多少用户走到了终点 | 该场景把用户带到结果的能力如何? |
| Conversion per step | 从步骤 N 进入步骤 N+1 的用户占比 | 到底哪个步骤在“漏人”? |
| Drop-off per step | 在步骤 N 流失的用户占比 | 用户最常在哪个步骤放弃? |
通常还会加上一些“投入/努力”类指标:
- 每个步骤的平均用时(用户在哪些地方“卡住”);
- 每步的交互次数(需要多少消息/点击);
- 以错误或需要重试结尾的步骤占比。
在 LLM 场景下,还会有更特定的指标,比如模型工具选择的准确率,或在特定步骤“幻觉式”回答的占比——这属于进阶内容,我们会在最后几个模块再详谈。
对 commerce 场景,还会在步骤之上叠加业务指标:
- 从 workflow 启动到支付的转化率;
- 从特定步骤(例如“创意推荐”)到支付的转化率;
- 客单价;
- 取消/退货占比。
重要的是:这些数字不是彼此孤立的,它们背后存在因果故事。drop‑off 高的步骤未必就是“坏”的:它可能在过滤不合适的用户,让后续只剩真正需要该场景的人。因此,分析不只是“算百分比”,还要能基于数据讲清楚故事。
要算出这些百分比与漏斗,首先需要原始事件:谁、何时、走过了哪个步骤(或没走过)。下一节我们来约定事件的格式。
4. 分析事件长什么样
在写代码之前,先约定我们要从小部件和后端发送的“事件”(event)格式。
一个分析事件通常包含:
- 谁:用户标识,或至少是会话标识;
- 哪个 workflow 以及其版本;
- 哪个步骤;
- 发生了什么(事件类型);
- 是否成功、耗时多久;
- 少量元数据(语言环境、设备等)。
workflow 的简化事件模式可以这样描述:
export type WorkflowEventType =
| "workflow_started"
| "workflow_finished"
| "step_started"
| "step_completed"
| "step_failed";
export interface WorkflowAnalyticsEvent {
eventId: string; // uuid
timestamp: string; // ISO 字符串
userId?: string; // 如果允许去匿名
conversationId?: string; // ChatGPT 会话 id(如可用)
workflowId: string; // 我们的内部标识
workflowType: "gift_selection";
workflowVersion: string; // 例如 "1.2.0" 或 "1.2.0-A"
stepName?: string; // collect_budget, suggest_ideas 等
eventType: WorkflowEventType;
toolName?: string; // 如与 tool 调用相关
success?: boolean;
errorCode?: string | null;
durationMs?: number;
metadata?: Record<string, unknown>;
}
几个要点:
首先,workflowVersion 对做 A/B 测试非常关键:没有它你永远不知道到底哪一个场景版本带来了更好的指标。
其次,conversationId 或其他关联 ID 能把事件串起来:小部件里的步骤、MCP 的工具调用、以及文本对话。在后续模块我们还会聊到追踪与可观测性,但从一开始就养成“端到端”标识的习惯非常有用。
第三,不要把一切都往事件里塞:完整消息文本、邮箱、地址等 PII 最好避免或进行强匿名化——我们会在结尾部分再详细谈。
5. 小部件中的埋点(Next.js + Apps SDK)
接下来是有意思的部分:如何让我们的 GiftGenius 小部件在用户走流程时自己悄悄上报步骤。
假设在此前课程里你已经写了类似下面的代码:
// components/GiftWizard.tsx
type StepId = "recipient" | "budget" | "ideas" | "review" | "checkout";
export function GiftWizard() {
const [currentStep, setCurrentStep] = useState<StepId>("recipient");
const [workflowId] = useState(() => crypto.randomUUID());
// ... 在此渲染不同步骤
}
添加一个小小的“分析层”作为 Hook。
useWorkflowAnalytics 钩子
我们做一个包装,它知道 workflowId、workflowVersion,并能把事件发送到我们的 Next.js API 路由 /api/workflow-analytics。
// lib/useWorkflowAnalytics.ts
import { useCallback } from "react";
import type { WorkflowAnalyticsEvent, WorkflowEventType } from "./types";
const WORKFLOW_VERSION = "1.0.0";
export function useWorkflowAnalytics(
workflowId: string,
workflowType: WorkflowAnalyticsEvent["workflowType"] = "gift_selection"
) {
const sendEvent = useCallback(
async (payload: Omit<WorkflowAnalyticsEvent, "eventId" | "timestamp" | "workflowType" | "workflowVersion" | "workflowId">) => {
const event: WorkflowAnalyticsEvent = {
eventId: crypto.randomUUID(),
timestamp: new Date().toISOString(),
workflowId,
workflowType,
workflowVersion: WORKFLOW_VERSION,
...payload,
};
// 简单地发送到 API;生产中可加缓冲/防抖
await fetch("/api/workflow-analytics", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
});
},
[workflowId, workflowType]
);
const trackStepEvent = useCallback(
async (stepName: string, eventType: WorkflowEventType, extra?: Partial<WorkflowAnalyticsEvent>) => {
await sendEvent({ stepName, eventType, ...extra });
},
[sendEvent]
);
return { sendEvent, trackStepEvent };
}
这里要点在于,该 Hook 并不依赖于某个具体 UI 步骤。它只知道 stepName 和 eventType 是什么。具体组件会告诉它:“我开始一个步骤了”、“我完成了这个步骤”等等。
发送 workflow_started 和 workflow_finished
在 GiftWizard 组件中,可以在挂载与卸载时分别记录场景的开始与结束:
// components/GiftWizard.tsx
export function GiftWizard() {
const [currentStep, setCurrentStep] = useState<StepId>("recipient");
const [workflowId] = useState(() => crypto.randomUUID());
const { sendEvent } = useWorkflowAnalytics(workflowId);
useEffect(() => {
void sendEvent({ eventType: "workflow_started" });
return () => {
void sendEvent({ eventType: "workflow_finished" });
};
}, [sendEvent]);
// ...
}
当然,用卸载来表示完成只是粗略近似:用户可能只是最小化了聊天窗口,或切到另一个对话。即便如此,这样粗粒度的指标也能让你大致了解有多少场景“走到某个地方了”。
跟踪各步骤事件
现在让每个步骤自己向分析上报。先写一个简单的包装:
interface StepProps {
stepId: StepId;
onNext: () => void;
trackStepEvent: (stepName: string, eventType: WorkflowEventType, extra?: Partial<WorkflowAnalyticsEvent>) => Promise<void>;
}
function StepRecipient({ stepId, onNext, trackStepEvent }: StepProps) {
useEffect(() => {
void trackStepEvent(stepId, "step_started");
}, [stepId, trackStepEvent]);
const handleSubmit = async () => {
// ... 校验,并保存到 widgetState
await trackStepEvent(stepId, "step_completed");
onNext();
};
return (
<div>
{/* 收件人表单字段 */}
<button onClick={handleSubmit}>下一步</button>
</div>
);
}
在 GiftWizard 中把 trackStepEvent 传入:
export function GiftWizard() {
// ...
const { trackStepEvent } = useWorkflowAnalytics(workflowId);
const goToNext = () => {
setCurrentStep((prev) => NEXT_STEP[prev]);
};
if (currentStep === "recipient") {
return (
<StepRecipient
stepId="recipient"
onNext={goToNext}
trackStepEvent={trackStepEvent}
/>
);
}
// 其他步骤...
}
类似地,在可能出错的步骤(例如在 suggest_ideas 中请求外部 API)里,失败时可以发送 "step_failed" 并附带 errorCode;成功加载选项时发送 "step_completed"。
这样我们就能得到:
- 清晰的事件序列:步骤何时开始、何时结束;
- 计算步骤时长:"step_started" 与 "step_completed" 的时间差;
- 看见哪些步骤更常以 "step_failed" 结束。
6. 后端/MCP 的埋点
客户端分析很重要,但小部件处在一个相对脆弱的世界:用户浏览器、iframe、沙箱限制等。因此应并行在服务端记录事件——在 MCP 工具或你的 App 的后端 API 中。
例如你有一个工具 suggest_gifts 在做真正的重活:访问商品库、应用筛选,并返回礼物。在该工具内部,你可以同时记录业务逻辑与分析事件。
TypeScript 的一个 MCP 工具处理器可以长这样:
// mcp/tools/suggestGifts.ts
import type { SuggestGiftsArgs } from "../schemas";
import { logWorkflowEvent } from "../analytics/log";
export async function handleSuggestGifts(args: SuggestGiftsArgs, context: { workflowId: string; stepName: string }) {
const startedAt = Date.now();
try {
// ... 主要的创意筛选逻辑
await logWorkflowEvent({
workflowId: context.workflowId,
workflowType: "gift_selection",
workflowVersion: "1.0.0",
stepName: context.stepName,
eventType: "step_completed",
toolName: "suggest_gifts",
success: true,
durationMs: Date.now() - startedAt,
});
return {
content: [{ type: "text", text: "找到 5 个礼物创意。" }],
_meta: {
// 提供给小部件的原始数据
},
};
} catch (e) {
await logWorkflowEvent({
workflowId: context.workflowId,
workflowType: "gift_selection",
workflowVersion: "1.0.0",
stepName: context.stepName,
eventType: "step_failed",
toolName: "suggest_gifts",
success: false,
errorCode: "SUGGEST_FAILED",
durationMs: Date.now() - startedAt,
});
throw e;
}
}
而 logWorkflowEvent 可以把数据写入与前端事件相同的表/存储,只是增加一个 "source" 标记:"backend"。
为什么服务端分析更可靠
首先,工具调用要么发生了,要么没有——这是确凿事实,而不是“用户好像点了按钮”的启发式判断。
其次,服务端更便于聚合:你可以统计每个工具被调用多少次、平均 durationMs 是多少、以及多少比例以错误收场。
第三,这能区分 UX 问题(用户没有走到会调用该工具的步骤)与技术问题(能走到该步骤,但工具经常失败)。
7. 如何解读数据:寻找瓶颈
假设你已经从小部件与 MCP 发送了 "workflow_started"、"step_started"、"step_completed"、"step_failed" 等事件,并且存储里已经积累了足够的数据。再假设你已经收集了 GiftGenius 的一些数据,并得到了按步骤的汇总统计。下表是 1000 个已启动 workflow 的假想数字:
| 步骤 | 开始该步骤 | 完成该步骤 | 该步骤流失 | 平均用时(秒) |
|---|---|---|---|---|
| recipient | 1000 | 950 | 5% | 12 |
| budget | 950 | 700 | 26% | 35 |
| ideas | 700 | 680 | 3% | 8 |
| review | 680 | 500 | 26% | 40 |
| checkout | 500 | 420 | 16% | 20 |
要关注什么:
首先,budget 是明显的瓶颈。drop‑off 高(26%),平均用时也显著更长。可能你问了太多与币种/税务相关的问题、文案不清晰,或者用户对预算没有把握。这是简化步骤、拆成两个子步骤或改写提问方式的好候选。
其次,review 也有较高的流失。也许礼物卡片的 UI 过于繁杂,或者用户不明白“点赞”代表什么。也可能模型返回的选项太多,让小部件像个无尽列表。此时不仅要看数字,还要看截图/会话录屏(如果你有),或者至少亲自以用户身份走一遍流程。
再次,checkout 流失了 16%——对 commerce 场景而言这意味着很多收入。这里需要定位这些流失发生在何处:下单流程里?支付服务商错误?还是用户临时改变了主意?这已经不是纯 UX 问题,而是 UX 与业务限制的组合。
要学会区分 UI 问题与模型问题。
- 如果用户经常返回上一步并修改答案——这是提问不清晰或表述不佳的信号。
- 如果步骤很快结束,但该步骤上的工具调用经常失败——是后端/MCP 的问题。
- 如果步骤耗时很长,同时没有错误也没有返回,可能用户在阅读冗长的文本,而这些文本并非必要。
8. Workflow 的实验与 A/B 测试
数字本身不会改善任何事。要让分析产生价值,必须会做实验:调整步骤,并比较是否变好。
在 ChatGPT App 的语境中,典型实验是比较两个版本的步骤或步骤序列:
- 多个简单页面的长向导 vs. 一个复杂的表单;
- 不同的提问表述;
- 不同的步骤顺序(比如更早或更晚询问预算);
- 不同的 tool gating 策略(第一步更少 tools,第二步更多)。
一个好习惯是把场景版本写进 workflowVersion,并附加实验标识,例如 "1.3.0-A" 与 "1.3.0-B"。
在小部件中的最简单 A/B 分流
在线上环境你会希望基于用户或会话做稳定分配(通过后端),但教学示例用随机选择就够了。
// lib/useWorkflowVariant.ts
import { useMemo } from "react";
export type WorkflowVariant = "A" | "B";
export function useWorkflowVariant(): WorkflowVariant {
return useMemo(() => {
return Math.random() < 0.5 ? "A" : "B";
}, []);
}
在 GiftWizard 中确定变体并把它传给分析:
export function GiftWizard() {
const [workflowId] = useState(() => crypto.randomUUID());
const variant = useWorkflowVariant();
const { sendEvent, trackStepEvent } = useWorkflowAnalytics(
workflowId,
"gift_selection"
);
useEffect(() => {
void sendEvent({
eventType: "workflow_started",
metadata: { variant },
});
}, [sendEvent, variant]);
// 后续可根据 variant 修改文案/步骤结构
}
在服务端,你可以把硬编码的 "1.0.0" 换成 "1.1.0-A" 与 "1.1.0-B",或者仅记录 metadata.variant,再在分析里按该字段分组。
A/B 测试的核心:预先选定目标指标。比如:“我们要把场景的 completion rate 从 42% 提升到 50%”,或者“把 budget 步骤的用时降低 20%”。没有目标指标,对 workflow 的任何重构都像是“挪了个柜子,看起来好像更好看”的装修。
9. 隐私与数据伦理
我们之前提到过,最好不要把 PII 直接塞进 metadata 或分析事件里。讨论指标时很容易兴奋过头,想把所有东西都记录下来。但要记住你是在 ChatGPT 里工作,用户完全可以合理地期待自己的私信不会原样被发往外部分析系统。
在进入关于安全与 Store 的模块之前,先遵循几条简单规则:
- 首先,不要记录用户完整的消息文本。可以替换为消息长度、回答类型(数字、“是/否”、列表选择),或匿名化特征,如“回答为空/不完整/已修改”。
- 其次,不要记录对业务逻辑非必要的可识别信息(PII):邮箱、电话、地址、全名。如确实不可或缺,请把它们存放在另一个受保护的域中,并严格限制访问。
- 第三,谨慎处理对话上下文。如果你保存了 conversationId,要确保在分析里不会在没有正当理由与法律基础的情况下,把独立对话“拼接”成超级画像。
- 第四,注意 OpenAI 的政策与 Store 的要求(我们会在发布与安全模块中详细说明),其中明确规定了哪些数据可以从 ChatGPT 外送,哪些不可以。在设计分析方案时,尽早把匿名化与数据最小化考虑进去,避免后期推倒重来。
最后要记住,UX 分析不是全面监控,更不是“大哥在看着你”。目标是改进场景、减少用户挫败感,而不是搞一块“大哥面板”,去追踪“谁在凌晨 2:37 没有走到 checkout”。
10. Workflow UX 分析中的常见错误
错误 1:“我们还没上线,指标以后再说”。
开发者常常在 App 已经被真实用户使用时才开始考虑分析。结果就是事件“事后补”,数据残缺不全,几乎无法比较新旧场景。最小漏斗("workflow_started"、"step_started"、"step_completed"、"workflow_finished")最好一开始就埋上,此时代码还相对简单。
错误 2:只记录成功,忽略错误。
有时日志里只有 "step_completed",却没人写 "step_failed",因为“本来就不该失败”。结果你只看到某步的人很少,但不知道是他们自己离开,还是被错误踢出了流程。务必同时记录成功与失败,并至少附上粗粒度的 errorCode。
错误 3:完全没有绑定 workflow 版本。
你在改文案、改顺序、加 tool gating,但事件里 workflowVersion 一直是 "1.0.0"。一个月后看图表,分不清哪些是改动前,哪些是改动后。固定场景版本,并在需要时加入 A/B 变体标识,是分析的必备要素。
错误 4:无缘无故做过度细致的埋点。
另一种极端是一开始就搭“完美”的 50 字段事件模式,记录每一个像素的点击、每一次回删。首先,这可能侵犯隐私。其次,这样的数据难以分析,你会淹没在噪声中。更好的方式是从一小撮真能回答具体产品问题的事件与指标起步,再按需扩展。
错误 5:步骤和场景命名不一致。
代码里叫 budget,分析里叫 collect_budget,报表里写成“关于钱的那一步”。几周后谁也记不清谁是谁。在设计 workflow 时,务必约定稳定的步骤标识(stepName),并在 UI、日志与报表中统一使用。
错误 6:存在没人使用的指标。
最令人沮丧的情况:你认真收集了大量数据,从小部件与 MCP 打通了事件上报,但没人打开仪表盘,更没有据此做决策。不要为分析而分析;始终问自己:“基于这个指标我能做出什么决策?”如果答不上来——这个指标暂时就不需要。
GO TO FULL VERSION