CodeGym /课程 /ChatGPT Apps /Workflow 的 UX 分析:按步骤衡量效果

Workflow 的 UX 分析:按步骤衡量效果

ChatGPT Apps
第 11 级 , 课程 4
可用

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 为例:

  1. collect_recipient — ChatGPT 与小部件收集收件人的基础信息(性别、年龄、关系、兴趣)。
  2. collect_budget — 询问预算与货币。
  3. suggest_ideas — MCP/代理挑选创意,并把礼物卡片返回到小部件。
  4. review_selection — 用户点赞/隐藏创意,并选出 1–2 个候选。
  5. 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 钩子

我们做一个包装,它知道 workflowIdworkflowVersion,并能把事件发送到我们的 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 步骤。它只知道 stepNameeventType 是什么。具体组件会告诉它:“我开始一个步骤了”、“我完成了这个步骤”等等。

发送 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 打通了事件上报,但没人打开仪表盘,更没有据此做决策。不要为分析而分析;始终问自己:“基于这个指标我能做出什么决策?”如果答不上来——这个指标暂时就不需要。

1
调查/小测验
工作流第 11 级,课程 4
不可用
工作流
工作流与逐步揭示工具
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION