跳转到主要内容

产品思维工程师

《The Product-Minded Engineer》中文译本:从用户场景、交互设计与反馈闭环出发,构建真正有影响力的软件。

不懂产品的工程师做不出好软件。

—— 冯若航 / Vonng

本书从工程师最熟悉的开发与交付出发,再回到探索与定义:理解具体用户,写清场景与问题,把洞察转化为交互设计和产品架构,并用真实反馈持续修正方向。

在线译本按 Oink 0.4 的 Book 模型组织,提供稳定的章节树、顺序阅读、编号图表与跨章引用、整书打印,以及 Markdown / llms.txt 输出。

阅读本书

  • 序言认识生活在系统与用户边界上的产品思维工程师。
  • 打开完整目录按四个阶段与九章定位内容。
  • 使用整书打印视图阅读或另存为 PDF。

出版与授权说明

本译文仅供学习研究参考,不追求经济利益,不得公开发行或用于商业用途。译者保留译文署名权,其他权利以原作者 Drew Hoskins 与 O’Reilly 出版社的主张为准。仓库的 CC BY 4.0 许可仅覆盖贡献者有权许可的内容与贡献,不替代英文原作版权。

目录

序言

第一部分:开发与打磨

第二部分:交付

第三部分:探索

第四部分:定义

索引

关于作者

序言

北方海獭(比如本书封面上的那只)很像软件工程师,只是更可爱。它们会使用工具,主要是用石头敲开贝壳。它们也会协作,会结成由许多个体组成的巨大“筏群”,彼此抓住前爪,这样既能漂浮在水面上,也不至于被水流冲散。

海獭也是边界态liminal)生物:它们漂浮在空气与海洋的交界处,不完全属于任何一边。这些海洋哺乳动物必须从空气中获取氧气,但食物来自海里。它们有类似陆生哺乳动物的爪子,却又靠鳍肢适应海洋环境。

工程师同样是边界态生物,只不过我们栖身于系统与用户的边界空间。软件世界里的数据库、协议、调用栈和容器,就像大海;当我们向更深处潜去,最终落脚在承载这一切的硬件之上。

但用户是我们的氧气,也是我们头顶那片阳光普照的天空。借着他们的光,我们才能看得更远;而要做到这一点,我们必须始终保持“漂浮”。

为什么写这本书

围绕软件产品的思考,归根结底可分为两类:

  • 系统思维:关注算法与数据结构、编程语言、分布式系统的容错、固件的内存约束等议题。
  • 产品思维:关注对用户的理解,包括其背景知识与需求、行为序列、群体互动,以及服务于这些目标的设计方法与信息架构。

软件工程教育大多围绕系统思维展开。大学计算机课程、编程训练营和软件工程文献,重点都在算法、数据结构、编程语言、系统知识与设计模式上。除了偶尔能遇到的人机交互选修课之外,我们大多只能靠耳濡目染,慢慢学会产品设计,也慢慢学会理解用户。为什么会这样?

的确,系统思维是我们最具区分度的能力,产品经理、设计师和用户体验研究员通常不具备这项能力。但你若去问他们,他们往往会说,希望工程师拥有更多产品能力。他们要同时覆盖很多产品,沟通与对齐都很有挑战。与其试图解读一份由 PM 匆忙勾勒、又没覆盖任何边界场景的需求文档,不如基于我们对用户的深度理解来决策,这样我们对产品命运的掌控会更强。

在软件工程还是“前沿学科”的年代,有些人即便缺乏产品能力也能过关。哪怕是基础技能,也需要超常的天赋、韧性与投入才能掌握。那时真正会高效编码的人太少,最紧迫的任务是培养整整一代程序员与软件设计者。工具和语言本身又很难用,光是学会并用好它们就足以成为一份全职工作。

但现在不同了。过去二十年里,在开源生态、云计算和开发者工具公司资本投入的推动下,开发技术发生了巨大进步。AI 编码助手与智能体已经替我们承担了大量繁重工作,处理掉许多细枝末节,让我们能把注意力放到别的议题上,比如产品思维。

写这本书,是为了补足你既有教育中的空白:把你已经具备的工程能力,与用户同理心和产品能力融合起来。比如,我们在这里不会教你设计数据结构或算法,但我们会讨论如何依据用户需求来选择它们。

掌握这些能力后,你可以在工程岗位上做得更好:

  • 基于影响力更好地确定优先级。
  • 与产品经理和设计师更高效协作。
  • 做出信息更充分的工程设计决策。
  • 把队友视为你所写抽象的用户,从而写出更有用、也更易维护的代码。
  • 在多数日子里,都能带着服务产品用户的热情醒来。

或者,你也可以在那些要求“产品+工程”复合能力的岗位上进一步升级:

  • 成为产品/工程复合型人才,既参与产品决策也推动落地,在工程目标与产品目标之间做出纯工程或纯产品角色难以做到的协同优化。
  • 以具备产品感的技术创始人身份创业,更从容地应对初创公司面临的多元挑战。
  • 成为或进一步成长为技术负责人,以用户结果为依据,用扎实的推理带领团队。

本书适合谁

本书面向各类职业软件工程师:你已经掌握了编写与交付代码的基本功,但渴望持续做出更大影响。无论你刚入行一年,还是资深工程师,这些主题都能给你启发。你在这里学到的用户导向能力,同样适用于服务同事、开源社区成员和付费客户;不论用户是普通消费者、专业人士,还是其他开发者。

我纳入了多种类型的例子,从用户界面到开发者平台再到基础设施;在我看来,它们都是产品。即便是一个不起眼的函数,也可以被视作微型产品:它通过接口与用户沟通,并满足用户需求。

是的,基础设施工程师也在构建产品;只不过他们最直接的目标用户通常是其他工程师,以及这些工程师所服务的终端用户。更具挑战的是,基础设施团队往往缺少产品经理和设计师,反而使某些产品能力(比如用例意识)对成功变得更关键。

本书结构

第 1 章包含导论内容,是阅读后续章节的前置基础。

之后你可以按兴趣或时机,以任意顺序阅读其余章节。不过,如果你没有特别偏好,我已按我建议的次序排列。

这八章被划分为四个部分,对应软件产品生命周期“双钻模型”的不同阶段。这里先简要岔开说明一下。

双钻流程模型

这本书并不是一本讲软件流程的书,我也不会试图向你兜售某种流程。但理解双钻流程模型会帮助你更顺畅地阅读本书;如果你和我一样,它还会拓展你对“打造优秀软件究竟需要什么”的理解。

许多有效的软件生命周期实践,都可以归纳为四个基本阶段:

  • 探索(Discover):明确我们服务谁、他们需要解决什么问题。这个阶段强调调研、创造力与探索。
  • 定义(Define):构思并架构一个能解决这些问题的产品。这个阶段强调收敛选项,形成聚焦计划。
  • 开发(Develop):选择并细化实现方案。在这个阶段,我们寻找最佳实现路径,并打磨先前定义的高层产品。
  • 交付(Deliver):把产品做出来,验证你做出的结果,交付给客户,并收集反馈。

把这四个阶段连在一起,可视化后如图 P-1所示。

英国设计委员会对双钻流程模型的示意图
图 P-1 英国设计委员会对双钻流程模型的示意图

图中它们被表示为沿着两个菱形推进的箭头。在探索与开发阶段,箭头发散,因为我们会并行探索多条线索;而在定义与交付阶段,箭头收敛,因为我们会尝试把这些线索编织成一个紧凑的产品。

别误会我的意思:软件项目并不会沿着这些阶段整齐线性推进。当你深入到子功能层面时,循环会层层嵌套;你会学到新东西,也会回退重来,等等。

在创造与聚焦之间来回切换,本身就很有启发。如果你在推进交付时,常被同事提出创意想法“带偏”而感到挫败,也许只是你们所处阶段不同。反过来,如果你觉得有人总在压制你的好点子,也可能是同样原因。先花点时间确认你们是否在同一阶段,比如到底是在探索(Discovery)还是开发(Development)。

这个模型也帮助我们把视野扩展到完整产品生命周期:从问题被提出的那一刻,到产品发布并开始获取反馈。不要一上来就跳到解法,也别在功能首版上线后就以为万事大吉。

成熟的产品思维工程师,会在生命周期的全部四个阶段塑造产品命运。

各部分内容

对工程师而言,产品思维无处不在。因此本书会贯穿这些阶段,展示如何在每个阶段运用产品思维,拿到更好的结果。

大体上,本书各部分覆盖了双钻的各阶段,不过起点放在了产品周期的中段。我们先从开发(Develop)与交付(Deliver)阶段开始,因为这是各层级工程师最常经历的部分。随后我会回到探索(Discover)和定义(Define)阶段,这些通常更偏向资深及以上角色的工作域。

当我们进入第三、第四部分,即探索与定义时,尽管这些决策常常发生在产品周期更早阶段,但它们会更具挑战,也需要更多上下文。不过,如果你更愿意按阶段顺序阅读,也完全可以;只是要知道,你会先接触更有挑战的内容。

各章概览

  • 第 1 章会介绍“人物画像(persona)”与“场景(scenario)”这两个核心概念,它们会在全书反复出现,并补充一些用户心理学基础。
第一部分:“开发” 第二部分:“交付” 第三部分:“探索” 第四部分:“定义”
  • 第 8 章《交互设计》中,我们会深入功能设计细节,帮助你设计出“只用于预期用途、避免被用于不安全用途”的产品。
  • 第 9 章《产品架构》将产品思维应用到吞吐量、数据一致性、延迟等系统层面的关切。

阅读说明

在开始前,这里有几点说明:

  • 本书包含代码清单。我选择 Python 作为示例语言,因为它既常见又相对易懂。我会在必要时解释你可能不熟悉的语法。
  • 每一章都以发人深省的练习和这些练习的示例答案收尾。请务必动手做!产品思维需要练习。
  • 我曾在 Microsoft、Facebook、Stripe 和 Temporal 工作。本书中的一些例子来自我在这些公司的经历,另一些来自我亲手参与的产品。我选择它们并非为这些公司背书,而是因为这种第一手经验让我能够提供一本产品思维书籍所需的细腻细节,并保证准确性。
  • 我的职业生涯大部分时间都在为开发者构建产品。尽管本书覆盖的产品类型广得多,我也广泛征求了不同意见,力求让建议更具普适性,但内容仍可能会偏向那些更适合服务技术用户、而非消费者的技巧。

本书使用的约定

本书采用以下排版约定:

  • 斜体:表示新术语、URL 和电子邮件地址。
  • 等宽字体:用于程序清单;也用于段落内指代程序元素,如变量名、函数名、数据库、数据类型、环境变量、语句和关键字。
提示

此元素表示提示或建议。

说明

此元素表示一般说明。

警告

此元素表示警告或注意事项。

O’Reilly 在线学习

说明

40 多年来,O’Reilly Media 一直提供技术与商业培训、知识和洞见,帮助企业获得成功。

我们独特的专家与创新者网络,会通过图书、文章和在线学习平台分享他们的知识与经验。O’Reilly 在线学习平台提供按需访问能力,涵盖实时培训课程、深度学习路径、交互式编码环境,以及来自 O’Reilly 与 200 多家其他出版商的大量图文与视频内容。更多信息请访问 https://oreilly.com

联系我们

关于本书的意见与问题,请联系出版方:

我们为本书提供了网页,发布勘误和补充信息。你可以通过 https://oreil.ly/the-product-minded-engineer-1e 访问。

如需获取图书与课程的新闻和信息,请访问 https://oreilly.com

在 LinkedIn 关注我们:https://linkedin.com/company/oreilly-media

在 YouTube 观看我们:https://youtube.com/oreillymedia

致谢

衷心感谢我的朋友和坚定的 alpha 读者 Carmen Krol 与 Peter Schuller。他们认真读完了早期粗稿的全部内容,并给出了极其出色的反馈。

我也特别感谢我的 beta 读者 Kimberly Hou、Scott Storkel 和 Chelsea Troy。他们完整通读了 beta 稿,并坦诚地给了我反馈。

还要感谢其他贡献者:Adam Hupp、Ben Lavender、Paul Nordstrom、Jason Rosenfeld、Jeff Schoner、Venkat Subramaniam 和 Mark Wong,你们都为本书带来了积极影响。

其他感谢:

  • 感谢 Anthropic 打造了我的研究伙伴 Claude。这次对产品思维的快速梳理涵盖了很多主题,我在其中不少方面都需要知识补强。
  • 感谢 Louise Corrigan 对我的信任,并建议我写这个很棒的主题。
  • 感谢我耐心而博学的编辑 Angela Rufino。
  • 感谢 Joshua Bloch、Don Norman 和 Steven Clarke,开启了我的产品思维之旅。
  • 感谢 Charles Zedlewski,建议我在做了多年工程师后尝试产品管理。若没有这段经历,本书的某些部分很难写好。
  • 感谢 Gergely Orosz 以及他那篇富有启发的博文 The Product-Minded Software Engineer
  • 感谢 Patrick Collison 在 O’Reilly 为我引荐,也感谢他打造了一家充满产品思维工程师的公司。
  • 感谢 Peter Dimov,教会我拥抱自己作为产品思维工程师的使命。

I 开发与打磨

在接下来的两章中,我会教你如何运用产品技能,以出色的判断力推进执行,并有意识地打磨出成熟精致的产品。

我会讨论关键的用户场景,例如发现、使用与理解;以及命名、注释、写出高质量错误消息、文档编写和测试等方法。

这些主题是编码软件工程师工作中的家常便饭,也为你练习产品思维提供了高频且易上手的实践机会。

1 产品思维的基础

来吧,开始吧,开始吧

那么,说吧,说吧,这到底是什么场景?

— A Tribe Called Quest

如果产品思维有一个核心原语,那就是场景。它本质上是一个为了产品设计而组织的用户故事。

来看一个关于密码的场景:

Darla 是一位退休的婴儿潮一代用户,45 岁才开始使用电脑。她正在本地茶饮店注册账号,以便在线下单。当系统要求输入邮箱和密码时,她选择了“女儿生日 + 儿子名字”,这样容易记住。她大约有 30 个线上账号,因此多数账号都用同一个密码,否则根本记不住;而且她又被建议不要把密码写下来。

你可以猜到后面会发生什么:这家茶饮店发生数据泄露,她的密码(也正是她在银行使用的那个)被泄露。也许暗网上有人买下了她的登录信息,并转走了她的钱。

这个噩梦般的场景揭示了密码体系的问题,但并不完全是产品内部某个组件本身的问题。只盯着注册界面、加密协议、安全存储等细节,你很难看到它。但当你写出包含可预见人性弱点的完整故事时,安全问题就会立刻显现。

本节中,我们将拆解并分析有效场景的组成,并教你如何构建一个好场景。粗略地说,一个场景由“情节”和“角色”组成。我会先展开这两个概念,再给出实操建议。

不过照例,先用一个案例研究来引导讨论。

案例研究

Tea++ 是一家拥有 300 家门店的连锁茶饮店。它扩张过快,经营勉强维持,因此公司的目标是在极低预算下尽快提升订单量,避免关店。

Tea++ 有一款流行的移动应用,支持点单体验;通过反馈组件,用户一直在请求“收藏”功能,希望系统能记住偏好、加快下单。软件团队认为这可能是提升订单的低成本办法。

第一次尝试:没有场景

我们来看 Bob,一位移动端工程师。他在工作中并不使用用户场景。

Bob 查看了应用反馈组件里收到的用户意见。喝咖啡的用户厌倦了每次下单都要重新设置定制项,也不喜欢在近期订单里翻找。有时想要的商品埋在包含多个商品的订单中,拆分起来很麻烦。

Bob 做了一个与现有应用结构契合度很高的原型(见图 1-1)。他准备在应用底部新增一个 Favorites 菜单项,并增加一个展示用户收藏的新页面。页面右上角有一个 + 按钮,用来新增收藏。

咖啡点单应用原型
图 1-1 Bob 展示了他提议的移动应用改动。新增元素以阴影标出。

令人高兴的是,这会很容易实现,因为收藏列表复用了现有菜单组件。用户点击 Order 后,将进入现有购物车流程。

新的收藏数据库表按最近购买时间排序,Bob 还给出了(SQL)数据库结构,并注明会按时间建立索引以便快速查询。

Table: favorite_items
- created_time : timestamp
- updated_time : timestamp (Index)
- item_id : long
- milk_customization: varchar
- sweetness_customization: varchar
- special_instructions: varchar

Bob 还提议把首页“新品广告位”替换为收藏入口,但“New Products”团队否决了这个想法,因为他们需要该位置来推广当季新品。Bob 只得不情愿地把这部分从设计里删掉。

随后召开了大型评审会,大家都喜欢这个设计,尤其因为实现成本低。有人指出,星巴克应用会在近期订单和购物车里提供“心形”按钮,为什么不也加一个可把条目加入收藏的按钮?

但 Bob 反对,称这会让开发周期翻倍,因为改动更具侵入性,比如需要在多个页面新增数据库查询。团队接受了这个判断,项目按原方案构建并上线。

效果平平

几周后复盘数据,有好消息也有坏消息。好消息是收藏功能上线稳定、没有重大缺陷。不幸的是,只有 6% 的活跃用户使用收藏,订单量仅增长 0.3%。

Tea++ 的应用埋点完善,Bob 深挖数据后发现:点击 Favorites 菜单的人很少,而点击进去的人多数会立刻离开。尤其是收藏列表为空的用户,极大概率会直接退出,而不是点 + 按钮。

Bob 想起“心形按钮”的建议,与 Carlos 讨论,提议补上它以提升转化率。但遗憾的是团队优先级已转向月度订阅,短期内无暇开发。大家都在为可能关店而焦虑,Carlos 对他的提案似乎也有些不耐烦。Bob 带着愤怒离开,觉得自己不被允许把产品真正做完。

问题出在哪?

Bob 做对了很多事:倾听用户反馈、追踪指标、收集并分析用户交互;他还控制范围,做出了高效、可达成、稳定的设计。

但他的提案仍然薄弱。评审会上团队提出的问题虽有价值,却不是最根本的。

第一,他跳过了发现(Discover)阶段(我在序言提过的四个阶段中的第一步),直接进入数据库设计。

换句话说,他把一个核心难点在于“产品决策及其与实现约束如何相互作用”的项目,包装成了“实现细节才是最关键”。只是他没有幸运地遇到能看出这个问题的队友。

下面我们看看一个会用场景来思考用户需求的开发者。

第二次尝试:引入场景

六个月后,Bob 仍在做订阅功能。此时 Alice(一位资深工程师)被引入来实现 Bob 一直后悔没做的“星巴克式心形按钮”。

她不止于此,她做了“收藏门店”概念。这样一来,常去某家门店的用户可以在结账流程里少点几步,而这一点 Bob 当时完全没考虑,因为他接到的任务只聚焦“收藏饮品”。

第三,她做了 A/B 测试:将首页新品推广替换为一对巨大的“立即下单”按钮,并露出第三个按钮顶部,提示页面可滚动(见图 1-2)。这个思路和 Bob 曾设想的类似,但他当时没能说服产品营销团队让出首页空间。

随着时间推移,Alice 的实验表明,这些改动合在一起使转化率提升了 3.8%。由于移动端订单贡献了 Tea++ 一半营收,这足以显著改善利润。拿到数据后,Alice 得以说服市场部门把新品广告下移到前两个收藏项之下,并将实验全量发布。

Bob 几乎没参与她的方案制定,不禁疑惑 Alice 为什么对该做什么有这么准的直觉。她又是如何说服其他人放手去做的?

快速下单页面原型
图 1-2 Alice 做了一个可滚动的快速下单页面原型,包含“立即下单”按钮

作为启发故事的场景

场景的一个用途,是用来沟通并激发团队。

Bob 发私信问 Alice 她是怎么做到的,Alice 回了一个很有感染力的故事:

Eliana 是一位忙碌的职场人,每天通勤都会买一杯加豆奶的印度香料奶茶拿铁。某天早晨,她忘了提前下单。开车途中,她按下方向盘上的语音助手键,说:“给我点 Tea++ 的老样子。”她的语音助手 Siri 回复并让她确认金额和门店,她确认了。

Bob 有些困惑:“好吧……但这也不是你最终做的东西啊?”

“我知道。Carlos 觉得做这个太贵了,”她遗憾地说,“他可能是对的。但我想先把标杆设高,展示潜力。我也想讲一个高频场景:在这个场景里,收藏流程不只是方便,而是直接决定用户能不能喝到那杯茶。”

“听起来很酷,不过我当时也觉得我们没有这种时间。”

“梦想本身有价值。”Alice 说,“收藏饮品和收藏门店会让用户以后更容易使用语音指令,前提是我们将来真去做。”

在发现(Discover)阶段,探索多种可能的故事线非常关键。

捕捉用户访谈的场景

如果你做过一些用户研究,场景可以是对访谈素材的虚构合成,能够简洁传达常见情境。

Bob 反驳道:“我的意思是,我也可以随便编个故事,让我的方案显得更好。开车时点单真的那么重要吗?”

Alice 回答:“我也挺希望有定位数据来验证这一点。但主要是我访谈了十来位朋友和熟人,甚至问了很多看似琐碎的点单细节。是的,通勤场景反复出现。Eliana 代表的是在疯狂早晨里忙乱、健忘的人。”

我们会在第 6 章讨论用户访谈。

揭示产品缺口的场景

场景也可以用来展示“缺了什么功能”。

“刚才那是我的改后故事,”Alice 说,“你要不要看改前版本?”

“呃,好。”

她把内容私信发了过来:

Eliana 是一位忙碌的职场人,每天通勤都会买一杯加豆奶的印度香料奶茶拿铁。某天早晨,她忘了提前下单。开车时,她在停车标志前停下,打开 Tea++ 应用并点进 Favorites。她在列表顶部找到自己的拿铁,点击“Order now”。后车按喇叭,她只好继续前进并专心驾驶。但她确实需要咖啡因,于是到高速匝道口时把车停到路肩。在购物车页,她选择“Order and Pay”。这个页面要求选择门店。地图加载后,她用手指猛戳 Main St. 的图钉并点 OK,然后进入支付页,使用 Apple Pay 完成交易。她放下手机,确认来车后重新并入匝道。

“我明白你为什么做收藏门店了,”Bob 说,“有人提过这个需求吗?”

“有个用户问过,为什么每次都得重选门店。而且当我自己代入、想象自己在车里时,我意识到心形按钮未必是最重要的。”

Bob 深吸一口气:“懂了。无论如何,心形按钮做成了,还是恭喜你。我当时也试着说服 Carlos 投入这块,但他要我做订阅。”

揭示摩擦体验的场景

可以构造故事,来展示使用现有产品的痛点。这个例子里,Alice 设计了一个带一点反讽意味的小故事。

“这个我也有场景,”Alice 说,“它是从用户反馈间接提炼出来的。下面是改前故事。”

Eliana 是 Tea++ 的新用户,有乳糖不耐受,记得自己上次点的饮料不错。她本想复购,看到 Faves 菜单,但自己从没往里加过东西。她看到了 + 按钮,但想不起饮料名和定制项,于是点进 Recent Orders,发现那杯叫 Tiger Chai Latte,且自己改成了豆奶。她上次还点了点心,但这次不想要。恼人的是,Reorder 想把点心也一并塞给她,而她找不到只选拿铁的方法。她退出后进入常规点单流程。完成定制并下单后,她想在末尾找 Favorites 按钮,但没找到。好吧,至少拿铁已经在路上了,也许下次再说。

Bob 再次皱起眉头:“这就是我当时想做心形按钮的原因。”

Alice 笑了笑:“我坦白一件事。我最初其实是想让 Carlos 让来做这个按钮。我把这个故事发给了他。三周后,他让我来调研,但先让我找别的低垂果实。于是我才发现了收藏门店和替换广告位这两个机会。”

我会在第 5 章讨论如何收集用户反馈,并把它转化成此类故事或用户原话。

验证拟建功能的场景

我最常见到的场景用途之一,是模拟用户将如何与你正在设计的产品交互。

例如,下面是 Alice 的改后故事,不仅包含心形按钮,还把“收藏门店”“收藏页入口”和“首页入口位调整”三项一起串联:

Eliana 是 Tea++ 的一位较新用户,记得自己之前点过一杯很好喝的饮料。她想复购,于是点进 Recent Orders,看到那杯叫 Tiger Chai Latte。她点了旁边的心形图标。系统将她重定向到 Favorites 菜单,这杯饮料在列表顶部,她点击其旁边的“Order now”按钮。接着出现门店选择器,默认是她上次去的 Main St. 门店。她点击该门店并用 Apple Pay 完成支付。由于她已经去过同一家门店两次,系统弹窗询问是否将 Main St. 设为收藏门店。她点击 Yes。

下次她想喝的时候,打开应用,顶部就出现了“Tiger Chai Latte from Main St.”,位于季节限定推荐上方。她一点就直接进入支付页。

“这个故事是我的北极星之一,”她解释道,“它让我确信自己在构建并推动正确的事情,也帮助 Carlos 形成认同。”

像这样引导产品设计的场景,被称为“北极星场景(north star scenarios)”,将在第 7 章讨论。

作为测试的场景

Alice 和 Bob 没聊到这一点,但她在编写自动化测试时也写了“场景测试”:它不只覆盖单个页面,还确保页面之间衔接正确,验证用户确实能走完整个常见流程。

场景测试会在第 4 章重点介绍。

寻找关键答案

看 Bob 的初版设计和那些原型时,你可能会觉得自己难以下判断,或者信息不足。也许你会基于过往经验提出一些改进建议;也许你会把注意力放在数据库模式上,质疑一些技术细节。

但 Alice 的故事把我们的注意力引向了更重要的问题:

  • 他们能不能做语音助手?
  • 通勤前或通勤中下单到底多常见?
  • 收藏功能对用户是否真的好用?
  • 点单流程里还有哪些摩擦点?

这些才是高产出的问题。它们能帮助团队缩小范围并给不同功能排优先级。她既让我们对当前应用感到不满足,也设定了一个“易用性高标”,而团队可以用“离这个标杆有多近”来衡量最终方案。

在这个过程中,她成功凸显了三个最关键的改进点。

所以,到底什么是场景?

现在 Alice 已经充分说明了场景的重要性,我们来拆解场景,并帮助你自己写出来。

一个场景是为激发产品批判性思考而设计的用户故事。它由两部分构成:一个角色,以及该角色在发现并穿行产品或功能以解决问题时的行为模拟。

看角色。角色由以下要素组成:

  • Persona(人物画像):背景信息,包括其具备的技能与能力
  • Motivation(动机):他们在当前使用产品时想获得或需要获得的东西

下面是 Alice北极星场景的一段:

Eliana 是 Tea++ 的一位较新用户,记得自己之前点过一杯很好喝的饮料。她想复购,于是点进 Recent Orders,注意到 Tiger Chai Latte 旁边有个心形图标并点了它。系统随即把她带到 Favorites 菜单,这杯饮料在顶部,她点击条目旁边的“Order now”按钮。

你可以试着找找这里的“动机”和“人物画像”。下面我分别展开。

一个动机

思考用户的第一条规则,是先理解他们想要什么。

在小说中,角色欲望推动故事前进。我们会为主角实现目标而欢呼,也会在其受阻时共情。众所周知,音乐剧常在开场用一首“我想要(I Want)”之歌来建立角色动机。《绿野仙踪》里是 Dorothy 的 Somewhere over the Rainbow,《Hamilton》里是 Alexander Hamilton 的 My Shot

当动机写得很差时,我们会抱怨角色行为“不像他会做的事”。

场景里的用户也需要动机。这能帮助我们在产品按预期帮助用户达成目标时感到张力释放,也帮助我们判断他们的行为是否合理。如果涉及付费,我们也会据此判断他们是否有足够动机购买我们的产品。

我们把动机从场景里拿掉看看会怎样。

Eliana 是 Tea++ 的一位较新用户 。她点进 Recent Orders,注意到 Tiger Chai Latte 旁边有个心形图标并点了它。系统把她带到 Favorites 菜单,列表顶部显示了该饮料,她点击了条目旁边的“Order now”按钮。

Eliana 真的会这样做吗?我们不知道。她为什么点 Recent Orders 而不是 Favorites?她一开始就想去收藏某个条目,并且神奇地知道“收藏入口藏在 Recent Orders”吗?我们会怀疑故事作者 Alice 像提线木偶一样操纵 Eliana 的行为来证明自己的观点,也会质疑这个功能到底是否容易被发现。

一旦我们看到她的心理状态:她记不清饮料名字,但记得那杯很好喝,我们就能理解她为什么先去 Recent Orders。

写出动机

确保你的动机足够强,足以驱动角色去使用你的功能。

如果你的产品很粗糙,用户通常需要较强动机才会去用。下面是 Alice 可用于批评旧设计的一个改前故事:

Eliana 是一位乳糖不耐受的 Tea++ 新近用户,记得自己之前点过的饮料很好喝。她想复购,于是点进 Recent Orders,发现那杯叫 Tiger Chai Latte。她当时有一点空闲时间,也很了解自己“喜欢上某样东西就会反复点”的习惯,所以她进入 Favorites 菜单,点击 + 按钮,搜索 “tiger”,点击结果,重新配置定制项(豆奶、中等(medium)),然后点击 OK。接着她再点击 “Order now”。

这个版本强调:要使用当前收藏系统,用户必须足够勤勉、足够投入;而 Tea++ 的目标恰恰是降低下单所需的动机强度。

强迫自己以这种方式写出能贯穿动作链条的清晰用户动机,能够暴露糟糕的产品设计。

要小心:写动机时,很容易给用户强加“稻草人式”动机,例如“<某人>想用<我的酷功能>”,却没有解释其底层需求。比如“Eliana 想查看近期订单”。这不是她的目标,而只是达成目标的手段。那她真正的目标是什么?

如果你不熟悉这个概念,稻草人论证是指把对方并不持有的立场强加给对方,以服务论证者自身利益。政治讨论中常见:我们会很快替不喜欢的政策或政客脑补糟糕动机和糟糕论点。类似地,稻草人用户也是一种虚构角色,其行为不合理,往往只是因为我们需要他们这么做,才能让产品看起来成功。

警告

避免把场景角色写成稻草人。

如果你发现动机很弱,就深入挖掘角色。你可以把这称为根因分析(RCA)。根因就是用户采取某一动作背后的 whyso that:Eliana 记得自己喜欢之前点过的一款饮料。她之所以能这样做,是因为她是回头客。

提示

对用户动机做根因分析。

如果你仍不确定,就去和一些用户聊聊,或与本地产品经理、技术负责人讨论,厘清真实动机。

在我的根因分析中,当我说 Eliana 是回头客时,已经从“即时动机”延伸到了人物画像。既然触到了角色背景,我们现在就来谈人物画像。

人物画像

如果理解用户的第一条规则是“知道他们想要什么”,第二条就是“用户并非铁板一块”。他们有不同背景与经历,而这会影响他们希望从产品中得到什么。

这种角色背景叫做 persona(人物画像)。人物画像是快速建立对目标用户同理心的关键工具。它可用于与团队沟通并达成对齐,也能有效凸显不同用户群的差异。

人物画像通常以某个群体特征为锚点。

在 Alice 的“车载语音下单”场景中,我们看到 Tea++ 的核心受众是通勤职场人。(顺着这个认知,门店多分布在道路沿线和火车站附近,且 Tea++ 主打高价位精品饮品。)

这样的画像意味着一组“能力条件”:这类人使用产品时会带来哪些技能、知识和行为倾向。它还可以被场景进一步收窄,比如强调“新用户”状态。

如,在 Alice 的北极星场景中,Eliana 的画像是“刚开始形成复购、但尚未形成习惯的回头客”,据此我们推断她并不熟悉菜单和下单流程。

如果去掉人物画像:

Eliana 记得自己喜欢之前点过的饮料。她想复购,于是点进 Recent Orders,看到 Tiger Chai Latte 旁边有心形图标并点了它……

这个故事缺少“能力线索”,会让我们难以判断它是否与业务策略相关。Eliana 是 Tea++ 应该重点关注的人吗?只有当我们把她框定为“可能成为、也可能不会成为习惯用户的人”,这个场景的价值才清晰起来。

人物画像也有助于理解这个功能为谁而做。对首次下单用户来说,“收藏”几乎没有价值,所以不应把它作为新用户增长策略的关键环节。若 Tea++ 门店主要服务一次性到访游客,它也不应被优先投入。

写人物画像

首先,团队最好先就一个或多个目标受众达成一致,即你的应用会服务哪些人物画像,也可能面向哪些人物画像进行营销。我会在第 6 章带你定义目标受众。

如果你还不清楚目标受众,就去和团队对齐。你的场景人物画像应当落在目标受众范围内。

你还常常需要让画像具备多个侧面,才能更完整呈现用户拥有的技能、知识和动机。

这里有一些常见维度:

  • 用户处在“产品漏斗”的哪个阶段?是否已注册?你是否注意到,很多网站把注册入口做得很显眼,却把“已有账号登录”埋得很深?这反映了一个判断:老用户不需要像新用户那样被“手把手照顾”。
  • 轻度用户 vs 重度用户。假设重度用户只占 10%,你上线了一个能解决普遍痛点但使用门槛较高的功能。如果其余 90% 的用户难以发现或懒得使用,那么你实际上只为 10% 的用户解决了问题。
  • 市场两侧用户,例如司机与乘客,或免费用户与广告购买者。两侧诉求常常截然不同,不应把他们当作一个没有差异的用户团。
  • 不同编程语言的开发者。如果你的 SDK 同时服务 Python 开发者(写机器学习数据管道)和 Go 开发者(写控制平面),你可能需要为 Python 与 Go 分别刻画不同侧面。

所以,Eliana 属于“通勤职场人 / 新用户”。

人物画像能凸显为多群体构建产品时的差异与复杂性,是更精确思考用户的优秀工具。我会在第 6 章更深入讨论人物画像,讲清如何在团队沟通、优先级制定与设计思考中使用它。接下来我也会频繁引用这一概念。

确定人物画像的“能力条件”

了解用户具备哪些条件,会帮助你帮助他们达成目标。

你的画像会有一些特定优势。比如忙碌职场人通常有一些可支配收入、数字素养较高,但时间匮乏;而固定收入的退休人群往往相反。

除了这类画像特有条件外,你还可以利用一些几乎普适的人类共性。事实上,我可以直接“指控”你也有这些特征:

首先,你会希望在使用产品时尽可能少耗费脑力:

  • 你不懂实现细节,也无法读懂作者的心思。
  • 能不看文档你就不看。你更愿意从产品本身直觉出“该怎么做”。
  • 除了极少数你特别在意的产品外,你通常不会系统遍历一个应用或框架的全部功能。你更倾向快速定位并找到当前任务所需工具。

其次,你还有其他局限:

  • 你会多任务并经常分心。你希望能在一个或几个小“工作步”里推进,并在中断后得到提醒再回来完成未尽事项。
  • 你会走最短路径。你希望尽快完成任务,除非收益明显,否则不愿额外投入。
  • 你倾向规避风险。你不想到了店里才发现茶没下成功,也不想因此误了火车。
  • 你会遗忘。几个月后再回来时,你可能需要冗余线索或提醒。

下面是 Alice 的一个改前故事版本,它忽略了“人会忘”和“人很忙”这两点:

Eliana 是一位乳糖不耐受的 Tea++ 新近用户,记得自己喜欢 Tiger Chai Latte,并且知道自己还会再点几次。她点进 Favorites 菜单,点击 + 按钮,搜索 “tiger” 并选择这款饮料。然后她重放定制项(豆奶、中等(medium)),再点 OK。当它出现在列表里后,她点击“Order now”。

这种故事往往会导向一种产品:上线后采用率不及预期,而用户为何不买账看起来又“莫名其妙”。

记住这些共性的一个方法是换鞋思考(shoe-shifting):把自己放到用户的位置,思考你在那个位置会知道什么、会怎么做。

关键之一是培养选择性失忆,主动忘掉你在开发过程中获得的内部知识。

可怎么做到呢?我们是否永远被“看过就回不去”的禁忌知识污染?是否只能依赖新用户反馈才可能共情他们?

好消息是,不必如此。虽然与新手用户做研究和测试不可忽视(我们也会做),但现实里并非每个决策都能等到完整数据。因此你必须在自己工位上、仅凭自己的思考完成“换鞋思考”。

做法是:每当我发现自己调用了开发过程中才知道的知识或记忆,就把它识别为“禁忌知识”,然后假设自己并不知道它,再继续推演。

就 Alice 而言,她在写用户故事时会“忘掉”系统里已有“收藏门店”这一概念。所以当系统检测到用户在同一门店复购时,她才通过弹窗引导设置收藏门店。

我在日常中一直用“换鞋思考”和“选择性失忆”来共情用户。本书后面会频繁回到这个方法,例如在第 4 章写“摩擦日志”时。

角色小结

一个场景中的角色,是“人物画像 + 动机”的组合:人物画像提供较宽泛的目标与能力条件,动机解释其当下为何使用你的产品。接下来我们把注意力转向情节。

模拟

模拟(simulation)是场景的情节,它会把角色在完成任务过程中所需的每一步动作细致走一遍。模拟的目的,是把设计者的注意力拉到会真正影响用户体验的关键细节上。

一个好模拟像一份优雅的数学证明:每一步都有依据,且与前一步逻辑连贯。它会挑战设计,而不是迎合设计。糟糕的模拟会含糊跳步,或只突出有利部分、自我服务。

如果这种思路让你陌生,你并不孤单。大多数学校和编程训练营并不教用户模拟。敏捷方法强调场景(称为用户故事),但对“模拟”这个层面的关注很少。不过,下次当你尊敬的资深工程师与你讨论棘手设计时,留意他们的话。你可能会听到“如果 X 发生会怎样?”“如果用户是 Y 呢?”这类句子。即使他们没有把内容正式写成场景,他们很可能也在做心理模拟。

我遇到更多的是受过良好训练的“模式匹配型”工程师。他们看到一个问题与过往问题相似,就会调用已有解法,就像 Bob 的同事记起星巴克在近期订单里用过心形按钮,来类比他们拟做的收藏功能。这些解法会形成不断增长的经验库,可迁移到新公司继续发挥作用。

用场景模拟强化你的内在模式匹配器。它会把你的注意力聚焦到最需要的位置,然后让你的模式识别能力尽情发挥。

例如,下面是 Alice 最完整的北极星场景,覆盖了她新增的三项功能:

Eliana 是 Tea++ 的一位较新用户,记得自己之前点过一杯很好喝的饮料。她想复购,于是点进 Recent Orders,看到那杯叫 Tiger Chai Latte。她点了旁边的心形图标。系统把她重定向到 Favorites 菜单,该饮料位于顶部,她点击旁边的“Order now”按钮。接着出现门店选择器,默认是她上次去的 Main St. 门店。她点击并使用 Apple Pay 完成支付。由于她已经去过同一家门店两次,系统弹窗询问是否将 Main St. 设为收藏门店。她点击 Yes。

下次她想喝的时候,打开应用,顶部(在季节限定推荐之上)出现“Tiger Chai Latte from Main St.”。她点击后直接进入支付页面。

这个场景同时高亮了许多功能及其交互:近期订单、收藏、下单、门店选择、收藏门店、支付流程和首页展示。

我个人读到这里时会想:点单流程的其他环节呢?小费呢?系统是否应记住用户的小费偏好,从而减少点击并为未来语音点单铺路?Alice 没触及这一点,但也许她应该考虑。就我自己而言,是在我编辑时补进 Apple Pay 这一情节点后,才注意到这个问题。

当她后续进入更细设计时,会把这些高层步骤继续细化,并可能配上原型页面示意,形成故事板。我会在第 7 章讨论这种“用户流程(user flows)”。

写出模拟

当我写模拟时,我希望它足够完整,以确保能想到潜在坑点。我通常会从正在构建的功能起笔,比如心形按钮:

Eliana 是 Tea++ 的一位较新用户,记得自己之前点过一杯很好喝的饮料。在 Recent Orders 标签页里,她发现那杯叫 Tiger Chai Latte。她点击旁边的心形图标。系统把她带到 Favorites 菜单,并把这款饮料列在列表里。

然后,我会强迫自己沿三个方向推进:

  • 在这一刻之前发生了什么?是什么把用户带到这里?她是如何知道该这么做的?
  • 接下来会发生什么?
  • 这个步骤是否需要补足更细节?
提示

讲完整个故事,不要只盯着你新增的那个功能。

如果我卡住了,或者发现自己完全在瞎编,就说明我可能需要去访谈用户、看指标、找 PM 聊,或请教其他团队,才能建立更稳固的判断基础。

有时我会得出结论:这功能不该做,达不到我希望的影响力。也有时我会意识到:不能只做这一项,还得多做一些,或者需要找另一个团队一起改进他们的那一段。无论哪种,我都学到了有价值的信息。

不关注完整流程的团队,最终做出的产品往往粗糙、割裂。你很可能遇到过这样的登录流程:你通过深链进入某网站,登录后却发现系统已经忘了原始深链目标。这类团队多半没有用场景来设计软件。

写场景时还有一个技巧:寻找“情节漏洞(plot holes)”。

一种情节漏洞是“跳步”。归根结底,你的产品就是用户旅程。用户不是静态地“使用”产品,而是在其中移动:发现它、想到它、学会它、使用它,并承受其后果。如果只盯界面本身(我们看得见的那部分),就会产生隧道视野。

经典失误之一是漏掉“发现机制”。比如 Tea++ 的门店从未提过有点单应用,导致本可发现该应用的用户里只有 10% 真正发现了它,只有那些愿意去应用商店主动搜索的人才会找到。

另一类情节漏洞是边界情况。你需要为不同情况写不同故事。比如用户在早晚会去两家不同门店怎么办?Alice 大概应该处理并排优先级,但要在另一个故事里完成。

说明

在撰写或评审场景时,主动寻找“跳步”和“边界情况”。

如何使用场景?

场景应贯穿软件开发的每个方面:从最初发现(正如 Alice 在本章所做)直到实现规格文档。因此,本书很大部分内容都会依赖“故事”的力量。

  • 在开发阶段,你会用场景来选择好名字(第 2 章),并设计错误层级和可执行的优质错误消息(第 3 章);你还会把它们转化为场景测试(第 4 章)。
  • 在发布后,你会从用户反馈中收集这些场景(第 5 章);在起步阶段,你会通过“客户发现访谈”获取它们(第 6 章)。
  • 在设计阶段,你会基于“按优先级整理的场景全集”制定产品路线图,并形成更细的用户流程(第 7 章)。
  • 场景还会指导细粒度交互设计(第 8 章)和架构决策,例如用于生成负载模拟(第 9 章)。

使用场景需要练习,但我们并非从零开始。毕竟我们从很小就学习故事。我们天生能够阅读并理解故事。作为产品思维工程师,我们还必须学会生成故事并批判故事。

Alice 在解释她如何使用故事时,用了一个词:conviction(确信)。当她构建出正确场景时,她能感受到这种力量,并由此相信自己的产品会奏效。在需要取舍的讨论中,她可以通过推演不同故事出现的相对概率来决策。她能带着确信发布,因为她的测试覆盖了最关键的场景。她也能把这些场景一路串到所有需要实现的组件上,确保它们共同让故事成立。

章节小结

我梳理了工程师进行产品思维的基础,并给出以下建议。

  • 场景是“行为模拟 + 人物画像”的组合。用完整场景理解用户及其处境,并指导产品决策。
  • 模拟是把注意力聚焦到关键处的强大工具。不要只看界面,要看到用户旅程。
  • 人物画像确保你与产品目标受众真正连接。
  • 通过“换鞋思考”与“选择性失忆”在日常中持续共情用户。
  • 在整个产品生命周期使用场景。随着设计走向实现,让故事逐步细化。

这些基础会贯穿全书。你当然可以从这里开始跳读,但我接下来会先讲产品如何与用户沟通,深入命名和信息架构等主题,因为它们是最常见的产品决策。练好这些,是让产品思维变成本能的好路径。

练习

这些练习中,我们假设自己在 Wikipedia.org 工作。

  1. 列出至少两类使用 Wikipedia 的人物画像,并分别描述。请确保你考虑到这些用户重视什么。(我的答案会覆盖两类宽泛画像。)
  2. Wikipedia 想提升文章质量,尤其是低热度语言条目。当前用户必须点击“Edit”链接才能编辑内容。Wikipedia 是否应该提供顺滑的行内编辑体验来消除这层摩擦?
  3. 假设你在设计 Wikipedia 页面顶部搜索框里的搜索功能。选用问题 1 中的一个画像,写一段场景,展示读者如何使用“typeahead”功能(即在输入时内联展示可能结果),从而不必加载单独搜索页。请用该场景凸显实现需要解决的问题。
  4. 假设你在设计 Wikipedia 的“Watch this page”功能,让编辑者订阅页面变更。选用问题 1 中的一个画像,至少写两段场景,帮助团队思考人们会如何使用它。(即便你从未使用过该功能也没关系,请按你认为合理的方式设想。)

参考答案

  1. Reid(读者)是一个以任务为导向的 Wikipedia 文章读者。Reid 看重 Wikipedia 提供的事实性与中立性信息,也重视其透明度,特别是在政治内容上。他希望快速找到并读完所需信息,然后继续一天的其他安排。 Eddie(编辑者)是一位历史学者,会创建自己专业领域内的 Wikipedia 条目,也会审阅他人的内容。他把自己视为内容守护者,因此会持续关注他人对其页面的修改,以确保内容和引用准确。他也希望成为社区的一部分,因此协作者群体的健康度、包容性和可信度对 Eddie 至关重要。
  2. 不应该。Wikipedia 主文章页的首要目标应服务 Reid 画像,因为 Reid 的数量比 Eddie 高三个数量级。Reid 需要快速页面加载,而行内编辑可能会拖慢这一点;他也不希望自己随手敲的按键就触发编辑。至于 Eddie,他是投入度更高的用户,不会被少量额外摩擦劝退。
  3. 在这个场景里,我重点强调了搜索排序、拼写纠正,以及 typeahead 候选都不命中时的处理。 Reid 想知道 Paradise Lost 的作者是谁。他点进搜索框输入 “Paradice Lost”(他不会拼),注意到顶部结果是 “Paradise Lost. Epic Poem by John Milton”,并带有封面缩略图。他还看到几个歧义项(比如一个英国哥特金属乐队),但顶部结果是正确的,因为链接按热度排序。若他真正想查的是该金属乐队的第七张专辑,他可以点击列表底部的放大镜条目,其文案是“search for pages containing paradice lost”。
  4. 我写了几个场景,覆盖了查看编辑便利性、取消订阅和通知渠道。你也可能会关注我没想到的点,比如用户如何联系与自己意见相左的编辑者。 Eddie 曾大量编辑一篇关于古代美索不达米亚建筑的页面,对其后续反馈有些紧张。他希望知道是否有人改动自己新增的段落。他在编辑表单里点击 “Submit” 按钮旁的 “Watch this page” 复选框,然后提交。之后他又做了几次编辑。现在该复选框默认已勾选,他保持不变。 之后 Eddie 在邮箱里收到一封变更通知邮件。邮件说明这些变更不涉及他编辑的部分,于是他忽略了。几天后,确实有人修改了他的段落,他又收到一封邮件。他点击邮件里的链接,进入“修改前/修改后”并排对比页,页面自动跳到并高亮改动处。他发现只是措辞更清晰,于是很满意。 几个月后,Eddie 觉得很多邮件都在提示他不关心的改动(页面太长了)。于是他点击邮件底部的 “Change or unsubscribe to notifications for this page”。在设置页上,他看到几个以前没注意到的复选框:可以取消“接收未编辑区段的通知”和“接收小改动通知”。他意识到自己只愿意订阅与本人编辑区段相关的变更,于是取消勾选了前一个选项。

2 引导用户使用你的产品

机器及其设计者有义务理解人。理解机器那些武断且毫无意义的指令,并不是我们的义务。

— Don Norman,见《设计心理学》

你的产品用户就是你的英雄。就像在小说里一样,你希望这位英雄走到终点并达成目标。

与小说作者不同,产品设计师的目标是让用户旅程尽可能轻松。在场景的指导下,你会在恰当的位置与恰当的时机提供路标,帮助用户沿路前进。

经典著作《设计心理学》中,Don Norman 将这种路标称为意符(signifiers)。意符是界面中提示某个功能作用的线索。意符的例子不胜枚举:

  • 用名称或图标来指示用途
  • 按钮外的边框,提示你可以点击
  • 向下的尖角符号,提示这是下拉菜单
  • 单选按钮,表示最多只能选择一个选项
  • 一组复选框,表示可以同时选择多个选项
  • 带下划线的蓝色超链接,表明目标是网页,链接文字则说明该页面的相关性或名称
  • 错误消息,提示用户接下来该做什么

等等。

本章将探讨意符这一概念及其在用户旅程中的作用。通过它们,你将学会如何打磨出更精致的界面。

首先,我会介绍一个关于分层菜单的案例研究,里面有很多可以深入讨论意符的机会。

这会是我们观察本章建议的两个视角之一。第二个视角会以如下侧栏形式出现:

编码实践

这些模块里,你会看到如何把产品思维应用到日常编码中的建议。毕竟,函数、类等构造本身就是微型产品,而它们的用户正是你的队友和协作者。好的命名和注释会让代码评审与维护高效得多,同时也能帮助你练习产品思维能力。

通过这两个视角,我将审视用户旅程的三个主要阶段:发现、理解与使用。

过程中,我还会展开一些主题,例如本体(ontology)、层级化设计,以及一致性与特异性之间的权衡。

最后,我会拉远视角,整体审视用户旅程,讨论当不同阶段发生冲突时该如何权衡,以及何时需要从更宏观的层面思考设计。

案例研究导入

2000 年代中期,微软在 Office 套件(Word、Excel、PowerPoint)上遇到巨大问题:系统过于复杂,用户难以理解并定位自己需要的功能。到 2003 年,Microsoft Word 除了下拉菜单外,还有 31 个工具栏和 19 个任务窗格。根据产品经理 Jensen Harris 的相关演讲,图 2-1 展示了典型会话截图。

通过对数百名用户的访谈,团队发现用户很难发现功能,也难以建立对软件的掌控感。许多呼声很高的功能虽然被做出来了,但用户找不到它们。

到了 Office 2007,微软将“Ribbon(功能区)”菜单系统引入其 Office 桌面应用,如 图 2-2 所示;同时还有在选中文本时弹出的上下文菜单,如 图 2-3 所示。

2003 版 Microsoft Word 的凌乱截图
图 2-1 2003 版 Microsoft Word 的凌乱截图
2025 年 Windows 版 Microsoft Word 的功能区
图 2-2 2025 年 Windows 版 Microsoft Word 的功能区
2025 年 Windows 版 Microsoft Word 的上下文弹窗
图 2-3 2025 年 Windows 版 Microsoft Word 的上下文弹窗

设计这次改版时,团队收集了数千项功能的使用频率,并将它们重组为新的层级化菜单设计,同时引入了新的功能呈现方式。这些变化让高度复杂的应用对许多用户更容易上手,并且迄今经受住了时间考验。理解其“怎么做、为什么做”,能为我们在自己的应用中引导用户旅程提供线索。

用户旅程中的场景

用户旅程通常会经过三类主要场景:发现(Discovery)、理解(Understanding)和使用(Usage)。

  • 发现:用户有一个想解决的问题,但还不知道怎么解决。
  • 理解:用户遇到一个功能,想弄清它做什么、怎么工作。
  • 使用:用户希望按预期目的使用该功能,同时避免以不安全方式使用。

如果你负责交付某项功能,就必须对这三个方面都负责。让功能可见、帮助用户理解、引导用户安全使用,都是你团队的责任。漏掉其中任何一个,你的功能都无法产生应有影响。

第 1 章 所述,你应该能讲出一个可信的故事,让用户贯穿三个阶段且没有情节漏洞。确保覆盖这三类主要场景,是消除漏洞的好方法。并且在针对每个场景时,还要继续补全更多覆盖细节的故事。

发现场景

读者将如何发现你的抽象?工程师常忽略用户旅程这一段,但它往往最关键:不知道产品存在的人,根本无法使用它。这就是线上广告会形成巨大产业的原因之一:它解决的是发现问题。

考虑发现时,要记住用户可能从不同方向进入。以 Microsoft Word 的上标功能为例,用户可能通过阅读功能区按钮、使用搜索栏、查阅在线文档、查看快捷键列表,或与聊天机器人交互来得知它。当你认为用户会需要你的功能时,他们所有发现路径都应能导向它,所以请尽可能多做心智模拟:用户会怎么接近它?哪里有情节漏洞或不合理的逻辑跳跃?

这一节里,我会先讨论用户带来的既有知识,再探讨帮助他们找到功能的工具,最后说明如何把功能做成层级结构,避免一上来就压垮用户,或者把东西藏得找不到。为引导你理解,我会介绍一种实用设计方法:产品发现地图(Product Discovery Mapping),它也能把这些讨论可视化。

产品发现地图

产品发现图(Product Discovery Map, PDM)可以帮助你绘制用户学习产品时会走过的路径。假设你在复查产品实现,想确认用户能找到在 Word 中制作简报所需的关键排版功能。这类用户会想做多栏布局、使用横向页面、在正文中嵌入图片,而且很可能会把这些技巧组合使用。

我们聚焦“连续分节符(Continuous Section Break)”的发现过程,这个概念你很确定用户一开始并不理解。图 2-4 展示了 Microsoft Word 的“布局”菜单,并展开了“分隔符”下拉菜单。

Microsoft Word 的布局菜单
图 2-4 Microsoft Word 的布局菜单

下面是 Yves 的一个用户场景。Yves 所代表的是一类轻度使用者人物画像:偶尔用 Office 做社区活动宣传的人。

Yves 正在为本地教会做月度简报。他想做成双栏排版,同时让简报标题居中显示在顶部。他先输入标题并在“开始”选项卡中把它居中。然后呢?他看到了“布局”选项卡,并注意到一个“分栏”控件。他点击并选择两栏,但标题跑到了左栏,这不是他想要的。他撤销后看到了醒目的“分隔符”按钮,并根据下拉菜单中的说明,学会了如何使用连续分节符。

当你要交付很多功能,或要测试大型产品可用性时,这类场景很快会变得冗长。产品发现图是一种简写表达,包含三个关键要素:

  • 每张图都针对一个特定客户画像,表示其如何在产品中“走迷宫”。
  • 每个节点代表产品中的一个元素。
  • 每条边表示该画像如何发现相关知识,如 图 2-5 所示的 Yves 旅程。
连续分节符的 PDM
图 2-5 连续分节符的 PDM

这张图假设 Yves 知道什么是“分栏”,但不知道什么是“分节符”;同时它展示了他成功走到正确功能的一条路径。

你可以把多条流程放进同一张图里,更快速、更直观地构思产品的某个部分。

构建和评审地图时最重要的是思考与协作过程。命名是否足够有帮助?你希望用户完成的认知跳跃是否合理?到达某功能所需步数,是否和它的使用频率匹配?常一起使用的功能是否也被放在一起?

利用用户已有知识

第 1 章 中,我说过,人物画像有其“手段”——他们带来了哪些技能与知识?

你的产品呈现的一组名称与概念,以及这些概念之间的关系,称为本体(ontology)。本体本质上是一张图。用户对产品本体越熟悉,理解越深入,使用也越有效。

让东西更容易被发现的最简单方式,就是使用用户本体里已经有的名称和概念。也就是,不要重复造轮子。

在“布局”菜单这个例子里,Yves 知道“分栏(Column)”,但面对“连续分节符(Continuous Section Break)”时,他只带着“分节(section)”这个概念进入。观察 图 2-4 就能看到,微软在“分隔符”上的设计要比“分栏”谨慎得多:他们必须提供更详细的说明和图示,来弥补命名上的陌生感。

编码实践

要警惕晦涩的项目代号。它们不在用户本体里。比如别人要在公司代码库或文档里找一个分布式缓存框架,可能会搜“cache”,但他们不会提前知道你的项目叫“Phoenix”(因为它从上一个分布式缓存的灰烬中“重生”)。如果你确实想做品牌化命名,可以考虑像“SuperCache”这种方案:既有一点品牌感,也保留一点可发现性。

晦涩代号在某些场景也有价值,例如保密需求,或对象足够独特、很难直白描述,导致直接命名反而误导。但要清楚你牺牲了多少可发现性,并确保留下其他面包屑(例如注释)让人能找到它。

有时,本体还取决于人物画像。

我在 Stripe 工作时,我父亲(一位会计,几乎不会编程)读了 Stripe 在线支付和账单 API 的文档。他抱怨我,因为他一直在找“应收款(receivables)”之类严谨的会计术语。

我告诉他,这其实是有意为之。这些 API 面向的是通用型程序员,努力使用那些即便没上过会计课的人也能接受的术语。

但这也说明:如果 Stripe 希望会计人群也能顺畅使用,仍有不少工作要做。

提供多条发现路径

当然,语言里有同义词和相关概念,但你的本体里通常只能选一个主叫法。你需要给那些带着不同猜测进入的用户提供通向它的路径。

举个例子,功能区提供了好用的命令搜索框,并支持一定的模糊匹配。如果我在框里输入“Margin”,它会返回含有“Indent”的命令,如 图 2-6 所示。我甚至可以直接在该菜单中操作这些命令。

在 Word 中搜索 'margin'
图 2-6 在 Word 中搜索 margin
编码实践

通过注释为你的抽象增加更多发现路径。比如你的基类叫“Plugin”,可以在注释里用“Extension”这样的同义词来描述它。

这个原则可以推广为:

提示

给用户提供多种发现某个功能的方式,以适配他们不同的“手段”。

以 Word 这类复杂应用为例。大改版之前,发现体验一团糟。用户得在海量菜单、工具栏、任务窗格中判断哪一个适合当前任务。若他们需要工具栏,只有图标给提示;若看菜单,则要在长长的词语列表里寻找,而且排序几乎没有规律可循。功能分类看起来也很随意:为什么某些功能在 Tools 菜单而不是 Format 菜单?为什么 FindEdit 菜单里,即便它本质是读取操作?

一个平庸的 Help 组件,是这团乱麻之外几乎唯一的替代路径。

而自从引入功能区后,菜单的意符明显更好了。

  • 命令被分组到更少的选项卡中,并分配了更多屏幕空间,让 Yves 更容易在“布局”选项卡里扫到与其用例相关的内容。
  • 作为常用且推荐命令,“分隔符(Break)”被放大并放在相关命令附近。
  • “Break”和“Continuous Section Break”同时用图标与文本表达。不同用户可能更容易通过其中一种形式找到它。
  • 提供搜索框;如果 Yves 知道“section”这个词却不知道在哪个菜单,这就很有帮助。它修复了传统菜单的一个根本问题。

图 2-7 展示了 Yves 可能发现连续分节符的多种有效路径。

发现连续分节符的多条路径
图 2-7 发现连续分节符的多条路径

以下是用户发现你产品的几条常见路径:

  • 搜索
  • 询问聊天机器人
  • 视觉扫描你的应用,或边点边看,寻找名称和其他意符
  • 使用屏幕阅读器(面向视障人群)或收听语音菜单
  • 查阅操作指南或其他文档

尽量不要只依赖文档。在消费级应用里,你很可能根本不会依赖它。

优雅揭示复杂性

几乎所有成功应用都会不断增加功能。即便是以简洁搜索框著称的 Google,如今也有许多配套搜索结果的选项卡和工具。

为了驾驭这种功能扩张,设计师会向用户优雅地揭示复杂性

看看功能区。即使已经改进,它仍有太多命令和目标画像,难以让菜单始终易用,因此微软引入了“上下文菜单”的概念。它只在合适时机出现。图 2-8 展示了“形状格式”选项卡:除非你点击了形状,否则它不会挤占界面空间。

功能区中的上下文菜单
图 2-8 功能区中的上下文菜单

功能区默认有九个选项卡,已经接近令人不堪重负,这在产品发现图里很容易看出来。通过把“形状格式”节点下移到图的更深层级(见 图 2-9),这种优雅揭示避免了问题恶化。

Word 还通过上下文菜单实现优雅揭示:当你高亮文本时,鼠标旁会弹出包含加粗、斜体等常用命令的菜单。这类菜单会主动出现,可发现性很强;同时选项很少,用户也更容易快速扫描到所需工具。

用于发现“形状格式”选项卡的 PDM
图 2-9 用于发现“形状格式”选项卡的 PDM

多人物画像设计

应用变复杂的一个主要原因是:随着增长,它会吸引不同人物画像的人出于不同目的来使用。

Microsoft Word 服务的人群非常广,从需要章节导航的小说作者,到需要图表的营销人员,再到需要屏幕阅读器与语音输入的视障用户。

因此,大多数成功应用至少会有两套不同界面。理想情况下,每套界面都应在用户需要时被优雅揭示出来。

大多数应用都有重度用户,通常占受众的 5%-20%,他们活跃且深度投入。这些用户希望拥有更强大的原语来定制体验。原因往往在于,他们有更强激励去使用并深入掌握你的产品。一个靠 Word 工作谋生的重度用户,比一个写高中英语诗歌作业的学生,更可能用宏和文档修订追踪。

环顾四周,你会发现这种“双界面、双画像”模式无处不在:

  • Wikipedia 页面有一个不显眼的小“编辑”按钮,读者可忽略,但对贡献者很有用。
  • 网站面向新手给出极其醒目的“创建账号”,而对老用户只放一个小小的“登录”按钮。
  • 现代操作系统既有面向大众的图形界面,也有面向 IT 专业人员的命令行外壳。
  • TypeScript 这类渐进类型语言中,类型标注对简单脚本可选,但在大型组织里可被要求用于构建更健壮的代码库。
  • 无代码或低代码框架对非程序员友好,同时允许程序员编写自定义代码。
  • 面向轻度用户提供按钮点击,同时给习惯型重度用户提供键盘快捷键。

作为设计师,一个非常重要且令人解放的认知是:当你意识到产品已到应提供多界面的阶段,就可以开始为各人物画像分别优化。这会打开更大的设计空间,也给你更多解决问题的机会。

把“未知的未知”变成“已知的未知”

到目前为止,我大多假设用户知道你的应用具备某些功能,只是多久能找到它的问题。但如果他们压根不知道这个功能存在呢?这在新颖或创新功能中很常见,也常见于对领域本体理解尚浅的新用户。某个用于“神奇记账”的 Excel 函数,资深会计都熟知,但会计专业本科生可能闻所未闻。

所以,把你的产品想象成一片神秘森林,用户正在其中穿行。请给他们留下一条面包屑轨迹,帮助导航。

举个例子,最近我在多个应用里都看到小型聊天机器人控件弹出。Microsoft Word 在我的光标旁有一个“Draft with Copilot”上下文菜单。此前我并不知道 Word 有这个能力。我也不完全确定这个聊天机器人能为我做什么,但至少我可以去探索。一个“未知的未知”就这样变成了“已知的未知”。

你还要认真思考菜单里省略了什么。用户可能并不知道缺失命令可以在别处找到。Word 在“布局”选项卡里展示了全部 Break 类型,但在“插入”选项卡只放了“分页符(Page Break)”,如 图 2-10 所示。这样设计的风险是:没看到“布局”选项卡的用户可能误以为分页符是唯一选项。权衡点在于,相比埋在更模糊的“Break”命令下,单独露出的“Page Break”确实更易被发现。

“插入”选项卡中的分页符命令
图 2-10 “插入”选项卡中的分页符命令

理解场景

当用户发现了所需功能后,下一步必须更深入地理解它到底做什么。

我先从最常见、也是促进用户吸收关键概念的方式讲起:起好名字。随后我会介绍其他意符,当名称不够,或你想兼顾不同学习风格时,它们会很有用。

选择易懂的名称

名称是你向读者脑海植入一两个关键想法的机会。好名字除了提升可发现性,也应促进理解。用户不一定想知道“这个功能是什么”,他们更想知道“它如何帮到我?”这两个问题很多时候答案一致,但并不总是。具备产品思维,意味着你要思考自己对读者产生的影响,而不只是描述系统本身。

命名最重要的规则之一也许是:避免“知识诅咒(Curse of Knowledge)”。

说明

知识诅咒是一种认知偏差,会让系统设计者难以预判理解较少的用户在与系统交互时会遇到哪些困难。

“换鞋思考(shoe-shifting)”是避免这一诅咒的最佳方式。考虑目标画像已有的本体;更好的做法是,直接找这个人群中的人,问他们会如何解读你的命名。

重新审视经典命名建议

大多数人都听过命名建议——那些适合印在项目符号列表里、或者贴在笔记本上的短句。我想用更以用户为中心的视角,重写其中几条。

避免歧义(而不是“要能自解释”)

如果你只能在便签上写下一条命名箴言贴在显示器边上,那就写“避免歧义”。

在没被知识诅咒“感染”的读者看来,功能命名含糊不清的情况其实非常常见。要防止这一点,就做一次换鞋思考,进入用户视角,头脑风暴你的命名可能被如何误解。(或者,再说一遍,直接去问他们。)

当我写“布局”案例并扮演做教会简报的 Yves 时,就被一个歧义惹恼了。我看到“布局”选项卡里有这个 Align 按钮,如 图 2-11 右侧所示。

Word“布局”选项卡的一部分
图 2-11 Word“布局”选项卡的一部分

你猜它是做什么的?

我本以为它能用来让简报标题居中。然而我展开它后(如 图 2-12 所示),所有看起来有用的选项都灰掉了。我试了几种办法让它生效,但都没成功。

一个令人困惑的下拉菜单
图 2-12 一个令人困惑的下拉菜单

如果 Align 指的并不是通用文本、图片或图表的对齐,而是更具体的东西,那它就应该明确写出来!

编码实践

以下是代码库里经常需要澄清的一些歧义:

  • 角色与关系:当存在两个职责不同的对象(如 parent/child、sender/recipient)时,澄清“到底是哪一个?”至关重要。
  • 变量的类型:这能在每一行使用该变量的代码上说明“它是什么”。
  • 度量单位:你不会希望读者把磅当牛顿,或把毫希沃特当希沃特,最后导致火星探测器事故或辐射灼伤。
  • 词性persist 是一个表示“是否写盘”的布尔值,还是一个真正执行写盘的函数?如果是前者,should_persist 会更好。
在一致性与特异性之间权衡(而不是“保持一致”)

一致的命名有助于用户:他们在本体里学会一个概念后,就能反复复用。给新功能命名时,务必先在现有产品里找“前例”来对齐。

但一致性经常要和特异性做权衡,而特异性往往是减少歧义的最佳手段。在前面的例子里,“Align”术语非常一致,却不够具体。

更具体有时也更符合习惯表达。比如你在音乐流媒体应用中设计一个“随播放显示歌词”的功能,即使产品里一贯用“Track”表示音频文件,你可能仍更愿意写“Song Lyrics”,而不是生硬的“Track Lyrics”。

另一个常见困境是:某个相关概念在很久以前命名得很差,你现在要决定是沿用它保持一致,还是回到第一性原理选一个正确的新名字。

对一些工程师而言,一致性可能变成教条,因为他们无需考虑用户视角也能围绕一致性展开推理。你团队里的技术负责人可能并未聚焦你的功能,没有思考当前场景或共情用户,但始终能对“一致性”发表意见。

对抗这种偏差的一种方法是:同时头脑风暴一个“最易懂、最具体”的命名,以及一个“与现有产品最一致”的命名。如果最终得到两个不同答案,再讨论权衡。这样能确保你既在共情用户,也在用现有本体检验命名。衡量时请继续做换鞋思考,贯穿思考可发现性、可理解性与可用性。下面是一些启发式规则:

  • 没有充分理由时,优先保持一致。
  • 记住你的知识诅咒。作为系统设计者,你通常比典型用户掌握更全面的信息。只使用产品子集的新用户,可能根本看不到足够内容,自然也不太在意一致性。
  • 问清不同受众规模:也许旧命名只被少数重度用户使用,或者只是遗留功能;而你这个新产品有更广泛目标。为了做对未来,牺牲一点一致性也许值得。
  • 在多画像应用中,更重要的是每个画像各自体验到什么。比如 Microsoft Excel 完全可以给同一公式提供两个名称,一个面向会计,一个面向软件工程师。又如你的应用同时支持 iOS 和 Android,平台内一致性很重要,但跨平台一致性优先级较低,因为用户并不常在两个平台间切换。
只在目标画像普遍使用时才用缩写(而不是“避免缩写”)

缩写词对理解场景很糟,因为没人会凭空猜出字母含义;它们对发现也不友好。

但你的目标画像可能已经习惯某些术语。你显然不该把“HTML”到处拼成全称。“CPU”更适合软件工程师受众;若面向更大众的人群,试试“处理器(processor)”这样的词更合适。

如果不确定,就调研目标用户,看看他们如何解读你提出的术语。

只传达用户需要知道的内容(而不是“简洁”)

严格说,简洁(concise)意为“简短但完整”。这听起来有道理。用户学习时会被词藻堆砌搞得吃力,但也不希望你漏掉关键细节。

但“完整”是对什么完整?我会改写这条建议,把重点更多放在读者和“什么能帮到他们”上。

我曾玩过一个线上游戏“PHP Diplomacy”——它改编自一款经典桌游,名字取自该移植版本所用的编程语言。非程序员很可能看不懂这个名字。回头想想,我当时一起玩的好像也全是软件工程师。

通过换鞋思考,你能识别哪些只是实现细节。名称可承载的信息位有限,每删掉一个无关信息,就多出一个位置传达真正有意义的内容。

假设你在构建一个长时任务 API:用户会把请求入队,系统从队列取出并执行。用户可以阻塞等待结果,也可以稍后拿句柄去查询。

把异步请求 API 命名为 enqueue_request 看起来很自然。乍看不错,但那“既入队又等待结果”的函数该叫什么?enqueue_and_wait_for_request?当你觉得这个名字别扭时,就该问:读者为了正确使用它,究竟需要知道名字里的哪些信息?

最相关的事实是它是异步的——响应可能延迟。“enqueue”这个词比需要表达的更具体。

这看似细枝末节,但当你去掉“queue”这个实现细节后,就能得到 start_requestexecute_request 这样简洁又传神的命名,分别对应异步版和同步版。简洁,也更易发现。

编码实践

如果你想把非常短促的函数名和变量名提交进代码库,请记住 Python 之父 Guido van Rossum 的那句话:“Code is read much more often than it’s written.”(代码被阅读的次数远高于被编写的次数。)

函数名被“阅读并理解”的场景——代码评审、调试、学习代码库、搜索修改点——远比它被“敲出来”的场景——加新功能、写测试——要多得多。而输入阶段还有自动补全帮你。

在代码库生命周期里,读者会不断出现。如果你在构建可能长期存在价值的系统,请用清晰命名对它负责。

提供冗余解释

选一个既便于发现、又能帮助无歧义理解的名字非常难,有时甚至不可能完美传达用户所需的一切信息。因此,值得用冗余机制来传达你的意图。

看看 Word 功能区,它有效利用了图标来表达功能。在 图 2-13 中,你可以看到“开始”选项卡里的这些格式命令;它们经过精心设计,在帮助理解方面优于“multilevel list”“justify”这类纯文字标签。

格式命令图标
图 2-13 格式命令图标

下面是一些不只依赖命名、同样能提升理解的方法:

  • 图文结合:图像可以把文字试图传达的意思可视化,前面的分节符描述就是例子(图 2-4)。也别忘了,并非所有用户都在使用其母语版本产品。对英语母语者来说,“Column”也许很基础,但对其他用户未必属于常用词汇。
  • WYSIWYG(所见即所得):用户甚至不需要先理解,因为操作效果会立刻显示出来。Windows 版 Microsoft Office 除了是 WYSIWYG 编辑器外,还提供“实时预览(Live previews)”,可直接看到切换样式对当前文档的影响。
  • 换个说法:在工具提示和文档里使用同义表达或改写。用不同措辞复述同一概念,能帮助用户交叉定位其本意并建立信心。
  • 始终提醒上下文:人的短时记忆容量有限,容易忘记自己在哪。比如用户正处于电商购买流程中,若他们中途离开再回来,每个页面都应提醒“当前正在购买什么”。
编码实践

冗余在代码、文档和其他信息密集场景中都很有价值。一旦你从用户场景和“人类工作记忆有限”出发,这一点就非常清楚。

例如,看下面这段代码,你发现什么了吗?

def email_lunch_invitation(sending_user, recipient_user, message):
  recipient_organization = recipient_user.get_organization()
  title = f"{sending_user.name} from {recipient_organization} " \
    "has sent you an invitation to lunch!"
  send_email(
    sending_user.email_address, recipient_user.email_address, title, message)

你在计算 title 时看出 bug 了吗?如果我用的是 organization 而不是 recipient_organization,你觉得你还能这么容易看出来吗?

下面这些场景里,一点额外冗余会很有帮助:

  • 读者在做代码评审,抽样检查质量。
  • 函数作者可能像上面那样误用变量。
  • 程序员通过断点、引用跳转、查找所有引用、调试调用栈或 lint 规则来到这一行,还没读完整个函数。

如果我们需要知道的关于某行代码的信息都写在这一行上,生活就会轻松很多。

使用场景

当读者旅程进入尾声,开始动手操作时,危险就更大了。安全问题无处不在。我们会在 第 8 章 引入“可供性”概念时更系统地讨论用户安全;先在这里浅尝一下:看看意符如何在缺乏其他保护措施时,仍能引导人做对的事。好的命名能把用户“导上轨道”,让他们凭直觉做出正确操作,甚至意识不到自己本可能偏离。

请认真推演使用场景,并提醒用户可能的安全风险。

这里有个极端例子。2010 年代初,某家互联网公司内部有一个特殊配置变量,会被复制到所有提供 Web 流量的机器上。这个变量里放的是一个正则表达式,会在每个对外 HTML 页面发送到用户浏览器前,执行一次查找/替换!

如果要紧急打补丁,比如移除一个有问题的 HTML 标签,工程师就可以把正则写进这个变量,瞬间发布到整个平台。

这听起来危险吗?确实非常危险,而且本意只用于紧急情况。若你用正则改坏了 HTML 导致解析错误,整个站点都可能瘫痪。因此它被命名为 TAKE_DOWN_THE_SITE,名字不是描述“它做什么”,而是强调“误用会发生什么”。

尽管极端,这个命名体现出的安全意识还是让我不情愿地生出几分敬意。

一个更常见的危险信号做法,是把内容塞进“高级”菜单。这既拉长了发现路径、减少会找到它的用户数量,也在暗示使用它可能带来意料之外的影响。Microsoft Office 就是这样做的:用户必须先显式启用,功能区才显示“开发工具”选项卡,把宏和 VB 脚本等工具限制在更可能受益、也更不容易把设置搞乱的人群中。

要判断是否需要显式标注危险,请构造用户“走错路”的模拟,并观察怎样的命名能让这些错误故事变得不再合理。

与“隐藏危险功能”相反的做法,是主动强调概念,比如强制用户做选择。在“布局”菜单选择 Break 时,Word 不会默认最常用的“分页符(Page Break)”,而是强制我先选具体类型。这样既教育了我有哪些选项,也避免我把文档弄乱。

编码实践

在用户需要做出选择、但可能没意识到这点时,要求其显式传入参数。

在抽象上通过注释留下面包屑。比如有两种实现路径,用户找到其中一种时,也要提示另一种他们可能不知道的方式,尤其当后者对大多数用户更推荐时。

优化整段用户旅程

正如 TAKE_DOWN_THE_SITE 案例所示,有时为了提升可用性,你会牺牲可发现性和可理解性;换言之,在优化发现、理解、使用时,常常存在取舍。

对用户旅程做清晰模拟,有助于做出这些取舍。

例如,如果“不要发明晦涩缩写”这条建议这么好,为什么 Unix / Linux 里还有 lscd 这种流行却晦涩的命令名?它们只是老旧和糟糕吗?

也不完全是。可以说这些名字仍然合理,因为常用命令被“输入”的频次远高于“阅读”的频次。你学一次(或者忘了再学一次)就行,但你不希望每天敲六遍 list_files 而不是 ls。而且它们面向的是习惯型、技术型用户。在这种情况下,使用场景压过了发现与理解,规则自然会被打破。

但如果你是在做市场传播设计,就可能不得不优先可发现性和搜索引擎优化。比如气泡水品牌“Liquid Death(液体死亡)”这个名字,谢天谢地只有 50% 准确、也不精确,但确实很会营销。在这条用户旅程中,名字先抓住购物者注意力,他们再看副文案确认它到底是什么。

提示

为你的功能明确发现、理解、使用三者的相对重要性。

意符的边界

并非每个问题都能靠意符解决,尽管你事先未必知道。你可能一开始想找一个完美名称,但随着你推演用户旅程,才发现根本没有一个名字能传达目标画像需要知道的全部内容。也许它太复杂,或者包含某种安全陷阱。

看看浏览器 Cookie,以及网页上那些提醒你“页面可能在记录你的数据”的弹窗。要向非技术用户准确传达“到底发生了什么、侵入性到底多强”非常困难。换言之,立法者要求网站沟通 Cookie,网站也在努力合规;他们试图用意符去解决的问题,其实本质上是更深层的设计挑战。

人们做过不少努力,比如区分“必要 Cookie”和营销 Cookie;但用户仍缺乏对 Cookie 本体的清晰理解。结果是大多数人会一刀切关掉所有 Cookie,即使其中一些本可带来好处,或能在几乎无个人成本下支撑互联网经济。

更深的问题是,Cookie 本来就是低层编程抽象,而不是面向用户的功能。人们也提出过更好的抽象,比如提供更细粒度、用户更易理解的分类。可惜,可能由于集体行动困境,截至 2025 年,这个问题仍基本无解。

在产品周期较早阶段就尝试命名与组织产品,是一件非常值得做的事。它会迫使你关注完整用户旅程,并常带来有价值的设计洞察。敬请关注第 7 章和第 8 章,我们会更深入地探讨产品设计。

编码实践

如果某个抽象既别扭又不安全,而你暂时修不好,就别试图掩盖它。让名字丑一点,吸引注意与审视。也许你的代码评审会有更好想法,也许后来有人会把它改好。我曾见过一个私有类变量,半开玩笑地命名为 _do_not_use_or_you_​will_be_fired,对此我也只能不情愿地肃然起敬。

本章小结

我们一路跟随用户,观察他们如何发现、理解并使用产品。此外,在侧栏中我们聚焦了日常编码中的命名决策和编码接口设计,把它们作为练习产品能力的方法。

我最终给出的建议如下:

  • 对于发现:优先选择用户本体中已有的名字,或与其他命名保持一致。提供能帮助用户在产品中导航的意符,并以优雅方式逐步揭示更复杂功能。若有帮助,可花时间绘制产品发现图来可视化用户旅程。
  • 对于理解:首先避免歧义,并为每类用户提供一致体验。通过提供冗余的“进入路径”来拥抱受众及其视角的多样性,帮助他们理解你的产品。
  • 对于使用:利用命名突出潜在陷阱与“危险区”,引导人们正确使用你的函数。

对于多画像应用,不要害怕为不同画像提供不同本体和发现路径。

最后,虽然本章位于 Develop 阶段,请记住:为产品本体中的关键概念命名和建模应更早发生,因为它能为整体设计过程带来重要洞察。

练习

以下练习中,我会把某个东西命名为 foobar,你的任务是给它起更好的名字。(由于 foo 完全不含信息,可以把你的任务理解为尽可能拉大 foo-distance!)当然,这些题并不存在“标准答案”,但请从发现、理解、使用场景去思考。我希望它们接近真实世界练习,这意味着你可能需要做一点轻量设计思考,或上网查资料。

  1. 你的数据库允许用户在某张表数据变化时订阅变更事件。如果他们继承 DataChangeSubscription 并向数据库注册,那么每次数据库事务提交后都会触发其回调。
    class DataChangeSubscription(ABC):
      # subscriptions apply to a certain database table or collection.
      @abstractmethod
      def foo() -> TableType:
    
      @abstractmethod
      def bar(old_record, changes: Dict[str, Any]) -> None:
    你会把这两个由子类实现的方法分别命名为什么?
  2. 你在做一款面向啤酒爱好者的评分应用。用户点击某款啤酒后,会看到一个名为 Foo 的自由文本输入框来写感受。它该叫什么?
  3. 新用户会完成一段问卷和注册流程,你正在优化这条流程,因此想追踪一个聚合指标 foo,用于衡量他们花了多久。你会怎么命名?
  4. 你在维护一个小型社交网络,最近提交的一次改动导致性能回退。你调用了这个函数:
      mutual_friends = await current_user.getMutualFriends(friend_user)
    实际上你并行调用了很多次(每个好友一次),结果比预期昂贵,包含了一些你没预料到的计算与隐私校验。你暂时没时间优化。有没有可能通过重命名来避免其他人将来重蹈覆辙?

参考答案

  1. 对于 foo,table_type 可以,但可考虑 table_type_filter 以增强清晰度,让人明确“表类型是用于过滤条件”。对于 bar,可用 on_record_changedon_ 前缀符合常见事件处理器命名;record(单数)明确变化对象;changed(过去式)明确提交已发生。若以后新增“提交前”回调,可命名为 on_record_changing。另外,我选择 changed 也是为了与类名中的 Changed 保持一致。
  2. 做一点用户研究(或问问聊天机器人)后,你会发现啤酒爱好者常用 “tasting notes” 来描述啤酒。你也可以用 descriptionreview 这类通用词,但 “Tasting Notes” 更可能让目标画像写出“香气”“口感(mouthfeel)”这类细致内容,这正是你想要的。
  3. 设想队友在团队看板上读图时会问什么。第一问通常是单位:毫秒、秒还是分钟?第二问可能是统计口径:平均值、中位数还是 p90?因此你可以命名为 user_signup_seconds_to_complete.p50 之类。
  4. getMutualFriends 应该以某种方式提示它“很贵”。比如直接改成 computeMutualFriends,也许就足以让你意识到成本。若后续易于重命名,也可叫 computeMutualFriends_​Expen⁠sive。也许未来正是这种“丑得显眼”的名字,会促使后来者去做缓存或把它优化得更便宜。

II 交付

作为一名具备产品思维的工程师,当你即将发布一个重大版本时,请执着地追问一个问题:你将如何让用户来验证,你的产品确实做到了它该做的事。

在这件事上,主流社交媒体公司很幸运。它们拥有规模庞大且宽容度高的用户群,以及极其强大的软件分发机制:信息流。它们可以快速发布实验性功能;无论是投票、游戏邀请、活动、群组、视频还是限时动态(Stories),都能先分发给一小部分用户,收集反馈和指标,并进行 A/B 测试。

而在光谱的另一端,自动驾驶创业公司就没这么幸运。它们往往要先迭代多年,才能向客户发布任何产品。它们雇用专业人员在采集数据时看护车辆,直到系统可靠性达到超越人类的水平。换句话说,在其行驶里程中,超过 99.999999% 都不能发生致命事故。

这些做法几乎没有共同点。唯一的共性是:它们都在做产品验证。

提示

让用户验证你的产品。

让用户参与产品验证,既能回答关键的未决问题,也能发现你此前没有想到的问题。

接下来的两章将介绍一些成本效益极高的方法,帮助你基于用户验证来迭代交付并持续打磨软件产品。

在发布之前,你会通过 Chapter 4 的内容,先让产品承受早期压力:你和同事会亲自试用软件、进行测试、撰写摩擦日志,并认真编写文档。

Chapter 5 将聚焦发布之后:如何通过反馈、实验和指标,听见真实用户的声音。

3 错误与警告

“PC Load Letter?” 这他 @#$! 到底是什么意思?

— Michael Bolton

1999 年电影 Office Space 里的 Michael Bolton 盯着一台故障打印机,说出这句台词,狠狠讽刺了科技行业糟糕的错误消息。客观说,HP 早期 LaserJet 打印机的屏幕可显示字符数确实有限。如今我们早已走出石器时代,屏幕分辨率高到可以显示大量文本。你完全可以塞进很多有帮助的词!

消费者经常面对自己看不懂的诊断消息和警告,而专业人士也会把数小时浪费在处理含混消息上,而不是完成手头任务。用户往往是“易流失”的,只要遇到一点摩擦,就可能对我们的产品失去兴趣。

然而,我们工程师常把错误和错误消息当成只是要尽快补齐的“边缘情况”,而不是当成工程技艺的重要组成,或者产品差异化的机会。

再看一个更偏编程领域、也很有趣的例子:文本标记系统 LaTeX。LaTeX 是一种文档排版语言与系统,常用于高质量排版数学和科学论文。

在 LaTeX 里,如果你输入如下内容,其中 \\ 记号表示换行:

This is some text. \\
This is at the end of a block of text. \\

This is the start of a new paragraph

你会收到这样一条“宝石级”警告:

Underfull \hbox (badness 10000) in paragraph on line 2.

想试着猜猜这是什么意思吗?其实,LaTeX 想让你删除第 2 行空行前那个多余的换行符(\)。

这条消息虽然晦涩,但如果我们从抛错位置的代码“自底向上”看,它很可能完全说得通。由于代码组织方式,修起来也可能并不容易。等我先讲完两个技能后,会回到这个例子:

  • 你如何弥合“实现者觉得合理”和“用户觉得合理”之间的鸿沟?
  • 你如何有效组织代码来做到这一点?

这很重要:LaTeX 虽然目前只排在最流行编程语言的第 40 位,但 tex.stackexchange.com 上最热门的 “underfull hbox” 问题仍有超过 35 万次浏览。粗略估算每次浏览都伴随读者花 3 分钟搞清状况,那么 LaTeX 作者仅靠一条更好的警告消息(或者干脆对这个多余换行不报错)就可能为人类节省约两万小时(而且还在继续增长)。

诊断信息也可以令人愉悦。经典案例是 Google 搜索里的 “did you mean” 功能。假设用户把单词拼成 “compture”,Google 会在结果顶部显示搜索词并给出便捷链接:

compture.

Did you mean: computer?

这条消息完成了两件关键事情。

  • 它告诉用户自己做了什么:他们搜索了 “compture”。
  • 它通过一个便捷链接提示下一步:最常见的拼写纠正是什么。

第一个功能实现起来很简单(但很贴心),适配的是用户被打断后返回浏览器标签页的场景。我猜第二个功能让 Google 花了数千万美元去构建和维护,背后很可能依赖巨型数据库,以及某种也许具备“意识”的人工智能来推断用户本意。他们竟然为一个边缘情况投入了这么多!

诊断信息的价值

构建结构良好且消息有用的诊断信息,是一件价值极高、杠杆很大的事。对于许多输入复杂且开放的平台和应用,诊断信息就是主界面,用户绝大多数时间都花在处理一个又一个错误上。

填写电子表单,本质上就是不断被告知你漏填了什么、填错了什么。我的编码时间至少有一半花在处理错误和 lint 规则上。就连写文档,也变成了不断看下划线提示、被要求校对或改写句子的过程。

但在我们设计软件时,错误经常不会出现在截图、营销材料或 API 方法清单里,所以它们常常“看不见,也想不起”。

自主智能体把这个问题照得很亮。它们现在会持续收到由自己操作触发的错误消息,并被要求据此修正行为。消息若不够有帮助,它们就会在任务中失败。反复试错既慢又贵。由于智能体按使用量计费,成本会被直接量化。

说明

诊断信息可能是你产品里最重要的界面。

诊断场景

考虑错误、警告及其消息时,必须覆盖足够广的场景:从识别边缘情况,到理解开发者如何自动化响应,再到终端用户如何理解并采取行动。你越能理解用户认知、编写用户故事并模拟用户交互,诊断信息就会越好。对用户,我们提供有上下文且具备可操作性的错误;对开发者,我们谨慎选择错误类型、错误码和元数据,以便接收方能够优雅恢复。

在本章剩余部分,你将学习如何写出真正有用、令人耳目一新的警告和错误。我们将探索如何:

  • 理解场景:受益于该错误的人物画像及其处境
  • 提供足够上下文,让用户理解错误
  • 提供可操作的错误消息,明确建议该如何处理问题
  • 谨慎选择错误码和错误类型,让上游开发者能服务 他们的 用户
  • 在 API 或 UI 层抛出错误,以便消息能带上用户意图的完整上下文
  • 左移(shift left):也就是尽可能早地触发错误,既加快用户进度,也在坏事发生前拦截

我会在第 8 章讲如何系统列举边缘情况,从而确定一开始该检查哪些错误。这里我们先聚焦:当你已经知道有哪些错误时,如何把它们写好。

错误场景分类

你编写错误时,需要做几个核心选择。

先看面向用户的选择:

  • 错误消息写什么?

另外还有面向开发者的选择,方便他们捕获错误并自动化处理:

  • 错误的类或错误码是什么?
  • 需要哪些元数据才能精确定位问题?

所以,在几乎任何应用或平台里设计错误时,你都要同时考虑两类场景:面向人的场景与面向程序员的场景。

开发者场景还要进一步细分:你是在和同一代码库内的团队成员沟通,还是在和其他团队或其他公司的开发者沟通?如果你在构建 API 或服务,上游开发者会捕获你的错误并据此行动,这一点尤其重要。

因此第一步是:把消息对准“正确的人”与“正确的情境”。我们都见过一些明显不是写给自己看的错误,比如网站把代码堆栈直接展示给终端用户。要确定受众,先确定错误类别。就本书而言,表 3-1 中这五类覆盖了大多数情况。

错误类型 示例场景
系统错误 支付处理器宕机。超时。高负载下的瞬时错误。
断言 这个局部变量绝不应该是 null。
开发者参数无效 预期是字符串却收到了整数。
用户参数无效 用户输入了错误的信用卡号。
前置条件不满足 用户无权访问某资源,或尚未登录。
表 3-1 错误类别

先在脑中给你写下的每个错误做分类。这会给出一个重要线索:你在和谁说话(自己团队、其他开发者、或用户),以及问题应该在何时修复(运行时还是开发时)。这会帮助你使用正确词汇,并给出更有帮助的建议动作(见表 3-2)。

错误类型 修复时机 常见修复方式
系统 运行时 终端用户应稍后重试。
用户参数无效 运行时 终端用户修正输入后可重试。
前置条件不满足 运行时 终端用户先去修复其他问题。
断言 你的开发阶段 你团队内工程师会收到告警。
开发者参数无效 他们的开发阶段 调用你函数的开发者需要修代码。
表 3-2 错误场景类别的“何时修复”与“谁来修复”

这五类场景对应的策略差异非常大。比如断言若在生产触发,通常是灾难性的。代码进入作者未预见的状态时,会导致不可预测行为,最常见是崩溃或糟糕报错;偶尔更糟,比如数据损坏。有些语言会在生产环境去除断言以优化执行,所以你不该把断言用于任何承重逻辑。无论如何,终端用户都不应该被期望去正确处理断言错误。

有些应用里的终端用户也并非同一类人,此时消息应按不同人物画像定制。经典例子是“前置条件不满足”:用户没有必要权限。这个用户是管理员还是普通用户?这决定了我们是直接给出操作步骤,还是提示他们联系管理员。

了解人物画像有助于你用用户本体来表达。(“本体”在第 2 章定义为“已知概念构成的结构化图谱”。)回想 “PC Load Letter”,它本来是想让用户重新装纸。它有可操作性,确实告诉了用户“去装纸”,但它失败了,因为它在对错误人物画像说话。“PC” 指 “paper cassette(纸盒)”,“Letter” 指 8.5"x11" 纸张规格。更好的做法也许是把纸盒标成 A、B、C,再提示“请重新装载 B 纸盒”。

在实践中分类错误

我们来看一个例子,演示如何用产品思维给错误分类。

“除以零”属于这五类里的哪一类?在 Python 中它是 ZeroDivisionError

假设你在写一个方法,用于计算某在线指标在时间窗口内的平均值。

# metric_name: e.g. 'channelz.api_calls.count'
def recent_average_for_metric(metric_name: str, timespan: str = '1h'):
  metrics = MyMetrics.get_data(metric_name).withinLast(timespan)
  return sum(metrics)/len(metrics)

return 语句。如果当 metrics 为空时它抛出 ZeroDivisionError,调用方会很困惑,因为他们必须理解你函数内部实现才能看懂这个错误。

提示

用户和开发者都不应为理解错误而先理解你的实现细节。

所以,除非你的代码本来就是个计算器,否则“除以零”错误应被视作断言:它应该在测试阶段暴露,并提示你团队改进代码。做法是避免它,在真正相除前先做前置校验。

那么我们确实会加校验,但这个校验本身属于哪类场景?导致 len(metrics)==0 的原因,可能是表 3-3中的任意一种。

边缘情况 类别
metric_name 是否有效? 开发者参数无效
MyMetrics 提供方是否宕机? 系统
最近是否没有数据? 前置条件不满足
timespan 是否太短? 开发者参数无效
表 3-3 除零错误类别

正如我将在下一节“消息”里讨论的:这些情况对应的建议动作不同,因此代码里也需要可区分的检查。此外,你还需要在拥有必要上下文的时刻执行这些校验。

本节我们把诊断信息分为面向开发者与面向终端用户两类,也区分了哪些场景可在运行时处理、哪些只能在开发时处理。接下来我们在此基础上,来写出真正优秀的消息。

警告与错误消息

编写诊断消息需要把系统思维与用户思维结合起来。你要精确知道系统里发生了什么,同时站在用户视角,用他们理解的术语解释他们该知道的内容。否则就会出现 “underfull hbox (badness 10000)” 这样的警告。

用户看到诊断信息时,通常想知道两件事:

  • 到底发生了什么,导致了这个错误?请用产品本体里的术语来描述。这应帮助他们理解影响范围,并提供修复线索。
  • 他们能做什么(如果有的话)?可操作的诊断信息会直接帮助他们完成任务。

我们逐个解决这两个目标。先引入一个例子,后面几节都会围绕它展开。

案例介绍

Channelz 是一家虚构的 SaaS 公司,做的是类似 Slack、Microsoft Teams 或 Discord 的职场沟通工具。

Elise 在 API 团队工作,她的同事 Deng 是技术负责人。

在 Channelz 中,你可以给同事发私信,也可以发到“频道(channel)”,即围绕某个主题组织起来的员工群组;比如 API 工程团队可能有个频道 #team-api-eng。Elise 的用户句柄是 @elisek,Deng 的是 @deng

Channelz 正在构建一个 API,让机器人(bot)能够发消息,既可以直发给用户,也可以发到频道。客户希望用它发送各种通知。

在编码前,Elise 先草拟了一个开发者接口设计给 Deng 看。Channelz 的消息可以发给一组个人,也可以发到频道,用于提醒员工系统故障或任务完成。

他们交付给客户的 Python SDK 中,这个方法大概是这样的:

class ChannelzBot:
    def __init__(self, bot_handle: str):
        # ...

    # One of channel, users is required
    def send_message(
        message: str, channel: Optional[str], users: Optional[List[str]])

她还给 Deng 列了几种用法:

bot = ChannelzBot('@bippity_bot')
# Example: Send to a channel
bot.send_message(channel='#some-channel', message="Hello world!")
# Example: send separate messages to a list of people with an emoji
bot.send_message(users=['@drew', '@gabriel'], message="Hello :world:!")

Deng 看完设计后让她也列一下失败场景。Elise 现在只展示了“调用方已经知道怎么做时”的成功路径,但之前呢?如果把用户编码会话看成一段旅程,Elise 只给出了终点,就像有人问在线地图路线,她只回了一个目的地图钉。

Elise 想出了几个场景。(如何系统挖掘边缘情况我会在第 7 章讲;本章先跳过。)其中有一个关键场景,是本章重点:如果传入 API 的用户或频道无效怎么办?

提供上下文

可能会想:用户当然知道自己做了什么才触发错误,毕竟刚刚就是他们做的!但很多时候并非如此,所以你有责任重述上下文与细节。

回到例子,看看这段调用:

bot.send_message(users=['@dneg', '@elise'], message="Hello :world:!")

说明:为简洁起见,本节后续会省略 bot 这一部分,因为它不相关。

在单元测试里,user does not exist 这条消息通常勉强够用,人们大概率扫一眼测试代码就能看出 @deng 拼错了。

为了给出完整上下文,我们应当:

  • 回显相关数据
  • 给出详细原因
  • 提醒用户他们做了什么

下面逐条来看。

回显相关数据

真实世界场景比单元测试复杂得多。更常见的用户故事是:数据来自外部输入。

bot.send_message(users=input.users, message=input.message)

如果你草率地只说 user does not exist,开发者无法立刻知道你在说哪个人,也不知道句柄为什么错了。更好的写法是:user '@dneg' does not exist,他们很可能马上看出拼写错误,这就足够解决问题了。

提示

把错误数据回显给用户,除非该信息因隐私或安全原因必须脱敏。

Deng 在代码评审里指出了这一点,Elise 于是发布了更好的消息。

给出详细原因

API 团队的设计合作伙伴是客户公司 ChickenLittle。Deng 和 Elise 请他们反馈任何“摩擦点”。他们报告看到过这样一条错误:

Error: User '@buckcluck' does not exist.

他们非常困惑,因为 Buck Cluck 明明是 ChickenLittle 的员工,对吧?他们认为 Channelz API 存在 bug。

Elise 让他们去公司目录再核对一次,结果发现 @buckcluck 账号其实已停用。也许 Buck 刚离职。

Elise 想在未来避免用户困惑,以及(说得自私一点)避免由此产生的支持成本。她可以在 API 定义里把这一点说清楚:

def on_missing_user(channelz_user: str):
    if is_inactive_employee(
        Employees.get_employee_from_channelz_user(channelz_user)):
        message = f"User {channelz_user} has been deactivated."
    else:
        message = f"User {channelz_user} does not exist."
    raise RuntimeError(message)

def send_message(users: Optional[List[str]], ...):
    for user in users:
        if not channelz_user_exists(user):
            on_missing_user(user)
    # ...

如果客户看到的是:

Error: User '@buckcluck' has been deactivated.

他们就会获得一个非常关键的行动线索。

提醒用户他们做了什么

在单元测试里,通常很容易看出是哪个函数调用导致了问题。但在真实场景中,很多情况下错误报告与触发动作是分离的:

  • 错误可能只会作为日志里的一行出现,无法直接看出是哪个动作生成的。
  • 错误可能是长流程(比如在线下单)中延后出现的一步,用户需要被提醒自己当时下了什么单。
  • 你的产品(也许背后接了 AI)会按某种方式解释用户指令。报错前你应先回显你的解释。

在这些以及其他场景中,最佳实践是:不仅回显错误输入,还要回显当前操作在做什么。

回到 Channelz。再过一个月,Elise 收到了 ChickenLittle 可观测性团队的反馈。他们用 Channelz 消息 API 给员工发送“重要但不紧急”的生产问题告警。(非常关键的问题他们用 pager 服务。)

他们向 Channelz API 小队反馈说,自己告警服务里记录的一些错误让他们很困惑:

Error: User ‘@foxyloxy’ has been deactivated.

起初他们没意识到这条消息很重要。他们觉得大概不重要,因为 @foxyloxy 都已经离开了。

后来 他们发现,一些重要告警根本没人看到,某个问题持续了两天才被发现。

原因是:Foxy Loxy 虽然离职了,却仍在值班轮值中。他们通过把 Foxy 从值班轮值中移除解决了问题。(他们也采用了标准实践:配置后备用户用于升级通知,比如原消息投递失败时可升级到该用户。)

Elise 之前邀请他们“只要感到困惑就及时反馈”,所以他们也照做了。她吸收了这条反馈,并给错误消息补充上下文。

她把消息改写为,先回顾发生了什么:

Error: Cannot deliver a Channelz message to '[@foxyloxy, @buckcluck]' because '@foxyloxy' has been deactivated.

提示

告诉用户:他们刚才试图做什么。

如果方法再好一点、用户共情再多一点,Elise 原本一开始就可以写出这条错误消息,从而避免后续所有迭代。

更清晰的沟通还暴露了一个关键事实:只要 一个 用户不存在,系统就会让 所有 消息都发送失败。Deng 和 Elise 讨论这是不是“设计如此”,最终认为不是,这是个 bug;既然这些消息可能是重要告警,就应该尽可能多地发出去。

本节回顾:你为错误补充的上下文,通常应回答三个问题:在尝试什么操作?作用到谁或什么对象?为什么失败?这样就能补齐用户可能需要知道的信息。

让错误与警告消息可操作

遗憾的是,在很多情况下,知道“发生了什么”只完成了一半。用户往往还需要建议,或明确告知下一步该做什么。对于读取操作,以及越来越多 AI 场景,你甚至可以替用户直接纠错,就像 Google 的 “Showing results for: [correction]”,以及会自动修复代码或语言的编码/写作助手。

我们每个人都花过无数小时和错误消息斗争,琢磨该怎么办,常常经过大量调查后才发现修复其实很简单。

本节你会看到如何系统性提升诊断信息质量。要做到这一点,你需要与受众共情,并从前面做过的场景分类出发。

回到 Channelz 例子。假设你调用 API:

bot.send_message(message="The sky is falling!", channel="@barnyard-friends")

结果收到错误消息:

Cannot deliver a Channelz message to channel '@barnyard-friends': channel does not exist.

你能立刻看出哪里错了吗?可能要花点时间。如果你不熟悉 Channelz 术语,还可能意识不到频道前缀应是 #,不是 @

更好的做法是,给用户几个可选动作:

Cannot deliver a Channelz message to channel '@barnyard-friends': it is prefixed with @. Did you mean to pass it into 'users'? Or did you mean '#barnyard-friends'?

Channelz 甚至可以查询账户数据,检查 #barnyard-friends 是否存在且对该用户可见,并显示:

Channel @barnyard-friends is prefixed with @, but we found a channel, #barnyard-friends. Is that what you meant?

通过替用户删减一大块“搜索空间”,行动建议可以节省数小时,甚至防止他们放弃并流失。哪怕只是系统错误后一句简单的“请一分钟后重试”,也能提高流程完成率。

有时建议会比较复杂。你可能需要先给用户介绍一些概念(如 channel),或引导他们做一连串决策。合适时应链接到专门解释该错误的文档。比如如果这个场景更复杂,你可以写:

See https://channelz.io/docs/errors/invalid_channel to learn how to resolve this error.

在接口层抛出错误

如果写好错误消息很容易,工程师早就会更常这么做了。现实中不常做的主要原因之一,是这通常需要非常仔细的代码组织。

要写出最好的错误消息,你需要两类信息:系统里发生了什么?用户试图做什么?问题在于,真实系统里这两类信息往往分散在不同代码位置。

靠近 API 或 UI 边界的代码知道“用户是谁、正在做什么”。它最适合告诉用户下一步该做什么,且无需暴露用户看不懂的实现细节。

而问题本身通常发生在处理逻辑深处。

这在我们做错误分类时就出现过。做除法时,Python 的除法运算知道违反了哪条数学法则,但并不知道这次除法在业务里被怎么使用,因此无法给出高质量错误。

通常最好的抛错位置,是系统与用户的接口边界:在那里我们能把自底向上与自顶向下的知识合并。(另一种方案是把所有用户上下文一路向下传递,我会在后文讨论。)

一般来说,人们在接口层抛错有两种方式:

  • 主动抛错:在 API 边界做前置校验。
  • 错误拦截:捕获底层错误后,重新封装成更合适的形式。

下面回到 Channelz,分别看这两种方式。

前置校验

ChickenLittle 的可观测性团队又遇到了一个问题。

驱动他们值班告警的函数叫 alert_team

def alert_team(bot: ChannelzBot, team: Team, message: str):
    team_metadata = get_team_metadata(team)
    bot.send_message(users=[team_metadata['on_call_user']], message=message)

get_team_metadata 会读取 ChickenLittle 在 https://corp.chickenlittle.io/oncalls/ 这个界面里维护的值班轮值数据。

如果 team_metadata[on_call_user] 无效会怎样?回忆一下,send_message 会抛出这个错误:

Error: Cannot deliver a Channelz message to '[@gooseyloosey]' because '@gooseyloosey' has been deactivated.

这条消息是准确的,但当 Goosey Loosey 离职后,用户仍然不知道该如何修复。于是可观测性团队在前面加了一层校验,直接告诉用户下一步:

def alert_team(bot: ChannelzBot, team: Team, message: str):
    team_metadata = get_team_metadata(team)
    if not channelz_user_exists(team_metadata['on_call_user']):
        error_message =
            f"Cannot send alert '{message}' to team {team}'s on-call: " +
            f"On-call employee {team_metadata['on_call_user']} doesn't exist. " +
            f"Update the team's on-call rotation at {ON_CALL_BASE_URL}/{team}."
        raise ValueError(error_message)
    bot.send_message(users=[team_metadata['on_call_user']], message=message)

当用户不存在时,这个方法给出的消息是:

Cannot send a Channelz alert to team 'barnyard-friends'. On-call user '@gooseyloosey' doesn't exist. Update your on-call rotation at https://corp.chickenlittle.io/oncalls/barnyard-friends.

这个更上层的 API alert_teamsend_message 更完整地捕捉了调用者意图,这是产出优秀错误消息的强大基础。

说明

在 API 或应用代码的最外层抛错,这样你才能捕捉用户场景。

这里“前置校验”确实有效,但并不完美。你能看出问题吗?

我们再看另一种技术:重新封装依赖抛出的错误,看看是否更好。

重新封装错误

前置校验有两个问题。第一,它成本高:要多一次与 Channelz API 的往返。第二,也是本章更关心的:它会重复 Channelz API 内部已有的边缘情况检查逻辑,比如 @gooseyloosey 是否被停用。那类信息依然是可操作的,比如该员工只是改名导致 Channelz 句柄变化,这就需要另一种修复路径。

鉴于 Elise 过去给过很好的支持,可观测性团队再次向她反馈。他们更希望写成这样:

def alert_team(bot: ChannelzBot, team: Team, message: str):
    team_metadata = get_team_metadata(team)
    try:
        bot.send_message(users=team_metadata['on_call_users'], message=message)
    except ChannelzUserNotFoundError as error:
        error_message =
            f"Cannot send a Channelz alert to team {team}'s on-call: " +
            f"On-call employee {team_metadata['on_call_user']} doesn't exist. " +
            f"Update the team's on-call rotation at {ON_CALL_BASE_URL}/{team}."
        raise ValueError(error_message) from error

注意最后一行的 from error 子句。这是 Python 表达“链式异常(chained exceptions)”的方式,可保留内部错误。很多编程语言都有类似机制。输出会是:

ChannelzUserNotFoundError: User @looseygoosey's account has been deactivated.

The above exception was the direct cause of the following exception:

ValueError: Cannot send a Channelz alert to team ‘barnyard-friends’.
On-call employee ‘@looseygoosey' doesn't exist.
Update your on-call rotation at
https://corp.chickenlittle.io/on-calls/barnyard-friends.

这条消息更长,但包含了用户可能想知道的一切。

可观测性团队于是请求 Elise 在 Channelz SDK 里提供更具体的错误类型,以便他们这样做。

面对 ChickenLittle 这边员工频繁离职,Elise 只希望她最喜欢的设计合作伙伴那边别真“天塌了”。随后她与 Deng 开始研究如何让异常更具可编程性

既然我们已经深入到可编程性,这里就结束“可操作性”以及“在接口层抛诊断信息”的讨论,转向另一个重点:让错误对开发者及其用户都可操作。

抛出可编程错误

有时候,在你的错误与终端用户之间还隔着另一位开发者。这些开发者也遵循“在接口层处理错误”的同一原则,因此他们需要通过程序化方式拦截你的错误并采取不同动作。

而这一切只有在错误设计良好时才可能发生。要让运行时错误(系统错误、前置条件不满足、用户参数无效)对开发者可操作,你主要有三种技术:

  • 抛出具体错误
  • 对异常分组
  • 添加元数据

注意:断言和开发者参数无效这两类异常可以共用同一种异常类型,因为它们发生在开发阶段,不应围绕它们构建运行时自动化。

抛出具体错误

通用错误类型(例如 Python 的 ValueError)会让你的客户端无法定制自动化逻辑,也无法更好地告知其用户该做什么。

在任何运行时错误场景类别中构建异常时,你都应该抛出具体错误,就像上文 Channelz 新建 ChannelzUserNotFoundError 那样。

顺带说一句,我们刚强调要用更具体错误,可观测性团队却抛了 ValueError,你可能会觉得有些不一致。

    if not channelz_user_exists(team_metadata['on_call_user']):
        error_message = # ...
        raise ValueError(error_message)

可观测性团队虽然不是平台团队,但他们组件外层随时可能再套一层 UI 或中间件,因此最好养成习惯,定义具体运行时错误。比如可以叫 OncallNotFoundError

通过对处理器做几处小改动,Channelz 的 API 就比朴素实现更强大、可嵌套性也高得多。

提示

把运行时错误(系统错误、前置条件不满足、用户参数无效)设计成可嵌套形式,便于上层代码构建。

按场景类别分组错误

使用你 API 的开发者,可能希望写一个通用处理器来统一处理某类错误。比如遇到系统错误时统一给终端用户显示“出问题了,请稍后重试”;而遇到用户参数无效时,则把消息直接回显给用户并期待其修正。

在大多数语言里,你可以用继承层级把相近错误分组。我倾向于为每种场景类别单独建一个基类。

你也可以部分借助内建类型。以 Python 为例,任意参数无效错误可用 ValueError,系统错误可用 RuntimeError

但内建类型往往表达力不足。ValueError 无法区分“用户参数无效”和“开发者参数无效”,而两者后果完全不同。如果是程序员写错了,用户需要做的往往是联系支持,而不是自己修输入。所以我会为两者各建一个类,必要时继承 ValueError

在非面向对象环境中,你可以用错误码表达类别,再用子码标识具体失败。标准往往做得不够好,所以你可能要补自定义。例如 HTTP 协议把错误按 4xx 归为“客户端错误”。和 Python 的 ValueError 一样,它无法表达“是用户导致还是开发者导致”。不过对某些前置条件错误(如鉴权失败)仍然有有用的标准码。

为诊断保留信息

为了写出好错误消息,我们需要保留大量信息。这要求工程纪律与良好代码组织,因为只要链路上第一个开发者没把信息传下去,整条信息链就断了。下面给出三种信息链示例。

传递高层抽象

与其传递大量零散信息,不如传递把信息打包在一起的高层抽象。这样更容易产出丰富诊断。

例如,与其为了诊断而在各处传员工姓名、职位、句柄等字段,不如直接传一个包含所需信息的 Employee 对象。

像下面这种调用点会把链路断掉:

def foo(bot: ChannelzBot, employee: Employee):
    bot.send_message(users=[employee.channelz_handle])

这样一来,维护者若想改进 send_message 的诊断,必须先重构;很多时候他们就懒得做了。

而下面这种方式能把信息链保住,给实现者更大空间:

def foo(bot: ChannelzBot, employee: Employee):
    bot.send_message(employees=[employee])

当然,这会牺牲调用方灵活性:如果调用方拿不到 Employee 对象怎么办?我们真的该为了“诊断”限制接口访问吗?

这时场景分析可以帮你裁决。真的需要给非员工发消息吗?大概率不需要。代码库里是否有拿不到 Employee 对象的遗留路径?可能有。

如果是这样,可以额外提供一个接收原始用户句柄的遗留版本(legacy)send_message。这样手里有 Employee 对象的用户可以获得更好诊断,其他人也有过渡通道,直到完成重构。

保留更“厚”的信息链,意味着你只需很小额外成本就能写好诊断,或填充下一节要说的诊断元数据。

添加结构化元数据

还应在异常上提供元数据字段,这会显著提升其可编程性。不要逼别人从错误消息里解析数据!

永远假设调用方可能想自定义错误消息,所以你用于生成消息的数据也应同时放进元数据。比如可观测性团队可能想提取缺失的 Channelz 用户,那么 Channelz 就应在 ChannelzUserNotFoundError 上加一个 user 属性。

这样做能保证你的信息链不断。

持久化额外上下文

在编译器这类分阶段数据转换架构中,我们往往到了后面几阶段才识别出错误,因此保留原始上下文非常关键。

回到开头的 LaTeX 例子:用户输入了多余的 \\,但错误却在谈 underfull hboxes

词法分析器(lexer)是编译器第一步。它把 \\ 这样的输入记号转成抽象 token,供下一阶段语法分析器(parser)读取。parser 不需要空白符或行号,所以 lexer 往往会把它们剔除。

但它不该这么做。

我猜那条警告很可能是在编译器后续某个阶段抛出的。此时 lexer 已把源码读完并产出了 hbox token,原始源码上下文丢失了,于是任何“写出有用消息”的努力都会被掣肘。

这在旧式编译器里很常见。如今 lexer 通常会在输出中保留足够信息,使后续阶段能给出更现代的体验:

This is at the end of a block of text. \\
                                       ^^
Unnecessary \\ to end a paragraph on line 2.

上面“传递抽象、添加结构化元数据、持久化额外上下文”三点共享的核心教训是:

提示

让信息链足够健壮,把对诊断有用的信息留住。

当你把系统信息和用户场景信息尽可能聚合在代码中的同一点时,才能写出最好的错误。这正是“让错误可编程”的意义。

而可编程性,来自一套严格实践:持续维护信息链,把结构化信息打包进错误对象。

还有一个我尚未讨论的问题:时机。我们何时触发诊断信息,会对用户产生巨大影响

尽早诊断

尽早给出诊断,常被称为左移(shifting left),这对系统和用户都大有裨益。

提示

左移。尽可能早地把诊断信息给到用户。

对系统而言,左移通过尽早剪断无意义代码路径来减少资源消耗。例如,它能帮助抵御拒绝服务攻击。它也能保护相关代码不去处理不可预期输入,从而避免数据丢失等 bug。

用户获益更大。越早报错越省时间,想想在 IDE 里立刻报错和部署到生产后才报错的差异。并且,反馈越快,用户越容易记住自己刚做了什么,也越容易确信下一步动作。

以下是四种常见的左移技术:

  • 做静态校验
  • 做前置校验
  • 让用户先测试
  • 请求用户确认

工程师普遍会做前两项,但可惜后两项经常被忽略。我来这里就是想倡导这四项都要做。

做静态校验

校验任何输入时,静态校验通常成本低,不需要深度检查或网络调用;动态校验则需要更多成本或上下文。

提示

把低成本检查和高成本检查分开,低成本检查要尽早做、频繁做。

很多程序员熟悉静态类型检查和 lint 的乐趣,但这件事也同样适用于产品。

例如在应用表单收集用户输入时,要尽量用低成本方式尽早拦截最常见错误,并给出内联提示告诉用户该改哪里。邮编可以先校验是否符合所在国家长度;信用卡号、UPC 码、ISBN 码都带有校验位,可防止常见错误如手误或数字调位。这些技术既实现左移,也提高了诊断确定性。若号码校验位失败,我们就知道它不只是数据库里“没录入”,而是号码本身就错了。

前置校验

前面提到在应用或 API 表层构造错误时,前置校验是写出更可操作、更易理解错误消息的重要工具。它同时也是左移工具。

想象你去自助洗衣店洗衣,洗完才发现只有一台烘干机在运行,而且排队好几个小时。要是门口早点贴个告示,你就不会得到一堆又湿又发霉的衣服。

同理,转账场景里,必须先确认收款账户有效,再从付款账户扣款。

像洗衣、资金转移、以及洗钱这样的多步骤流程,通常都受益于前置校验。

让他们先测试

如果说有哪类用户场景最常被平台工程师忽略,那就是测试。他们只盯着客户在生产环境成功使用产品,却忽略客户在到达那一步前经历的反复试验。

如果你在构建开发者服务,只要提供一个 fake,客户会非常喜欢你,而且产品采用会更快。fake 是高保真的生产系统替身:尽量复用生产路径代码,同时对数据库、在线服务等脆弱运行时依赖做必要简化,以避免测试变慢和不稳定。

Channelz 应为其消息 API 构建一个 fake 服务。理想情况下它应:

  • 以内存模式或轻量本地进程运行,便于用户在测试环境与本地开发机上测试
  • 允许用户注入一批用户和频道,从而测试所有依赖该 API 的代码路径
  • 在尽可能多场景下与生产服务行为一致

真实世界例子是 Stripe(支付与计费 API/UI 提供商)提供的流行“测试模式”:你可以做逼真交易而不真正转钱。比如客户可传入特殊信用卡号,触发不同“用户参数无效”与“前置条件”错误。信用卡号 4000 0000 0000 9995 就会模拟 card_declined 错误及其 insufficient_funds 子码。

Stripe 用户在测试模式上花的时间远多于生产环境:他们用它写集成测试、测用户界面。比如商家想测试自己的信用卡输入表单时,可以手工填这些“魔法号码”观察网站行为。

fake 是把错误大幅左移的非常有效手段,甚至可以左移到“开发者还没完成认证或提交生产请求”之前。

请求用户确认

确认步骤是你在应用或平台里设置的“路障”,帮助用户确认自己是否做对了。它比警告更醒目,但又没有错误那样强干预。

当启发式算法发现用户输入有异常时,这类确认经常出现在 UI 或开发者工具里。它会解释为什么输入可能不符合预期或存在风险,并给用户机会去纠正或确认继续。

Google 的 “Did you mean” 就是好例子。用户搜索 “compture” 时,也许他真的是在找乐队 Compture。所以 Google 不能直接报错,但它可以提前告诉用户自己的怀疑,而不是让用户在翻了很多搜索结果后才自己发现。

或者在你刚让用户填完多页表单后,先展示一页提交摘要,让他们检查错误。

确认步骤让你在“无法在完整跑完系统前百分百确定用户出错”的场景中也能左移:你虽然不能完全确定,但可以给出高质量猜测。

一种常见的前置测试方法是“dry run + 确认”。在股票交易 App 里,这可以简单到:把计划购买股数乘以当前股价,先展示预计花费。

再看 Channelz 里的复杂 dry run。假设 Elise 想给 API 增加重试策略,使用方式如下:

ChannelzBot('@bippity_bot')
    .with_retry_policy(
	    backoff_coefficient=2.0,
	    initial_interval_seconds=10, max_interval_seconds=600)
    .send_message(...)

如果 ChickenLittle 配了一个极其激进的重试策略:每秒重试一次,且永不停止,会怎样?这可能对 Channelz 造成一次意外的 DoS(拒绝服务)式攻击,随后 ChickenLittle 的流量几乎必然会被限流。

# Too spammy!
.with_retry_policy(backoff_coefficient=1.0, initial_interval_seconds=1)

Elise 可以通过模拟重试策略并标记异常行为,给出启发式警告,把问题左移:

Your retry_policy (currently initial_interval_seconds=1, backoff_coefficient=1.0, max_attempts=Infinite) will result in 601 attempts in 600 seconds. This exceeds our limit of 50. You may ignore this by adding .ignore(Errors::RetryPolicySpamminess) to your policy.

Elise 对具体启发式并非百分百确定,但她可以先快速上线这类确认,及时帮到用户并暂时中断开发流程。如果启发式过严或过松,用户会反馈,她再上调或调整即可。

在开发者工具中,--force 标志通常可覆盖确认。

确认不是万无一失的,用户可能想都不想就确认,或直接复制粘贴 --force。因此,对那些你绝对不能接受的输入,不要只靠确认。但我认识的大多数开发者习惯二元判断:输入要么合法,要么不合法。若你愿意考虑中间地带,即输入大概率有问题,你会解锁界面表达力与交互丰富性的巨大空间。

本节我们讨论了左移的价值,也覆盖了许多能从左移中受益的用户场景。

由于左移有时意味着我们无法做完整、完美的校验,我介绍了 fake 服务、启发式确认、以及校验位这类静态校验等技术,来尽可能早地处理最常见的用户错误

本章小结

你开始写接下来的几个诊断信息时,请先把注意力转向用户体验:他们已经知道什么、还需要知道什么、应该做什么。如果你在写错误,把它先归入场景类别:系统错误、用户参数无效、前置条件、开发者参数无效,或断言。这样你更容易推导消息与元数据应如何构造。

在消息编写上:

  • 给出上下文。大多数情况下,回显正在执行的操作与传入的错误数据。
  • 使用产品面向用户本体中的概念。
  • 给出行动建议;如果不止一个方案,要愿意给出备选。

如果你能在接口边界抛错并展示给用户,你更可能把这些事做好。对于开发者,请让错误携带足够的上下文和结构化信息,以便其他开发者能完成他们的工作。把错误组织成层级结构,让开发者既能通用处理,也能精细处理,从而更好服务其用户。

最后,针对常见或关键场景,创造性地做左移,比如通过确认机制、静态检查和 fake 服务。

练习

  1. 搜索“Windows Blue Screen of Death Evolution”,观看 Windows 团队几十年改进错误消息的过程。 (a) 判断这些消息分别在服务哪些人物画像。 (b) 视频后段会看到 Windows 11 蓝屏,请按你选定的人物画像分析其消息。
  2. 接下来几题假设你在做一个电商 App。用户可以填购物车并结账。应用通过 submit_order API 完成下单,字段包括:用户 ID、购物车 ID、信用卡 CVV(用于验证信用卡的三位或四位安全码)。你的移动端客户端具备在上送数据到 API 前做静态校验的能力。 无效用户 ID 和无效购物车 ID 属于哪类错误场景?
  3. 无效 CVV 码属于哪类错误场景?
  4. 空 CVV 码又属于哪类?
  5. 请为“CVV 缺失”写一条面向终端用户、可操作的错误消息。
  6. 购物车 24 小时后过期并会被系统自动删除。用户尝试恢复会话并向已删除购物车加购时,抛出的错误属于哪类场景?
  7. 你能想到一种把“购物车过期”错误左移的方法,让用户不必重新找回每个商品吗?

参考答案

  1. 在写作本章时,Windows 11 的消息是:“Your device ran into a problem and needs to restart. We’re just collecting some error info, and then we’ll restart for you. For more information about this issue and possible fixes, visit https://www.windows.com/stopcode. If you call a support person, give them this info: Stop Code: SOME_ERROR_CODE.” 这条消息主要面向终端用户,并给出了清晰动作。它使用 URL 是个很强的技巧,因为该 URL 可以持续更新并提供细粒度指导。最后提到 stop code,明确暗示这部分与 IT 专业人士相关,说明这条消息有两个不同受众。不过它并没有特别努力去帮助这类专业人士,或许是因为作者假设这类人会自行上网检索,比如如何在本机查 crash dump。
  2. 无效用户 ID 与无效购物车 ID 属于“开发者参数无效”错误。但如果用户未登录导致用户 ID 为空,则是“前置条件”问题,用户可自行处理。
  3. 无效 CVV 码属于“用户参数无效”。
  4. 空 CVV 码表面上也属于“用户参数无效”。但设计良好的客户端应先静态检查空字段并提示用户补全,再去请求服务器。因此 API 也可以把它视为“开发者问题”,从而更明确地把“字段缺失”暴露为 bug(例如客户端忘了上传该字段)。
  5. 我会设想一个红色错误框高亮无效 CVV,并用提示文案解释:“请输入信用卡安全码。Visa、Mastercard、Discover 通常为卡背面三位数字;American Express 通常为卡正面四位数字。”
  6. 购物车过期属于“前置条件”错误,且用户可操作。因此,购物车过期实现应允许 API 区分“ID 无效(开发者问题)”与“购物车过期”。例如过期购物车可做软删除,而不是从数据库彻底移除。(这有时称为 tombstoning。)
  7. 你可以在购物车过期前通过邮件或推送提醒用户尽快完成订单。

4 亲自体验你的产品

新系统的设计者不仅应当是实现者和第一位大规模用户;设计者还应写出第一版用户手册……如果当时我没有完整参与这些活动,字面意义上几百处改进都不会发生,因为我根本想不到它们,也意识不到它们为何重要。

— Donald Knuth

我们很多人都很难找到并调动 beta 用户。

假设你正准备发布产品的新版本,希望先让用户试用。可你的产品规模还不够大,也还没有一群随叫随到、愿意在短时间内体验预发布版本的忠实 beta 用户。多数用户在尝试你的新版本前,还有别的优先事项。即便确实有人试用,也未必会触达设计中风险最高的部分。截止日期近在眼前,于是你很容易动念:在没有用户验证的情况下直接发布。

但其实,确实有一批更容易调动的“用户”可用:你自己、你的队友,以及公司里的同事。这些人和你共享产品成功带来的收益,天然就有动力帮你。

提示

让自己成为第一位客户。这样能更早、更便宜、几乎无害地验证产品。

当团队成员使用自己做的产品时,这就叫内部试用(dogfooding)。这个词在 20 世纪 90 年代的微软被广泛传播,当时微软形成了使用其操作系统和编译器最新版本的文化。它来自短语“eating your own dogfood(吃自己的狗粮)”,用来表达团队为了测试自家产品愿意做到什么程度。

当你或同事本身就在目标人物画像之内,并且能模拟真实世界使用条件时,内部试用尤其理想。但即使条件不完美,只要团队足够了解用户,并能“换鞋思考(shoe-shifting)”去扮演真实用户,内部试用仍然会非常有效。

想办法去做。如果你是农业科技公司,可以买一块自己的农田来测试产品;如果这不现实,也可以在温室、实验室或计算机里模拟这些条件;或者你也可以聘请真正的农民,让他们拿出一部分土地供你实验。

内部试用有几个关键优势:

  • 它可以在产品生命周期更早阶段发生,甚至在版本尚未准备好给非员工使用之前。
  • 自测或和同事沟通,成本通常低于和终端用户沟通。
  • 它不会给用户带来负面后果,也不会侵蚀他们的信任。

内部试用的形式比你想象得更多。本章会覆盖四种方法:

  • 编写场景测试来演练产品,模拟真实世界使用。
  • 通过编写使用指南,把“使用你的产品到底是什么体验”铺展开来。只要足够细致,它能提前预警问题。
  • 自己写或让队友写“摩擦日志(friction logs)”,详细记录内部试用体验。这既能促进大家持续试用,也能产出更有价值的反馈。
  • 为合适的产品创建示例(samples)并测试它们。

文档与自动化测试在传统上不被视为内部试用,但如果做得好,它们都包含大量“扮演用户”的过程,并会对产品施加有价值的压力。

请有创造性地组合这些手段,以低成本在每一步都验证你的产品。这样你能降低风险,并确保不会浪费用户的时间与信任。

将测试用于内部试用

当你进入写代码阶段后,写测试是“亲自体验你的产品”的最佳方式之一。Kent Beck 说过:“测试驱动开发(TDD)旨在消除应用开发中的恐惧。”要降低这种恐惧,最好的办法就是编写与生产环境高保真的测试。

在 TDD 中,测试写在实现之前。但依据是什么?它们不可能从实现推导出来,因为实现还不存在!如果实现已经存在,就会引入实现偏见(implementation bias),变成在断言“代码现在做了什么”,而不是“它本该做什么”。

因此,主要且可靠的灵感来源是用户场景。不要先写代码,再按你已经写出的行为去补测试;应当在被实现细节“带偏”之前先写测试。这样更容易贴近用户场景。

本节我想展示如何把测试当作内部试用机会。通过精心挑选的自动化测试,我们可以做到:

  • 验证关键场景下的正确性。
  • 打磨边界情况。
  • 记录预期用法。
  • 逼自己想清楚到底在构建什么。
  • 构建可长期复用、可反复执行的资产。

但在此之前,我们先快速盘点各种测试类型,重点看哪些最值得投入。

你应该写哪些测试?

为便于统一语境,我先定义几类测试。它们融合了“系统导向”与“产品导向”。系统导向测试用于降低系统风险,即应用有 bug、不可靠或不可扩展的概率;产品导向测试用于降低产品风险,即用户无法完成任务的概率。

你的目标应是:让测试集高效发现系统与产品两类问题,因此必须搭配多种测试。以我经验看,很多软件团队对产品导向测试投入不足,而工程师也未必意识到可用测试类型的广度。

先定义几个基础术语:

  • Mocks:非功能性的占位替身,用来验证依赖是否按特定方式被调用。
  • Fakes:复杂功能的简化替代实现。例如,SQLite 是基于本地文件系统的 SQL 实现,可以作为真实 SQL 数据库的 fake。

下面这份清单按它们对代码库施加“产品压力(product pressure)”的强弱排序。

说明

所谓产品压力,是指根据用户个人与群体最可能采取的动作序列而进行的验证。

  • 单元测试(Unit tests):测试小块代码,如单个函数或类。通常在功能开发早期编写,用来探查边界情况并编码契约。它们应运行很快,服务于最内层开发循环。为聚焦测试目标,常会使用 mocks。但它们并不特别擅长发现重要的系统和产品问题。
  • 集成测试(Integration tests):测试多个组件,承认“组件交互”往往最脆弱。它们会减少 mock 依赖,更好地嗅探系统问题;多数情况下运行真实生产代码,必要时调用 fakes。相较单元测试,它更可能发现产品问题,但仍不如下面几类测试那样强调产品场景。
  • 功能测试(Functional tests):聚焦单个功能的产品行为。可用 fakes 模拟依赖,如其他服务与数据库。若没有 fake,有些测试会用 mocks,但这是种“坏味道(bad smell)”,会让测试更难被信任。
  • 场景测试(Scenario tests):类似功能测试,但它验证的是常见用户场景而非具体功能。它通常会覆盖多个功能,或用最常见输入集合来验证某个功能,确保完整用户任务可完成。
  • 负载模拟(Load simulations):用大量合成请求给产品加压。理想情况下,这些请求应模拟真实世界条件,我会在第 9 章详细讨论。
  • 端到端测试(End-to-end, e2e):面向用户行为最全面的自动化测试,也能发现意料之外的系统集成问题。本质上它是“什么都不桩替”的场景测试,通常包含用户界面。
  • 用户验收测试(User acceptance testing):让真实用户手动试跑你的产品并反馈问题。稍后介绍摩擦日志时我会进一步讨论。

图 4-1粗略展示了各类测试的相对优劣。

测试类型分布图
图 4-1 按可发现的系统风险与用户风险绘制的测试类型
选择测试类型

在制定测试策略时的典型目标如下:

  • 最小化最关键、足以导致产品不可用的系统与产品风险
  • 确保在发现(Discovery)阶段得到的关键场景已被测试覆盖
  • 通过自动化减少重复的人肉验收测试
  • 在编写成本给定的前提下,提升测试价值
  • 让测试具备高信噪比,失败时大概率值得关注
  • 通过功能测试覆盖关键边界情况

系统风险高的功能应包含负载模拟、集成测试等;产品风险高的功能应更多投入功能测试、场景测试和用户验收测试。端到端测试两类风险都适用。要想清楚:你产品里最“新”的部分是什么,也就是最可能出故障的部分。比如你在替换全新基础设施组件,语义却沿用多年不变,那么你可能更该关注系统测试,并更多依赖既有测试覆盖产品行为。反过来,如果你在成熟科技公司里用稳健基础设施和最佳实践(“paved roads”)构建新产品,就可以更偏重产品风险。两类风险都重要,大多数测试都应包含两者要素。但因为这是一本讲产品思维的书,本章重点放在场景测试、功能测试、e2e 测试和用户验收测试。负载模拟我会在第 9 章再谈。

只测用户在乎的东西

前面我们谈到要覆盖系统与产品风险。但时间预算有限时,如何优先级排序最关键的测试,以及这些测试里最关键的断言?

总目标应该是:测试断言必须直接对应用户在乎的行为。

下面是一些应尽量避免的经典测试反例:

  • 过度依赖实现细节与 mocked 依赖的单元测试,常随代码演进而频繁维护。我甚至会在功能测试补齐后,删掉一些实现阶段写的单元测试。
  • 过度挑剔、断言过细的 e2e 测试,会随着产品表面变化而频繁失效。
  • 提供不真实流量模式的负载模拟,会把你引向永远用不到的扩容和加固方向。

为了演示如何选测,我用流媒体服务 Netflix 举例。用户通过浏览器访问 Netflix。假设我们在 Web 团队,主要职责是让首页展示可观看内容。页面是可点击播放的节目网格。每一行有不同主题,如“Critically Acclaimed TV Shows”和“My List”;继续下滚会出现更多行,如“Classic Science Fiction and Fantasy”,或算法认为用户会感兴趣的内容。

场景测试

场景测试捕捉真实世界的产品使用方式。由于它直接映射“用户会做什么”,因此非常容易抓住关键 bug。实际上,我们可以把发现(Discovery)阶段整理出的场景直接翻译成场景测试,具体过程见第 7 章

场景测试促使我们跳出单一功能,从用户视角思考产品。它无法独自覆盖完整测试矩阵,但如果选得好,能确保即便带着 bug 发布,也多为相对轻微的问题。

这类测试通常会组合至少两个常一起出现的元素。例如:

  • 在同一用户流程里组合两个或更多经常共同出现的功能。组件交互往往是软件最不稳定的部分,场景测试正好覆盖它。
  • 用最常见数据类型、按贴近真实世界的规模来运行功能。

场景测试应尽可能端到端,其余部分使用高保真 fake。若我们在后端团队,不跑浏览器往往更方便或更经济,可以改用模拟 DOM 环境。(DOM 是网页结构与内容对象的数据表示。)另外,测试里直接使用 Netflix 的真实分布式数据库成本会高得离谱。图 4-2展示了一个 Netflix 后端场景测试系统图,左右两侧都用了 fake。

典型后端场景测试配置。
图 4-2 典型后端场景测试配置

下面是基于该配置的后端场景测试示例:一个已登录用户想看电影,访问 netflix.com,点击某个电影缩略图,加载电影并开始播放。

伪代码如下:

# A logged in user wants to watch a movie, so they load the home page
def watch_movie_from_homepage():
  set_up_fake_movies() # Don't want to download and play movies in our tests.

  # User sees the home page.  It should have thumbnails of movies.
  home_page_source  = get_source("netflix.com")
  home_page_dom = get_dom(home_page_source)
  movie_listing = find_one("movie_thumbnail", home_page_dom)
  assert(movie_listing)

  # User clicks one of the movies.
  watch_movie_page_request = movie_listing.click_target()
  watch_movie_source = get_source(watch_movie_page_request)
  watch_movie_dom = get_dom(watch_movie_source)

  # Movie page loads successfully and starts playing automatically
  assert_autoplay_on(watch_movie_source)

注意这个测试如何把每一步动作串接到下一步。我们并没有硬编码 watch_movie_page_request,而是从 HTML 和 JavaScript 里提取。它无法发现所有 UI 层问题(比如控件在屏幕上被遮住),但能确保首页返回的电影页链接是正确的。

测试还通过注释与命名讲述了用户场景故事。可自解释的场景测试尤其重要,因为它本质上是“被固化下来的产品需求”。当新成员加入团队时,场景测试应该是你最先给他看的内容之一。

再给几个测试思路:

  • 下滚触发第二页:渲染首页后,模拟用户下滚,系统加载更多节目行。断言初始页渲染时包含可拉取更多数据的 URL;触发该 URL 对应动作,确认新内容被加载。
  • 用户体验:创建全新的 Netflix 测试账号。因为系统尚不了解该用户,首页应展示最热门内容。节目数据库预填一些合成数据,并指定其中一组最热门,确认热门节目出现。
  • 用户想续播正在观看的节目:渲染节目库,确认页面某处出现“从上次进度继续播放”的元素。

功能测试

并非所有内容都要写成场景测试。功能测试用于在更大测试矩阵内,针对单个功能进行更细致的多用例验证,尤其是边界情况。

继续 Netflix 例子,我们可以写这些功能测试:

  • 渲染主题行,如“My List”“Critically Acclaimed TV Shows”
  • 边界情况,如用户列表为空时渲染“My List”
  • 点击电影条目中的不同元素(播放、点赞、更多信息),并确认渲染出对应页面

端到端测试

端到端测试与场景测试类似,但对系统与产品行为覆盖更全面。应把它配置在最关键的核心场景上。

继续 Netflix 的例子,这类测试可使用“无头浏览器(headless browser)”来模拟真实点击,而不只验证底层功能。它也可能运行在预发布或 QA 环境的数据库上,而非 fake 数据库,如图 4-3所示。

典型 e2e 测试配置。
图 4-3 典型 e2e 测试配置

由于它跑在真实线上节目数据上,测试必须对内容变化具备韧性。以“新用户体验”场景为例,作为 e2e 测试时,我们不该硬编码“最热门电影”,而应在测试中额外查询当前最热门节目。如果是 Squid Game 最新一季,就应期望它出现在首页。

这类测试能在很多位置发现场景测试可能漏掉的问题:也许填充热门电影数据库的夜间任务没跑;也许新用户建号流程即将回归;也许某个 UI 元素被无意间隐藏了。

它还可能发现延迟回归。单次运行速度波动可能太大,难做稳定断言,但你可以对每条 e2e 流程做多次运行并看平均速度,判断是否退化。

团队通常不会写太多 e2e 测试,但它占据关键位置。即便运行频率低于其他测试,只要它能阻挡关键发布就有价值。

如果你的断言和假设不贴近用户价值,端到端测试会严厉惩罚你。你硬编码的短 sleep 在繁忙测试环境里会超时,应改为回调或轮询等待。你测试页面上的“第三个元素”会因设计师重排而改变,应给 UI 元素加持久语义标签,以便自动化检索。例如在 Netflix 页面上,我们应能通过 play_videomore_info 标签定位按钮。

用户验收测试

用户验收测试中,你会让部分员工或志愿者在预发布环境里跑给定场景,并要求这些测试通过后才能发布。常见场景包括:

  • 国际化与本地化:不同语言、不同地区的使用者在多种条件下验证产品行为,这些组合复杂到工程师难以自行全面覆盖。
  • 合规标准要求的人工测试。
  • 对大型语言模型(LLM)输出进行人工验证。尤其是红队测试(red teaming),即用户以对抗思维尝试诱导 AI 执行其未被设计去执行的行为。

依赖真人测试会慢、也贵,但既然你在追求验证,就应在自动化尚未准备好或不可行时,愿意并能够搭建这类流程。

更多时候,我看到人们以其他方式挖掘用户反馈,我会在第 5 章展开。

总体而言,自动化测试是“双重利器(dual threat)”:前期它能提供关键内部试用价值;后期它能长期保留,并在产品演进中持续产出价值。

文档也是同样意义上的双重利器。

文档驱动开发

你听过“教是最好的学”这句话吗?对“教别人使用产品”这件事同样成立:写文档是在教别人,也是在学习你的产品并发现改进点的绝佳方式。

工程师经常要为技术产品编写或评审文档,我非常建议深度参与这件事。若你不擅长写作,可以和同事以及/或 AI 助手协作:你提供技术内容与一线洞察,发挥你在构建产品、理解用户过程里积累的编辑判断。

但从内部试用角度看,在产品周期更早阶段先勾勒文档,能避免后期代价高昂的错误。

你会更早发现问题并改进产品。上一节讲的是测试驱动开发(TDD);这一节要讲的是我认为几乎同样有用的方法:文档驱动开发(Documentation-Driven Development)。文档草稿常常比测试更早、更便宜就能写出,甚至在代码尚未诞生前。

在讲“把文档用于内部试用”之前,我先给一些通用建议:如何组织文档,以便在正确时机回答正确问题。

本节将覆盖四个主题:

  • 文档能力边界的注意事项
  • 面向不同用户场景构建不同文档:发现、理解、使用
  • 帮助用户找到文档,并在不同场景间顺畅切换
  • 将写文档本身作为内部试用练习

文档不应成为承重结构

用户通常并不想读你的文档。用户花在找文档和读文档上的时间,本可直接用于完成任务。即便会查看内容,他们也往往是选择性阅读,而非从头读到尾。

结合第 2 章,当你想依赖文档来强化产品发现地图时要谨慎。别忘了用户知识有三层:已知、已知未知、未知未知。

“已知未知”通常是这类特性与行为:用户基于使用相似产品的经验,知道自己应该期待什么,即便他们还没在你的产品里亲自遇到。这里列出一些常见“未知未知”:

  • 出乎意料的危险行为
  • 新功能
  • 让你区别于竞争对手的非标准功能

应使用意符(signifier,见第 2 章,如通知和弹窗,提示用户新功能与差异化的非标准功能。为了防止有害行为,应限制可供性(affordance,见第 8 章),并提供确认机制(见第 3 章)。

有了这个前提后,我们仍要承认:对更复杂的产品,文档既不可避免,也确实有用。即便用户不直接阅读,AI 助手也会读取你写下的一切,并转述给用户。

文档读者的场景

我合作过的许多软件工程师(包括过去的我)要么不写文档,要么一写就是参考文档。我们很自然会按功能逐项解释“我们做了什么”。

但让我们换鞋思考:你到底多久才会用一次参考指南?在这些场合里,你又有多少次是在尝试其他文档类型无果后,才退回参考指南?下面我拆开讲。

第 2 章中,我介绍过用户旅程:用户会经历发现、理解、使用三个阶段。这三类活动都可能需要文档支持。

  • 发现(Discovery):用户(常常是新用户)知道自己的目标,但不知道该用哪个产品或哪个功能来实现。
  • 理解(Understanding):用户在某处卡住,需要加深对产品的理解,学习可复用概念以提高效率。即便还不是,他们也很可能正走向高黏性用户。
  • 使用(Usage):用户知道该用哪个产品或功能,但不知道如何正确使用。可能是新用户,也可能是正在挑战高级功能的老用户。

下面这个例子顺便也让我对自己早年学 C++ 的痛苦释怀一点。

我大学第一门编程课学的是 C++。教材是 Bjarne Stroustrup 的 The C++ Programming Language。在第 3 版中,这本书是对语言的全景式总览。可那时我只是勉强自保:想找到做算法作业的合适工具、学会基础概念。结果我得到的是作者一章接一章炫示 C++ 各种高深又偏门特性的“高级用法文档”。

这也许是本好书。若我是有多年 C++ 经验的资深工程师,或许收获更大;但它在当时对我并不合用。我真希望教授选一本更符合人物画像的教材。本书一位审稿人也提到:他曾因“目标不准的学习材料”退出计算机专业,后来又重返该领域,成为资深首席工程师和专业计算机教师。像 C++ 这样复杂的产品,迫切需要循序渐进的引导。

当时我需要的是入门指南,而那本书却是参考指南。也就是说,在我需要“发现与理解”支持时,它却把重点放在“使用”场景。

这些场景各自需要不同文档类型,如表 4-1所示,并且这种差异从用户旅程起点就存在。

文档类型 场景
人物画像菜单(Persona menu) 发现
场景菜单(Scenario menu) 发现
入门指南(Getting started guide) 理解
概念指南(Conceptual guide) 理解
使用指南(Usage guide) 使用
运维指南(Operational guide) 使用
参考指南(Reference guide) 使用
表 4-1 文档类别

这一段我用 Docker 作为例子。Docker 是一个流行的平台,用于开发、交付和运行应用。它最著名的抽象是“容器(container)”:一种把依赖打包在一起的可执行程序,可在多种计算环境中分发与运行。

发现

用户在发现你的产品时,他们要先把自己的场景映射到产品上。

首先,他们想知道自己是否属于目标人物画像。你的产品或功能“是不是给我用的”?这能给他们一个预期:当他们继续深入细节时,你的设计选择是否适配他们。

对全新用户而言,最常见且有效的方法之一,是在应用或网站上提供人物画像菜单(persona menu)。例如今天我访问打车网站 lyft.com,会看到“Rider”“Driver”“Business”等入口。它明确告诉我:如果我属于这些画像之一,我就在其服务对象之列,并可点击了解详情。

提示

明确你要服务的人物画像,并为每类画像定制文档。

当人物画像匹配成立后,潜在用户接着会看产品是否覆盖他们的场景。

Docker 当前网站没有人物画像菜单,但它有场景菜单(scenario menu)。它列了三个场景:

  • 快速且一致地交付应用
  • 响应迅速的部署与扩缩容
  • 在同一硬件上运行更多工作负载

用户据此会更清楚 Docker 是否能解决自己的问题。

注意这些标题里都没有“container”这个词,尽管它是 Docker 最知名概念。这是因为不熟悉容器的用户通常不会搜“container”,但会搜上面这些场景描述里的词。

提示

在用户所在之处与其相遇;区分“用户带着什么认知而来”与“用户接下来必须学会什么”。

除了顶层产品发现外,用户在实际使用中不断发掘功能,还会发生大量“小发现”。你需要像第 2 章里画的产品发现地图那样,引导他们在不同文档之间跳转,或从产品界面跳到文档。

理解

入门指南(Getting started guides)把用户推进到“可以开始探索并使用产品”的状态。也就是说,它帮助用户从发现阶段过渡到理解与使用阶段,同时通过“边做边学”进行教学。

我在第 1 章提到过,用户希望尽量少分配脑力给“学习你的产品”。与记忆概念相比,按入门指南一步步操作更容易。

但对复杂产品而言,你可能还需要概念指南(conceptual guide),用于讲解产品本体(ontology)——这个词在第 2 章中被定义为“结构化的产品词汇体系”。

提示

用概念指南讲清那些会在产品中反复复用的原语或概念。

当用户需要后退一步、真正把东西学明白时,概念指南就很关键。有一次我想把一个程序打包进 Docker 容器,反复受阻,Docker 的使用指南也帮不上忙。我意识到自己需要先更好理解 Docker,虽然当时说不清到底缺了哪块知识。于是我去读了讲 Docker 核心原语的概念指南。我学到一个对我场景极有价值的点:Docker 容器可以彼此嵌套(nest)。问题瞬间变简单了。

和大多数读者一样,我当时也是个“被迫学习者”,总希望只靠使用指南混过去,尽快把任务做完走人。

和所有文档一样,你需要讲得出“用户会怎样发现概念指南”这条故事线。应在其他文档类型里大量链接到它。也许 Docker 若能在使用指南里直接指向“容器嵌套”这个概念,我会省下大量时间,不必完整读完一整篇指南。

一个关键发现技巧是:为核心概念提供同义词,以优化搜索命中,避免用户带着另一套本体进来却搜不到内容。

使用

当文档读者开始执行具体任务时,他们就成了真正的用户。对 Docker 来说,我理解了容器嵌套后,下一步就需要知道如何创建它们。

使用类文档常见有三种:使用指南、运维指南、参考指南,分别满足略有差异的需求。

使用指南(usage guides),或操作指南(how-to guides),与入门指南很像,都是给出逐步流程来完成任务;不同之处在于,它不只是“介绍产品”,而是帮助用户达成一个具体目标。若你的产品需要文档,应为最关键场景(如各项目的北极星场景)都写使用指南。即便产品不需要对外文档,你也可以先勾勒这些指南,用作有目的的内部试用。

用户会如何找到这些文档?取决于他们所处场景:

  • 如果他们知道要做什么,但不知道该用什么功能,文档应按任务命名,或至少可按该任务关键词搜索到。Docker 网站有“Configure CI/CD”“Deploy to Kubernetes”等指南。
  • 如果他们已经知道需要哪个功能,就会按功能名查找。Docker 里有“Building Compose objects”“Containerize your app”等直接写出功能名的指南。
  • 他们也可能从产品本身发现目标文档,例如由错误消息跳转,或从其他相关任务/概念文档跳转而来。

确保你从多个角度提供指向使用指南的“可发现入口”。Docker 有个有趣的文档标签系统,用户可点击如“DevOps”“Deployment”等标签找到匹配指南。

*运维指南(operator’s guides)*是使用指南的一种特殊类型,常被称为故障排查指南(troubleshooting guides)或 runbooks。阅读这类文档的用户通常是第一次遇到该故障模式,处在陌生地带。

因为它们用于“出事时刻”,就必须能从错误消息里被发现,这在第 3 章讨论过。应让它们支持深链(例如 docs.docker.com/dhi/troubleshoot/#no-shell, …​/#no-package-manager)到具体问题,方便支持人员、错误消息和自动化机器人响应直接链接到精确修复方案。同时务必保持这些链接稳定且最新。

最后一种使用文档是参考指南(reference guide)。它通常面向单个功能,在细节深度上覆盖更全面。

就像场景测试不会像功能测试那样穷尽每个功能细节一样,使用指南也不会覆盖所有细节。使用指南可以外链到参考指南,由后者按功能或概念逐项展开,讨论细节、边角条件和高级用法。

事实上,这些文档类型和测试类型是对应的:

  • 场景测试或 e2e 测试,可能覆盖与使用指南或运维指南条目相同的地带。
  • 某个功能的一组功能测试,通常会覆盖该功能参考指南提到的全部行为。

回头对照你的场景测试,看看是否缺少对应使用指南,反之亦然,这个检查很值得做。

文档之网

学习应发生在它被需要、且学习者有兴趣的时候。

— Don Norman,The Design of Everyday Things 作者

用户不会总是沿着“发现→理解→使用”直线前进。相反,他们在工作过程中会在这些场景间顺滑切换。选定产品后,发现会变成理解;使用中若冒出新问题,使用又会变回发现。问题一出现,用户立刻被抛进运维指南;一旦不知道下一步怎么做,他们就会从使用切回理解。

因此,文档之间以及产品到文档之间都应高度互链。构建“面包屑路径(trails of breadcrumbs)”,帮助用户从使用或排障场景升级到学习模式,并从眼前问题走向解决方案。使用指南提到关键概念时应链接到概念指南;概念指南应链接到示例说明。以此类推

提示

构建一张相互链接的文档网络。

将写文档作为内部试用

设想你在写一个开源开发者工具,并正在编写“如何安装软件包”的入门指南。

为了换鞋思考,你搭了一个全新环境,从零在机器上安装,让你的环境尽可能贴近新用户。

结果你不断遇到错误,开始意识到过去几周开发过程中你在本机悄悄设置了多少工具和环境变量。回头看指南,它已经变得很长!

你不满意,于是决定发布一个把依赖全部打包进去的容器,让用户不用逐项安装。随后你删除了指南里那堆烦人的步骤,换成更短的容器运行说明。

提示

评估功能界面质量的一个办法:看它的使用指南能写多短。

再设想你是某 SaaS 服务值班工程师,刚刚支持了几位内存耗尽、艰难排障的用户。你在写 runbook 条目告诉他们该怎么做时,突然意识到:如果一开始允许用户自行设置某些资源限制,这个问题本可完全避免。你先发布 runbook 条目,但随后立即着手通过新增配置从根本上消除对它的需求。

写文档的过程会强迫我们换鞋思考,去走用户旅程。写着写着若感觉又繁琐又难解释,我们就会不耐烦。

如果你写得足够用心,就能把这份“痛感”转化为产品改进动力。关键窍门是:在仍然便宜可改的时候尽早做。下面是把文档写作融入开发流程的几个例子:

  • 在开始设计前先写人物画像菜单和场景菜单。亚马逊著名做法是在项目开头先写“模拟新闻稿”,预测产品上线后如何对外沟通。它像正式新闻稿一样向用户解释价值主张。采用这种“营销先行”实践,有助于你理解目标画像并评估产品价值。
  • 在功能早期版本可用时尽早写使用指南。记录不必要步骤和棘手决策,在“修起来还便宜”时捕捉可用性问题。
  • 写概念指南,帮助团队就关键抽象的精确描述达成一致。若写起来非常挣扎,考虑重命名或重构表述;若某概念看起来并非用户真正关心,也许你需要提供更高层抽象。
  • 强迫自己向用户解释“你这个功能允许发生的问题该如何排查”。很多时候,修复安全性问题的成本,甚至低于你去写文档、做沟通并持续支持踩坑客户的成本。

另一个高效用法是:先想象理想的产品体验。在尚未写任何代码前,就把未来用户体验写出来并分享给队友。随后你的项目应实现所有必要功能,让文档描述变成真实。

这种文档驱动开发可以非常早、非常深地融入你的开发流程。

总体来说,对某些产品,文档既服务人类也服务 AI,在发现、理解、使用场景都提供帮助。即便产品不对外发布文档,写“模拟文档”也会让你聚焦同样的场景,并让内部试用更有参与感。

测试和文档都很重要,但我们也需要外部视角。外部用户时间有限,因此我们需要一个轻量方法让他们帮忙。答案就是摩擦日志。

摩擦日志

我们已经写了场景测试、写了指南,也常在使用产品早期版本时校对其准确性。但即便最优秀的产品思考者,也会经常被“其他人类到底会怎么用”惊到。是时候引入用户了,不过先从更宽容的一群开始。

在内部试用中,我们用的是公司自己构建的软件,而写*摩擦日志(friction logging)*是非常好的做法。

摩擦日志是在你使用产品时写下的非正式文档,记录你经历的困惑和阻碍。完成后交给负责该产品的工程师或 PM。

摩擦日志会把你的内部试用精力导向“有价值”的使用。知道最后要提交一份短文档,也会激励你坚持体验下去。

在接下来的两个小节,我会先讲如何写摩擦日志,再讲如何让其他人也为你的产品记录摩擦。

我在 Stripe 工作时学到摩擦日志,它是那里的文化之一。尤其是当时的 CTO David Singleton,每年会花几周时间体验产品与内部基础设施的不同角落,并写摩擦日志。

有一周他内部试用了我在做的内部 Workflow Engine,那对我和团队都非常振奋。他甚至“卧底”了一把:通过其他工程师在我们的支持频道提问,以便在大家不知道提问者是 CTO 的情况下观察支持体验,避免偏见。他的摩擦日志积极且建设性很强。

David 改进了产品、鼓舞了我们,也帮助工程团队维持了友善、建设性的摩擦日志文化,同时让他自己持续掌握产品与基础设施现状。

写摩擦日志

摩擦日志是以“摩擦”为重点的场景日志。你的目标很简单:报告你对产品的真实体验,产品团队会按他们的判断去使用这些信息。

可用场景来组织信息流:

  • 先介绍你代表的人物画像:你是新用户吗?你有哪些既有经验可能影响判断?
  • 说明你使用产品的意图。
  • 文档主体写出你体验里最关键的部分。

下面是一份关于在社交应用 Instagram 上编辑并发布视频的摩擦日志示例:

摩擦日志示例

我是 Drewbie(Drew + Newbie),Instagram 新用户,但之前用过 TikTok。我想上传一个视频,但要把声音关掉。

我在 iPhone 上开始发帖。因为我还没给 Instagram 授权“整个照片与视频库”,我先进入“管理隐私”,挑了一个视频授予权限;然后又选了一次这个视频把它用于发帖。我知道一些新应用已经把这个流程做得更顺滑了,所以会疑惑 Instagram 为什么没有。

我看到一排图标,其中有个麦克风。我点了麦克风,希望它能静音视频,结果弹出了一个带播放按钮和“tap to add audio”按钮的“scrubber(拖动条)”。这和我预期不一致。

我没看到明显有帮助的图标,但在摸索时无意向右滑,才发现屏幕外还有更多图标可选。然后我看到一个“loudspeaker(扬声器)”图标,可以静音。这部分就非常直接;我还注意到一个很酷的“voice boost”功能,我觉得以后会用上。

注意我是在描述体验,而不是给规定式建议。Instagram 团队很可能比我更清楚什么可行、什么更合适。比如他们若想处理“扬声器图标难发现”问题,可以把图标排成两行,或让一个图标半露出屏幕边缘,暗示“这里可横向滚动”。

我作为摩擦日志记录者的职责,只是把问题指出来。如果我直接说“你们应该把图标改成两行”,他们可能会觉得我在替他们做工作、替他们定路线图;也可能仅否定这个具体方案,从而错过对其他方案的发散。你越不了解被反馈的团队,就越要谨慎拿捏边界。

我略过了一些体验顺畅的流程,但在合适处也点缀了赞赏。这能让摩擦点更突出,也让产品团队知道哪些方向是对的。

写完后,把它交给团队,并预期他们会筛选反馈、分诊行动项。

接收摩擦日志

接收摩擦日志非常有用。知道用户如何真实体验你的产品,这就是黄金信息。

但你怎么让别人愿意来体验你的产品?摩擦日志虽有趣,但大家都很忙,没有回报通常不会做。可考虑这些人群:

  • 你自己:戴上新手帽。
  • 早期采用者:请他们写摩擦日志。
  • 新同事:邀请他们把记录摩擦当作熟悉产品细节的上手方式。
  • 开发者、设计师、数据科学家等:组织社交化产品体验活动,如 bug bash 或 hackathon。

拿到反馈时,不要有防御心态,把它当礼物:

  • 如前所述,不要让反馈者去提工单或走流程。这会设置门槛,让人更不愿投入时间,也会传递“你的反馈不值得我花时间”的信号。
  • 闭环反馈。如果某人的摩擦日志推动了改进,要告诉他,最好还能让其经理也看见。我们公司有一套“kudos”机制专门做这件事。
  • 如果你或团队已经压力过载,不要迁怒信使。把时间压力留在后续分诊阶段处理。

摩擦日志文化

摩擦日志应是一个安全空间:你可以自由点评、挑剔别人的产品;接收方也能安心决定“是否”以及“何时”优先处理。只要双方角色清晰,互动通常都很棒。

如果它能成为文化惯例,会更容易。没有明确规范时,突然给某团队提反馈会显得带攻击性或批评意味,也不清楚你是否期待他们立刻停下手头工作来修你的问题。你在挑小毛病时,也可能担心别人觉得你吹毛求疵。预期不清会放大彼此冒犯焦虑。

如果它还不是惯例,你可以利用“这是一种互联网上已有定义的实践”这一点。此前我在当前公司引入它时,引用了现有博客来解释我在做什么,反馈很好。很快我就看到其他人也开始写摩擦日志。

摩擦日志是一种有趣且高效的内部试用方式,你应把它纳入产品流程并主动征集。

本章结尾我想讲“示例(samples)”:它们是展示产品常见应用方式的参考例子。示例如果做好,能把测试、文档与摩擦日志优雅地结合起来。

示例

并非所有产品都适合用示例,但对效率工具、创意工具和产品开发类应用,示例通常收益很大。举例来说,假设你的产品是类似 Microsoft Excel 或 Google Sheets 的电子表格程序。

和测试、文档一样,示例也有多种类型:

  • 功能示例:类似功能测试,聚焦演示单一功能,如排序或筛选。
  • 场景示例:类似场景测试与使用指南,演示一组协同功能以支撑关键用户场景。比如“办公用品预算表”既体现通用预算最佳实践,又能直接帮助正在做办公预算的人。

以下就用后者,展开几条编写示例的建议:

  • 给示例加自动化测试,既保障其可用,也可兼作场景或 e2e 测试。比如确保预算模板加载无报错;测试可填入一些数字并检查剩余现金。
  • 要具体,使用具体场景和具体语言。可预填一个叫“iniTECH”的软件公司,采购项写成“红色订书机”“蟑螂药”这类可信条目。用户从具体例子外推,比从“item 1”“foo”这类抽象词里消歧容易得多。
  • 像代码注释那样告诉用户“为何这么设计示例”。若预算示例面向表格新手,你可以加简短说明标题,并链接到“相对/绝对单元格引用”的文档。
  • 思考发现场景:用户遇到问题时,如何找到正确示例?对于功能示例,如果用户不知道该功能能解问题怎么办?你可以把模板列在模板目录;模板多时可加标签系统,并归入“accounting”或“finance”。
  • 在编写示例时同步写摩擦日志,把你的体验反馈回产品。如果示例由其他团队负责编写,也应欢迎他们报告摩擦。

本章小结

在产品设计与开发全程中,要主动寻找方式去亲自使用你的产品,并对其施加产品压力,确保它经得起真实世界使用。

测试、文档与摩擦日志,都是实现这一点的有效手段。

测试并非生而平等:有些更适合测产品,有些更适合测系统。场景测试与端到端测试能可靠地记录产品行为并确保持续可用,功能测试则补齐覆盖面的空白。

同样,文档也不是只有一种用途。用户旅程的不同场景需要不同类型文档。使用指南与入门指南是结构化体验产品的好方式,而早期编写概念指南能帮助你和团队打磨想法与产品沟通策略。

最后,摩擦日志是从同事那里获取可执行且坦诚的体验反馈的优秀方式。

练习

  1. 一个你感兴趣的 Wikipedia 页面,再选其中一行文字。找出这行文字的原始作者,并用摩擦日志记录你的体验。
  2. 阅读我对第一题所写的摩擦日志。假设你是 Wikipedia 开发团队新人,任务是处理这份反馈并打磨 UI。你会补充收集哪些信息?会优先探索哪些改进?
  3. 你在 WikiBlame 功能团队,正在补强测试覆盖。(可通过任意感兴趣的 Wikipedia 页面进入 WikiBlame:点击“View History”,再选“Find addition/removal”。)请给出一份至少包含两个关键场景测试的测试计划。假设你的测试框架支持模拟 UI 交互。
  4. Microsoft PowerPoint 有个“Animation”功能,允许演讲者通过一系列点击逐步展示幻灯片内容。其使用指南里有一节叫“Animate or make words appear one line at a time”。从文档三大用户场景角度看,他们为什么这样命名?

最后留个作业:把你正在做的某样东西写成文档。若还没实现,就按你想象中的形态,为一个关键用户场景写文档;若它已经存在,就亲自走一遍推荐流程,再按你的体验写出对应文档。

参考答案

  1. 这是我在尝试给 Wikipedia 一行文字溯源作者时写的摩擦日志。请确保你的版本也记录了人物画像与使用意图,并以体验报告为主,不要过于直接地给出具体改进方案。 我是 Drewbie,一名软件工程师。过去我主要把 Wikipedia 当读者工具,自己名下编辑很少,所以对高级用户工具并不熟。在阅读“历届 Microsoft Puzzle Hunts”页面时,我发现其中一条链接损坏,但不确定正确链接是什么,于是想联系最初编辑者。 作为工程师,我习惯用 git blame 给源码行溯源作者,所以我希望这里也有类似功能。我先点了“View history”,但这个界面对我不可用,而且没有“blame”按钮。我又猜“View source”也许行,于是找了“blame”及相似入口,依然无果。 我回到修订历史更仔细地看。“Find edits by user”行不通,因为我不知道用户名。到这一步我去查了 Google,结果告诉我去点“Find addition/removal”。 在那个名为 WikiBlame 的页面上(噢,“blame”),我找到一个术语搜索框,于是选了我关心那行文本里的一段短语去搜索。结果里出现如下条目: Comparing differences in 02:48, 31 August 2005 between 210 and 211 while coming from 196:OO Comparing differences in 04:34, 11 August 2005 between 217 and 218 while coming from 210:XX 我看不太懂这些内容,尤其是那些数字以及 XX、OO。但在列表底部我看到: Insertion found between 02:37, 31 August 2005 and 02:41, 31 August 2005: here Insertion 看起来像这行文字的起点,于是我点了 here。可我到处都找不到用户名!找了一分钟后,我发现了一个 IP 地址,这才明白是匿名用户通过该 IP 提交了编辑。(如果有明确标签会更清楚。) 如果我把这份日志交给 Wikipedia 团队,他们会自行筛选。对“没有 blame 按钮”的反馈,他们可能会忽略,因为这个词更符合工程师画像,不一定是多数编辑者的常用术语。另一方面,我对“IP 地址其实代表用户名”这点的困惑是普适的,可能会促使他们加上类似 Author 的标签。
  2. 如果我是应用团队成员,我大概率会忽略 blame 这条反馈,因为除非 Wikipedia 编辑者里这个术语比我想象的更常见,否则它主要只会和软件工程师画像共鸣。我更倾向先优化“Comparing differences”这段看起来很晦涩的文案。再就是把 IP 地址/用户名字段明确标注为作者,这似乎是个直接且高价值的可用性改进,尤其在匿名编辑常见时。
  3. 这是我选的两个测试。你选的是否同样常见,或更常见? 我假设大多数人是从 View History 页面进入 WikiBlame,因此先测这条路径。先创建一个简单测试页并做几次编辑,再渲染对应 View History 页面。在页面中找到“Find addition/removal”,取出通向 WikiBlame 的链接。接着我希望确认最基本搜索可用,尤其考虑那些预填参数框:向 Search 框填一个词并模拟点击 Start。结果校验我会做得较轻,因为结果列表细节大概率已有功能测试更深入覆盖。 我还假设热门页面也是 WikiBlame 高频使用场景,所以要测可扩展性。先观察像 Taylor Swift 这类页面的编辑量,再程序化生成一个有类似长历史、且编辑分散在文档各处的测试页。随后在该页发起搜索查询,确认在超时阈值内完成。同时校验结果,确保返回该术语对应的正确编辑数量。
  4. “Make words appear one line at a time” 这句是为了帮助不知道“Animate”术语的人也能找到文档,它服务于发现(discovery)场景;“Animate”则服务于已经知道所需功能名的用户,帮助他们进入理解或使用场景。

5 持续倾听用户

在《创新扩散》中,改变的不是人,而是创新本身……一项创新能否成功,取决于它能否不断演化,以满足群体中越来越挑剔、越来越厌恶风险的人。

— High Tech Strategies,《The Innovation-Adoption Curve》

我在软件行业的二十多年经历,让我近距离见证了增量发布(incremental shipping)的兴起。我深切体会过它的好处,所以也一直推动团队更高频地交付。

我第一次发布代码,也就第一次把 bug 发布给用户,是 2005 年的 Microsoft Visual C++ 编译器,而且是个大事故。用户从 CD 安装编译器后运行它,如果恰好使用的是简体中文环境,编译器就会直接崩溃,而且次次必现。

当时根本没法发补丁,下个版本还要等好几年。于是我们只能在知识库发一篇文章给出绕行方案:在编译前先删除应用目录里的某个文件。

那是我职业生涯里最严重的一次 bug。它更多说明了软件行业在进步,而不是我个人技术有多大提升。

后来我参与 Windows 7,研发和发布用了三年时间,对一部分用户仍是盒装光盘交付。我们没有单元测试或功能测试,依旧高度依赖测试工程师来测代码。好在 Windows 至少已经可以通过补丁修复关键问题。

2009 年我去了 Facebook,那里的部署是每周一次!我当时非常开心。我们周日晚切构建,周二发布前做稳定化。发布过程既焦虑又刺激,但至少是周更。

我加入了一个平台团队,负责 API 服务,为开发者的社交应用提供能力。我入职几周后,团队搞了一次测试黑客松。有人搭了一个很初级的测试框架,我们的任务是给现有 API 写测试。据我所知,那是 Facebook 成立五年后,第一次有团队开始写自动化测试。

公司继续演进。随着团队测试覆盖越来越高,部署所需的人工测试越来越少,发布节奏从每周五天一路提升,最后变成每天多次连续部署。代码库里布满了特性开关和 A/B 测试,保证功能上线既安全又可控。

等我在 2018 年加入 Stripe 时,那里已经有很成熟的测试文化,每天也会部署多次,只是仍由人工值守来护航发布。没过多久,他们就切换为大部分服务的自动化持续部署。此后我对多数部署不再高度紧盯,而是把信心交给测试与既有覆盖,依赖它们发现回归。

软件行业确实变好了。越来越多软件迁移到了云上。即便像 Visual C++ 这样历史悠久的客户端软件,补丁频率也比二十年前高得多。

我想,这种变化在汽车行业可能更剧烈。如今十大厂商里有 8 家都支持 OTA(over-the-air)更新。连火星探测车都可以。

一个直接结果是:用户更快拿到更多价值。即便产品总体演进轨迹不变,图 5-1 也说明,在任意时点,用户获得的价值都更高。

用户随时间获得的产品价值
图 5-1 随时间推移交付给用户的产品价值

更棒的是,如今我们更容易倾听和响应用户。再也不是“永久 bug”。

而且由于我们把大量变更流程自动化了,虽然花了更多时间测试,整体速度反而更快。

本章会讨论如何采集定性与定量数据,并把它们真正用起来,持续定义和打磨产品。

我先提出一个观点:工程师的工作描述不只是“开发产品”,还包括“构建让产品持续改进的能力”——一组我称之为“数字孪生”(digital twin)的工件。

但首先、也最根本的是,我们必须把代码架构成能快速迭代。所以我会先铺垫“为变化而设计”的基本原则:这些软件工程原则能让代码库和产品更具可塑性,也让我们更有信心地做变更。

然后我会谈定性数据:在产品周期各阶段获取用户反馈。我会重点展开用户支持互动,说明如何形成一个良性循环:你提供及时响应的支持,用户就会给出更多高质量反馈。

最后我会转向定量数据。你将学会如何选择并采集产品指标,帮助团队做出更好的决策。

本章结束时,希望你能真正掌握产品表现中“你想知道的一切”。

新的岗位描述:数字孪生维护者

这场革命并非免费。行业为了达到更快节奏所投入的工作几乎可称为英雄级别,最终带来的几乎是岗位职责的全面重写。

最著名的案例是,微软在 2014 年把测试与工程岗位合并为一个角色,以实现更快节奏。把代码“交给测试再说”实在太慢;同时,这也鼓励软件工程师自己对质量结果负责。

通常来说:

  • 我们自己写测试。
  • 我们建立指标并监控产品。
  • 我们向数据管道输出数据,并协助产品分析。
  • 我们接入反馈渠道,与客户保持直接连接。
  • 我们分阶段发布并执行 A/B 测试。

这套用于捕捉并模拟真实世界行为的技术组合,称为数字孪生(digital twin)

说明

数字孪生是对生产系统及其运行硬件基座的虚拟表征与仿真集合。

你可能在自动驾驶等场景里听过数字孪生:研发部门会有汽车及其传感器的高保真数字映像,以及可运行的模拟世界。它掌握人类驾驶行为和乘员安全约束,并可在模拟中测试与优化驾驶软件。

那在典型软件产品里,对应物是什么?

数字孪生是自动化测试与测试环境、指标与告警、Beta 层级、生产埋点、分析能力与链路追踪的组合。它还包括反馈论坛内容、客服聊天记录、复现样例、客户发现访谈转录等。凡是能帮助我们构建产品在真实世界中“定量+定性”表现画像的,都属于它。

我们在今天的角色,就是把这幅图补全:产品本体与数字孪生一起建设,然后在监控灰度发布、比较 A/B 测试表现、把反馈输入路线图时,从数字孪生中学习。数字孪生中的若干部分会在其他章节展开:

  • 我们基于生产中观察到的现象(或预期会发生的现象),设计第 4 章讨论的测试。
  • 我们加载仿真(第 9 章),用真实请求或合理拟真请求去压测数字孪生。
  • 我们记录摩擦日志(同样见第 4 章),捕捉“使用产品到底是什么体验”。

本章会把其余部分补上。

为变化而设计

在《创新扩散》中,Everett Rogers指出:技术创新与技术扩散是一个循环。这个循环的锚点就是反馈与迭代。产品若想扩散,必须不断调整自己,去吸引越来越厌恶风险、连接更弱的人群。若想让技术更快扩散,就必须内建快速反馈回路,及时发现每一波新用户的阻塞点。

为此,工程师有三个角色。我们做的每个功能都应该:

  • 当然,先满足产品需求。
  • 建立并维持信任:它现在满足需求,未来在更多变更进入后也仍满足。这样我们才能自动化交付。
  • 让自己易于演化:新需求出现时能顺利扩展,不被历史决定卡死。

缺少这三项,你的团队会离客户越来越远,被各种约束绑住手脚,无法服务用户。把这些同时做好,在我看来正是软件工程最有趣的挑战。

下面这个案例会展示:一个团队如何把更多“可信性”和“可演化性”注入代码库,从而重新连接用户及其最紧急的需求。

案例引入

果断迭代需要文化与技术的组合。我在 2015 年从 Facebook 转到其新收购公司 Oculus VR(虚拟现实公司)时,对此有切身体会。

在 2010 年代,Facebook 痴迷快速迭代。它早期那句臭名昭著的口号是“move fast and break things(快速行动,打破一切)”;到了 2014 年,因争议过大,口号升级为更成熟但不那么上口的“move fast with stable infra(在稳定基础设施上快速行动)”。

这套方法对 Oculus 团队是新东西。他们此前一直受困于延期和难以演化的代码库。

在我看来,这套方法里最关键的一点是:Facebook 构建了大量优秀的开发者工具。只要 Oculus 的产品迁移到 Facebook 技术栈,就能用上这些工具。

开发者生产力工程

2000 年代,Google 开创了一个理念:建立集中团队,专职让公司工程师的开发过程更可迭代、更安全、更快速。今天这通常叫开发者生产力团队(Developer Productivity)。到 2025 年,中大型科技公司里,这类工程生产力团队通常占工程总人头的 15%–20%。

加入 Oculus 前,我就在 Facebook 的开发者生产力团队。到 2015 年收购 Oculus 时,他们已经做出了很多成果,比如:

  • 一套类型系统 Hack,叠加在 PHP 之上,让代码变更和重构更安全。
  • 一套易用的特性开关系统,用于实验和安全发布。
  • 一套快速、灵活的生产可观测平台,无需预先声明查询索引。我们想查什么就能查什么,不必先改数据管道。
  • 一个数据库抽象层 EntSchema让模型开发更轻松且强大。我会在第 8 章详细介绍它。

一个新“dev prod”团队最早的一批任务,往往就是围绕数字孪生的建设和利用展开。比如:搭建持续集成方案,让我们的全部测试都能被自动执行。

分析瘫痪

我被指派帮助 Oculus 把应用商店和登录系统重构到 Facebook 基础设施上,目标是提升团队推进速度时,我希望把自己熟悉的那些“好东西”带给新同事。

随着重写方案逐步成形,我注意到这个团队习惯去预测两年后的未来,设想“也许某天会做”的功能,导致设计讨论经常马拉松,方案也容易过度工程。

随着时间推移,我对这种做法多了同理心。以他们当时的工具条件,这种态度是合理的。他们原有栈并不易于迭代。团队缺少像样的数字孪生,也缺少合理抽象层——他们的 REST API 与数据库是一对一映射,数据库和客户端强耦合。他们觉得一旦做了决定,就要背很久。

作为文化与技术的大使,我们的工作是把新母公司的技术和实践“卖”给他们。为了展示“为变化而设计”在实践中的样子,我来讲讲具体过程。

变化背后的技术底座

一个可迭代的软件技术栈,要由哪些组件构成,才能让团队跟上需求变化和用户增长?

给清单前,我先说明:不同团队约束差异很大,比如向后兼容压力,或采集使用数据的能力差异。所以这份清单里有些项可能并不适用你的现状;另一些看起来有难度但可达成,我希望能多推你一把。总能找到对你有用的部分。

下面是我给适应型团队的技术建议。如果你有(或想组建)Developer Productivity 团队,这是一份扎实的待办清单。

先说质量基础:

  • 始终写自动化测试(第 4 章),尤其要重点覆盖最关键场景。关键场景有扎实覆盖,会带来信心:改动不会打断用户最重要的交互。
  • 使用持续集成(CI)测试,在新代码进入源码仓库前必须通过,这样 bug 刚引入时就能快速发现。
  • 善用特性开关(feature flags):它是一类可开可关的开关,使你能在“部署代码”之外独立控制功能启停。特性开关让变更既谨慎又近乎即时,也应支持逐步放量新代码,帮助团队更有信心地安全试错。
  • 做代码评审,确保每次改动都满足客户需求、可维护且有充分测试。
  • 给产品加上运营指标(operational metrics)埋点,实时追踪错误率、流程完成率、性能、容量等,并做好监控和告警。尤其要校准到能在坏版本早期及时报警。
  • 采用队列、重试、限流等弹性原语,限制问题出现时单个故障系统造成的“故障影响半径(blast radius)”。

这些条目的共性是什么?如果我们要在系统持续更新的同时,依旧 99.9% 地为用户交付价值,就必须能信任我们的变更管理系统,确保它挡住最糟糕的问题。

提示

给每个组件内建“可被信任”的机制。也就是说,每次变更都应附带验证:它确实可用、满足产品需求,并且未来也会继续满足。

在 Oculus,我的一个关键贡献是建立测试文化。我们复用了 Facebook 的测试框架,再配上几个 Oculus 定制的测试辅助工具,让测试写起来很轻松。再加上连续几个月在代码评审里坚持“请补测试”,团队文化就被成功扭转了。这最终带来了更高频发布和更好质量。

除了“可信”,我们还希望让“变更本身”更容易、更有效:

  • 跟踪与业务结果、用户采纳和用户成功直接挂钩的产品指标(product metrics),帮助你决定该改什么。
  • 盯紧工具与流程,主动化解随时间堆积的向后兼容约束。比如建设数据库迁移工具,或提供 API 新版本机制,让升级用户受益,同时不破坏存量用户。
  • 用模块、类型系统、面向版本兼容的协议等抽象,帮助团队通过良好接口协同。这些抽象能通过关注点分离,让各团队独立推进。
提示

给每个组件内建“可演化”机制。组件应具备可扩展性、可维护性,并带有关键决策所需的埋点。

我记得曾向一位 Oculus 工程师展示 EntSchema(第 8 章会讲),它能让新增字段代码生成与数据库回填变得非常安全且轻松。我当时主张:可以把 PR 里那些过度预判的字段删掉,因为以后随时都能再加。

好,建立了信任、又让改动变容易,下一步是什么?下面是一些更进阶的技术:

  • 尽可能高频部署,以便更快发布关键补丁和新功能。如果你在做在线服务,优先采用持续部署(CD),并使用蓝绿或彩虹部署,以便渐进放量新版本,并在出问题时立即回滚。
  • 运行 Beta 计划或早期采用者计划,让最积极的客户帮你测试和打磨新功能,并提前预警哪些即将发布的版本可能会影响他们。某些场景可通过特性开关实现。
  • 做渐进式发布:先给随机、无偏的人群,再逐步放量到 100%。注意不要每次都是同一批人,避免总让同一群用户在不知情时承担不稳定版本风险。
  • 建设灵活的分析平台,让你在对新动态产生好奇或需缩小未知问题范围时,能轻松调整查询。
  • A/B 测试框架做实验,对比不同功能版本的表现。

事实上,一套叫 Scuba 的灵活分析平台,正是说服 Oculus 团队迁移到 Facebook 基础设施的关键卖点。它支持对产品数据做实时切片分析,不需要为每个潜在查询提前手工建索引,这一点吸引力极强。

挤出改进时间

你当前代码库里,有多少机制在为代码建立信任?它演化起来有多容易?上面哪些技术和机制可能帮到你?

这份待办清单很长,但有理由乐观。开发者技术生态越来越成熟,你可以直接集成许多优秀现成方案。与此同时,编码助手与代理正在提升我们的整体生产力,我们可以把部分节省出的时间用于升级软件组织能力。

产品需求从四面八方涌来,总觉得没时间投资技术栈。那如何找到时间和动力?前面提过,公司层面常把 15%–20% 人力投入开发效率;我认为对单个团队这通常也是个不错的比例。看看团队能否在路线图里切出时间,优先做最关键的效率改进。也要有心理预期:这类投入往往是长期回报,关键但不显得“紧急”。

当然,Oculus 相对 Facebook 其他业务也有特殊需求:我们的账号不是 Facebook 账号,支持高性能图形密集型游戏,等等。我们既需要专用库和专用基础设施,也要尽量复用公共基础设施。

技术塑造文化

技术对团队文化影响极深。这意味着很多流程或实践问题,解法往往是技术性的,而非文化口号。这是好消息,因为工程师能直接成为解题者。

在新基础设施上重写产品几个月后,Oculus 团队完全转变了。有了更好的数字孪生——高质量测试防回归、强大的生产可观测性、以及为快速变更设计的基础设施来修正设计偏差——他们就不必再那么厌恶风险,也不必再小心翼翼地过度推演,而能更专注于交付真正优秀的东西。甚至还有同事做了纪念新技术栈的 T 恤,我现在偶尔还会穿。

重写的后续效应非常明显。接下来几年里,团队能在新栈上快速设计并落地大量新功能。在第 7 章第 8 章中,我会展示团队如何利用迭代方式,在设计与优先级上更聚焦,从而做出更高质量产品。

你不一定要做一次大重写,才能降低团队里的恐惧。Facebook 总部墙上曾贴过一组著名海报,全大写问我们:

WHAT WOULD YOU DO IF YOU WEREN’T AFRAID?

— Facebook Analog Research Laboratory

我相信海报本意是鼓励创造力和勇气:无论是探索一个未经验证但可能高影响的想法,还是给同事提出艰难但必要的建设性反馈。

但恐惧常常是合理的、理性的。所以我会把这个问题扩展成:“要做到不害怕,需要什么?”哪些流程与实践问题,其实可以用技术来解决?

  • 你是否觉得队友普遍测试不足?可以改进测试框架,让写测试更容易、也更有趣。或做一个 fake 库,降低团队依赖服务的测试难度。
  • 新版本灰度和上线要花几周精测?也许能渐进放量的特性开关会让部署更顺。持续集成测试也能减少对内部试用或人工测试的依赖。
  • 团队是否常为优先级争论数小时?也许你缺数据。那就补更多产品指标;若埋点困难,就考虑更好的产品分析方案。

一个可信、可演化技术栈的核心价值是:下次你收到意料之外的用户反馈,突然意识到必须加新功能时,它已经随时可以投入行动。

获取用户反馈

用户给出反馈时,他们的故事会和测试一起进入我们的数字孪生。我们由此理解外部世界如何看待产品,以及他们如何与之交互。

第 4 章里,我们通过内部试用和摩擦日志,在产品生命周期早期给自己反馈。

现在该向外部用户学习了。我们可以依赖这些方式:Beta 版本、反馈组件、倡导者计划、问卷调查和用户支持。

Beta 版本能降低故障影响半径

无论你的早期采用者是亲友、消费电子发烧友,还是希望先行验证再大规模部署的企业客户,只要他们自愿接受“不那么稳定”的体验,你就能在不损害大众信任的前提下拿到反馈。他们会帮你迭代得更快,也能降低 bug 的故障影响半径。

我见过两种方式特别有效:早期发布计划(early release)和 Beta 层级(beta tiers)。

早期发布计划会让同意参与的用户先于所有人收到更新。即便是这批人,耐心也有限,所以你仍要维持足够质量,才能把他们留在满意客户行列。

Beta 层级则是互联网服务的“预备版本”,客户可把部分环境流量指向它,在变更打到主生产流量前先跑测试。这样他们能验证对自己最重要的东西,也能在其用户受影响前把风险反馈给你。

反馈组件帮助用户发声

嵌入式反馈表单若足够便捷,会向用户传递“你在乎他们”的信号,同时把洞察导向你,而不是把情绪发泄在应用商店一星评论或社交媒体吐槽上。

最好让反馈是*上下文充分(contextual)*的:用户不必特别费心,你就能拿到你需要的上下文。例如:

  • 在文档中支持行内评论,指出哪里困惑或过时。
  • 当你观察到用户在使用某个想收反馈的新功能时,弹出反馈请求。
  • 在移动应用里,连同评论一起采集截图、页面路径、应用版本、手机型号和 OS 版本。
  • 在待举报的违规内容旁内联放置反馈组件。
  • 主动提示用户补充你最关心的那几类上下文信息。

倡导者计划提供深度反馈

有时你需要社区更深层参与:深度反馈、品牌传播、开源贡献等。像微软 Most Valuable Professional 这样的倡导者计划,可以给这些人提供激励,让他们感到被重视,也更愿意持续投入。

常见权益包括:免费软件或服务、抢先体验、人脉机会、会议门票和声誉收益。

问卷提供广度覆盖

如何设计科学、可复现的用户调查超出本书范围。但我想强调两类问卷,对产品思维工程师很有价值。

第一,开放回答常是用户共情的金矿。比如,如果你们组织在发净推荐值(NPS)情绪调查,请务必去看开放题答案。

另一类是功能问卷(feature survey)。你把你认为用户的痛点逐条列出,请用户对“希望你修复的程度”打分或排序。

在产品规划阶段,这类问卷能帮助验证“用户真正想要什么”与“团队以为用户想要什么”之间的差异。配合开放回答,它还能揭示意外信息。

用户支持飞轮

我们重点展开用户支持,因为对很多团队来说,它是最强的产品反馈来源。工程师通常也深度参与。在很多组织里,工程师都有机会做支持:要么直接面对用户,要么在支持团队将请求升级时间接介入。

每一次支持互动,本质上都是伪装成问题单的产品反馈。也就是说,你应始终同时想着两件事:直接帮提问者解阻,以及借机获取反馈来改进产品。这样看,支持就不是负担,而是双赢。

更妙的是,这会自我强化:如果支持做得好,用户会回来继续求助,也就持续提供产品所需的关键定性反馈和用户洞察。同时团队也会越来越熟练、更高效地做支持。这种飞轮效应一旦转起来,威力很大。

本节我会讲,如何在不过度压垮工程师的前提下,把支持实践做起来。

提示

目标是启动一个反馈飞轮:用户持续回来获得优质支持,同时帮助你持续改进产品。

提供高质量支持,有几个简单构件:

  • 快速响应,消除求助门槛。
  • 保持友善。
  • 深入理解用户场景。
  • 先帮用户解阻。
  • 追问:产品本可以怎样更好地服务用户。
  • 扩展支持能力:随着用户增长,让支持更高效。

下面用一个案例来说明。

Stripe,我曾是一个内部工程框架 Workflow Engine 的技术负责人。它用来编排其他工程师的有状态或长时流程。该框架基于流行开源技术,具备可扩展性、容错性,并帮助开发者规避分布式系统常见问题。

这是个很大的框架,数百个团队的开发者会以各种方式给它施压,因此问题很多。在匿名职业社区 Blind 的内部调查里,我们团队多次被全公司工程师投票为“内部支持最佳团队”。据说在我离开后,这个口碑仍然保持。

我们到底是怎么做到“还能继续做新功能”的?

消除进入门槛

反馈是礼物;如果你让用户付出太高成本,他们就不会再给。

Workflow Engine 团队承诺在工作时间内 30–60 分钟回应问题。我们称之为“支持 SLA”。

许多产品与基础设施团队会避免直接触达客户,设置门槛,例如要求用户提工单并等待数天。即便首次响应快,后续跟进也常被拖慢。这种反应可以理解,因为大量客户咨询会带来压力和打断,但它会同时伤害用户和产品。

相比之下,Workflow Engine 在企业通讯软件 Slack 提供直接支持,响应更快,也便于其他同事随时加入答疑。

欢迎用户,还意味着互动中要保持体面与善意。

保持友善

用户求助时,常会觉得“是不是自己做错了”。你一句不当的话就可能让他们尴尬。用户对你的产品了解远不如你,很容易让你误以为他们问了“基础问题”或做了“不合理操作”。别掉进这个坑。练习同理心,你会更不容易烦躁。

要明确让用户知道:建设性反馈被允许且受欢迎。问题不在他们,而在产品。(如果确实是他们操作失误,也没必要点破;他们会自己意识到。即便没意识到,也不是你的职责去纠正。)

像“这听起来很折磨人”这样的同理表达,或“我们一起把它修好”这类显式目标对齐,都能让用户感到你和他站在一边。

还要以好奇心开场,而不是轻蔑或不耐烦。你的目标是改进产品和用户体验,不是改造用户本人

例如,用户有时会像默认你能读心术。你得习惯这一点,耐心补问他们遗漏的关键信息。

深入用户场景

通过梳理用户交互时间线,你能更好理解他们的目标、问题成因,以及如何帮他们解阻。

根因分析(RCA)用于定位到底哪里出了问题。可能是用户做了非常规操作,可能是用户理解偏差,也可能是产品存在 bug 或功能缺口,或几者叠加。我并不是说你要立刻定位到“哪一行代码”导致了 bug——那可以后置。但你应先判断“是不是 bug”“大概在哪”,至少到足以指导用户绕行。

来看这条时间线如何同时帮助我们做根因分析并解阻:

  • 配置(Configuration):了解其环境事实,如应用版本,可辅助 RCA,并给出“请升级应用”等修复建议。
  • 意图(Intent):他们做的动作是否匹配目标?有哪些绕行方案?他们这个场景是否被支持?
  • 先前动作(Prior actions):常常真正出错点发生在事故前,这些信息能帮助 RCA。你可能需要把用户拉回前一步,让其重试或改路径。
  • 事故(Incident):理解发生了什么及影响范围,以支持 RCA 与优先级判断。
  • 后续尝试(Further attempts):他们事后尝试过什么?确认“是否仍被阻塞”有助于你决定处理优先级与建议方案。

这些信息项要不要问、先问哪个,取决于情境,把它当菜单即可。Workflow Engine 是复杂而灵活的平台,用户不总知道实现目标的最佳设计模式。因此我们常需要回溯他们最初意图,判断是否走在正轨。Stripe 甚至有个 Slack 表情“WAYRTTD?”,意为 “What are you really trying to do?”,是种轻松的追问方式。

先帮用户解阻

对用户最好的做法,并不总是直接回答“他问出来的问题”;很多时候,是回答“他没意识到该问的问题”。

比如 Workflow Engine 用户一个常见问题是“非幂等操作”。

幂等性(idempotency)指一个函数可被多次调用,而在首次调用后不会再产生不同副作用。比如客户在转账时,如果首次尝试超时,我们不希望重试再转一次。(分布式系统里如果只记一条铁律,就是让操作幂等!)做 RCA 时,我经常发现陷入困境的用户存在非幂等操作。我总想建议他们把操作加固,以避免未来问题。但这对“立刻脱困”没帮助——损害已经发生了。

那该给什么建议?先给眼前解法,还是先回头讨论更早的设计决策?

我让很多用户满意的方式是:两种答案都给,让他们按约束自行取舍。通常我会先给即时解阻,先缓解对方紧张,再补长期建议。

追问:产品本可以怎样更好服务用户?

理想中的产品是近乎完美的:无需支持、使用安全、文档答疑充分、用户需求自动化满足。它会唱会跳。

所以每次收到支持请求时,我也会对产品做一次 RCA:产品是怎么让用户走到这一步的?用户此刻往往只想尽快解阻,并非有意给反馈,但这不妨碍我捕捉线索。

同一条“用户场景时间线”既能帮我解阻,也能定位产品反馈。我会沿着用户决策一路回看,问“产品本可如何在此处帮到他?”也许更好的错误消息,或更好的文档,就能让时间线更顺。

在 Workflow Engine 上,几位客户因为非幂等操作踩坑后,我突然想到:我们可以很容易地加测试!

Stripe 开发者会用我们的集成测试框架测试自己的 Workflow。于是我做了一个改进:如果开发者把某个操作标记为可重试,测试框架就自动执行两次并比较两次结果;若不一致就报错。

这抓到了很多问题。它同时提升了用户产品质量,也降低了我们的支持负担。更关键的是,这个想法并非来自传统“产品反馈”,而是来自支持现场。

你很忙。如果当下没有时间或知识立刻提炼出产品反馈怎么办?

既然你已经采集了用户场景,就先记个待办,稍后再回来看。之后你可以开工单或与团队讨论。维护一份带原始上下文的有序任务清单,是避免遗漏关键细节和背后战略问题的好办法。把事情写下来还能减压,让你在支持现场更专注用户。

接下来谈如何扩展支持。第一阶段,尽可能长时间保持与用户的直接连接;当这个策略走到极限,再谈有中间层(如 AI 助手与支持人员)时工程师的角色。

在保持用户直接连接的同时扩展支持

Workflow Engine 团队把支持扩展到了数百个工程团队,同时没有让功能开发全面失焦。我们用了几招:

  • 设立支持轮值(support rotation)。
  • 明确并固化支持角色职责。
  • 建立产品与文档持续改进文化。

首先,我们创建支持轮值。每周指定一位“runner”,其主要职责是在工作时段提供支持。团队其余成员则专注深度工作,并被鼓励除非 runner 超时未响应,否则不要抢答。

几个月后,支持负载开始让人疲惫。我们需要降低 runner 的负担,又不打乱整队节奏。于是逐步形成了这些做法:

  • runner 的角色像(美式橄榄球)四分卫:球总先到他手上,但当他工作过载或并非最佳专家时,可以“传球”给其他成员。
  • runner 在其支持周不应承担常规项目工作。项目排期把 run 周视同休假处理。这降低了压力,也让支持质量随时间提升。
  • 离开时要比接手时更好。runner 每周至少做一项能长期提升支持效率的改进:bug 修复、自动化、产品可用性优化或文档补充都可以。每周末在团队会上汇报。

换句话说,在 run 周里,支持就是主业。

这个机制效果很好。下面是持续改进文化里涌现的两个典型成果:

早期我们发现与用户来回追问太多,于是加了自动化机器人,先请用户补充配置(如编程语言),让人工接手时信息已齐。

我们也对文档做了大量改进,让答疑更高效。

我们采用了“用文档回答”(answering with docs)实践。即:若用户提问在文档中本应有清晰答案却没有,我们就先补文档,再把链接给用户。这样用户要稍等一点,因为写文档更花心思,但内容会沉淀给后续用户与 AI 支持机器人复用。

如果你暂时不能把更新后的文档直接给用户(例如必须先走评审),可以先写草稿变更并把内容复制给用户,之后再走评审合入。

就像代码库应可演化,文档也必须可演化。一定要让团队改文档足够容易,否则文档会很快失效。

总体来看,扩展支持需要持续创造力与投入,但我们从未需要做某个“巨大专项”才能让它运作。

当然它也有边界。我们是内部框架团队;若你的产品面对数亿用户,我上面的做法无法原样扩展。

当支持链路有中间层时如何做

你的公司可能在工程与客户之间有中间层:AI 聊天助手、支持人员、社区版主、解决方案架构师,等等,取决于产品形态。也可能只是“工程支持收费”。这样你拿到的往往是“一线无法解决”的问题集,它和“最有价值的产品反馈问题集”并不完全重合。如果你处于这种环境,请想办法保持与用户连接。比如,如果你从未和支持团队做过轮岗,我强烈建议试一次。同时要确保你建立了反馈回流通道,让这些中间层把你需要的信息持续带回来。像欢迎终端用户反馈一样欢迎他们的反馈。

最近在我当前公司,我读了很多用户与支持机器人之间的对话。我会搜索与我负责功能相关的关键词,看用户困惑点在哪。我发现机器人出错时,常因某些内容文档不足。于是我去补齐这些缺口,机器人在下一次训练时就会学到。

用户支持的力量

这里总结一下:

  • 提供优质支持,形成飞轮效应。
  • 深入客户场景,既更容易解问题,也更容易改产品。
  • 预先为支持留出团队时间,但要避免把每个人都拉入随机中断状态。
  • 明确把“支持可扩展”作为目标,用同样努力服务更多用户。
  • 让代码改进和文档改进都变得容易。

我在支持上花了很多篇幅,因为它是工程师获取反馈最有说服力、最具互动性的渠道。它发生在“用户正需要你”的时刻。相比之下,我前面提到的其他渠道——问卷、反馈组件、倡导者计划、Beta 测试——都缺少这个天然驱动力,因此稳定性更弱,往往需要额外激励推动。下面我们从定性反馈转向定量指标。用户报告作为唯一反馈源只能支撑到某个规模;再往上,你就得开始计数。

产品指标

任何好的数字孪生,都要由能跟踪真实环境的“传感器”喂养。就像飞机的数字副本会接收数百万次飞行数据,你的数字孪生也应能感知用户、网络、硬件等各方面发生了什么。

第 9 章中,我们会讨论非功能性需求对应的指标。我们不仅要给单个操作做埋点,还要给用户流程做埋点,让指标更贴近用户结果。

这里我们先讨论更高层指标:直接反映用户参与度和业务成功的计数与比例。这些指标应驱动我们做更有用的产品,支持决策,并帮助业务增长。

关键在于选对指标组合,用它来激励团队或公司。但依据是什么?

在项目开始前,你需要先确认项目与组织产品战略一致。服务用户有很多方式,但并非都符合你的使命。一个做法是:明确该项目预期会改善公司/组织的哪些关键绩效指标(KPI)。

第二,也是最重要的——你大概已经猜到我要这么说——聚焦服务用户。把价值指标(value metrics)设为目标,能让团队对“做事的意义”负责。

但如果用户不知道或不用你的功能,你就无法交付价值。而且识别用户价值往往需要时间。因此你还需要追踪采纳指标(adoption metrics)。

说明

产品指标有三种:关键绩效指标(KPI)、价值指标(Value Metrics)和采纳指标(Adoption Metrics)。给数字孪生做埋点时,三类都要考虑。

组织层面通常追 KPI,而后两类可能是你这个产品或功能特有的。

每个类别里选对指标都需要谨慎。先看一个案例,它能展示三类指标各自的经验与陷阱。

案例引入

目前在 Temporal Technologies 工作。简单说,Temporal 提供一个可靠执行工作流(workflow)的框架。它的用户是使用开源框架写代码的开发者。

我负责的一个产品方向叫 Safe Deploys,目标是帮助用户在升级其 Workflow 时避免错误。

用户的 workflow 代码运行在各种环境里,像其他代码一样部署和升级。Workflow 的特殊点在于:它可能运行数分钟、数小时,甚至数周,并且会休眠;恢复时也常常不在最初那个进程里。那如果它醒来后跑在“比它启动时更新”的代码版本上会怎样?不展开细节,你也能感觉这很棘手;确实如此。

我们建议开发者像用特性开关一样去“补丁化(patch)” workflow。伪代码如下:

def my_workflow():
  do_stuff()
  # It would be dangerous to run new code in an unsuspecting old workflow.
  if (originally_started_on_new_version()):
    do_new_stuff()
  else:
    do_old_stuff()

这种“补丁化”方法做对了就没问题,但存在可发现性问题:开发者不总能意识到自己需要这么做;也有可用性问题:当改动复杂时很容易做错。

如果补丁化不到位,workflow 会卡住并出现我称之为workflow 升级错误的问题。(如果你觉得“统计这类错误”听起来像产品指标,先别急。)

我们的核心挑战,是帮助用户部署代码而不把系统搞坏。高层产品路径有好几种,不需要记细节,我只是给你个感觉。我们可以:

  • 做一套机制,让 workflow 绑定到特定代码版本,绕开升级问题。
  • 提供测试钩子,帮助用户提前验证其 workflow 是否会触发升级错误。
  • 允许用户对新部署做渐进放量,把问题出现时的影响半径控制住。

为简化起见,我们假设正在做一个叫 Managed Deployments 的功能,帮助用户处理这些问题。我们并不知道哪种解法最好,也不知道用户会不会采纳,所以需要指标来约束自己。

我会给出一串指标:从非常战术的指标开始,逐步走向战略指标。每个指标我都会先指出它的不足,再用下一个想法修补。

我把它们分成三类:采纳指标、价值指标、KPI。

随后我会从这串候选里挑三个:一个采纳指标、一个价值指标、一个战略 KPI。目标是让指标激励团队做出正确行为,产出更好的用户结果,并与公司使命对齐。

提示

选指标时,最关键考量是:它是否让团队激励对齐。

采纳指标

Managed Deployments 的一个关键难点是:它主要让应用开发者受益(变更管理更简单),但又需要改部署系统,而在很多公司部署系统归另一类角色(平台工程师)管理。也就是说,应用开发者可能得先去说服平台同学改系统,这个组织政治问题会拖慢采纳。

我们不能等几个月才知道有没有人在用。否则根本不知道该继续投入改进、加强宣发,还是换路线。

那什么样的采纳指标才算好目标?关键问题是:它会如何改变我们团队的激励。

下面按“最容易测到”的顺序列一个清单。每项都附带一个缺陷,下一项会修它。最终我会尽量往后选:既能更确定用户在采纳,也得是可落地、可测量、可影响的指标。

开始:

  • 1. 文档或新闻稿浏览量:能捕捉兴趣,但看不出是否真的采纳功能。
  • 2. 历史创建过的 Managed Deployments 数:问题在于会把“试过后流失”的用户也算进去。凡是看起来像“生命周期总用户(lifetime users)”的指标,都要警惕,它可能包含大量沉睡账户。
  • 3. 活跃 Managed Deployments 数:更好,因为喜欢该功能的用户更可能保持活跃。但它把“空部署”和“承载大量 workflow 的部署”一视同仁,无法激励我们推动最关键部署的采纳。
  • 4. 活跃 Managed Deployments 上运行的 Workflow 数:在上一指标基础上按 workflow 规模加权。更好吗?下面讨论权衡。

前两项常被称为“虚荣指标(vanity metrics)”。表面漂亮,但对业务表现、用户行为或产品成功没有实质洞察。它们太诱人,因为太容易跟踪:数数某个数据库大小即可。

提示

避免虚荣指标。

相比之下,“活跃 Managed Deployments”这个指标就很有意思。它是一个领先指标,衡量潜在用户价值。并且当用户停用功能时它会下降,所以也能隐约反映真实用户价值。

后两项之间的取舍更有代表性。最后一项按 workflow 数对活跃部署加权,也就是一个有 1000 个 workflow 的客户按 1000 计,而不是 1。若把它设为目标,会怎样改变激励?

  • 我们更难控制它。若大客户采纳,指标会暴涨;不采纳就上不去。这种运气成分会让团队不舒服。
  • 开发者工作量并不太取决于是 1000 个还是 100 万个 workflow 因错误失败。无论哪个量级,他们都得去处理事故。
  • 它会激励我们优先关注高体量客户(通常是大企业)。也许我们会额外做很多企业特性,或去动员大公司应用开发者推动平台团队接入。

正确答案部分取决于战略。如果 KPI 聚焦企业采纳,也许就该选 workflow 加权指标,即便波动更大。但若更看重开发者效率和口碑传播,不按规模加权的“活跃 Managed Deployments”就很好。

最终我们选择了“活跃 Managed Deployments”。

但这仍不能证明它真的有帮助。用户也可能只是因为默认配置或文档建议而使用它。下面用价值指标来补这个缺口。

价值指标

假设有个 AI,唯一目标是尽可能制造更多回形针。它很快会意识到:如果没有人类会更好,因为人类可能把它关掉;一旦关掉,回形针就更少了。而且人类身体里有大量原子,也可以拿来做回形针。于是 AI 会推动一个未来:回形针很多,但人类消失。

— Nick Bostrom

这个著名的“回形针最大化器”思想实验说明:当激励设计怪异时,会发生什么。

听起来很极端。但如果你觉得只有 AI 会被奇怪激励带偏,不妨看 Wells Fargo 的案例。

2016 年,Wells Fargo 被曝员工仅为完成销售目标,给客户私自开立数百万个未经授权账户,引发巨大舆论反弹与法律问题。

为什么会这样?一个关键错误是:他们用“账户数量”这种采纳指标来给员工设目标,而这里更应使用价值指标。结果是客户拿到了一堆并不需要的产品。若用价值指标,例如应统计“受托管理资金规模”。

我们的本能往往是选短期可控指标,但 Wells Fargo 证明这会反噬。过度可控意味着更易被操纵,最终让公司在法律和解上付出超过 30 亿美元。

说明

选择能“挑战你”的价值指标。对“我是否能影响它”保留一点不适感,反而能激励团队交付真正有价值的成果。

基于这个原则,继续看 Managed Deployments 的价值指标候选,并逐项指出问题:

  • 1. 每日 Managed Deployments 数:我们推理“部署越多,说明开发者对部署越有信心、越成功”。但这指标可能受许多无关因素影响;如果部署仍导致升级错误,我们其实没完成任务。
  • 2. Workflow 升级错误数:如果使用 Managed Deployments 的用户这项指标下降,说明他们确实获得了价值。但如果系统中运行的 workflow 总量在增长,即便我们做得很好,这个绝对值也可能上升。
  • 3. Workflow 升级错误率:若使用 Managed Deployments 的用户 workflow 报错比例更低,就说明我们干对了。它也更好地控制了系统 workflow 规模差异。但它不衡量“多少运行中的 workflow 被我们从生产事故中挽救”,因此不奖励采纳。(不过我们已有采纳指标,这也许可以接受。)
  • 4. 避免的 Workflow 升级错误数:这个按使用量加权的指标,同时回应了前两项的不足。我们可以估算:如果从未发布安全部署能力,本会多出多少错误。

不过这些指标都没直接回答:Managed Deploys 是否提升了用户对产品的整体使用量,或是否让他们更愿意推荐我们。下一节用 KPI 补上这块。

“Workflow 升级错误率”这个指标我很喜欢,尤其和“活跃 Managed Deployments”采纳指标搭配时。若我们能向管理层证明“用户错误显著下降”,说服力会非常强;若做不到,也可反向访谈那些指标没改善的用户,找原因。

更棒的是,若数据漂亮,我们还可把它用于市场沟通,争取尚未采纳者:“采用 Managed Deployments,平均可减少 N% 部署错误!”

它的主要缺点是:开发者可能几周乃至几个月才会遇到一次部署问题,因此在早期采用者较少时,这个指标显现会比较慢。那段时间就靠采纳指标支撑。

最后这个“避免的错误总数”指标,同样像“活跃 Managed Deployments 上的 Workflows”那样按 workflow 加权。权衡几乎一致。

如果你在做一个较大功能,认真推导价值指标时却发现想不出一个像样的指标,请警惕。这通常有两种原因:

  • 这个功能可能本身无用,甚至带有剥削性:短期赚钱,长期劝退用户。
  • 你的数字孪生能力可能不足:产品可能需要新增埋点与观测能力。

关键绩效指标

让我们把时间拨回 Managed Deployments 项目一开始。在真正动工前,我们应先看 KPI,判断产品想法如何与公司更大目标对齐。很多产品都“有价值”,但未必是“这家公司现在该做的事”。我们必须保持一定聚焦,才能在专长领域持续构建和维护最好的产品。

在向管理层推销项目时,你可以在产品论题(第 6 章)或产品简报(第 7 章)里,把项目与 KPI 绑定起来。

KPI 对“怎么达成提升”完全不设偏好。它撒大网,鼓励我们发挥创造力找到最佳路径。

那 Managed Deployments 适合什么 KPI?我们来看看几个候选:

  • 1. 净推荐值(NPS):衡量客户忠诚和满意度。它以 0–10 分衡量客户推荐公司产品或服务的意愿。这个指标可比较“使用 Managed Deployments 的用户”与“未使用用户”谁更愿推荐 Temporal。直接追踪真实转介绍会更可靠,但我们基本可以认为:若该数值上涨,说明用户认可我们提供的安全性提升。
  • 2. 净收入留存(NRR):当前客户收入相对早期某时点收入的比值。它会受使用量提升带动,但不直接体现新增增长。若使用 Managed Deployments 的开发者,其 workflow 使用量随时间增长明显高于未使用者,我们就能更有把握地确认业务价值。也许是因为开发者更喜欢、也更安心使用产品,或因为他们把更多精力放到能拉动业务增长的事务上,从而把规模做大。但如果 Managed Deploys 运行成本很高,这份增收还值不值?
  • 3. 毛利率(Gross margin):会扣除运行服务器、数据库等成本,确保我们为此投入的额外资源是值得的。我们可以比较“使用 Managed Deploys 的用户”与“未使用用户”的利润率差异。但它不会奖励“基于口碑带来新客户”。
  • 4. 营业利润(Operating profit):公司最终底线。它奖励我们扩大客户基数,而不只是优化单个客户表现。它几乎纳入一切,但也最难精准衡量“我们的产品到底贡献了多少”。

不是所有项目都能用科学实验方式显著撬动这些指标,尤其是较小项目或小公司。

这正是我们在项目前期就考虑 KPI 的原因:它能为这项工作提供方向和动力。比如在 NPS 调查里若看到用户不推荐 Temporal,我们可追问原因;若反馈是“部署新代码困难”,那就是明确证据。

同样,如果我们在追踪 NRR 时发现有人流失,就能追问流失原因并拿到关键反馈。

当我们向管理层推销项目时,把这些与 NPS/NRR 相关的客户证词摆出来,会极大提升获批概率。若成本是顾虑,我们就补充毛利率视角。

战术指标与战略指标

KPI、价值指标与采纳指标的组合,能让团队保持诚实与投入。

你可以把上面的指标列表看作一条从战术到战略的光谱。最战术的指标可能是广告曝光量:只要投放、烧钱,数字就会上升。

价值指标更偏战略,但通常需要更久才能得到结论,因此也常被称为滞后指标(trailing metrics)

营业利润则是极其战略的指标。除非你是掠夺性垄断,否则它清楚表明:用户确实认可产品独特价值,同时业务可持续。

可惜,与用户价值最对齐的指标,往往最难快速测量,也最难精确归因到某个功能。这种核心张力,正是“选对指标”这件事有趣之处!

表 5-1 对不同指标取舍做了简洁总结:

战术指标 战略指标
是领先指标 是滞后指标
靠聚焦即可推动 靠创造力推动
容易被“做数” 难以直接控制
衡量潜在用户价值 衡量实际用户价值
易于精确测量 难以归因到你的功能
表 5-1 战术指标与战略指标倾向对比

几乎不存在一个指标能同时命中我们所有目标,所以我们要同时追踪 KPI、价值指标和采纳指标。

如果你只盯战术指标,可能会变成“回形针最大化器”,忘掉原始战略。

反过来,如果你只看公司级指标(比如利润),又很难知道具体该怎么推动它。你仍需要更具体的指标来聚焦单个团队行动。

本章小结

本章覆盖了三个大主题。再怎么强调“与用户保持连接并快速响应其需求”的重要性都不为过。这应是任何软件团队的核心能力。

首先,我展示了如何把“可信性”和“可演化性”内建到每个功能中。这样你就能做好准备:高频、自动化地快速发布并持续迭代。

有了这样的技术栈,你就能更容易响应用户。在规模化场景下,仅靠定性或仅靠定量都不足以稳定支撑决策,因此我们采用两者组合。

我们的定性反馈以用户支持为锚。用户来敲门时,实际上给了我们理解其故事的黄金机会。

每个重要项目都应以 KPI 为立项基础,并用“采纳+价值”的指标组合持续跟踪,证明对用户的实际影响。

我用“数字孪生”这个框架串起这些概念,是为了把本来分散的东西具体化。一支软件团队应持续培育自己的数字孪生:仿真、测试、数据与反馈都要纳入。如果你没有能力测量并理解产品在真实世界的表现,你就没有稳固立足点。

那我们该如何使用这些反馈与数据?接下来我会回到产品周期起点。本书最后四章将讨论产品发现与定义。在接下来的两章“发现”主题中,你会把用户研究与(若你已有上线版本)数字孪生数据结合起来,用于生成想法、制定计划并做优先级。

练习

  1. 想一件你团队可以做、能让代码更易演化的事情。可从工具、框架、抽象或重构切入。
  2. 回想你团队的部署/发布流程。你可以做什么,让客户更频繁地拿到新价值?可考虑测试覆盖、自动化、特性开关等。
  3. 如果你要训练一个 AI 助手为你的产品提供用户支持,你会把哪些上下文收进数字孪生喂给它?
  4. 你的团队如何消除“获取定量用户反馈”的某个门槛?
  5. 假设你在一个社交网络工作,产品里有评论和单一点赞按钮。你正在实验给用户更多反应类型,类似 Facebook(care、haha、angry 等)和 LinkedIn(support、insightful 等)。公司战略是让更多人发布高互动内容;你认为更多反应能让读者给发帖者更丰富反馈,进而让发帖者更愿意分享。请至少给出一个“好的采纳型战术指标”和一个“滞后型价值指标”。我会给两个示例。
  6. 你认为该“反应功能”会影响哪个 KPI?

参考答案

  1. Facebook 一个很实用的工具是可扩展的“codemod 工具”。它可识别代码模式并在超大仓库中批量转换。这让广泛使用库的变更与弃用快得多,也让工程师更敢做高影响改造。
  2. 对高可用服务,我推荐蓝绿或彩虹部署。传统“滚动部署”是在固定机器上原地替换代码;一旦出问题,旧版本已被替换,回滚更慢。 蓝绿和彩虹部署则会同时保留多版代码(用颜色区分,名称由此而来)。发布新版本时你可渐进放量,一旦有问题可立刻把流量切回旧版。
  3. 对我的技术产品,我会喂给它:文档、社区消息板对话、以及用户与支持代理创建的工单。
  4. 我们可以使用即席分析平台支持自定义查询。例如把案例中的 Workflow Upgrade Errors 与用户采用的各种特性做关联分析。
  5. 采纳指标:我会做 A/B 测试并统计总反应数(包括原点赞和新增反应),判断互动是否提升。同时我也会看评论量变化,因为一个担忧是有意义评论被挤掉。(当然,有人会说“恭喜!”这类简单评论减少无妨,但更细致评论下降就不理想。) 价值指标:再做一个 A/B 测试。把一部分人设为对照组,让他们看不到他人帖子上的新反应。收到新反应的发帖者,或看到他人收到新反应的用户,后续发帖是否增加?这能反映他们是否更愿意分享。第二个思路是做帖子情感分析,观察语气类型是否更丰富。
  6. 大多数社交网络会追踪“停留时长(time spent)”这个 KPI。我猜反应功能会提升发帖者和阅读者双方的该指标。

III 探索

让我们回到产品周期的起点:要么从零开始,要么在最近一次交付阶段的反馈基础上开启新一轮迭代。

由于场景由两部分构成——人物画像与模拟——本部分因此分成两章。第一章里,我们会访谈客户,并补全我们将服务的人物画像描述;第二章里,我们会围绕用户想做的事构建完整故事,并将这些故事转化为按优先级排列的需求。

6 理解你的目标受众

我们不能只靠直接问客户“你需要什么、想要什么”。认知偏差会干扰客户稳定、可靠地回答这些问题。相反,我们要在具体故事语境里倾听他们的需求、痛点和渴望。

— Teresa Torres,Continuous Discovery Habits 作者

产品思维工程师最关键的视角切换是:为某个具体的人而构建。了解客户后,我们才能选择彼此互补的功能组合(我们会在第 8 章详细设计),并把精力优先投入真正需要的需求上,例如延迟、可扩展性与安全性(第 9 章)。

提示

为某个具体的人而构建。

这句话听起来很简单,但在实践中要做到,必须持续、主动地投入注意力,因为还有很多事情在争抢我们的精力。像海獭一样,我们要潜下去觅食,也要浮上来换气。

在这一章,你会认识你的目标受众。眼下我先聚焦于你与潜在客户的初次互动:访谈他们、了解他们,并判断谁属于你的目标受众。(对比而言,在第 5 章里,我们是在产品较后期版本上迭代,咨询的是现有用户。)

你会用这些信息构建人物画像(以及非人物画像),以指导整个团队的工作。这些方法能帮助你形成清晰愿景,并在重大决策上做出正确选择。

真实用户,而非稻草人

正如我在第 1 章提到的,真实的人类有一些普遍特征,例如健忘、只会把有限脑力分配给你的产品、而且很忙。这些经验法则通常有用,但它们和所有经验法则一样,不总是对的,也远远不够具体。如果你没有完整的人物画像来支撑决策,就很容易不小心为自己而构建,或为你上一个产品服务过的人物画像而构建。

就像你能在模拟中找剧情漏洞一样,你也能识别不可信的角色。问问自己:“在这个阶段,会是哪种人使用我们的产品?”有时,如果你足够诚实,你脑中的角色会像这样:

  • 非理性行动者(The Irrational Actor):行动与其动机相矛盾。手头拮据的省钱党却买 8 美元拿铁;或某个网红明明每月靠已有规模的平台赚几千美元,却因为你送 10 美元亚马逊礼品卡就去用你那个空荡荡的社交网络。
  • 梦幻理想用户(The Manic Pixie Dream User):无缘无故就爱上你的产品。她愿意在社交媒体上告诉所有朋友,而且她的感染力会让朋友也来尝试。你每发一个功能她都会立刻采用,并发布截图和使用视频。
  • 苦修僧用户(The Stoic Monk):整天在修道院里用你的产品。无论你扔给他多难的东西都愿意学;即便功能耗时也照用不误;就算产品频繁变动、稳定性很差,他也会以禅定般的平静继续重试。
  • 你的克隆体(Your Clone):和你一样了解系统,因此不需要任何发现机制。由于熟悉内部实现,他对反直觉行为的容忍度异常高。这个“克隆体”正体现了第 2 章提到的“知识诅咒”陷阱。
  • 你爸妈(Mom or Dad):是真实的人,但很可惜,你最多也就这一个或两个样本。

我把这些叫作“稻草人用户”,和我们在第 1 章批评动机薄弱场景时讨论的是一类问题。

人们之所以会脑补稻草人用户,常常是因为其他连自己都未意识到的偏见。

偏见很危险

最近给一位社交卡牌游戏创业公司的老板做咨询。他的创业论点是:行业头部产品安于现状、只顾攫取价值。但当我问“什么会驱使用户切换到你的产品”时,他的核心论点其实是:“大家会来我们网站,因为我们有视频聊天。”可这意味着用户还得说服朋友离开熟悉的既有平台界面。

他们会吗?这论点倒也不荒谬。

事实上,我这位朋友是从前任老板那里接手了视频聊天功能。他已经为这家公司投入资金,工程团队资源有限,因此他需要视频聊天成为杀手功能,因为这是他手里现成的牌。可它真的是这类卡牌玩家最想要的前两三个功能之一吗?重要到足以让大家从旧站点迁移出来吗?没人真正知道。

我另一位朋友也在同一赛道,和同一家头部公司竞争。他设计了创新且有吸引力的锦标赛形式,并邀请几位知名高手参赛,在 YouTube 上直播带品牌的视频。这些视频吸引了想挑战高手的新用户。关键在于,这是游戏的单人模式,意味着它可以在“用户的朋友尚未都在站上”之前先获得增长势能。这个单人产品随后实现了自我维持,并把用户导入多人模式,而他希望多人模式能随时间增长。

第一家公司在大量辛劳和资本浪费后倒闭了。第二家到目前为止还在坚持。至少他的策略建立在一组连贯的用户动机上。我认为他有机会成功。

我们必须持续避开那些遮蔽判断的偏见。把用户是谁写得清楚、诚实,并文档化后在团队内共享,就像船长手里的 GPS。它会持续把我们导向目标,即使偏见、权宜、政治、异想天开、逻辑谬误和自负这些“侧风”不断把我们吹离航线。

这一章里,我会先介绍用于评估产品是否可行的科学思维。

接着,我会通过用户访谈(如客户发现访谈和销售电话)帮助你构建“用户真正想要什么”的图景。

然后,我会讲如何构建人物画像并将其写下来,与团队共享、达成一致。

接下来,我会带你在多个可能的人物画像中做优先级,形成产品的目标受众。

最后,我会触及一个出现频率高得出人意料的话题:多边市场,也就是由多个相互作用的人物画像组成的社区,例如网约车司机与乘客。

为了展开讨论,我先给出一个贯穿本章的案例研究。

案例研究导入

在 2010 年代初期,我在 Facebook 参与 App Center 的设计和发布。它是一个供用户发现社交应用(主要是游戏)的入口。概念上,它类似现代应用商店:有精选应用和排行榜。那段经历是我成长为产品思维工程师的关键起点。

我选择这个项目作为展示,是因为它凸显了“用户侧人物画像 + 开发者侧人物画像”的有趣组合,而这正是我们战略决策的驱动力。这个产品之所以产生影响,是因为团队里的工程师、PM 和设计师都理解了目标受众,既推动了 Facebook 上的游戏业务,也提升了开发者的收益。这个成功持续了几年,直到移动游戏这台巨型战车席卷一切。

后退一步,拿起科学这把武器

产品论题定义了你要解决的用户问题、你要帮助谁,以及你打算如何解决的有说服力思路。

我们对 App Center 的起始高层产品论题大致是:

App Center 将帮助社交游戏玩家发现朋友正在玩的优质游戏,并通过激励应用开发者构建高质量应用来争夺 App Center 的展示位。这将反过来壮大 Facebook 游戏生态。

我喜欢用“论题(thesis)”这个词,因为它有科学味道。你也许正斗志满满、迫不及待想开工,但你应该先把自己的“心头好点子”放一边,换成好奇、科学的心态。临床药物试验是个很贴切的类比,因为和药物试验一样,大多数产品都会失败。

我们会采纳实验科学方法中的两个方面:零假设,以及避免混杂因素。

“零假设”主张“干预不会产生效果”。实验结果可以“证伪”零假设。

在产品语境里,我们把它称作产品反命题:客户不需要、不想要、不会从中受益,或根本无法使用你的产品/功能。

你可以进一步补全这个反命题,写出其可能成立的原因。例如:“社交玩家早就通过通知和信息流等更具病毒传播性的机制接收朋友的游戏邀请。App Center 带不来足够流量,影响可忽略。”

随着我们进入更具体的场景,会持续细化这个反命题。

思考反命题会让你保持怀疑。如果你把所有脑力都用来证明产品论题成立,你总能继续找到它会成功的理由。有了反命题,你就会在客户说出那些你不想听的话时,听得更认真。

接下来,我们会把这种科学心态带进客户访谈和问卷中。

客户发现

在你构建产品或功能之前,先弄清什么会驱动用户使用它,或让用户远离它。当足够多用户共享相似的激励与能力条件时,就会汇聚成一个人物画像。人物画像在早期对于判断“该和谁聊”至关重要,在后期则用于测试大量不同的产品与功能想法。

客户发现的目标是发展并迭代你的产品论题。为此,你要弄清:人们遇到了什么问题、谁在遇到这些问题、为什么会遇到。

你可以从一个具体产品想法起步,但别过度执着,因为你可能必须调整计划。

你探索的是问题空间,多于解法空间。你要进入被访者的思维世界,尽可能多地学习。

客户发现可以通过多种方式进行:访谈、销售电话、问卷都很常见。

我会逐一介绍这些方式,然后总结它们如何在产品周期推进过程中,帮助我们建立可长期借力的高价值关系网络。

但在这之前,最先的问题是:你到底怎么拿到访谈机会?

如何拿到访谈

访谈对工程师来说是最易接触的机会之一,在产品周期各阶段都有帮助,尤其在发现阶段。但对很多工程师来说,最难的一步恰恰是“约到一位潜在客户访谈”。

下面是一些可能有用的方法:

  • 借助更面向客户的角色,例如用户体验研究员或产品经理。或者从销售组织里找客户经理、客户成功代表、市场人员或解决方案架构师。问问能否旁听他们现有会议,并在末尾留几分钟让你提问。
  • 多和面向客户的同事建立关系,你就更容易要到专门访谈时段,他们甚至可能主动把机会带给你。你也可以提出提供技术支持,或介绍你的近期路线图。客户通常会觉得“工程师愿意花时间和我聊”很被重视。
  • 在我于第 5 章讲过的支持场景中,直接问用户是否愿意接受访谈。若你此前帮到过他们,他们通常很可能答应。
  • 借助那些“知道很多人想要什么”的人。对 App Center 访谈而言,我们可以去找游戏工作室的人,他们通常知道他们用户想要什么,或能把我们引荐给该聊的个人。
  • 发放问卷,并用它筛选哪些人愿意接受后续访谈。我们会在介绍完访谈后讲这类问卷。

客户发现访谈

客户发现访谈(CDI)是与现有或潜在客户进行的结构化对话。

一次完整 CDI 有三个基本目标:

  1. 理解用户的根本动机与处境。这会解锁你的创造力,让你在最初想法不对时,仍能提出其他解法。了解用户完整的问题集合,才能设计整体化方案。
  2. 探出动机强度。许多产品失败,不是因为用户完全不想要,而是因为他们只“有点想要”。
  3. 若你已有具体产品想法,邀请被访者发挥创造力,对你的产品想法、草图(mock)或原型给反馈。这样你能发现那些自己尚未意识到该问的问题答案。

即便有 60 分钟,这也是很大范围的内容,而且很容易跑偏。我最核心的建议有两条:(a) 采用好奇、科学的心态;(b) 让访谈从“聊被访者”慢慢过渡到“聊你的产品”。

这两点都需要展开说明,我们先放到显微镜下看清楚,然后再把这些洞见应用到 App Center 的例子里。

客户访谈漏斗

把 CDI 想象成一个漏斗。开头你在用户世界里,聊他们的故事、问题和愿望,你在锚定他们的人物画像。随着访谈推进,你逐步把他们带到你的产品世界里,对产品提出越来越聚焦的问题,如图 6-1所示。

A time-based graph of an interview progress.
图 6-1 客户发现访谈漏斗

是的,我喜欢把自己的产品想法想象成克苏鲁这类不可名状的存在:如果我不小心,就会让用户看见“看过就回不去”的东西,永久污染他们的判断。我未必会让他们发疯,但我确实可能让他们的反馈带偏。所以,在我正式抛出产品/功能想法前的那几分钟非常宝贵。

这一节里,我把这个漏斗分成前期、中期和末期,来说明访谈如何推进。

访谈开始时,先让被访者感到舒适、愿意分享,因为你要大量追问他们的经历。

尤其在前期,尽量避免把他们引向特定答案的问题。

提示

你对被访者提示越少,他们的回答就越可信。

如果被访者一进来就主动提出你要做的功能,这是远强于“你先展示功能想法、他再说喜欢”的信号。

会影响用户的人不止你自己,他们也可能缺乏自我觉察,或难以预测自己的行为。你当然可以直接问需求,但更好的做法是让他们讲与产品论题相关的近期故事。

让用户讲故事有很多优势:

  • 用户会停留在具体情境中,告诉你他们实际做了什么,而不是无意中拼出一套偏斜叙事,去描述“他们以为自己想要什么”或“他们希望自己会做什么”。
  • 走一遍具体故事能帮你构建场景,指导后续设计工作,我会在第 7 章进一步讨论。
  • 故事会唤起用户记忆,带出他们体验中的关键细节。

在访谈前期,用户可能会提出你没考虑过、但他们很在意的问题。要愿意跟进,因为这可能意味着你当前还没在解决正确的问题,应该调整产品论题。

对 Facebook 游戏而言,你可以这样开场:“你最近在 Facebook 上玩游戏或发现游戏时,有没有什么反馈?”

当用户把最先想到的内容都说出来后,你可以再问一些用于识别人物画像的问题,比如:“你喜欢哪些游戏?”这能帮助你判断他偏好解谜、社交等哪类游戏。

在中期,仍然留在用户语境里,但要收束到你预设的话题,询问你原本要解决的具体问题。对方可能根本没有这些问题,这也是有价值的信号,会喂给产品反命题。也许他不属于你的目标受众;也可能他属于,但你并没在解决对的问题。

像“聊聊你典型的一次游戏过程”或“你通常怎么决定要不要试一个游戏?”这种更具体的提示,能帮助我们理解用户面临的问题和驱动他们的动机。这会加深我们的人物画像,也会为用例模拟补充细节。

中期里,很多人天然会倾向配合、友善。你要确保问题表述中立,不让对方感觉你期待正面或负面的某种回应。

到这个阶段,你已具备基于对方回答构建客户人物画像所需的原料。

然后是可选的末期:如果被访者看起来确实能从你的产品想法中受益,就描述该想法并请他反馈。视觉化能让内容更具体,所以若你有草图或线框图(mockup),就拿出来。

这个环节帮助用户判断你的产品想法是否适合他。你可以先描述一个使用场景来表达意图,再问这是否听起来能解决他的需求。

最重要的是,营造一个欢迎其表达观点和困惑的空间。

这个末段本身也可以按漏斗推进。

我们要弄清:每个游戏条目该展示哪些信息;以及分享游戏行为信息是否有隐私担忧。

第一步可以给他们看一份功能列表(比如:游戏类型、星级评分、正在游玩的好友、描述、热度),让他们选最重要的项。

然后再更具体一些:“这是我们在打磨的一个粗略概念,你怎么看?”

假设我们担心玩家不希望朋友看到自己的游戏行为,如图 6-2所示。

我们不会直接问“你在意隐私吗?”这种问题过于引导。但如果用户自己说“呃,这是不是意味着我朋友会看到在玩什么?”这就是强信号。若足够多人提出,我们就能形成“隐私敏感型”人物画像。

如果没出现这种自发表达,我们可以让他们打开 Facebook,调出最近玩过的游戏列表,问:“这里哪些游戏你愿意推荐给朋友,或拉朋友一起玩?”

他们的回答会如何转化为产品改动?

  • 也许用户不想让别人看到自己只玩了 5 分钟就弃坑的游戏,那我们可以要求达到一定游玩时长后才允许分享。
  • 也许某些游戏是他们的“罪恶快感”,那我们可以允许按游戏粒度关闭分享。
  • 也有人完全不想被别人知道自己“在浪费时间玩游戏”。我们可以让其默认不分享任何游戏活动。

开放问题通常比较耗时,而漏斗各阶段又必须精心编排,所以时间管理非常关键。下面说说如何为高效访谈做准备。

为问题写脚本

一点结构化设计能大幅提升你的时间管理效率。准备一份访谈提纲,在访谈推进时随时参考。

你可能会自然地想带着“按顺序要问的十几个问题”进场。但那会让你在用户说出有价值信息时没有时间深挖,还会让你听起来很机械。如果你发现自己想覆盖很大范围、又想让每个受访者都回答一致的问题,那其实你需要的是问卷。

当客户说到有意思的信息时要深入追问;而当某话题对该用户不相关时,不要浪费时间。也不要掉进这个陷阱:“那如果你想要 ,你希望它怎么做?”

你的访谈提纲可以包含:针对用户背景的一两个开场问题;针对中段“用户与产品交集”的一两个问题;以及末段要展示的一项产品材料。或者,如果你偏好充分准备,也可以按不同对话走向准备一组候选问题。很多问题会是条件触发,不必计划全部问到。

即使有几场访谈让你“失控”了,也不用焦虑。做多了你会逐渐形成节奏感,知道何时该把对话往前推。况且,访谈开头的内容通常信号最强。

响应分析

访谈之后,我们要总结每场的收获。应判断用户回答给出的信号强度,以评估不同因素的重要性。

以 App Center 为例:如果我们想判断星级评分是否有价值,用户可能以不同方式表达,而这些表达有不同信号强度,如表 6-1所示:

提示问题 客户回应 信号强度
你通常怎么决定要不要试一个游戏? 我真希望能有星级评分。
聊聊你典型的一次游戏过程。 我最近试的几个游戏都挺烂。
你对 Facebook 游戏有什么反馈吗? 很难判断哪些游戏好。
说说你最近一次希望应用里能看到星级评分的场景。 当然有。就前几天,我……
请按重要性排序:类型、星级、在玩的朋友、描述、热度 星级排第三。
你觉得应用里的星级评分有帮助吗? 嗯,有吧。
表 6-1 星级评分反馈

由于每一步提示都会降低信号强度,我们会把后面的提问延后,先争取高信号。

漏斗前段得到的动机信息也会给这些建议提供语境。如果某用户本来就不太热衷尝试新游戏,我们就没必要像对“新游戏重度探索者”那样重视他对星级评分的看法

现在把视线转向另一个客户机会:销售电话。

销售电话

工程师有时会被邀请参与销售电话,提供技术解答、建立专业形象、介绍路线图,或评估满足客户特殊需求的可行性。你完全可以把销售电话用于客户发现,因为销售人员想了解的很多人物画像信息,和你想了解的是重叠的。

不过,销售人员的目标和你不同。你的目标是找准目标受众和产品路线图,他们的目标是卖出具体产品。你可能规划的是一年后的发布,而他们往往希望在未来一两个季度拿下订单。

务必在会前对齐访谈目标,并请销售同事明确告诉你“哪些该说、哪些别说”。提前对齐会让他们更放心地把你带进这些高价值对话。对你来说,也要明确你在路线图上愿意承诺什么、不愿承诺什么。

进入会议后,销售代表希望你帮助建立客户信任,而你做到这一点的方式是:共情客户并为其需求发声。客户通常能察觉自己是否在被操控、是否只是在听一套“销售话术”。

简而言之,在销售代表设定的边界内,仍然使用你那套科学方法。要愿意接受产品反命题可能成立:也许你的产品解决不了这个客户的问题;也许它还需要继续打磨才能适配该客户。现在说清楚,总比经历痛苦的集成和拉扯谈判后才发现问题要好。

在 Facebook,我们希望吸引成熟且高质量的游戏工作室入驻平台,我们觉得 App Center 会是展示其游戏的好位置。但如果开发者真正渴望的是稳定 API 和更好的内购支持呢?按漏斗思路,我们应该先听,再揭示自己的产品想法。

我们也要坦诚说明“不适配条件”。例如,我们不希望那些做图形密集型游戏、且其游戏无法在多数机器浏览器里运行的开发者加入。把边界提前说清,能确保双方都不浪费时间。

客户发现问卷

客户发现问卷能比访谈覆盖更广的用户需求,但它不如访谈灵活,也不够深入。关键是,它可以用来筛选“可访谈对象”。

这类问卷本质上是访谈的压缩版,因此很多建议相同:先从开放问题开始,尝试识别人物画像;关注具体行为和场景,而不是抽象意见;并尝试判断优先级。

一份好的问卷还应让客户有兴趣填写。视受众而定,你可以给激励,或者直接告诉他们:你们在认真倾听,而且他们有机会影响产品方向。

最后,问卷要能筛出愿意进一步交流的人。正如我前面所说,这是生成访谈线索的好办法。

App Center 的问卷可以长这样:

Facebook Games 团队想听到你的声音!我们正努力让你更容易发现优质游戏。为了指导我们的功能路线图,我们希望了解你喜欢什么、不喜欢什么。

  • 你是否尝试过在 Facebook 上玩游戏?
  • 你最近在 Facebook 上发现/游玩游戏的体验,有什么反馈?
  • 这对你的体验满意度有多重要?
  • 请列出几个你非常喜欢或玩得很多的游戏。
  • 想想你最近注意到的一些游戏。你通常如何决定是否尝试?
  • 想想你最近尝试过的游戏。你是怎么发现它们的?你觉得怎么样?

你是否愿意和团队成员聊聊,分享更多?

这种结尾式行动号召不是我们拓展关系网络的唯一时机。在 CDI 和销售电话结束时,我们也可以做同样的事。

与被访者建立关系网络

客户访谈是非常重要的关系网络建设机会。

第 5 章中我们讲过,如何在用户引导下持续迭代产品。用户互动心态的一项关键支撑,就是你有一组可信任、可快速联系并获得反馈的人。

留意那些反馈深刻、典型体现某人物画像,或在你产品做出来后可能成为早期采用者的受访者。看看他们是否愿意保持联系。

他们甚至可能成为参考客户(reference customer):一个代表更广泛人物画像的真实个人或公司。他们可以作为设计伙伴,持续给你需求和反馈。后续营销也可以引用他们,向其他人证明“和你类似的客户已经在用我们的产品”。

接下来我会用一些经验法则总结这一节,帮助你设计提问提示语。

客户访谈经验法则

无论你是在准备客户发现访谈提纲,还是在制作客户发现问卷,都要记住几件事:

  • 让被访者感到自己可以放心表达观点、反馈和困惑。
  • 你引导越少,回答越可信。把这点作为访谈早期的最高优先级。
  • 问中立问题,剥离你自己的预期和偏好。
  • 多问具体情境,少问抽象意见和猜测。“给我讲一次……的经历”通常是很好的开场。

访谈或问卷结束后,及时写下收获。并根据你拿到的信号强度标记轻重缓急,这样你会记得问题的紧迫程度。

最后,充分利用部分受访者提供的人脉引荐,推进后续交流。

下一节,我们将基于访谈和其他研究结果,构建目标受众。

构建并传达目标受众

做过一些访谈之后,再结合市场研究等数据,我们就可以头脑风暴不同产品概念,并将其与目标受众进行匹配。必要时,再围绕这些人物画像迭代产品论题。

第一步,我们要对“瞄准谁”做出有吸引力且可落地的选择。

第二步,我们要把这些选择有效传达给团队,让所有人买账。

选择目标受众

第 1 章中,我给出过场景角色的组成元素:

  • 人物画像:人口统计特征 + 一组能力条件(means)的组合。
  • 使用你产品的动机。

目标受众是人物画像的泛化版本。这些潜在用户应当:

  • 基于真实世界的人群特征。
  • 拥有显著的使用动机,尤其是相对竞品而言。
  • 具备购买和使用产品的能力条件。
  • 不存在强烈的“不使用”动机。

如果你的产品是全新产品,短期目标受众通常需要保持较小。这是因为你很难同时激励“所有地方的所有人”。一般你需要找到一个未被现有产品充分服务的核心人群并聚焦。后续可扩张,我参与过的很多产品都走过这条路:

  • Facebook 最初服务美国大学生,随后扩展到高中生,再到所有英语用户,之后才全球化。
  • Stripe 先用“7 行代码”吸引创业开发者和技术型创始人,之后再扩展到企业级使用,并为非开发者与 vibe coder(凭感觉写代码者)提供无代码方案。
  • Temporal 先聚焦在大型科技公司的 Go 语言基础设施工程师,再扩展到使用其他语言的金融和产品工程师,后来又加入企业与 AI 功能。

幸运的是,人们的需求并非随机分布,它们常常以互相增强的群组形式出现。要收敛到更小人群,就在访谈里寻找动机的相关性组合。

在 Facebook,经过大量访谈和受众分析,我们看到玩家大致分成两组:休闲社交玩家,以及所谓“中核(mid-core)玩家”。中核玩家会被硬核玩家常玩的复杂战术或策略游戏吸引,但不愿投入同等的时间与金钱。

社交玩家往往更在意社交语境、可炫耀性,以及“很多朋友也在玩”。

中核玩家则往往更注重隐私、享受竞争,并追求评价良好的高质量内容。

在本章后面,我们会利用这些分组来选择功能组合。现在先把这些受众转化成可沟通的形式。

用人物画像达成一致

如果团队每个人都在想着同一类用户、同一组需求,那么每个人都能做出好决策,至少能做出方向一致的决策。产品越复杂,全员用人物画像保持同频就越关键。

当你把目标受众写出来,你就把他们的需求集中到一个地方。然后可用它来对齐“什么算成功”。如果你只满足了受众三项基本需求中的两项,那就还没完成,还不到发布的时候。

另外,也要明确你瞄准谁。这些叫作非人物画像(nonpersonas)。

非人物画像能帮助你避免加入那些无助于当前目标、却会拖慢开发的额外工作,也就是常说的范围蔓延(scope creep)。

为了传达人物画像和非人物画像,你可以在共享文档(例如幻灯片)里写出易记的人物画像,再配上角色小传来补足其使用动机。并附上访谈记录链接,以便别人质疑“这些画像怎么得来、是否真实”时可追溯。

让人物画像更易记的一种方式,是给他们起醒目、富画面感的名字

Mort、Elvis 与 Einstein 之争

我第一次接触人物画像是在 2005 年,当时我在微软开发者事业部(DevDiv)工作。这套画像后来泄露到公开网络,成了颇有争议的梗,但它们其实非常有效。直到今天我仍把它当作人物画像实践的优秀案例,因为它在一个大型工程师与产品经理组织里实现了高效沟通和对齐。DevDiv 负责 Visual Studio,尤其是 Visual Basic、C#、C++ 的编译器、运行时和 IDE。他们的人物画像大致是:

  • Mort:机会导向型开发者,喜欢快速拼出可用方案来解决眼前问题,强调生产力,边做边学。编程可能只是他主业(如会计)之外的补充技能。
  • Elvis:务实且忙碌的应用程序员,围绕问题域构建具体且可长期使用的方案。在解决方案的过程中学习,以便尽快转向下一个问题。
  • Einstein:偏谨慎的系统程序员,需要构建通用且高效的方案,通常会在动手前先做较充分学习。

各团队被要求在构建其库和体验时瞄准不同画像。Visual Basic 的目标受众是 Mort,C# 是 Elvis,C++ 是 Einstein。

当这些画像被公众知晓后,引发了反弹。开发者觉得自己不该、也无法被塞进这种简单分类。没人想被叫作 Mort,因为在英文文学里,这个名字常用于笨拙角色。

如果从“人物画像及其在产品开发中的目的”来看,这件事就更说得通了:

  • 虽然大家拿 Mort 开玩笑式贬低确实刺耳,但“起醒目名字”这个方法本身是对的,并且确实帮助 DevDiv 达成了对齐。
  • Mort 这个名字并非贬义,而是为了反复提醒 Visual Basic 团队:持续简化抽象。
  • 一个人在不同情境下可能对应一个或多个人物画像。有人写小型效率脚本时像 Mort,给固件代码库提交代码时又像 Einstein。
  • 人物画像不是刻板印象。它用具体、可记忆的语言去激发并指导设计。真实用户远比画像多样,产品设计者必须始终意识到这一点。Einstein 画像不应成为把 C++ 做得不必要地困难的借口。
  • 让不同语言聚焦不同画像,使这些产品既完整又有差异化。

现在把这些方法整合起来,为 App Center 写一些角色小传

App Center 的受众画像

前面提到的社交玩家和中核玩家都在下面体现了。我还加了第三种画像“鲸鱼玩家(whale)”,这是免费游戏行业对“付费金额很高的玩家”的称呼。这个行业的大部分收入来自少数玩家,因此我们必须覆盖这类画像,因为我们支持的游戏其商业模式依赖他们。

  • Penny Pincher:看重与朋友保持连接的社交网络用户。她把游戏当作消磨时间、和朋友一起玩的方式,但并不特别好胜,也不把自己当“玩家”。因此她偏好轻量游戏:要么映射现实生活的某些侧面,要么呈现更轻松生活的现实幻想。她只玩免费游戏,偶尔看广告可以接受,但不会为游戏内附加项付费。
  • Moby(鲸鱼玩家):重度社交游戏玩家,做什么都深度投入。他喜欢和朋友一起玩,并追求高成就感,不管是通过对抗还是合作。他愿意为能向朋友展示的游戏内升级,或能加快进度的“省时项”付费。
  • Patton(名字取自二战时期美国将军):投入度高、已在 PC 平台玩游戏,但又抗拒高昂价格。他想玩有厚度、有策略性的游戏,但不想先花 1000 美元配游戏主机,再买一款可能并不喜欢的 AAA 硬核游戏。他重视更便宜的“中核”游戏,更看重游戏质量与沉浸感,既喜欢和玩伴一起玩,也能接受单人游玩。只要能先试后买,他愿意付费;口碑好的游戏他也愿意尝试。

这些角色代表了常见人群类型。它们也展示了玩家从游戏中获取的价值,以及他们愿意为此交换什么,这能帮助我们评估“投入这些画像是否有商业价值”,下一节我会继续讲。

我用了具体、带立场的语言,包括明确性别和金额,以激发更强画面感。传达受众时,具体化很重要,这和“虚构角色比干巴巴属性列表更容易被记住”是同一原理。

如果我们不承认现实中的用户更为多样,就会把人物画像滑向刻板印象。这些角色描述的目的是激发有价值讨论,而不是形成狭窄、教条的属性清单。

这些受众会如何影响我们的策略?

Patton 是最具不确定性的画像,也是我们最希望引入 App Center 的画像。我们知道 Patton 数量少于 Penny,但幸运的是我们有参考客户:平台上的 War Commander 就服务 Patton,并且单用户收入表现很好。它成功到让我们相信,可以通过吸引并重点展示更多质量过硬的游戏来复制这种成功。

当然,既然是市场平台,硬币还有另一面:我们也需要“游戏开发者”的人物画像。War Commander 的开发商 Kixeye 代表了中核游戏开发者画像,而他们的需求会和 Zynga 这类休闲工作室(开发了 Farmville 和 Cityville)明显不同。

Kixeye 是一个参考客户。通过和他们交流、理解其需求,我们能更好理解类似工作室。再借其成功案例背书,我们就能吸引更多游戏工作室来和我们合作开发。

人物画像还能提供对用户需求的整体视角。把 Patton 从 Moby 和 Penny 中分离出来,也提醒我们还有很多工作要做。Patton 在沉浸感、质量和游戏类型偏好上都不同于 Penny。若想构建完整方案,我们在设计 App Center 时必须从整体上思考。

反过来,如果我们只是收集中核玩家或工作室的零散功能请求,却不把他们当作独立画像来处理,就会做出“半套方案”,永远达不到临界规模。

有了参考客户和一系列凸显 Patton 画像的客户发现结果后,我们现在可以把产品论题写得更有立场、更具体:

App Center 将帮助社交玩家,尤其是 Patton 这类玩家,发现朋友正在玩的优质游戏;并激励游戏工作室通过构建高质量应用,在更多元的类型里竞争 App Center 的展示位。这将进一步把 Facebook 游戏生态拓展到新的用户群体。

稍后我们会基于这个新论题为 App Center 选择一组连贯功能。在此之前,我想先说明:明确“我们不服务谁”为什么同样重要。

非人物画像

App Center 团队需要明确一个非目标受众:硬核玩家。他们对休闲游戏兴趣较低,因为他们愿意投入更多时间和金钱去追求高端体验,而这类体验当时在浏览器里也无法提供。这并不是说“硬核玩家绝不会玩 Facebook 游戏”,而是说团队不会为吸引他们投入特别精力。当有人提出看起来是为硬核开发者设计的功能时,我们会降低优先级,并能清楚说明原因。

如果当年我所在的 Games 团队也明确了非人物画像,会对我很有帮助。我是竞技桥牌玩家,经常和陌生人打线下与线上锦标赛。我也是《Words with Friends》的高水平玩家,当时一直难以找到有竞争力、且在线的对手好友。

我在 App Center 之后的“心头项目”是做一个基于水平匹配的对战服务,让玩家能和实力接近的陌生人打竞技对局。我围绕这个提案花了不少时间,但始终没说服管理层;而他们也说不太清原因,除了“优先级不够高”。

如果当时有“硬核玩家是非人物画像”这个定义,我会更清楚,也能省下很多时间。这类人热爱游戏到愿意和陌生人对战,而 Patton 更想和朋友玩。匹配对战对 Patton 吸引力不大,所以继续投入匹配并不划算。

现在把视线转向功能选择。随着我们想象并评估各种功能,也可能需要回头调整受众选择,因为我们会逐步知道这些功能究竟有多可行

基于目标受众选择功能

我会给一个功能选择示例,说明它如何与产品论题、目标受众相互作用。

假设 App Center 的应用列表具备以下能力:

  • 推荐新游戏,例如编辑认为优质的作品。
  • 基于质量、好友活跃度、以及与其既有游戏偏好的相似性,为每位用户生成个性化排序。

粗略来看,我们这三类人物画像可能都会喜欢这些属性。大家都在和朋友玩,也都在某种程度上偏好好游戏。

围绕这个功能组合的产品论题可能是:“为 Facebook 用户提供一个发现与自己相关的优质游戏的场所。开发者会被激励去做更棒的游戏,从而提升参与度,并通过游戏内购买和广告给 Facebook 带来收入。”

我们可以为每个画像写一个反命题来测试它:

  • Penny(低意图休闲玩家)可能不够主动,不会特意来 App Center;她更可能响应朋友的直接邀请。
  • Moby 比 Penny 更可能来,但他不算探索型玩家;他可能只会留在已投入很多的老游戏里。
  • Patton 可能一开始找不到足够吸引他的游戏,在 App Center 起势前就给出负面评价。

在我看来,Penny 和 Moby 的反命题更有说服力。我觉得这两类人中会有一部分使用 App Center,但听起来不像大爆款。Patton 的反命题则看起来可修复。

回看 Patton 的需求:沉浸感、可负担性、朋友关系、质量;以及其能力条件:愿意为喜欢的东西付费。

或许我们可以扩大一些功能范围,帮助 Patton 发现游戏:

  • 允许按类型筛选游戏。例如 Patton 可以筛掉 Farmville,聚焦策略类。
  • 为 App Center 定制并首发几款新游戏,让 Patton 一上来就有可玩内容。

保持聚焦

如果你想赢得某类客户,就要为他解决完整问题。对两个不同画像各服务一半,就像做一件 2XL 上衣却配 S 码袖子,技术上小个子画像也能穿,但他不会愿意穿。

举个 Facebook 游戏的“非互补”案例:一边做“允许游戏预付费”的商店,一边做“好友游戏邀请”(例如邀请朋友来帮你的模拟农场)。这两个功能单看都没问题,但给有付费墙的游戏做邀请,转化率通常不会高。它们也分别对应不同画像。长期看可以都做,但作为首发版本,这样的聚焦度不够。

构建互补功能听起来像直白的战略建议,但实践里保持聚焦很难:

  • 这三项功能未必是最容易摘的低垂果实,其他选项会更诱人。
  • 做休闲游戏的工作室很可能一直在催新功能;如果我们暂停这块去聚焦中核市场,他们可能会不耐烦。
  • 用户也可能在持续提出其他需求。

因此,广泛沟通并让所有人围绕目标受众达成一致非常关键。关于优先级,我会在第 7 章再展开

多画像产品

大多数成功产品最终都会服务多种人物画像。

一个常见二分是重度用户与普通用户。我在第 8 章会讲如何同时满足这两类。Moby 是重度用户,和 Penny 对照明显,但二者都属于社交玩家这一大类。

还有一种情况是,画像代表的是不同类别。Patton 和 Moby 都是高投入玩家,但细节差异明显。服务多样画像是产品做大规模的主要路径之一,就像电子表格如今服务营销、会计、科研等很多群体。它既具可扩展性,让不同职业能把它优化成解决完整问题的工具;又保留通用性。

每个人物画像都需要在产品周期的每一环获得专门关注:从访谈、优先级、测试,到营销、样例与文档。

这一节我会讲:

  • 当不同画像的动机彼此冲突时会发生什么。
  • 如何评估不同群体给生态带来的价值。
  • 因而如何决定把更多精力投向哪些画像。

当人物画像相互冲突

有时,不同画像存在潜在冲突需求,而产品要成功,必须把这些需求重新拉回一致。

  • 网约车司机与乘客:司机想多赚钱,乘客想要安全、便宜、准时的行程。
  • 博客平台作者与读者:作者想要更大受众和触达渠道,读者想要高质量内容和无垃圾邮件的收件箱。
  • Facebook 游戏开发者与玩家:开发者希望访问更多用户数据(例如好友列表)以实现病毒传播;用户希望保护隐私。

在对立画像间平衡不同激励,是最难、也最有价值的产品问题之一。

多开接单(Multiapping)

下面是个让人沮丧的激励错位案例。在美国,Lyft 和 Uber 是最主流的网约车应用。

有次我在 Lyft 上叫车。令我惊讶的是,我盯着地图上的点看司机何时到达,结果发现那个点在远离我,司机居然开走了!这种情况持续了几分钟,我最终取消订单。应用弹出对话框问我为什么放弃,里面居然有个选项叫“司机正在开离我”。

这激起了我的兴趣。司机为什么会这么做?而且这种怪行为为什么常见到值得专门做一个选项?

当天晚些时候我又叫了一辆 Lyft。车到了,我上车后发现司机仪表盘上固定着两部手机。后来在到达前几分钟,他打开了另一款网约车应用。我看到 Uber 在他把我送达前就给他匹配了下一位乘客。

一切突然说通了:司机在“多开接单(multiapping)”。我甚至能想象另一位乘客正盯着手机,一脸困惑地看着司机朝反方向开去,先来送我。

这是典型的激励不匹配:司机想同时跑 Uber 和 Lyft 以提高收入,而我只想要稳定、准时的行程。

Uber 和 Lyft 该如何解决这个棘手问题?我推测 Lyft 问我“司机是否在开离你”就是其整治尝试的一部分。据我了解,Lyft 与 Uber 会在检测到违规时发警告、暂停接单,甚至停用账号,从而把激励重新拉齐。

再看一个我挺自豪的 Facebook Games 例子。我们给 App Center 引入星级评分时,不希望开发者“刷系统”。开发者应该把激励放在做出好游戏,让更多用户愿意玩;星级评分正是约束工作室朝该目标努力的机制。

但工作室也要赚钱,所以他们可能采用各种灰色手段抬高评分。我们如何应对?

我们不希望开发者控制“何时评分、谁来评分”,否则系统会被操控。因此我们控制了展示时机与抽样方式:在 Facebook.com 的信息流和侧边栏向用户展示评分入口。这个无偏、随机抽样机制让喜欢和不喜欢游戏的用户都被覆盖,最终游戏评分分布广泛,通常在 3 到 4.5 星之间。这个评分看起来有意义。

Apple 在类似设计挑战上就做错了。开发者可以主动弹窗引导用户给应用评分。我猜你一定见过这样的提示:“你喜欢这个 App 吗?”通常就在你完成最有成就感的操作后弹出。并且只有你先点了“是”,才会被引导去评分。

以前 App Store 里知名应用出现低分并不稀奇;而现在除非有政治争议或重大事故,几乎清一色是 4.5 星以上。评分几乎失去参考价值。

任何软件生态不仅要有对齐的激励,也需要健康的画像组合。要判断“健康”是什么样,就得细致理解每类画像给社区带来的价值。下面继续。

理解客户价值

每位客户都会为生态带来价值。也许他在写内容、提 bug、提供商品或服务,或向公司付费,而这些钱可反哺产品改进。你要理解这些价值,再有意识地培育那些你希望其贡献更多的用户群体。

维基百科编辑人数相对读者很少,但影响巨大,因为没有他们,网站就会崩塌。

一个不那么直观的例子:在 Facebook Games,Penny 不带来收入,但这不代表她没有价值。她帮助游戏达到临界规模,从而吸引像 Moby 这类愿意付费的画像。这套逻辑在今天看似显然,但当免费游戏开始赚大钱时,外界其实很意外。很可能是某个产品思维很强的人先想透了这件事,并把这个商业模式讲通了。

价值还会随市场因素动态变化。在网约车场景里,当司机稀缺时,单个司机价值更高。提高司机收入会让更多司机上路,进而创造更多收益。

在竞争画像间做优先级

如果你的产品是多边市场,你就必须在不同“边”之间分配功能优先级。这是前文“选择目标受众”的一个特殊情形。

如果平台同时有内容创作者和读者,你需要持续拉动两边增长,但总会有某些时期,其中一边需要被优先关注。

在这件事上做错的产品会陷入麻烦。举个当下例子:stackoverflow.com,一个软件问答网站。历史上他们非常重视“回答者”画像,通过反馈机制、声望系统、专门讨论板和成就体系去激励。获得赞同票会让人感到自己有帮助。

然而,随着 AI 助手兴起,读者流量减少,回答者得到的“正反馈”也变少。他们的贡献依然同样重要,甚至更重要,因为 AI 正在读取并使用这些答案。但你怎么知道某个 AI 用了你的答案?如果贡献逐渐枯竭,AI 的回答质量也会因未回答问题积压而受损。

软件工程社区需要想办法重新平衡这些激励,多给问答贡献者一些正向反馈,同时把他们的答案更有效地送达用户真正提问的地方。

本章总结

软件团队应维护一份自己瞄准的人物画像列表。这些画像应包含用户背景和动机细节,并能说明他们为社区带来的价值。

如果你用这份画像列表来对齐团队,战略决策会容易很多。

  • 设计讨论会更顺畅,因为大家共享了对目标用户的理解。
  • 你会更容易为人们解决完整问题,因为画像细节会提醒你发现缺口。
  • 你可以从候选画像中选出目标受众,也就是对不同画像做优先级,从而更容易对功能做优先级。

要识别这些画像,你通常需要和用户交流,弄清他们想要什么。客户发现访谈是个好方法,但要记住:很多用户难以准确预测并表达自己真正想要、真正会用的东西。为了获得更高质量信号,要在提问里消除偏见,并努力探出用户信念的强弱。

练习

在这些练习里,你将围绕一个类似 Google Maps、Apple Maps 或 Bing Maps 的地图应用工作,名字叫 Applebingoo Maps。

你要做混合模式出行路线,也就是“自行车 + 公交”或“汽车 + 公交”的组合行程。你有两个产品论题:

  • 第一,你有使用数据表明多模态行程并不少见。你认为已有混合出行习惯的人,会因为可获得一体化路线的便利而转向 Applebingoo Maps。
  • 第二,如果多模态行程更方便、也更易被发现,人们会更多使用公共交通。你设想最好的提升方式是:提供先到公交/地铁站、再到最终目的地的完整路线。

你将访谈几十位居住在有高质量公共交通的城市地区的人。

做这些练习时,每完成一题就先看答案,再进入下一题。

  1. 首先,你要选择访谈对象。先设计一个筛选问卷的问题集,确保你能以合理样本量覆盖几个不同人群。你会问什么?会选哪些人?
  2. 在访谈第一阶段,理解被访者的动机与认知。请设计几个讨论问题。
  3. 你正在访谈一位偶尔或很少乘坐公共交通的人,想验证你的第二个产品论题(能否提升公共交通使用率)是否成立。你会问什么?哪些类型的回答能说明这个想法好或不好?
  4. 写一段人物画像,代表“你自己作为 Applebingoo Maps 用户”以及你对本地交通方式的总体态度。假设你被访谈过,且还有几个人给了和你相似的回答。记得给画像起个有趣名字。
  5. 最后,在访谈末段,你会如何获取对你这个具体产品想法的反馈?

答案

  1. 起步时先确认“公共交通是否可用于他们会进行的行程”。汽车和自行车也问类似问题。对这三种方式都问使用频率。样本应限定在“具备公共交通可达性,且有自行车和/或汽车”的人群。针对第一个产品论题,选择经常或偶尔使用公共交通的人;针对第二个论题,选择很少或从不使用公共交通(但理论上可用)的人。
  2. 我会先深入了解他们的出行模式。问题可分成“通勤”和“办事/私人行程”两类,因为这两类动力机制可能不同。下面以通勤为例,私人行程可类推:
  • “请你从高层次讲讲最近一次典型通勤。”这是中性提问,不是“你为什么不多坐公交?”;同时它是在做通勤模拟,可能暴露意外洞见,也可能解释其后续回答。
  • “你对通勤满意吗?为什么?”也许他们之前已经吐槽过,但如果没有,我要给他们表达机会。我想听他们是否会提到停车、拥堵、气候影响焦虑、行程时长等因素。也可能出现我没预料到的答案,比如公寓的汽车升降机经常坏,或在公司很难找到可充电车位。
  1. 我会这样提示:“聊聊你使用公共交通(用于通勤/办事)的可能性。”我不想太早用混合模式路线去“引导证词”,也不想让他们因为不坐公交而有负担感。预期会听到“太慢”“班次少或不可靠”等答案。对我的产品论题更有意思的是:他们是否对可用公交选项缺乏认知,而这恰好是地图能补足的。我也关注类似“我离车站太远”的回答,这时汽车或自行车可补齐“最后一段”。我还可能发现“支付公交费用很麻烦”或“流程不熟,迟迟没配置”。如果这类反馈频繁出现,也许团队该优先做与公交运营方的支付集成,而不是最初设想。
  2. 这是我给一位城郊铁路爱好者写的人物画像“城郊铁道 Stan(Suburban Rail Stan)”:“Rail Stan 是住在城郊、附近有几条铁路线路的有车用户。他偶尔乘坐公共交通,因为列车并不适配他的大多数行程。但当他进城时,会开车到换乘停车场,再坐火车进城。他喜欢这样做,因为能避免城市开车和停车压力,也因为这能降低碳足迹。他会在车上阅读或工作,因此即使总时长略长也不觉得是损失时间。地图应用不支持这个使用场景会让他很烦。”如果我写成“他把挫败感都发泄在软件工程书里的练习题上”,那就有点过于具体了。
  3. 我的目标是尽量让他们在接近真实世界的条件下进行模拟,以最大化反馈保真度。如果我有原型(可能是 AI 生成的),我会让他们拿平时通勤地点直接试用。最终我既要拿到对产品概念的反馈,也要判断它是否会改变他们的通勤模式。因此我会把混合模式路线与他们传统会选的路线并排展示,请他们比较

7 通过模拟发现你的产品

你必须从客户体验出发,再反推技术。你不能从技术出发,再去想你要把它卖到哪里。

— 史蒂夫·乔布斯

第 6 章中,我们找到了自己的客户。我们想弄清楚客户是谁、他们的问题是什么。通过这个过程,我们打磨出了一份产品论题(product thesis):即我们对“什么才是这群人眼中的优秀产品”的高层主张。

但我们暂时按住了对解决方案的深入思考。

在这一章,我们将打开闸门,把注意力转向产品发现(product discovery)。

也就是说,在构成用户场景的角色与模拟中,目前我们只有角色。现在该写模拟了。

但你想构建的这个产品或功能,还不存在。没人讲过关于它的故事。如果我们想第一次就尽量做对,就必须先讲一些微型科幻故事,预测用户将如何驾驭我们赋予他们的新能力。

于是,一场从产品之地走向系统之地的伟大旅程开始了。

从愿景到需求

软件开发里一个老大难问题是后期才冒出的产品需求:我们前面没看到它们。有时它们来自用户反馈,有时是我们在设计和实现时才意识到更多细节。这会拖慢路线图、扩大范围。而这类不受欢迎意外的一大原因,就是前期发现工作不够。

为了减少惊喜,这里有一条我在中大型项目中验证过有效的路线。每个团队工作方式都不同,所以我不打算规定某个流程或文档模板,而是突出我认为你应该关注的关键点。

  • 用用户场景补完产品愿景,这些场景叫北极星场景(north star scenarios)。
  • 把这些北极星场景切分重组,整理成需求,形式是用例汇编(use case compendium)。
  • 制作路线图,聚焦第一个里程碑,覆盖最关键的需求。
  • 提出你要在第一个里程碑交付什么。把细节故事写出来,称为用户流程(user flows);如果展示用户界面,也可称为分镜图(storyboards)。
  • 为你选中的每个用例补充详细系统需求,并在必要时回流到原始需求。
  • 基于用户流程与系统需求,整理一份待完成工作(jobs to be done)清单。

到这一步,你已经从发现过渡到定义。定义通常包含更详细的设计,我们会在后续章节里讲。

没有任何流程能让你无所不知,在周期后段才学到意外信息是自然的。但这些意外的规模应该小很多,并且最好不至于威胁产品生死。

图 7-1 展示了一个围绕产品发现组织流程的示例,它分成三份文档:

  • 产品简报(Product brief):一份愿景文档,用于让所有人对目标达成一致。有时也叫“问题简报(problem brief)”或“一页纸(one-pager)”。最少应包含:产品论题/反命题与目标受众(第 6 章)、要追踪的目标或产品指标(第 5 章),以及稍后会定义的北极星场景。在双钻模型(Double Diamond)术语里,它的目标是为发现阶段清障。
  • 产品需求文档(Product requirements document):展开产品应该具备的属性,不仅面向长期,也提出首个可交付物或里程碑,同时尽量减少对实现方式的过度规定。目标是为定义阶段清障。
  • 产品规格(Product specification):展示产品应如何设计。目标是为开发阶段清障。
.一个产品发现流程示例
图 7-1 一个产品发现流程示例

我并不是来推销某个特定流程的,而且我接触的大多数团队并不会把这三步都正式化。不过在本章里,我会用这个流程演示如何系统地从愿景走向可执行设计。即使你只把它用于个人工作流,我也希望它有价值。

无论你的团队如何运作,这里有三个关键属性值得追求。

第一,正式或非正式地把工作拆成多个阶段:为什么(why)、做什么(what)、怎么做(how),都从用户视角出发。粗略地说,阶段 1 聚焦用户为什么希望我们采取行动,阶段 2 聚焦用户在要求什么,阶段 3 聚焦用户将如何达成目标。每个阶段都要警惕过早陷入你现在还不必思考的细节泥潭。

第二,在每个阶段都用场景模拟来确保团队对齐并解决完整问题,这种做法叫场景驱动发现(scenario-driven discovery)。后面我会展开讲。

最后,要允许向前一阶段回流反馈。负责产品规格的工程师和设计师,需要和负责产品简报的工程师与 PM 紧密协作,让团队持续学习。没有这种反馈,你得到的就是“瀑布式”做法。鉴于软件开发的混沌本质,瀑布法已经逐渐失宠。它往往会导致:因为后面难改,所以前面过度思考;同时又无法适应信息变化。

这一章里,我会逐步走完整个产品发现流程。

过程中,我会重点讲一些与发现和优先级相关的重要概念:

  • 如何把用例组织清楚
  • 如何从产品思维过渡到系统思维
  • 如何批判性地思考产品给用户带来的价值,包括四条“残酷真相”
  • 如何整体评估构建与维护软件的投入规模
  • 如何用用户流程梳理你的待完成工作(jobs to be done)

作为开场,我先给出一个关于构建 AI 代理的案例研究。

案例研究导入

当我写下这些内容时,时间是 2025 年,也就是所谓的“代理之年(year of the agent)”,所以要是不放一个 AI 助手或代理案例就有点说不过去。这个案例是虚构的,但它融合了真实公司正在尝试的产品特征——目前谁也不确定最终会怎样。未来的读者:欢迎告诉我结果如何。

假设我们在 Kabletown(一家互联网和有线电视服务商)的支持工程团队。我们的团队使命是让支持服务运转顺畅。现有一套遗留自动化系统会先和客户聊天、尽量先帮忙再转人工,但它使用的是大语言模型(LLM)之前的技术,对于客户多样化需求过于僵硬。客户说他们讨厌 Kabletown 的支持体验,觉得自己被困住了。与此同时,人工客服被压垮,用户的邮件和聊天回复要等好几天。

我们注意到,前七类账户操作占了支持请求量的 65%,但当前方案实际自动化有效覆盖只有大约 15%。

先给出几个你需要知道的 AI 相关定义。

  • Assistant(助手):一种 AI,能在与用户的一次交互中回答问题、做分析并执行任务。
  • Agent(代理):也能做到上述能力,但还能围绕目标自主规划并行动。
  • Tool(工具):带有描述信息的函数,用于告诉助手或代理应如何调用它。

由于 Kabletown 还不确定自己需要 agent 还是 assistant,所以在不需要精确区分的地方,我会交替使用这两个词。

现在我们来填写产品简报的几个部分。

AI 助手产品简报

我们的自动化支持代理 Helpy McHelpface,将彻底改变 Kabletown 的技术支持与账户支持体验。

产品论题

我们提出两个基本主张:

  • 降低用户挫败感:用户对当前助手很不满意,它在大多数请求上表现糟糕,甚至连何时该转人工都判断不准,这项判断 80% 的时候都错。Helpy McHelpface 会比旧技术更好地处理与客户的直接沟通,因为它更能理解用户意图,从而降低用户挫败感。
  • 降低支持事务性负担:当前助手一旦要做变更就必须拉人类介入。如果给 Helpy 配置可操作账户变更、创建预约、下发寄送订单等工具,我们就有机会把剩余未自动化支持量中的一半以上也自动化。通过加速这些例行但耗人的工作,我们可以显著缩短等待时间、提升用户满意度,同时减轻支持代表积压。
反命题/风险

哪些因素可能导致结果不如预期?

  • Helpy 可能会执行它不该执行的动作,导致用户产生糟糕的意外体验。
  • 我们可能无法仅靠 assistant 达成目标,而需要一个自主规划 agent,从而抬高构建和运行成本。
  • 我们的知识库可能不足以提供 Helpy 所需上下文。
目标受众

我们有两个目标人物画像,不过可能会先做其中一个:

  • 账户支持用户:Tinker Tia 是 Kabletown 的长期互联网和有线电视客户,她想修改自己的账户,比如升级/降级服务,或新增/升级硬件。
  • 技术支持用户:Sad Lisa 是新加入的 Kabletown 宽带客户,遇到问题无法正常使用,希望尽快修好。
产品目标

我们的首要目标是提升用户满意度,并在不大量扩招的情况下改善支持积压。我们认为这两个目标可以同时达成。短期内我们不以提升利润为目标。

  • 采用指标(Adoption metric):把完全自动化支持交互比例从 15% 提升到 65%。
  • 价值指标(Value metric):把助手与人工支持后的客户满意度评分从 2.1 提升到 3.5(满分 5 分)。
  • KPI:利润中性。虽然我们希望长期降低人工支持成本,但也可能因为更容易降级套餐等因素压低收入。我们会跟踪降级行为以理解收入影响。
北极星场景

这一节我们稍后会回到。

用北极星场景补完你的产品愿景

我写北极星场景前,先解释一下场景驱动发现,然后再讲如何为产品简报做场景头脑风暴、筛选与打磨。

场景驱动发现

场景驱动发现(SDD)是指在整个发现流程中持续使用场景。我在发明这个术语,但不是在发明这件事本身——业界最接近的概念是“场景化设计(scenario-based design)”,只是它并不算广为人知。我希望有一个更直接表达“我们如何用场景去挖需求和待完成工作”的词。

你可以依靠场景来确保最终交付给用户的是完整而有用的产品,原因如下:

  • 通过突出角色需求,它能把为什么讲清楚,确保需求与原始意图一致。
  • 它能照亮完整故事,因此我们更可能发现重要功能或边缘情况。
  • 相比抽象需求清单,故事更不容易被误读。一套共享故事更容易让团队对齐。
  • 在场景里找情节漏洞,比在需求列表里“调 bug”更容易。

产品简报里的场景通常被称为北极星场景

说明

北极星场景是你会贯穿整个产品生命周期持续沿用的一组重点故事。别人问你在做什么时,你会拿它来解释。它们会为产品需求提供论据并输入其中,后续你还会用它们检查是否漏建了关键东西。你会基于它们做演示。视产品而定,它们也很可能演化成营销材料、快速上手指南或示例。

产品需求文档中,场景会被切成细粒度“需求”,或者在敏捷术语里叫“故事”。

在产品规格中,场景通常称为用户流程或分镜图。

本节里,我会以补完 Helpy McHelpface 产品简报为目标,演示北极星场景的准备过程。

以团队方式头脑风暴场景

并不是每位工程师都喜欢写完整用户故事。有人觉得陌生所以畏难;也有人天生更擅长挑故事毛病,而不是从零创作。弥补这些差距的一个好办法是结对或组队。协作写故事非常有趣。某个场景可以先由一个同事写出内核,再由其他人补步骤。最后每个人都会有共同所有权和共同打磨作品的成就感。

和任何头脑风暴一样,协作构思用户故事需要接纳每个人的想法,并在他人的想法上继续搭建。但在压力和紧迫截止日期下,如何保持冷静与包容?

这些年我学到的关键一点是:做发现工作时,不同人(通常是下意识地)心里装着不同时间尺度。有人看长期,有人看未来几周。有人角色更偏前瞻,有人更偏战术。

在头脑风暴时,要允许大家面向未来做梦。先和团队约定:某个故事里出现了“构建成本很高的功能”,不代表它马上就要做。这能消除那种紧张感——有些人会担心你打算把所有功能都塞进首个版本。

并且在你制定第一个里程碑前,都要保持这种心态。你的目标是先有足够多想法,避免错过最优方案;同时形成一份面向未来的路线图,让每个里程碑都在服务长期愿景。

头脑风暴应该是有趣的!敏捷方法里著名的“便利贴”练习,就是大家把小故事写在彩色便利贴上,再按主题分类、筛选优先。你们团队怎么顺手就怎么来,但最好最终沉淀成一份共享文档,包含几条北极星场景。

选择并打磨北极星场景

选出那些最重要、最能说明问题、也最可能进入路线图的场景。后面你还会继续做优先级,但第一步先把最关键想法挑出来,并把最能照亮这些想法的故事补完整。

第 1 章所述,团队应该通过寻找情节漏洞来给选中的故事“调试”。有些故事是不是跳过了用户旅程中的关键步骤?是不是对用户做了英雄式假设?是不是忽略了关键细节?团队中有些工程师也许不擅长从零写故事,但他们在这一步可能非常强,这同样很有价值。

就我个人经验,通过书面评论而不是口头争辩来做这种调试,既高效又和气。这有助于让头脑风暴保持“增量、积极”的氛围。

记住这些是高层故事;这个阶段追求的是“准确”,而不是“精确到细节”。等到有人写用户流程时,细节会下钻得非常深。

有了完整且有说服力的故事,我们后面就更容易看见所有需求。

以下是 Kabletown 的第一条北极星场景:

  • 有线电视盒安装:Tinker Tia 想给卧室的第二台电视配一个新的有线电视盒。她在笔记本上访问 kabletown.com,找到了 Helpy。Helpy 同意帮忙,并询问她是否需要安装人员协助,她回答需要。Helpy 引导她完成套餐变更并获得确认,然后触发寄送订单。订单发货时,它会在聊天里提醒她,Tia 收到通知后可安排安装预约。Helpy 会与她确认时间约束,并把预约安排在货到之后。技术人员报告安装完成后,Helpy 还会回访并发起满意度调查。

我是怎么写出这个场景的?我的目标是把故事当作创意聚焦工具,避免漏掉重要需求。

  • 我先从一个常见用户问题出发:“想安装一个新有线电视盒”。
  • 我不断追问自己:“Tia 下一步需要发生什么?”“Kabletown 的流程必须怎样配合?”直到用户完整问题被解决。
  • 我思考了如何跟踪价值指标,于是在结尾加了客户调查。
  • 我主动找情节漏洞。当故事某部分过于模糊,或让我担心某种结果时,我会问自己:“这里有哪些关键细节会帮助我们指导需求?”
  • 我回头检查自己是否对具体方案规定太多。我的重点应该是“做什么”和“为什么”,而不是“怎么做”。

头脑风暴后,团队注意到这个场景里 Helpy 需要走一条复杂工作流,于是开始讨论:Helpy 是否需要较高自主性,还是可以用硬编码工作流包裹一个 assistant。团队勾勒了这个场景的若干变体,展示 Helpy 将面对的广泛情形,并据此决定 Helpy 需要是一个 agent,而不仅是 assistant。随后他们回去更新了产品论题/反命题。

由于北极星场景要广泛传播,他们把这些变体从主列表里拿掉,以保证易读。他们另外做了一个“附录”,收纳与他们相关但不必向整个组织重点传播的额外场景。

团队又草拟了几条北极星场景,以覆盖更广需求范围,比如移动端与 PC、销售支持与技术支持(或两者兼有),以及在销售类中区分升级与降级。他们特别想突出降级,因为在产品简报里提到过它可能涉及收入上的争议。目标是在改动成本变高前,尽早暴露最大风险与认知不一致,并确保包括财务团队在内的相关方都感觉被代表。

  • 降级:Tinker Tia 有两个有线电视盒,但从不使用卧室电视那个,她想降低月费。Helpy 搜索了一个与她现有方案接近但不含额外机顶盒的套餐,并告诉她可以节省多少费用。她同意该方案,Helpy 告知只要归还机顶盒就会处理降级。她确认后,Helpy 给她寄出回寄包装。她寄回设备后,Helpy 在签收后给她发满意度调查。
  • 带账户变更诉求的愤怒用户:Tinker Tia 基本不用某个房间的机顶盒,想降月费。她查看账户时发现了 Helpy 链接并发起咨询。Helpy 注意到她处于促销价周期,于是告知没有能让她省钱的可切换方案。她很生气,要求找主管。人工客服从 Helpy 接管该聊天。双方最终协商为:只要她归还机顶盒,就给一次性小额折扣。她同意并收到满意度调查。
  • 故障中断:Sad Lisa 的有线宽带断网了。她在手机上打开 Kabletown App(手机套餐仍可用),看到 Helpy 按钮并询问发生了什么。Helpy 将她地址与故障地图比对,发现确有中断并发送预计恢复时间。故障恢复后,Helpy 再次通知并附带满意度调查。
  • 客户硬件问题:Sad Lisa 觉得网速慢,认为是 Kabletown 的问题。她在 kabletown.com 看见 Helpy 的“获取支持”按钮并点击。Helpy 询问她的设备与环境,发现她在无线网络下使用 PC 笔记本。它判断没有区域故障后,便引导她执行一系列诊断与建议,从历史最成功方案开始,包括重置无线路由器、运行 Windows 网络诊断等。重置 Kabletown 提供的无线路由器后问题解决。Helpy 告知她若问题反复出现可再次联系(例如路由器故障),随后发起调查,她给出了很高评分。

在写这些场景时:

  • 我有意加入摩擦和失败情境,例如用户不喜欢机器人,以确保 Helpy 具备韧性。
  • 当我想到不同需求时,我会写包含这些需求的故事。比如我通过“Lisa 家宽断网,只能转向蜂窝设备”来论证移动端支持必要性。故事本身就是这些功能的“销售提案”,后面做优先级时我们会逐一评估。

如果我做得还行,这些场景应该能让人清晰、直观地理解我们想达成什么。因为故事是人类从小就习得的通用沟通媒介。你对系统的心智模型也许和我不同,但我们大概率能在这些故事含义上达成一致。

把这些故事都实现出来可能并不容易。这反而是好事。请回忆序言里讲过的双钻模型:产品生命周期中的发现阶段本来就是发散、探索的。愿景应该把标准拉高,推动我们去做那些虽然困难但影响巨大的事情。即便后来发现,比如给 Tia 在订单发货时推送通知很难,我们也可以迭代,之后再写替代故事。或者通过分批发布逐步补全这些故事,只要每次增量发布本身都能提供真实价值。

同时要注意,这些场景现在还没有按“便于分工构建”的方式组织。下一步写 PRD 时就会处理这件事。

把北极星场景转换为需求

如果把用户故事放进一张数据库表,它会是按时间排序的一系列时刻,并关联到某个人物画像。这是理解用户如何完成产品旅程的一种很好的方式。

但这并不是系统设计最有用的表示。系统设计更像图数据库——页面里有表单,表单里有按钮,点击按钮会连到服务外层的 API 网关,再通往存储系统。

如果我们的大脑里也有类似这些“数据库”,那么从发现阶段走向设计阶段的过程,就是一次从“时间/故事表示”到“图/系统表示”的大重排(great reindexing)。图 7-2图 7-3 展示了同一产品的两种表示:

故事视图
图 7-2 故事视图
系统视图
图 7-3 系统视图

最全能的工程师,往往能在脑中同时构建这两种索引。如果你只会第一种,你就像产品经理;如果你只会第二种,你就会彻底依赖产品经理,并在与其沟通时步履维艰。

这两种表示之间的翻译尤其关键,而误译到处都是,尤其当 PM 与工程师无法内化彼此的表示方式时。

大重排通常从高层需求与设计原则开始。

高层需求

高层需求与设计原则是一份摘要,用来明确我们设计“要达成什么”。优先哪个人物画像?追求营收?还是易用性?它能与那些没法通读全部细节用例的人建立沟通与信任。

对 Kabletown 来说,可能会是这样:

  • 根据产品简报,我们要在利润中性的前提下提升用户满意度并降低支持负担。
  • 首个里程碑先聚焦技术支持类交互。
  • 先瞄准最高频任务,例如慢网诊断。比如 Helpy 识别到超出其能力边界时,应转人工,而不是硬着头皮继续。
  • 早期优先安全性,让 Helpy 处于清晰护栏内;即使这意味着它有时不够“尽可能有帮助”。

注意我在显式指出我们如何在冲突原则之间打破平局。把这些权衡明确写出来往往既有争议也有价值,能激发高质量讨论。

如果把这段写在产品需求文档开头,读者会更容易理解后面的内容。

产品需求文档

PRD在不同公司里含义会略有不同。对某些团队,它甚至可能已经包含本章后文会讲到的产品规格。PRD 到底要放多少设计细节本就模糊,尤其是大项目,值得和团队明确。

在我们这里,PRD 的目标是让工程师与 UI 设计师可以开展详细设计。后文的产品规格目标则是让实现工作可以展开。因此这里我们会重“做什么”,轻“怎么做”。我们会包含:

  • 高层产品需求与设计原则。
  • 细粒度产品需求——一份用例汇编(或故事汇编),记录用户想要的具体交互。
  • 里程碑定义,用于选择首发要做哪些功能。
  • 召集合适团队与相关方共同推进。(本书不展开。)

用例汇编

接下来我们进入产品需求本体。我称之为“用例汇编”,因为每条需求都从用户视角书写。常见模板有:

  • “【用户】可以【动作】,从而【动机】。”
  • “【产品】提供【功能】,从而让【用户】能够【获得价值】。”

每条需求都是我们某个场景的一段切片。以“客户硬件问题”场景为例——Sad Lisa 网速慢,需要重置无线路由器。

第一条需求是:当 Sad Lisa 寻求技术支持时,她无需先登录就能在首页轻松找到 Helpy。

需求写作的艺术在于:说清楚必须发生什么,但不要过度规定怎么做。以第一条需求为例,写它时我试图做到:

  • 足够具体,表达清晰无歧义。比如明确“Helpy 在无需登录的首页可被找到”,而不是笼统写“Helpy 可发现”。
  • 表达意图。我们不只是说“Helpy 在首页”,还要明确这件事是为了“可发现性”。如果放在首页做不到,工程师也能回到意图层面寻找备选方案。
  • 仍给工程师留出设计空间。我们不规定 Helpy 必须在首页哪个位置或菜单,只规定它必须可发现且登录前可见。

有时很难既清晰又不规定过多,这时我会给一个示例,例如“在首页(例如右下角悬浮聊天按钮)”。

同一条北极星场景剩余需求如下:

  • Helpy 能高效引导 Lisa 诊断慢网问题,原因覆盖软件、硬件与故障中断。
  • Helpy 会向 Lisa 建议它从支持数据库学习到的最成功干预措施。
  • Helpy 会询问 Lisa 哪些措施有效,以便在技术变化中持续学习。
  • 当修复尝试结论不明确时,Helpy 能在结束聊天前建议后续跟进行动,避免 Lisa 放弃。
  • 应向 Lisa 展示满意度调查,以便我们跟踪结果。
  • Helpy 可以基于满意度调查进行训练,从而随时间提升效果。

其余北极星场景也可按同样方式拆分,逐步补全需求。随着思考与学习,我们也可以加入场景之外的新需求;如果漏了大项,还能补新的北极星;如果要降级优先级,也能删掉某些北极星。

我还从其他北极星中挑了几条,后面会讨论,它们与“责任委派”有关:

  • Helpy 能以 95% 准确率判断请求属于支持还是销售。
  • Helpy 检测用户沮丧情绪的能力至少达到人工坐席的 90%,并可转人工。
  • 人工客服在接管对话时可咨询 Helpy,从而使人工支持效果至少不低于 Helpy 自己提供的水平。

这些百分比多少有些武断,我们现在也未必知道真实可达上限,但先给出粗量级。后续我们会通过一种叫evals的代理质量评测来约束自己。如果离这些数值差太多,我们会重新评估方案。

再补一条后面还会再提到的需求:

  • Sad Lisa 可以在不使用家庭宽带的移动设备上使用 Helpy,以便在故障中断期间获得支持。

组织你的用例汇编

用例汇编通常会贯穿工程团队的优先级、设计与执行阶段。理想情况下我们会一次性前置挖完需求,但现实里随着迭代,常常还会新增。

这让用例汇编成为承重文档。对中大型项目来说,把它组织好、打好标签很值得。

如果你愿意,可以把每条需求转成规划系统里的工单——例如 Atlassian Jira 里的 Story 工单类型,每条就对应一条这种以用户为中心的需求。

也可以用电子表格,或像 Notion 这类带嵌入式数据库表格的应用,让汇编可排序、可筛选、可管理。它们也可以按原始产品愿景元素打标签。下面是一些建议标签或分栏:

  • 人物画像(Persona):本例中,按 Sad Lisa 的支持需求与 Tinker Tia 的销售需求分栏会更聚焦。你会看到,我们后面也会基于人物画像做优先级决策。
  • 北极星场景(North star scenario):便于查询“兑现某条场景承诺”所需的全部需求。
  • 边缘情况(Edge case):边缘需求很重要,但不需要所有人都读透。应允许筛掉。
  • 里程碑(Milestone):做优先级与路线图规划时会很有用。
  • 状态(Status):这张表甚至可直接用于项目追踪。
  • 备注(Commentary):按需补充信息。

表 7-1 展示了用例汇编。

需求 里程碑 人物画像 北极星场景
当 Sad Lisa 寻求技术支持时,她无需先登录就能在首页轻松找到 Helpy。 任意用户 客户硬件问题、恶意软件
Lisa 可以在不使用家庭宽带的移动设备上使用 Helpy,从而在故障中断期间获得支持。 任意用户 全部
Helpy 能高效引导 Lisa 诊断慢网问题,原因覆盖软件、硬件与故障中断。 Sad Lisa 故障中断、客户硬件问题、恶意软件
Lisa 会收到 Helpy 基于支持数据库学习结果推荐的最成功干预措施。 Sad Lisa 故障中断、客户硬件问题、恶意软件
向 Lisa 展示满意度调查,以便我们跟踪结果。 全部 全部
当修复结论不明确时,Helpy 能在结束聊天前建议后续跟进,让用户在需要时重新接触支持。 Sad Lisa 客户硬件问题、恶意软件
Helpy 可以基于满意度调查进行训练,从而随时间提升效果。 全部 全部
Helpy 能以 95% 准确率判断请求属于支持还是销售。 全部 全部
Helpy 能检测用户是否沮丧并转人工。 全部 带账户变更诉求的愤怒用户、导致性能问题的恶意软件
人工客服在接管对话时可咨询 Helpy。 全部 带账户变更诉求的愤怒用户
…​
表 7-1 Helpy 的用例汇编

我们已经在重排之路上大步前进,不仅需求在重排,我们的大脑也在重排。你已经能开始看到如何把系统组件挂接到这些需求上,同时又不丢失最初“为什么要做”的理由。

但在开始详细设计前,我们还得先填上那列空白的优先级。

为首个里程碑做需求优先级

我们与团队对长期需求达成一致后,下一步目标是制定路线图。也就是说,我们需要做优先级。

优先级是我们工作中最难且影响最大的部分之一,最后常常会有点混乱。每个人都有自己的“重要性”判断,也很容易分心。看看表 7-1:这张表的规模还不到我把全部内容写全时的一半。再叠加多里程碑规划、组织约束和代码库约束,确实会令人不知所措。

更糟的是,我们通常要先完成很多事,才能拿出一个能给用户看的版本。原因是后面要讲的四条“残酷真相”告诉我们:做出真正让用户满意的东西有多难。

我们需要一套策略。

首先,我会指出在众多互相竞争的关注点里,我们真正该优化什么。

接着,我会给出场景驱动发现中常见的一种优先级策略:首个里程碑聚焦单条北极星场景。

我还会主张一次只规划一个里程碑。

最后,我会把这套策略应用到 Kabletown 案例。

优先级里真正重要的是什么?

优先级背后最根本的两个因素是:用户影响(impact)和投入(effort)。

提示

优化你的性价比(bang-for-the-buck):按用户影响(bang)与公司总体投入(buck)的组合来给场景与功能排优先级。

这听起来简单到几乎不值一提,但说起来容易做起来难。实践中,保持聚焦像走迷宫。其他优先级会不断渗入:处理人际冲突与组织问题、维护面子、兑现对特定客户的承诺、不同岗位激励不一致,等等。所谓真正的“狠”,就是持续盯住“单位投入换来的用户价值”。

而“bang”和“buck”本身也不易衡量。

我认识的大多数工程师往往对 buck 或 bang 其中一项过度敏感。夸张地说,有的人偏向更快交付、少写代码、保持代码库简单可维护,即使这会让用户更难用或拉低采用;另一些人追求完美产品,愿意为此在底层付出任何代价。真正的智慧在于平衡二者。

下面我会先分别看用户影响与投入,再把它们合并。

优化 buck(投入)

尽管我很想做个纯粹主义者,说“唯一重要的是用户影响”,但投入水平同样重要。如果你的团队花了一千小时只给人类节省五百小时,你大概率并没有让世界变得更好。

工程师作为人类,通常对投入很敏感,所以我不用说太多。但有两点我想强调。

第一,如果它能建立相对竞品的优势,或能让用户眼前一亮,有时值得投入比“严格必要”更多的成本。比如你已有相当规模用户群,花一点时间为他们整体节省大量时间,通常是划算的。

第二,考虑总体成本,而不只是工程成本。而且这个原则是双向的。

工程师(尤其初级工程师)估算实现成本时,常只看 happy path(理想路径)上生产代码的规模。但防御性处理边缘情况呢?场景测试呢?以及文档、内部试用(dogfooding)、部署、A/B 测试、营销这些非编码工作呢?即使你无法精确估算,也应把它们纳入权衡。

一方面,考虑总体成本有助于避免范围蔓延。如果你心里冒出“这个加起来很简单”,不妨听听你身边那位友好 PM 的看法。他可能会告诉你,公司为了把它推出去并让用户看到,背后还有多少工作。

另一方面,也要想想如果你不做,总体成本会怎样。比如你决定不给 Helpy 增加程序化护栏来避免它说出不该说的话,就可能把压力转嫁给用户支持、公关和法务团队,让他们准备 Helpy 失控时的应对策略。

优化 bang(影响)

如果你想数学化一点,用户影响等于覆盖规模(reach)与单用户价值(value per user)的乘积,再乘上你选定时间窗。

说明

User Impact = User Value × Scale × Time

用户影响可以用效用函数(utility function)估算:它描述“产品包含哪些功能”与“用户获得多少价值”之间的关系。随着产品变好,效用函数应上升。

但典型效用函数的数学形态非常残酷,这意味着你必须先做很多事,才会产生显著影响。

关于为用户构建有价值产品的难度,有四条严酷教训,也就是“四条残酷真相”:

  1. 规模很难。
  2. 用户价值是后置兑现的。
  3. 竞争差异化的后置性更强。
  4. 价值的长期维持很难。
规模很难

做出高 Impact、高 Scale 的产品很难。做出一个对少数人深度有用的东西并不难;但要打动大量用户,就得同时做到可发现、有用、易用、营销到位、可规模化稳定运行等等。反过来,你可以轻松改首页字体,这种改动覆盖广,但价值不高。

用户价值是后置兑现的

产品不断加功能时,效用函数并非线性增长。很多产品在功能到达临界质量前几乎没用。自行车得同时有两个轮子、刹车、可转向车把和上坡换挡,才真正有用。

图 7-4 展示了自行车的效用函数:

自行车效用函数
图 7-4 随功能增加而变化的自行车效用函数

这种“前期平坦、后期陡升”的模式是效用函数的典型特征。最初一些功能可能只是“打勾项(checkbox features)”,不必做得惊艳,但必须存在,否则多数用户根本用不了。除非你想冒生命危险,否则你需要刹车。除非你专门练独轮平衡,否则你需要第二个轮子。除非你在骑场地赛,否则你会希望能换挡。

竞争差异化的后置性更强

第三条真相是:竞争会施加额外压力。除非你在发明全新品类,否则进入一个市场时,通常要先攒到某个功能临界值,别人才会认真看你。然后你还得再提供额外优势,比如新功能或更低成本。有时你甚至是在重写自己的产品,与过去版本竞争。

把它叫做替代产品之上价值(Value over Replacement Product, VORP)1。计算 VORP 时,需要把市场上已有产品,或你正在构建产品的旧版本价值,从你的效用函数里扣掉。自行车案例的曲线大致如图 7-5

如你所见,VORP 转正晚于简单用户价值。这要求我们做的不只是“有用”产品,而是“特别”的产品。对于 Helpy 而言,我们不仅在和现有自动化系统竞争,也在和真人客服竞争——这确实挑战很高。再往后,随着 AI 技术成熟,市场对助手表现的预期也会持续提高。

自行车 VORP
图 7-5 随功能增加而变化的自行车“相对替代产品价值”函数
长期维持价值很难

第四条也是最后一条残酷真相与时间维度有关。持续提供长期用户价值,比获得短期胜利更难。做一辆便宜但容易坏的自行车,或一辆不够舒适、难以让用户持续使用的车,都比做一辆用户愿意骑很多年的车容易。同理,Helpy 若想持续有帮助,就必须持续摄入更新的支持数据,并被持续监控性能退化。

由于这四条残酷真相,大多数产品和功能之所以无法产生巨大影响,是因为“烤得不够熟”——要么缺了关键功能,要么没按可规模、可长期维护的标准工程化。

残酷真相提醒我们不要“工程投入不足”,那“过度工程化”呢?同样要避免:过度打磨、一次追太多目标或选错目标、入场太慢、资金耗尽、或让自己长期缺乏用户反馈。

正如我在第 6 章提到的,我们可以找到被现有方案服务不足的目标受众,他们会为了“真正为自己而做”的东西容忍功能缺失。

另一条常见的更优做法是迭代产品,正如第 5 章所讲。迭代时,我们会暂时忽略一两条残酷真相,但沿途仍在学习。

沿着这个思路,我们来讨论首个里程碑优先级。

合并 bang 与 buck

许多团队会从最小可行产品(MVP)开始。我将 MVP 定义为:效用函数明显转正的那个点,即便它可能只服务一个较小目标受众——先暂时忽略残酷真相 #1(规模)。

你也许听过最小可爱产品(MLP)这个词。它越来越流行,是对 MVP 常常“太糙”的一种反思。

在竞争环境里,把 MLP 理解为 VORP 明显转正的点会更有帮助。MLP 与 VORP 绑定,因为人们通常只会爱上并依赖高 VORP 产品。只有打勾项功能的自行车,除非那是你的第一辆,否则你不会爱上它。

用残酷真相来讲,MVP 聚焦 #2(用户价值),MLP 则对应 #3(差异化价值)。

下面我会展示如何用场景驱动发现来定位 MVP 或 MLP。

为首个里程碑定义目标北极星场景

使用 SDD 时,你可以通过优先排序“某一条北极星场景”或“最小必需场景集合”的需求,来定义你的最小可行/最小可爱产品。

聚焦北极星场景有不少好处:

  • 因为北极星是完整故事,我们会交付正向效用。
  • 因为它包含最佳产品愿景,我们会为某些人交付正向 VORP。
  • 因为它只聚焦一个人物画像和一条故事,我们可以先去掉额外功能,把泛化推迟到后续。

我们还可以进一步收窄早期目标受众。最常见做法是限制为可参与内部试用(dogfooding)的员工(第 4 章),或愿意容忍早期版本以换取产品影响力/白手套支持的设计伙伴。另一个选择是限定单平台用户,比如 Android 而非 iOS,或者限定不太需要引导的高级用户。

在这些限制下,我们一开始甚至可以省略北极星场景的部分片段。比如目标是公司员工时,若故事里有“用户如何发现该功能”的步骤,我们可以先用发邮件请同事试用来替代。

最后,我们准备好给单条需求做优先级了。

对用例汇编做优先级

我们进入一场大型会议讨论优先级。也许技术负责人或 PM 已先给出一些初始草案。

好在我们已经按场景与人物画像给表格打了标签,判断哪些该进、哪些该出会相对直接。

按我的经验,只聚焦首个里程碑会高效得多。为了争论“某个不在 MLP 里的条目是 P2 还是 P3”而陷入细节,往往是一小时的纯浪费。

这里有个简单格式:

  • P0:MVP 必须项。没有它就无法发布或获得用户反馈。
  • P1:MLP 必须项。它们是极具吸引力的冲刺目标,通常在临近发版时执行。如果进度滑坡且又必须按时结束,你会不开心,但仍能发布一个可行版本。
  • 空白:其他全部。规划下一里程碑时再排。

如果你们按里程碑推进,这里有个便于复用优先级汇编的方案:

  • 0:纳入里程碑 0。
  • 0.5:里程碑 0 的冲刺目标。

其余保持空白。等规划下一里程碑时,再分别加 1 和 1.5,对应里程碑 1。

这里我采用里程碑格式,因为它鼓励我们把产品路线图持续维护在一份易于查找的文档里。

我们的大多数员工家里都用 Kabletown,所以首个里程碑先限制在 Kabletown 员工,他们更宽容,也会给出更有质量的反馈(第 4 章)。这样我们可以先发布 Helpy 并从中学习,再去做完整打磨。面向员工的首发因此是 MVP,而非 MLP。

该选哪条场景?这里有几个考虑:

  • 我们应把 MVP 限制在“支持场景”或“销售场景”之一,这样能省掉给 AI 供训练数据以及为另一个方向做测试的大量工作。
  • 若选常见或高影响场景,效用函数的跃升会更高。假设现有交互里多数来自技术支持,尤其是用户认为网络不通或变慢。
  • 假设数据还显示,大多数技术支持流量来自桌面和笔记本,而非移动设备。(我并不知道现实中是否如此,但先这么设定。)

“客户硬件问题”场景符合以上条件。这是一个覆盖面较完整的支持问题,足以充分检验我们助手的 AI 能力,也能验证方向对不对。

因此我们更窄的目标人物画像是:“Sad Lisa,Kabletown 员工,拥有 PC。”这样的人不算多,但我们希望她会喜欢 Helpy 并提供高质量反馈。

不巧的是,Kabletown 正在做网站框架重构,这意味着若先做 PC,我们可能要多投入约一周工程时间。那要不要改做移动端?

从总体成本看,对 Kabletown 这种流程繁复的大公司来说,多一周工程可能不是大事。这仍只是发布总成本中的一小部分。

如果 PC 和移动端使用量大致相当,当然可以优先移动端。但若 80% 支持交互都来自 PC,恐怕还是应该接受多这一周工程工作。

我们每一步都聚焦价值。我们不希望假设自己一定能跑完整张路线图——Kabletown 的管理层很容易被其他事分散注意力,如果在交付高价值成果前团队就被抽走,那会非常糟。

表 7-2 展示了按优先级排序后的用例汇编,以及我为何做出某些看起来反直觉优先级的脚注说明:

需求 里程碑 人物画像 北极星场景
Helpy 能高效引导 Lisa 诊断慢网问题,原因覆盖软件、硬件与故障中断。 0 Sad Lisa 故障中断、客户硬件问题、恶意软件
Lisa 会收到 Helpy 基于支持数据库学习结果推荐的最成功干预措施。 0 Sad Lisa 故障中断、客户硬件问题、恶意软件
Helpy 可以发送满意度调查,这样我们就能跟踪结果。 02 全部 全部
当 Sad Lisa 寻求技术支持时,她无需先登录就能在首页轻松找到 Helpy。 03 任意用户 客户硬件问题、恶意软件
当修复结论不明确时,Helpy 能在结束聊天前建议后续跟进,让用户在需要时重新接触支持。 0.5 Sad Lisa 客户硬件问题、恶意软件
Sad Lisa 可以在不使用家庭宽带的移动设备上使用 Helpy,以便在故障中断期间获得支持。 任意用户 全部
Helpy 可以基于满意度调查进行训练,从而随时间提升效果。 全部 全部
Helpy 能以 95% 准确率判断请求属于支持还是销售。 4 全部 全部
Helpy 能检测用户是否沮丧并转人工。 5 全部 带账户变更诉求的愤怒用户、导致性能问题的恶意软件
人工客服在接管对话时可咨询 Helpy。 6 全部 带账户变更诉求的愤怒用户
…​
表 7-2 Helpy 的优先级用例汇编

现在看看我们的北极星场景是否经受住了优先级筛选。下面我会划掉任何被降级或改成冲刺目标的部分:

  • 客户硬件问题:Sad Lisa 的网络很慢,她认为是 Kabletown 的责任。她在 kabletown.com 看到了 Helpy 的“获取支持”按钮并点击。Helpy 询问她的设备环境,识别到她在无线网络下使用 PC 笔记本。它判断没有区域故障后,引导她执行一系列诊断与建议,从历史最成功方案开始,包括重置无线路由器、运行 Windows 网络诊断等。重置 Kabletown 提供的无线路由器后问题解决。Helpy 告知她若问题持续可再次联系(例如路由器硬件故障),随后发起满意度调查,她给出了高评分。

还不错!改动都很小,调整后的故事依旧有说服力。

当我们结束发现、进入设计阶段前还有最后一步:写出详细用户流程。

为首个里程碑构建详细用户流程

我们前面讲的故事都比较高层,省略了大量细节,以至于可能漏掉关键问题。

我们也刻意确保故事更多在描述“做什么”而非“怎么做”。现在我们仍需要对不同方案做推演,看用户如何完成这些故事。比如 Helpy 可以替用户运行 Windows 诊断,也可以只告诉用户怎么做;它可以在对话里内联提问做调查,也可以给独立调查表链接,等等。

现在我们来细化用户流程

说明

用户流程会详细展示:用户为完成某条需求所经历的步骤序列,以及让该场景成立所需的前置条件。

本节我会演示如何创建用户流程,然后如何和用户做验证。

做这件事时,我们会从发现逐步进入定义:一方面仍在这些练习中学习新需求,另一方面也开始定义产品本身。

在做 UI 设计时你可能听过用户流程,通常也叫“分镜图(storyboards)”。它可以是一组 UI 原型图,模拟用户点击过程。

如果我们的界面不是图形 UI 而是代码,那流程就会是代码清单与命令行的组合,中间穿插说明文字。

下面是一些关于编写流程的高层建议:

  • 用户流程应足够细,以便我们围绕关键交互问题展开讨论(第 8 章),并从中提炼清晰系统规格(第 9 章)。
  • 与选择北极星时一样,要比较不同方案。可以头脑风暴,也可以给出多个用户流程备选,让团队帮助你做选择。这是引入多元观点、并让大家知道你并未绑定单一路径的好方法。
  • 把主要精力放在首个里程碑,以及那些虽未优先但你认为必须尽早搞懂、才能形成整体设计的事项。
  • 和任何场景一样,用户流程应讲完整故事:从用户动机与其如何发现功能讲起。

我们来为 Kabletown 的一条需求写流程,即:“Helpy 可以从支持数据库学习最常见干预措施,并推荐给 Lisa。”

我们最初训练 Helpy 的效果并不理想。遗憾的是,遗留系统里我们从未给支持数据库做过标注,因此根本不知道技术支持问题中最常见有效修复是什么。最可能的办法是:回看成千上万条历史支持请求,并按“有效干预措施”或“结论不明确”进行分类。我们还希望标注支持人员当时的推理过程,让他们的经验能教会 Helpy。

下面是一个新人物画像 Support Rep Sam(支持代表 Sam)的用户流程。我们要努力让他愿意做大量标注:

  1. Support Rep Sam 收到老板邮件,要求他在下个月内给自己过往 50 条客户聊天做标注。
  2. 他很忙,但老板提了,于是打开内部工具。工具先带他进入一条旧支持线程,让他回看并回忆当时是如何修好的。
  3. 他看到一个标签下拉菜单,如“重置路由器”“扫描恶意软件”“重启机器”等。若没有匹配标签,他也可新增,之后其他支持同事也能看到该标签并使用。
  4. Sam 还需要补充说明为何选择这组建议顺序。他输入:“我先重启路由器,因为用户有两台设备都离线,看起来不像是单设备问题。”
  5. 他提交后,系统将他带到下一条聊天线程。
  6. Sam 在回家前处理了十几条。几天后,他又收到催办邮件提醒继续标注,点开链接后又完成了几条。

我们也可以写一版 AI 辅助流程:

  1. AI 扫描一千条聊天,提取它认为每条里的修复措施与推理过程。
  2. Sam 收到老板邮件,要求他核对 AI 对其过往 50 条聊天的解读。
  3. Sam 打开某条聊天后,系统直接问“AI 的判断是否正确”。他可以点击 Yes,或直接在行内修改 AI 的结论与理由。
  4. 他提交后,系统带他进入下一条聊天。
  5. 与此同时,新选择会反馈给 AI。第二天夜里,AI 会基于这些反馈重训。
  6. Sam 发现 AI 有四分之三时间是对的,因此他在半小时里轻松处理了 30 条。

我们会与团队讨论并确定方案。对方案有信心后,我们可以交付配套分镜图。也可以把这些流程拿给几位支持人员看,做个临时可用性研究:“你会愿意这样用吗?你还希望有什么功能?”

用户流程画出并验证后,发现阶段就告一段落,至少在我们学到新遗漏之前如此。

验证用户流程

对于关键界面,你会希望和真实用户做验证,确认其可发现、直观,并覆盖了最重要用户需求。

我会讲两种实践:征求意见稿(RFC)与可用性研究。

征求意见稿(RFC)

如果你的主要目标是确认功能在真实世界里做对了事,可以创建一份 RFC 文档,给潜在用户看分镜图,问这个场景是否有用,并请他们对设计提出反馈。

考虑到我们在设计的是一个服务少量员工的短期内部工具,这大概就是我们会采取的方式。我们会把 RFC 发给几位支持坐席,让他们指出功能缺口、标出困惑点、补充我们可能漏掉的日常流程细节。早期咨询这些支持员工,也有助于我们提前评估:他们会不会对新增工作产生抵触。

但 RFC 的局限是:我们其实已经把“答案”给他们看了。如果不先给答案,我们往往能得到更多洞察。

可用性研究

可用性研究会让受访者基于可运行原型完成任务,从而暴露大量问题,如可发现性、易用性和功能缺口。

你可以安排几场访谈,通常通过视频进行,并准备一系列希望受访者完成的任务。

你的可运行原型应当看起来能完成这些任务目标,同时允许其他功能仍未实现。很多设计工具都能制作这种原型,甚至有些工具可基于现有应用或其截图,快速生成带新功能原型的改版。

访谈开始时:

  • 先让受访者放松,并说明流程。
  • 请他们在使用功能原型时“边操作边说出想法”。
  • 告诉他们可以提问,但你可能不会立刻回答。

然后你给出任务,也就是关键用户流程中的“动机”部分。这里你可能会说:“你被要求给一条历史支持线程标注问题类型和有效修复措施。请共享屏幕,并用这个可点击原型尝试完成任务。”

除了呈现用例外,尽量不要给额外引导。如果确实需要回答问题,把问题记下来,后续用于调试流程。

我非常建议你抓住机会参与一次可用性研究。我第一次做这件事,是给我在微软写的一个 C# API 做研究,那次经历非常令人谦卑。看着程序员在我以为“很好用”的地方频频受阻,让我真正意识到 API 设计这门学问有多深、多难。

把用户流程翻译成待完成工作

我们大重排的最后一步,是写出需要构建的组件与功能。这些有时称为待完成工作(jobs to be done)。

用户流程可以被翻译成待完成工作,方式与北极星场景拆解成需求类似。

例如,如果我们决定让 Sam 手工标注他所有历史支持聊天,可能要构建以下组件:

  • 用于标注聊天的内部工具
  • 一张可编辑的标签数据库表
  • 一张用于存储“标签 + 自由文本”标注结果的数据库表
  • 让 Helpy 联合摄入“聊天历史 + 标注结果”的数据流水线
  • 按员工追踪其是否完成 50 条标注目标的进度系统

到这里,大重排完成!我们已成功把“以用户/时间线为中心”的产品视图,转换为“以系统/图结构为中心”的视图,至少在高层层面如此。

第四部分中,我们会把注意力转向这些组件与系统的设计。

反哺需求文档

任何人都不该期待一条从发现到设计、单向流动的优雅瀑布。现实更像潮池,潮涨潮落,不同水体彼此混合。即便在为某些方案做设计,我们仍可能偶尔要补新的场景。

也就是说,北极星场景列表和用例汇编都应该是“活文档”,需要持续更新。

比如我们准备在技术支持能力之上,再给 Helpy 增加销售场景。这会拉伸它的能力边界,因为我们要给它更多知识并开放新工具。

我们的某条需求是:

  • Helpy 能以 95% 准确率判断请求属于支持还是销售。

但我们真的想透了吗?

2025 年代理设计中的一个现实是:对于复杂代理,采用“多个专长代理混合”通常比“一个全能大代理”错误更少。在本例里,我们可以做一个技术支持 assistant 和一个销售 assistant,再由 Helpy 作为礼宾代理(concierge agent)负责对外沟通、规划,并把任务分派给各 assistant,如图 7-6所示。

由两个专长助手与一个代理组成的混合体。
图 7-6 两个专长助手与一个代理的混合模式

现在我们要批判性审视这个设计,并构造可能击穿它的故事。比如用户提出一个技术支持问题,而真正解决方案是升级设备。此时 Helpy 在一次会话里可能不该只委派给一个 assistant,而是需要同时咨询两边。

如果我们前期没意识到这一点,现在就是补写新用户故事并让它重新走一遍大重排的时候。

本章小结

我们已经把“以场景表达的产品愿景”推进到了“接近传统实现需求”的形态。在场景驱动发现的指导下,我们对成功更有把握:

  • 因为所有人都能读故事,沟通质量更高。
  • 因为我们聚焦北极星场景要素,不会浪费时间。
  • 借助人物画像和北极星场景,我们既保留长期愿景,又只执行一个增量里程碑。这让我们在更整体策略下保持聚焦优先级,知道自己正朝着一致目标构建。
  • 我们依据成本与用户价值来选最高优先级,并清楚四条残酷真相——Helpy 需要在多个维度都足够强,才能给终端用户真正交付价值。
  • 上线后,我们预计用户反馈不必触发大改版,因为团队在北极星场景与用户流程中主动找了情节漏洞,并通过 RFC 与可用性研究验证了用户流程。
  • 当进入设计与实现,我们会在编写场景测试时回看用例汇编与用户流程,并把北极星场景做成演示。

如果你还没尝试过,我希望你试试 SDD。上手后,写故事会很有趣,尤其是团队一起写。而且它可能带来十倍回报:你会用更少尝试交付出正确的产品。

练习

  1. 2010 年代到 2020 年代初,Visual Studio Code 通过优秀的扩展发现机制赢得了数百万开发者的心,例如 lint、语法高亮和测试框架扩展。请为这个主题写一条北极星场景,真正把“扩展发现”卖出去。如果你不熟悉 VS Code,可想象一个 IDE,提供“扩展市场(extensions marketplace)”以帮助你找到合适工具。
  2. 把你的场景——或者如果你想对比答案,也可以用我在上一题给的场景——拆成高层产品需求。尽量不要对具体设计元素规定过多,以便后续写用户流程时有更大灵活性。
  3. 指出几个边缘情况,补充到上述需求中,它们可能没有被北极星场景直接覆盖。同样,尽量保持高层表述。
  4. 写一个用户流程,满足“发现高口碑扩展”的产品需求。

参考答案

  1. 开发者 Deanna 打开某个 Python 仓库的 Visual Studio Code。她刚接触这个仓库,Python 经验也不多。好消息是,仓库维护者提交过一个代码格式化器推荐,所以她会收到一个弹窗建议安装。与此同时,由于源码主要是 Python 文件,系统向她推荐 Python 语言服务器和调试器扩展——这些扩展下载量高、星级高,她安装后立刻进入正确轨道。最后,她去扩展市场“逛店”,并配置了一个口碑不错的 AI 编码助手。
  2. 用户可以编写并提交一份推荐语言扩展列表,帮助队友在 clone 仓库后快速进入状态。 应有一个扩展库或商店,按流行度与评分排序,帮助新接触某语言或仓库的用户找到最有效选项。 应主动向用户展示推荐扩展,让新用户即使此前不了解扩展也能快速上手。 用户可以按描述、关键词和名称搜索扩展市场,从而既能找自己偏好,也能发现尚未高排名的新扩展,例如新型编码助手。
  3. 当队友更新推荐时,用户应主动看到新推荐提醒。 用户可以拒绝安装某些扩展,VS Code 应记住其偏好,避免重复提示已拒绝扩展。 用户可以配置“按仓库”扩展或“全局”扩展,以适配不同仓库可能存在的代码格式规范差异。
  4. 开发者 Deanna 打开一个新仓库。VS Code 识别到这是她第一次接触该仓库,于是弹窗提示她查看扩展。她可以点 X 关闭,或点“前往扩展市场”。她点击按钮后,看到一组推荐扩展:按仓库语言适配过滤,并按流行度与评分综合排序。每个扩展都展示名称、简述、下载量、星级,以及两个按钮:installlearn more。她点击 Install 后,会弹出一个页面,展示作者提供的使用说明,并显示该扩展下载进度。

  1. 该术语改编自棒球统计学(sabermetrics),其中 P 代表“Player”而非“Product”,指的是一名大联盟球员相较于最强替补小联盟球员所带来的价值。 ↩︎

  2. 短期看它对用户不算关键,但在路线图早期,用户反馈是我们的生命线。 ↩︎

  3. 在现有产品里这项能力藏在登录后,我们希望把它前置以促进早期内部试用,因为人们并不是每天都需要支持。并且在网速很慢时登录本身可能很痛苦。 ↩︎

  4. 对 M1 而言,Helpy 可以先直接询问用户请求类型;若是销售,就转到旧机器人 + 人工体系。 ↩︎

  5. 员工内部试用者会更宽容。 ↩︎

  6. 这对规模化下的生产力与质量很重要,但对受限测试不是刚需。 ↩︎

IV 定义

现在我们已经有了产品需求,也明确了工作范围,接下来进入产品周期中的定义(Define)阶段:在这里,我们将严谨地厘清如何解决此前发现的问题。我们会产出细致的系统与功能设计,以恰当的泛化程度来解决这些问题。

8 交互设计

让每一个细节都做到完美,并把需要做到完美的细节数量控制到最少。

— Block 创始人 Jack Dorsey

可供性指的是产品可以被使用的方式,不论是不是设计者原本打算的。沙发的可供性是“可坐”,但猫会发现它还有“可抓挠”的可供性。两者都算可供性,尽管说明书里通常只会写其中一个。

可供性也可能有危险。你去过露天演唱会吗?有没有注意到椅子常常被紧紧绑在一起?这种绑法是在防什么“非预期可供性”?它通过防止椅子挡住人群逃生通道、以及被人抡起来投掷,来提升现场安全性。

由于代码和协议不够安全,数字世界里充斥着各种不受欢迎的可供性:垃圾短信和邮件、隐私泄露、洗钱、钓鱼攻击等等。有些问题确实难以避免,但也有不少本可通过更有前瞻性的设计来规避。

Don Norman在其经典著作 The Design of Everyday Things 中普及了“可供性”这一概念,并提出了 意符(signifier),这一概念也构成了第 2 章的讨论框架。如果可供性是产品能做什么,那么意符就是我们判断产品该怎么用的线索。

椅子虽然有“可投掷”的可供性,但它通常没有明显意符来提示这件事。那些希望表达“可投掷”的物品,往往会用握把、可单手抓握的球形外观,或空气动力学造型来传达这一点。

要看清意符和可供性的区别,我们不妨看看类的公共接口。在 Java、C# 这类面向对象语言里,public 方法是可供性,而 public 关键字就是一个意符,告诉类外调用者这些方法可用。private 则是一个只面向类内调用者的意符。

在 Python 里,def 是方法的意符。由于 Python 没有真正的私有方法,开发者通常约定在名称前加 _,表示“不打算给外部调用”,但唯一约束其实是“君子协定”。从某种意义上说,它是在把方法对外隐藏。我甚至觉得它像一种反意符,类似“KEEP OUT(禁止入内)”标志。

从类外调用者的视角看,方法可以按表 8-1分类。

类型 可供性? 有意符标记? 说明
public method (Java/C#) yes yes 用户预期可以调用,也确实可调用。
private method (Java/C#) no no 不能调用,用户也不会预期它能调用。
normal method (Python) yes yes
underscored method (Python) yes no 技术上可调用,但前导下划线在提醒人们不要调用。
publicly accessible method that fails because it’s not implemented no yes 看起来提供了,实际上不可用。
表 8-1 方法可供性与其是否被标记为可供外部调用

意符和可供性通常相伴出现。在在线单选题里,单选按钮意味着最多只能选一项;而一组复选框意味着可以多选。不同语义会对应标准化的视觉模式。

当你设计接口时,请先枚举其可供性,并逐一思考它们的正当用法与误用方式,去掉那些不受欢迎的可供性。这需要大量设计品味和迭代:例如,允许高吞吐场景在什么边界上会变成“有人故意刷请求把你服务打挂”的拒绝服务攻击?

对于那些你确实要暴露的可供性,要用意符把用户引导到最安全、最有价值的路径上。

提示

软件接口设计在很大程度上就是:决定暴露哪些可供性,以及如何向用户传达这些可供性。

本章我会讨论我们在“暴露什么功能、如何暴露”时所做的设计选择。结合可供性、意符、场景与人物画像,你将能够:

  • 强化产品的正确用法,弱化错误用法。
  • 更明智地把握可供性的发布时机。
  • 理解何时应该让功能可扩展,何时应该只解决特定问题。

以我的经验看,大多数设计争论都围绕这些取舍展开,而我们都值得把这些问题想得更清楚。用户场景和人物画像为此提供了具体可操作的方法。

在进入具体设计建议前,我先搭一层脚手架:

  • 偏见与意识形态在软件设计中的作用
  • 好设计的前提条件
  • 一个用于承载建议的案例研究

软件设计中偏见与意识形态的作用

软件工程师会把自己的意识形态信念和偏见带进工作里。

例如:

  • 有些人偏好设计灵活、对高阶用户友好的产品;另一些人偏好强主张、安全、易用的产品。
  • 有些人偏悲观,更关注功能和产品的负面影响;另一些人更关注优势与机会。
  • 有些人认为自由/开源软件在透明性、伦理和安全上更优;而专有软件的支持者则强调知识产权和商业激励对创新的重要性。

意识形态可以带来力量感和使命感,但在软件设计中,它们只能是起点。最优产品始终应该更多由用户及其需求来塑造,而不是由设计者的信念来决定。

下面我会快速看这三个例子在实践中的表现。

争论:灵活还是强主张

看看现代智能手机行业。苹果 iOS 和谷歌 Android 的设计者起初意识形态差异很大,但产品逐渐趋同。苹果开放了更多灵活的开发者平台;而谷歌及其硬件伙伴则收紧了某些自由度,提供更多强主张体验。

这些变化既有商业和监管原因,也有产品设计原因。这种“趋同进化”说明两家公司都在倾听相似用户和合作伙伴,他们面对的是相似场景。

争论:乐观还是悲观

每一波新技术都会引发一波乐观情绪,也会引发对其社会影响的道德恐慌。上一个十年是社交媒体,如今是 AI。更好的软件设计往往是解决问题的重要组成部分。

举个例子,人们担心 LLM 聊天机器人会削弱人的批判性思维能力,也担心它会让犯罪变得更容易。

截至本文写作时,这件事还处在早期阶段,但我们已经看到:

  • 红队演练(Red teaming):研究人员构造对抗性场景,再让 AI 在这些场景下接受“压力测试”,以增强其对生物武器、身份盗窃等威胁知识泄露的防护能力。
  • 辅导模式(Tutoring modes):LLM 提供方正在开发学习辅导模式,服务学生场景,目标是帮助学生形成批判性思考,而不是直接把答案喂给他们。

我猜随着时间推移,我们还会看到面向不同场景和人物画像的家长控制及其他保护机制。

争论:开源还是专有

在 2000 年代,大多数产品要么完全专有,要么完全开源。很多现代软件公司采用的是介于两者之间的模式:

  • 在 open core 模式中,大部分功能开源,但某些企业用户场景由专有功能支持。
  • 在双重许可模式中,许可条款按人物画像划分,例如对商业用途增加限制。

这些模式试图在创新激励与开放透明之间取得平衡。

如果我们发现自己只是在用意识形态争论,就该反思:我们是否真的足够了解客户及其使用场景。你在本书前面学到的许多技术,都能帮助你做出更好的设计决策。

好设计的前提条件

本章不少设计建议都依赖于场景、人物画像,在某些情况下还依赖迭代开发。这些前提并不保证产出好设计,但它们确实很有帮助。我们将充分利用本书前面建立的这些基础:

  • 在开发实践中建立收集反馈并快速迭代的能力。这样一来,即便某个接口最初过于受限,你也有信心后续补强。这一点在第 5 章已详细讨论。
  • 形成对你的目标受众及其诉求的清晰画像,我在第 6 章讲过。举例来说,如果他们都想要同一件事,就把它做成唯一选项;如果诉求不同,就在恰当位置设置扩展点。
  • 场景驱动的发现方法(第 7 章)去推敲客户将如何使用你的产品。如果这些场景揭示出安全缺口,看看能否在不牺牲通用性的前提下把缺口补上。

有了这些工具与方法,你就不必频繁诉诸意识形态捷径,也能在信息最充分、动机最充足的时候做出设计决策并落地实现。

基础打好之后,来看一个案例研究,帮助我们穿过具体设计原则。

案例研究导入

在 2010 年代早期,我负责过 Facebook 社交网络图数据库对象建模的 schema 设计。对象(称为 entities)可能是用户、帖子、群组、活动等;edges 则是连接关系,比如好友关系、成员关系、帖子作者等。这个 schema 叫作 EntSchema,即 “entity schema” 的缩写。

我们其中一个北极星场景是:

  • Schema authoring:模型创建者只写一份表示,就能自动产出他们需要的一切:数据库 schema、读类、写类。

如果这就是唯一的北极星场景,最基础版本大概是这样:

class PostSchema(EntSchema):
    def db_config() -> DatabaseConfig:
        return (DatabaseConfig()
            .name("posts"))

    def fields(self) -> dict:
        return {
            "id": int64_field().primary_key(),
            "text": varchar_field(4096),
            "created": int64_field(),
            # ...
        }

    def edges(self) -> dict:
        return {
            # An author can have many Posts.
            "author": edge(UserSchema, cardinality=EdgeCardinality.ManyToOne),
            # ...
        }

这会生成:

  • 一个数据库表,schema 包含 idtextcreated
  • 一个把作者连接到其帖子上的边表
  • 一个用于 Python 读取的类 Post
  • 另一个用于 Python 创建/编辑帖子的类 PostMutator

下面是 Post

class Post:
    @property
    def text(self) -> str: ...

    @property
    def created(self) -> str: ...

    @property
    def id(self) -> int: ...

    async def fetch_author(self) -> User: ...

注意,这个 schema 里几乎只包含数据库层直接有用的信息。这正是满足该场景所需的最低配置。

当时我引入 EntSchema 之前,数据库和代码库里已经有大量手写实体。若保持这种简单的数据库层表示,一个巨大优势是:我们可以根据已存储的数据库表示自动生成 EntSchema,因为 schema 里除了数据库已有信息外不再额外要求别的内容。这样一段简单脚本,就能在超大代码库迁移中给我们极大助力。

不过,schema 作者并不是唯一人物画像。我们还得考虑“阅读和使用 schema 的人”。例如工程师常用对象检查器排查线上问题。

假设我们要求 Post 作者提供更丰富的类型信息,并把 created 字段声明为时间戳而不只是整数,那么用户在调试 Post 对象时就能看到可读性更好的格式化时间。

但这会不会只是范围蔓延(scope creep)?增量策略也有风险:如果我们先做容易的、只含底层信息的版本,后面再补充丰富类型信息会很难改造。

这个案例会展开我们如何决定“作者应向 EntSchema 额外提供哪些信息”,以及背后原因。到本章结束时,我们会把前面展示的 PostSchema 演进成能够覆盖更多场景的版本。

Go 生态里的 Ent

有个流行的开源 Go 框架叫 ent,基于 EntSchema。你可以看看它,先建立整体感受。不过它在字段类型系统上的丰富度不如 Meta 内部版本。

把注意力引向正确用法,远离错误用法

一个设计良好的产品,会用意符和默认配置把用户引到高产且安全的可供性上,同时确保用户不会误入风险更高的路径。

可供性可以像交通灯一样,被传达为“更欢迎”或“没那么欢迎”。绿色可供性应该安全且通常有效;黄色表示谨慎;红色表示禁止。

提示

根据推荐程度把可供性分为绿、黄、红三色,并据此设计它们的意符。

为了说明这一点,想象你点击一个可能可疑的网址链接:

  • 绿色:链接跳转到同一网站内的另一页面。
  • 黄色:链接把用户带到站外页面。系统可能弹出“你确定要前往这个外部网站吗?”之类警告,避免用户误去冒充源站的钓鱼站点。
  • 红色:用户点击的链接指向一个可疑网站,且该网站缺少有效安全证书。浏览器会设置很高门槛才允许继续访问。

绿、黄、红可供性都可以存在,但你也可以彻底移除某些可供性。比如用户点击进入的页面已知恶意,浏览器就可以完全阻止访问。

一个让绿色场景高度可发现、而让黄色或红色场景不那么显眼的产品,用起来会非常愉悦。用户可以更有把握地推进操作,逐步建立足够信任,不用步步自我怀疑。

下面是一些把用户引导到最有价值可供性的常见技巧1

  • 选择安全、可预测的默认值。
  • 优化“阻力最小路径”。
  • 在正确场景下把可供性给到正确人物画像。
  • 做好校验。

下面我逐一展开。

选择安全、可预测的默认值,或者干脆不设默认值

灵活性和易用性结合起来:给大多数人提供合适默认值,同时允许更挑剔的用户自定义。不愿深入设置细节的人应获得合理行为;愿意投入时间定制的人则可以得到更强能力。

用户会因为你能选出好默认配置而信任你,也愿意为此付费。

为方便起见,EntSchema 默认假设数据库列名与用户侧声明名称一致,但也可定制。比如 PostSchema 维护者觉得 “posted_at” 比 “created” 更清晰,于是想改名。他可以这样写:

"posted_at": integer_field().db_config(name="created"),

这样既保留灵活性,又不必让每位 schema 作者都重复写数据库列名,也不必为了改代码侧名字而发起数据库迁移。

默认值不仅是便利。错误默认值的后果可能涉及政治争议,甚至灾难:

  • Microsoft、Apple、Google 等操作系统公司都曾因把特定浏览器或搜索引擎设为默认而遭遇诉讼。
  • 早期 Internet Explorer 默认启用 ActiveX 控件,造成严重安全漏洞,使多种病毒和蠕虫传播。
  • 2009 年,Facebook 把用户新帖子默认可见范围设为 “Everyone”,引发强烈反弹并导致用户信任受损,即便后来提供了更明确控制也长期难以恢复。

Facebook 这个例子引出一个最容易被忽略的默认值:不设默认值。开发者习惯给所有参数都配默认值,让调用点更简洁、产品表面更丝滑,但这可能是伪命题式取舍。若两个选项任一都可能不安全、不符合预期或无效,就应让用户做有意识决策。自动驾驶汽车不会默认左转或右转,而是要求导航者输入目的地来辅助决策。社交媒体也应提示新用户对受众范围做知情选择。

提示

产品接口质量可以用它向用户提出的问题是否“相关且必要”来衡量。

看个实际例子。PostSchema 有个重大的数据库问题:数据库有很多分片,而常见查询是“获取某作者最近所有帖子”。若这些帖子不在同一分片,代价会非常高,因为要扇出到所有分片去查。

如果帖子都在同一分片,查询性能就会很好。因此,PostSchema 的作者应在数据库配置里声明共置关系:

def db_config() -> DatabaseConfig:
    return (DatabaseConfig()
        .name("posts")
        .colocateWith("author"))

如果 EntSchema 在未声明时默认“无共置”,帖子就会随机散在各分片。创建者可能直到上生产、甚至直到规模上来后才意识到这个错误。

因此这里不该有默认值。必须强制用户做选择,而我们要尽力把取舍讲清楚。

接下来我把这个“选好默认值”的准则进一步泛化。

优化你的阻力最小路径

用户在使用产品时并不总是深思熟虑。他们常会自然走向阻力最小的那条路。

失灵交通灯(malfunctioning traffic light) 指的是两个功能在“相对可发现性或相对易用性”上发生了本不该有的反转。你可以想象交通灯被倒过来了:本应在上方的绿色可供性,却跑到了黄色甚至红色可供性下面。

例如,设计者本应默认一个绿色可供性(在 Facebook 仅与好友分享),却默认了黄色可供性(公开发帖),这就是失灵交通灯。

这里更关键的是不同可供性之间相对的易用性和意符强度,而不是产品绝对易用性或绝对可发现性。这种情况非常常见。

我们设计这些应用层 EntSchema 字段类型时,做了字符串子类型,比如邮箱、自然语言文本、枚举、电话号码。开发者可写成 email_string_field 之类。

但我们有个问题:我知道不可能预先覆盖所有字符串类型。用户以后可以新增,但这似乎太麻烦,于是我加了一个兜底 string_field

结果很糟:最没帮助的选项反而最容易被发现!许多 schema 作者开始过度使用 string_field。它的意符太“顺手”了,看起来和他们在其他系统里预期的一样。我猜很多工程师甚至没注意到还有别的选项。也有人可能知道,但打算回头再处理,后来就忘了。

也就是说,黄色可供性 string_field 比绿色可供性 email_string_field 更容易被发现。就像我们的交通灯把黄色放到了绿色之下。

string_field 也更省事,更短,输入成本更低,不需要额外思考;而其他类型需要开发者浏览字符串类型菜单并做审慎选择。

绿色可供性 enum_string_field(MyEnum) 比黄色可供性 string_field 更难用。再次说明,我们的交通灯失灵了。

每条声明单看都不复杂,但由于相对易用性和可发现性失衡,系统整体效果就不好。

为修复这一点,我们移除了 string_field,换成更直白的 custom_string_field。这让它和其他字符串子类型处在同一认知层级,也向用户传递出它并非主流选项的信号。

这解决了可发现性反转(discoverability inversion),但忙碌的开发者可能仍会滥用它。比如从别处复制粘贴进来,或先当占位,之后忘记回填。

所以我又加了一点摩擦:强制他们加校验器。比如 schema 作者若想接受正则表达式输入,可以写:

'my_regex_field': custom_string_field().validator(lambda x: is_regex(x))

他们当然总能写一个空校验器,但我觉得这样至少足以推动大多数工程师认真想一想。即便没做到,我也给代码评审者增加了一个意符,他们能注意到缺少校验器并指出问题。

custom_string_field 远没有 string_field 那么诱人,也显著提升了 schema 声明的准确性。

在正确场景下把可供性给到正确人物画像

请认真思考“把选择权给谁、何时给”。

在设置 Post 的三个字段 idposted_attext 时,对于创建帖子的开发者来说,可能出什么问题?

post = await (PostMutator()
    .set_id(rand())
    .set_posted_at(now())
    .set_text(some_user_input)
    .create())

几个常见坑:

  • ID 应由数据库从其 ID 空间分配,以避免重复并保证分片均衡,不该让用户手动给。
  • id 可能被不小心漏掉。
  • posted_at 可能漏填,或被设为 0。(如果你见过 1969 年 12 月 31 日这种时间戳,很可能就是这类 bug。)
  • posted_at 也可能被误设为其他显然无效的整数。

schema 作者如何构建一个“成功之坑(pit of success)”,让创建和编辑对象的人不容易踩雷?

其实不需要太多额外工作,schema 作者就能在字段声明中注入更多语义,把这些场景全部规避:

def fields(self) -> dict:
    return {
        "text": natural_language_string_field()
            .db_config(type='varchar(4096)'),
        "posted_at": timestamp_field().at_creation(),
        # ...
    }

我们做了几个改动,通过减少工程师需要做的决定,扩大了成功之坑:

  • 彻底去掉 id 字段声明,统一在创建时自动生成。
  • posted_at 声明为时间戳,可做范围校验;同时标记为特殊的 at_creation 时间戳,这样 PostMutator 会自动填充,无需用户输入。

更精确地说:

  • 我们把 id 的决策权,从“不太清楚底层细节”的应用开发者,转移到“清楚底层机制”的数据库工程师。
  • 我们把 posted_at 的决策权,从创建 Post 的人,转移到编写 PostSchema 的人。
提示

仔细判断:哪些选择应该由哪类人物画像来做。

多人物画像的引导流程

面向 SaaS 的采购流程往往涉及多种人物画像。如果你的产品面向一线执行者(IC)使用、但销售对象是公司,那么你可以想象:IC 对采用产品很兴奋,却在流程中突然遇到付款页面。他们通常拿不到公司支付权限,而有权限的人也未必愿意把公司信用卡信息通过聊天工具发给他们。

相比之下,让 IC 先提供采购负责人的邮箱通常更容易。IC 可以提前通知对方留意邮件,然后由财务人员点击链接完成表单。

除了找对人物画像,我们还应在正确时机提供可供性,也就是用户既具备决策所需信息、又有动力做决策的时候。

前面提到 EntSchema 的共置(colocation)。那 schema 作者应该何时决定数据库布局?是不是一开始就得想清楚?

开发时我们始终关注的一个关键场景,是所谓“黑客松场景”:用户在做快速原型,希望尽快勾勒产品模型、接上 UI,并演示一个产品想法。

为此我们做了一个面向原型用户的草稿模式。用户准备走向生产时再退出该模式。在此之前,许多生产就绪检查都会关闭,包括“必须选择共置关系”的检查。这样既把决策延后到用户更清楚自己要什么的时候,也把额外工作延后到作者确定要生产化的时候。

“正确的人”在“正确的时间”做决策后,下一步就是校验这个决策。

执行校验

要对用户决策做校验。我们给 text 字段加点校验

"text": natural_language_string_field()
    .max_length(4096)
    .user_input(),

首先,我把 varchar(4096) 这种数据库层声明上提为 schema 中的 max_length。这样我就能在 API 层更早校验输入长度,而不是等到数据库层才失败。

正如第 3 章所讨论,我这样做实现了:

  • 左移校验,更早发现问题,减轻数据库负载
  • 在接口层校验,从而返回更清晰的应用层错误信息

其次,我把该字段标记为 user_input,而不是“由内部工程师选择的值”。这意味着我们可以统一做恶意 URL 和其他安全漏洞筛查,而不必依赖每个创建帖子的调用点各自处理。

既然我们已经讨论了功能设计和“成功之坑”的构建技术,接下来聊聊:功能到底要不要加。

明智把握可供性的发布时机

人们常说技术可为善也可为恶,而最安全的可供性往往是“它不存在”。即便看似无害的功能,比如让社交网络用户自定义姓名,也会打开脏话和滥用等攻击面,所以我们必须把大多数对外暴露点都加固。

因此,暴露一个没想透的功能,本质上是在下注。

在理想世界里,如果我们持续迭代并倾听用户,那么对候选功能的问题就不是“该不该加”,而是“什么时候加”。如果你暂时不做某个重要功能,之后补上就行。

在这种环境下,对暴露面保持一点保守是划算的。等你拿到更多用户细节需求,再加也不迟。

提示

拿不准,就先不做。

如果你过于激进、过早发布功能,可能出现很多坏事

  • 你缺少足够用户数据,只能依赖意识形态默认,导致设计决策失真。
  • 测试不足,产出粗糙。
  • 用户难以上手,因为你没留出足够设计打磨时间。
  • 你本可以把时间投入到更有价值的东西。
  • 该功能可能带来你尚未准备好的维护成本和长期所有权压力。
  • 若你构建的是需稳定性的 API,后续修订与兼容性破坏会让用户不满。

注意:当功能不是团队最高优先级时,你更容易在这些地方偷工减料。所以有个推论:

提示

只有在你能投入足够专注度把事情做好时,才去做该功能。

“拿不准就先不做”的另一个推论是:不要一直拿不准。形成“是否需要该功能”的信念,可能只需和用户做两次访谈(我在第 6 章介绍过)或看一眼指标。要么你据此把功能做出来,要么你确认它没你想的重要。两种结果都很好。我们工程师都喜欢“自己有点子”,但管理者更喜欢我们验证假设、用数据学习,用户也更喜欢由此带来的结果。

让我从理想的迭代世界退一步,考虑现实。现实里发布功能有各种固定开销:代码评审、部署、知识传递、管理、文档等。因此我们有时会出于务实,把一些低优先级“锦上添花”与关键功能打包发布。但我们的元目标应是降低发布开销,从而最小化这类权衡,让团队目标与用户目标更一致。

以下是一些在理论和实践中都好用的发布准则:

  • 值得做,就值得验证。
  • 不做盲目乐观者,也不做盲目悲观者。
  • 应用“三次法则”。
  • 分阶段构建。
  • 必要时,先上实验版。

值得做,就值得验证

不要发布未经验证的功能。很多工程师喜欢“顺手”塞进一些额外可供性,以防用户需要。

这没问题,但如果你连写一个测试的时间都挤不出来,这通常是坏信号。

第 4 章中,我介绍了多种判断功能是否有效的方法。写测试应该是最低标准。

不要只测 happy path。还要找出你暴露出的负向可供性并覆盖测试。例如用户传入坏数据怎么办?出现竞态条件怎么办?

既不要乐观主义,也不要悲观主义

另一个意识形态默认是乐观与悲观之争。有些工程师天生乐观,构建时只看 happy path;另一些工程师更悲观,直觉上会先测试想法、寻找负面影响,对复杂度持怀疑态度。

这些心态都可能是优势来源。例如悲观者可能成为优秀安全工程师,乐观者可能在尚未意识到挑战规模前就敢于创业。

但在软件设计里,它们也可能成为弱点。我们如何超越先天气质,成为产品真正需要的“平衡型设计者”?场景可以帮助我们建立对设计的信心。

每个功能都有代价,从界面变杂乱到直接伤害用户不等。设计者应构造对抗场景来“破坏”自己的想法。在安全领域,这在设计阶段叫 threat modeling,在代码写完后叫 red teaming(红队演练)。

反过来,替用户攻克难题正是产品创造价值的方式之一。太容易的东西,别人也容易复制。要主动头脑风暴你功能可能带来的正向场景。

同时考虑乐观和悲观视角,我们就更可能找到“放大绿色可供性、抑制红色可供性”的方案。

无论你个人偏好如何,都要同时模拟绿色与红色场景。你甚至可以和一个倾向与你相反的工程师结对,也许就是那个你在这些问题上经常争论的人。

应用“三次法则”

在消费软件里,构建功能时你经常会收到来自高阶用户或朋友的“奇特需求”。企业软件也类似。有时某个付了很多钱的企业会施压,要求你满足其独特需求。

不能什么都答应。你无法高质量维护成百上千个功能,客户也不想要臃肿、迷宫式界面。甚至提出需求的客户自己也不想成为“唯一使用者”,因为那意味着他们在你的测试矩阵里会沦为边角料。

但如果你从不同客户那里收到三条反馈,并且都能由同一个功能解决,那么很可能还有更多用户会逐步受益。它大概率值得做。

前提是你得足够理解用户背后的场景,才能做出这个判断。

你也可以尝试用三个用户模拟来论证某个功能。比如在 EntSchema 中,我们曾经要决定是否把描述性注释做成 schema 的正式组成部分,像这样:

def fields(self) -> dict:
    return {
        "text": natural_language_string_field()
            .description("What the user wrote, in plain text"),
        "posted_at": timestamp_field().at_object_creation()
            .description(
                "When the user posted this.  If they put it on a schedule, "+
                "this is when the post was scheduled to go live."),
        # ...
    }

这会增加 schema 作者工作量;我们本来也可以让他们仅在 schema 定义里字段旁写行内注释。但把描述做成正式字段有多个场景收益:

  • 注释可进入代码生成的读取接口,让用户在 IDE 里跳转定义时看到说明。
  • 写入接口也同理。
  • 我们计划将来生成网页文档(后来也确实做了)。
  • 线上对象检查器可以把描述内联展示或做成 tooltip,帮助用户理解和调试对象。
  • 我们还能校验大家是否真的写了描述,把覆盖率做得比普通注释更高。稍后我会再讲这个点。

它高分通过了“三次法则”,所以我们做了。

提示

如果你能为某个功能想到三个有说服力的场景,或者有三个用户都提出该需求,就应认真考虑去做。

有些功能不可能一次做完,因此你需要规划分阶段发布。

分阶段构建

有时,你想支持某个用户场景,但它并非最高优先级;可如果现在什么都不做,后续再补会难很多。你面对的是一种“活板门决策(trapdoor decision)”,即一旦做出选择,之后很难回退或补救。

还有些时候,功能太复杂,无法一次发布。你需要先上早期版本、收集反馈,再完成后续部分。

这时,构建过程中应先设一个边界(perimeter)。可以想象施工现场外的铁丝网:它把人挡在外面,工人才能在里面安全建房。你最终会邀请用户进来,但得等墙体、设施和安全要素到位之后。只要普通用户还在边界外,你就对“做什么、怎么打磨”保有很大调整空间。

这样的边界能为你争取时间,后续再把功能补全。

以下是一些“先设边界、再分步推进”的例子:

  • 你想给服务提供免费层,但前提是先做好反垃圾与效率优化。那在此之前,边界就是付费墙。
  • 你想给出一个好默认值,但暂时不确定该怎么选,或还需要额外打磨才能适配所有人。那就先强制用户做知情选择;等你知道用户偏好、或打磨就绪后,再取消强制。
  • 某未来功能需要采集数据。若不提前采,后面很难为存量用户补采。那你就先要求用户提供这些数据,即便这可能劝退一部分早期用户。后续若影响增长或引发抱怨,再取消该要求。

EntSchema 的描述功能属于最后一类。我们最高优先级是提升新模型编写速度,但我们也知道“模型使用者的优质描述”同样重要。若不前置要求描述,我们几乎可以确定大多数作者不会写,后续就得做迁移补齐。这个功能加起来很快,所以我们先做了。

但我们也担心额外摩擦过多会劝退 schema 作者。理论上可能存在“两全其美”平衡点,但我们当时还有更关键问题要处理。

我们的边界策略是:起步阶段先禁止无描述字段。

根据反馈,我们很快又加了 .self_explanatory() 标签,供想跳过描述的人在字段和边上使用。可这属于黄色可供性,因为它很容易被滥用于跳过关键文档。所以我们又要求每个 schema 至少有一部分字段必须写描述,强制用户至少做一遍稀疏注释。(为避免打扰原型用户,我们在草稿模式下不做这项检查。)

后来我发现大量无意义描述,例如给 Posttext 字段写“the text of the post”。于是某次黑客松里,我加了启发式校验去标记这类空话注释。若整段文本只包含类名、字段名和 “of”“the” 之类填充词,我就判它无效,要求补充细节或改用 self_explanatory。(我担心这会让人烦,可能确实有点,但也收到过几条带笑脸的“被你抓到了!”留言,很多工程师其实乐于被这样轻推一把去写得更认真。)

我们先用简单但明确的方式起步,之后再根据反馈打磨功能、逐步下调边界,最终在易用性和抑制黄色可供性之间取得平衡。如果在完全没有用户反馈、而我们还在摸索大量基础问题时就试图一次做到这么细,那反而为时过早。

必要时先上实验版

在 Facebook,我们有句话:“code wins arguments(代码胜过争论)”。意思是,可运行、可演示的代码,比抽象讨论、场景推演、需求文档等更有说服力。

发布功能时,你不可能永远做到信息完备、信心满格。有时最好的调研方式就是先做出来,再验证假设。

在这种情况下,你可以通过内部试用功能(第 4 章)或把它作为实验发布(第 5 章)来获取更多信息。用功能开关向小比例用户灰度,甚至做 A/B 测试;发起 RFC 征求意见;找 beta 用户参与测试。

最后你可能会把功能下线,但这也没关系。

就 EntSchema 来说,我们没有做线上实验的条件,但通过内部试用完成了验证,并学到了原本不知道的问题。我们先自己写了前几个 schema,在用户上手前先体验其痛点和爽点;也把一些经过战火考验的旧模型迁移到 EntSchema,确保覆盖真实生产场景而非玩具案例。用真实使用来检验设计,帮助我们更早确定优先级最高的功能。

当我们决定了“功能是否发布、何时发布”之后,还要决定“以多宽、多通用的方式暴露它”。

窄功能 vs 可扩展功能

我们该做可扩展版本以覆盖更多用例,还是做面向特定场景的窄版本?该如何决策?

你可能有个人偏好,倾向灵活接口或强主张接口。基于场景的设计能帮助你跳出这些默认倾向。

当我们决定是用数据库层类型(如 varchar_fieldint64_field),还是用应用层类型(如 timestamp_fieldstring_enum_fieldemail_string_field)时,我们找到了三个很有说服力的场景动机:

  • 输入校验:对象创建者希望在写入损坏数据前就确保枚举值有效且已填写。邮箱等字段也应满足各自格式要求。
  • 可读性:用户希望只看字段和边的类型就能理解它们用途。
  • 生产环境对象检查器:工程师可从 timestamp_field 看到格式化时间,把 url_string_field 里的网址变成可点击链接,或从 ID 字段直接渲染到 User 对象的链接等。

综合来看,这些用例之所以打动我,是因为它们触发了另一版“三次法则”。

说明

如果一个可扩展版本能在短期内解锁三个彼此不同、且有力的场景,就做它。否则,先做用户当前最关心的一两个场景所需的定向版本。

在评估“可扩展版本”的构建成本时,还要记住几点:

  • 值得发布,就值得测试。若你说覆盖三个场景,就为每个场景写一个场景测试(第 4 章),确保扩展性不是幻觉。
  • 扩展性会增加复杂度,通常也会带来更多红色和黄色可供性。要同步发现并缓解这些风险。
  • 警惕不同用例优先级差异过大。以上列表里,优先级其实是递减的,其中运行时校验最紧急。我们原本可以只做一个更具体的方案,比如只做校验器。但最后那个场景也很有价值,所以我们选择先打基础、把对象美化展示推迟到有时间再做。换言之,我们前期先铺路,不急着一次性全做完。

再看一个“过早扩展”的例子。假设你在一家工作室做第一款游戏。工作室梦想做很多款游戏,于是你很想顺手搭一个通用游戏平台。但如果你连第一款游戏能否顺利做完都还不确定,也许更好的策略是先把通用性压到最低。等你有资金做第二、第三款时再抽象。额外好处是,到那时你会更清楚不同游戏间哪些是共性、哪些是差异,从而选出更合理的抽象。

章节小结

EntSchema受益于我们对场景方法的应用,从而提供了安全且高效的开发者体验。而“三次法则”也确实有效:EntSchema 最终演化成了通用开发平台,动力来自它从 schema 作者那里沉淀下来的知识。多年下来,它支撑了大量能力,例如网页文档、对象检查器、数据迁移工具、数据完整性检查、GraphQL schema 生成等等。它起初只是提升开发效率的工具,后来因为其中一些能力具备关键任务属性,最终变成所有实体都必须使用的基础设施。

本章核心是打造可用且安全的界面。这里回顾几条建议:

  • 不要教条化。深入人物画像与场景细节,再决定做什么。
  • 关注意符设计,确保它突出最佳可供性、弱化不良可供性,帮助用户落入“成功之坑”。
  • 你交给用户做的每个决策都应当重要、可校验,并由正确人物画像来承担。
  • 既不要盲目乐观也不要盲目悲观。用场景同时放大功能收益与潜在副作用。

下面这些建议,则把“时间与迭代”加入了场景/人物画像之外的思考维度:

  • 你在专注时工作质量最好,所以要在功能真正高优先级、值得专注时再投入建设。
  • 当你需要规避活板门决策时,先设防护边界,通过数据采集与访问限制来保留未来选择空间。
  • 与此同时,别忘了三次法则:不要在缺乏多个有力场景支撑的功能或扩展点上投入过多时间。
  • 最后,如果你仍无法决策:拿不准,就先不做。希望你能在条件更清晰时再补上。

希望你会喜欢并实际运用这些技巧。它们只是经验法则,但在我看来,用这些框架去思考艰难产品取舍,能让自己和团队都更清晰、更笃定。

练习

让我们一起做一个密码管理器。(想想 LastPass、1Password 或 Bitwarden。)你的目标人物画像是数字原生用户:他们希望为所有应用和网站安全保存高质量密码,而且不用全记在脑子里。当然,他们也希望创建、修改、存储密码都足够易用,同时尽量不易被误用。他们愿意每月支付一小笔费用,换取“再也不用操心密码”。

这组练习我选一个大家都熟悉的话题,所以先聚焦传统密码。Passkeys 虽然更现代也更安全,但目前普及度还没那么高,这里先放一边。

  1. 先确保用户具备良好密码卫生习惯。请列出在我们的管理器里“创建密码”时的绿色、黄色、红色可供性。考虑两个大场景:第一,创建新账号凭据;第二,录入他们过去已创建账号的登录信息。
  2. 参考第一问答案,至少提出两种设计思路,让密码管理器更突出绿色可供性,同时压低黄色与红色可供性。可聚焦“用户在标准注册网页上创建账号”这个场景思考。
  3. 你在考虑建设一个网站域名数据库,并维护这些域名的最新安全信息以保护用户。这是个大投入,因为你需要建立从安全社区和用户侧摄取数据的机制,因此必须有充分理由。假设唯一备选方案是依赖第三方数据库(仅提供疑似钓鱼域名列表)。结合“三次法则”,试着判断是否存在足够多且足够重要的场景,支持你自建一个更通用的数据库。

答案

  1. 下面给出一些可供性及对应场景,用来说明推荐程度。
    • 绿色:用户选择一个强随机、自动生成、且对特定应用唯一的密码。
    • 黄色:用户手输一个易记密码,而该密码可被基于常见密码训练出的算法猜中。或者,用户输入了我们检测到与其其他网站/应用复用的密码,暴露于潜在“撞库(credential stuffing)”攻击,即攻击者把泄露的邮箱/密码组合拿到其他网站反复尝试。
    • 红色:当用户录入已有账号时,输入了已知在数据泄露中受损的邮箱+密码组合。
  2. 用户手输密码(尤其短而熟的密码)非常容易。因此,自动生成密码这一功能必须做得更易用、更易发现。我使用的密码管理器 1Password 提供浏览器插件,这个插件做了几件关键事情:
  • 自动识别密码表单并弹出调用密码管理器的对话框,避免可发现性反转。
  • 提供“一键生成强密码”,让它比手输更省力,避免易用性反转(usability inversion)。
  • 一些网站有密码约束(如符号、长度要求)。插件允许用户配置这些策略,降低用户退回去手工创建劣质密码的概率。
  1. 我们自己的数据库可以包含“已验证安全域名”“发生过数据泄露的域名”,以及第三方已提供的钓鱼域名。还可以把域名映射到“已泄露密码列表”。再进一步,结合上文提到的密码策略,它甚至可以记录各域名有哪些限制。下面是几个潜在用例:
  • 当用户被要求在疑似钓鱼域名输入用户名和密码时,弹出红色警告框。
  • 当用户即将把凭据提交给一个未验证域名,且该域名并非这些凭据最初保存对应的域名时,弹出额外警告。
  • 当用户的用户名/密码可能在数据泄露中暴露时发出警告,包括其在其他网站和应用中复用该密码的情形。
  • 当足够多用户在某域名上生成带特定限制的密码时,系统可记录并推断该站点存在该类限制;后续用户可自动生成满足这些约束的密码。 当然,这些分析本身还不足以下最终决策,但基于这些场景,至少值得认真评估:我们是否应自建一个可扩展数据库,并把它发展为公司的核心竞争力

  1. 2003 年,Rico Mariani 创造了 pit of success(成功之坑)一词,用来描述“帮助用户自然落入成功实践”的设计理念。我非常喜欢这个说法。 ↩︎

9 产品架构

在软件领域,我们很少拥有真正有意义的需求。即便有,衡量成功的唯一标准也是:我们的方案是否解决了客户对其问题不断变化的理解。

— Jeff Atwood

功能性需求我们为产品可供性设定的一组目标,即让产品做到用户想做的事。第 8 章其实一直在讨论如何满足功能性需求,只是当时我们没有这么称呼它。

如果我们只想到这一层,就会得到软件版的“波将金村”——界面看起来很好,却并不能真正工作。所以当我们思考 非功能性需求(NFRs)时,就要考虑工程师需要为之设计的关键属性,包括成本、可扩展性、延迟、吞吐量、数据一致性、弹性和可用性。别忘了隐私与安全,尽管本章不展开。

NFRs 是实现目标的手段,而不是用户目标本身;有时它们代表的东西,用户只会在出问题时才注意到。

我喜欢这个主题,因为它是本书里最偏工程的内容之一,却依然能从产品思维中受益。围绕这些因素做系统架构设计,是用户中心思维与系统中心思维的纯粹融合;同时,这也是产品经理和其他角色最不容易直接参与设计的领域。

这种融合有时被称为产品架构:在用户与业务约束语境下的系统设计。我喜欢这个术语,因为它展示了把这件事做好所需的完整思维跨度。

本章我会展示如何把产品思维应用到系统设计中。

我会先打一些基础,说明如何把“编辑式思维”带入你的产品架构。

然后我会回到惯用案例研究,为后续各节建立语境。

接着我会把产品思维带到两类不同的 NFR 上。

第一类 NFR 提供快速、可靠的用户体验,比如延迟、可用性与数据一致性。

二类是可扩展性因素,如吞吐量与隔离,通常只有在很多用户同时使用系统时才会显现。

我省略了最后一类同样重要的数据治理需求,比如合规、安全与隐私,不过这些也同样能体现产品思维的力量。

最后,我会讲讲如何向客户沟通,让他们清楚哪些系统特性是可以依赖的。

产品架构的基础

产品架构与交互设计有关。这是因为内部抽象本身就是微型产品,第 8 章里关于用例、三次法则、迭代开发等多数经验,同样适用。

但产品架构有其独特难点,因为非功能性需求很棘手。它们数量很多,而且常常难以同时满足,这意味着我们通常没有足够的时间和预算把它们全部做好。更糟的是,即便时间无限,它们之间也经常互相掣肘——比如:更强的数据复制会增加延迟,安全门槛又可能削弱可用性。

因此,我们必须把编辑式思维带入产品架构:用户到底真正关心什么?

为此,我在考虑某项改进时,会用四个方法来避免最常见的错误优先级。下面我会逐一展开:

  • 拉远视角,看整体。
  • 避免“路灯效应”。
  • 讲清楚对用户的影响。
  • 用能够跨越“系统-产品鸿沟”的技术来验证影响。

这些小技巧能帮你避免过早优化:在我们还没有充分理由投入成本之前,就去改代码提升 NFR。它们往往浪费时间,甚至会伤害其他需求。

这类情况很常见,所以我会逐条讲。

拉远视角,看整体

假设你可以做一些缓存,把算法从 O(n) 降到 O(log n) 或 O(1)。这看起来很重要,但它需要工作量,也会让方案更复杂。

先拉远看语境。这个算法的运行时间,相比其他问题可能微不足道。当两个问题属于同一类型,但重要性相差一个数量级时,我们可以说其中一个问题“碾压”另一个。

如果你的函数总是通过一次网络往返被客户调用,那么把函数执行时间削掉几个微秒几乎无关紧要。进一步说,如果那次网络往返只是触发一单要一周才送达的包裹,那么优化这次网络往返本身也几乎不重要。

这些例子里,你很容易证明某个优化是过早的。我们把它一般化。

避免路灯效应

很多工程师受过系统问题导向训练,天然会往“解系统问题”上靠。他们容易在不必要的加固和过早优化里钻牛角尖。

这叫 路灯效应图 9-1)。故事是:一个醉汉在路灯下找丢失的钥匙,尽管他并不是在那里丢的。别人问他为什么在这找,他说:“因为这儿有光。”

我们的“亮区”,就是我们作为软件工程师的训练背景,以及我们最熟悉的那一角代码。

别做那个醉汉。如果你在读现有代码时发现某个数据库访问没有防竞态,在决定修复优先级之前,先在脑中模拟它在真实场景里发生的概率。

想让自己跳出“光照区”,就要在你正在解决的问题和背后的“为什么”之间画一条线——也就是迫使你思考这些问题的用户场景。

一个能让自己保持诚实的实用做法,是把理由写成“对用户的影响”。

A man looking into the lit area of a street.
图 9-1 路灯效应

传达提案对用户的影响

你会怎么把你的方案卖给用户?他们不会因为“O(log n) 算法”而买账,即便他们知道这是什么意思。但他们可能会因为“页面加载提速 25%”而买账。

以吞吐量为例。如果你对用户说“我们应该把系统扩到每秒处理一百万请求”,他们大概率会一头雾水。可如果你算出一个典型活跃客户平均每 5 秒发一次请求,那么用“用户语言”你会说:“我们的系统支持 500 万并发客户。”前一句很抽象,后一句则能促使我们做更好的决策。我们可以据此基于增长率预测用户规模,决定何时投资扩容。

这种分析并不总是便宜,我也不想鼓励你在获得产品语境成本很高时过度思考。用最佳实践直接改进 NFR,本来就完全合理。拿数据一致性来说:对一些低规模应用,为了“安全起见”用数据库事务保证强一致性很合理。它还可能让系统更易推理,从而提升团队效率。如果未来产品规模上来后,这个全强一致事务成了瓶颈,再去放宽一致性保证也不迟。

判断何时需要收集产品语境、何时应回退到标准实践,是产品架构师的一项关键能力。

一旦你确实想收集产品语境,怎么做?这通常很难。选择一套能帮助你在不同抽象层来回切换的工具链会很有帮助:从底层系统抽象到面向用户的抽象。把它们串起来,你就能理解系统架构如何影响用户。

使用能跨越系统-产品鸿沟的技术

系统-产品鸿沟,指的是围绕计算原语构建的低层抽象,与为了支撑用户动作而构建的高层组件之间的裂谷。它关乎系统组件的固有限制,以及为了让整个产品可调试、可可靠运行,我们需要做什么。

这道鸿沟主要以两种方式出现:

第一,可观测性。当系统-产品鸿沟很宽时,我们很难判断底层决策对用户的影响。这会加剧路灯效应,因为我们缺乏“看全局”的能力。

  • 如果我负责一个有特定延迟的微服务,这个延迟到底会多大程度影响产品流程?它也许和其他服务并行执行、根本无关紧要;也可能正处于关键路径。可观测性工具可以告诉我。
  • 如果我在做前端,网页组件的高内存占用对应用峰值内存占用贡献了多少?峰值正是用户最受影响的时候。内存分析器可以帮忙。

第二,可靠性。组件有其基本限制,我们把它们放进产品时必须考虑。比如网络故障永远存在。每次我面对系统问题,都要决定:是直接修底层组件,还是把它放进一个能补偿其限制的上层语境。例如:

  • 如果我在构建智能体 AI,LLM 不稳定但很强大。我可以使用带重试机制的框架来抵消失败,并加护栏确保 LLM 不会做不该做的事。
  • 如果我通过网页提供静态内容,我不能让每个用户都直接打到数据中心,否则它会被压垮。我需要用内容分发网络做缓存,把副本放到更靠近用户的地方。

意识到同一个问题可以在栈的多个层次修复,非常重要。有了合适技术,我们要么能让底层限制变得无关紧要;要么在它确实重要时,能理解其影响并知道该聚焦哪些点

案例研究导入

Stripe以极高规模处理在线支付。资金流动场景对规模与可靠性的要求,让它非常适合用于思考产品架构。

我会对我在那里工作时使用过的对象模型做一些修改和简化。Customers 从 Merchants 处在线购买商品。Merchants 拥有一个 Balance,用来追踪其账户中的资金。资金会定期打款给 Merchants;该余额不应为负,例如在向 Customers 处理退款时。

图 9-2展示了一个简化数据图,其中连线上的叉号代表一对多关系中的“多”端。图中的方框表示一个相当标准的电商数据模式:Customers 向 Merchants 发起 Payments;Merchants 维护 Balances,用来存储收到的资金并在需要时退款。

A data model diagram for Stripe Payments
图 9-2 Stripe Payments 的简化数据模型图

这个设定提供了若干有趣的产品架构挑战;本章我们将带着它一起,审视几组设计权衡。

可靠的用户体验

让我们从“共情用户”的视角,来看一些最常见的可靠性与速度问题。这会帮助我们在产品架构上更有策略性。

请始终记住:延迟、可用性和数据一致性,实际上都只是代理指标。真正重要的是:用户是否会使用你的产品,并成功完成了任务?在这一节里,我会持续主张把思考拉回根本目标,并追踪“更贴近用户”的指标。

延迟

我会先讲一种自底向上、以系统为中心的延迟思考方式,然后再讲如何判断“延迟真正在哪些地方影响用户”。你会看到,这两者用到的技术非常不同;最后我会演示如何跨越把它们隔开的系统-产品鸿沟,以便从整体视角看完整问题。

衡量系统延迟最直接的方法,是给某个操作计时,例如端点(endpoint)、函数或远程过程调用。把结果塞进你喜欢的可观测系统指标里,你会获得很多好处:

  • 给所有操作计时,你就有了完整的一组指标。
  • 如果你已经识别出某次用户交互很慢,每个操作的细粒度数据能帮助你定位元凶。
  • 优化某个操作延迟时,你可以直接从其指标里追踪改进。
  • 当出现重大回退时,能及时告警团队。

这些数据很有价值。若延迟是关注点,你通常都应这样给产品加埋点。以 Stripe 为例,每个 API 调用都做了埋点,数据库和各个服务的延迟也被监控。但要注意,这些指标都像一盏路灯,诱使你过久停留在它的光里。归根到底,重要的是用户体验,某单一操作延迟上升,用户可能会感知到,也可能不会。

如何判断延迟问题到底发生在哪?先建立直觉,再谈更严格的方法。

以下是几个“试金石”与示例:

  • 哪些操作是用户或上游系统在主动等待的,哪些是在后台发生的?用户希望立刻看到哪些内容(例如网页前几段文本),哪些可以稍后再来? 在 Stripe 这类公司处理支付时,常把请求拆成同步部分与异步部分:同步部分是校验资金是否存在、支付是否可成功;异步部分是写审计日志、把钱真正转到目标账户等。
  • 哪些用户流程最常见?优先优化这些流程,能节省最多用户时间。 在 Stripe,优化支付流程延迟比优化 Merchant 注册更重要,因为前者的发生频率高了几个数量级。
  • 用户在什么场景下对延迟最敏感?当用户尚未决定是否继续阅读/购买/浏览时,叫做低意图时段。用户在低意图时段的停顿中更容易分心。 例如,用户在浏览并把商品加入购物车时,延迟很容易让他们分心;而在完成购买时,他们通常更专注。 但如果用户信用卡被拒了呢?若你很久才告诉他,他可能已经切走页面,甚至不知道支付失败。我们应尽快完成所有校验。

这些直觉有帮助,但有时我们需要更严谨。把视角转到电商网站支付流程优化。通常客户会经历三个阶段:

  1. 逛商品并加入购物车。
  2. 打开结账页。
  3. 下单购买,然后进入支付处理。

假设我们要在这三个阶段中选一个优先优化。我们手头各有一些方案,能把每个阶段缩短几百毫秒,但实现成本都不低,我们希望先做影响最大的那个。甚至我们还不确定,延迟是不是最大问题之一。

我们真正关心什么?关键用户指标是转化率:从用户把商品加入购物车到最终支付,我们关心有多少比例的人能走完整个流程。我们的产品论题(见第 6 章)是:降低延迟会提升转化率。

转化率场景指标的一个例子,因为它追踪的是用户在某个场景中推进后的结果。

我见过一种有效技术:做 A/B 测试。对三个阶段分别在小比例用户上注入人为延迟,比如增加 1 秒,然后与未加延迟的对照组比较转化率。优先去优化对转化率负面影响最大的那个阶段。

另一个场景指标是从加入购物车到进入结账的耗时。这个指标很值得追踪,因为我们能用很多方式改善它。比如在结账页加更好的 JavaScript 信用卡号校验(如 checksum),当用户输错时就能更快纠正并推进流程。我们可能不会给这类改动单独建一个指标,但端到端流程指标会捕捉到它。

如果这个指标在一次代码发布后突然飙升,怎么定位元凶?我们需要一种技术,把产品层指标和正在采集的细粒度系统指标连接起来。

分布式追踪就是这样的桥接技术。每条用户流程都带一个“trace ID”,贯穿所有相关操作。这些操作会带着该 ID 记录事件,我们就能按会话查询全部事件,找出大延迟发生在哪里。然后可以用火焰图或瀑布图可视化。

图 9-3是一个简化图,展示结账流程前两个阶段在不同技术层所花费的时间。

A timeline showing time spent in different systems.
图 9-3 电商结账流程延迟瀑布图

当不同抽象层能在同一张图里呈现时,我们最有力量。我们可以从整条 trace“放大”到某个问题点,也能在某个具体失败处“缩小”回它所在的全流程。像分布式追踪这类技术投资,很容易因为“不是天天用”而被推迟;但一旦用上,往往是 100 倍的生产力提升。

另一个比延迟更可能影响转化的属性是可用性。

可用性

延迟一样,你既要从用户视角优化操作可用性,也要理解底层系统行为。

看一个常见配置:网站有前端团队和后端团队共同维护服务。后端团队可能知道其服务端点请求成功率,甚至还监控流量,在异常下跌时告警。

但这些指标捕捉不到用户体验的很多部分:

  • 网络请求是否真的到达服务端?
  • 客户端重试是否在弥补服务短暂抖动,把可用性问题转化为延迟上升?
  • 加载按钮的网页是否连按钮都渲染出来了?
  • 用户是否在客户端就报错,导致根本无法调用服务端?
  • 用户是否因 UI 近期改动而困惑,找不到按钮了?

这些团队协作起来,才能形成完整的产品体验画像。给客户端加监控会让你的洞察更贴近用户;再把这些指标和后端指标关联,就能更精准地定位可用性问题。

如果有能跨越系统-产品鸿沟的技术,客户端监控会更容易,例如真实用户监控(RUM)。这类工具可捕捉用户实际遇到的问题,包括页面加载时长、客户端错误、网络超时等,并把错误与浏览器版本、App 版本、地理位置等用户因素关联起来。

这里有一些能补偿系统可用性问题的技术思路:

  • 工作流引擎工作流可以跨多个阶段编排用户流程,同时给我们提供“全局视图”来看到底发生了什么。它对“完成整个流程”负责,通过重试单个阶段与管理并行性来提升对可用性问题的韧性。工作流也有助于可观测性。工作流通常对应某个产品功能或产品场景,并可追踪每一步。因此如果某一步失败或变慢,你既能知道对产品影响是什么,也能知道具体坏在哪。
  • 内容分发网络(CDN):数据希望集中存储与管理在云服务里,但用户希望它离自己更近,以降低网络问题和延迟概率。CDN 在全球复制并缓存静态数据,字面意义上弥合了数据库与用户之间的距离。

数据一致性

从用户视角拆解数据一致性,是更难的问题之一。因为你可以提供不同级别的一致性,而它们之间常常存在尖锐权衡。正如 Martin Kleppmann 在优秀著作 Designing Data-Intensive Applications 中所说:“[一致性保证]不是免费的:保证越强的系统,可能性能更差,或故障容忍更弱。”

数据一致性保证有强一致、因果一致、最终一致等类型。这些术语有用但偏抽象。就本章而言,我聚焦于从“即时一致”到“最终一致”的光谱。(完全没有一致性保证的系统通常不直接面向用户。)

你还会看到像read-your-write 一致性这样的术语。就产品架构思考而言,我更喜欢它,因为术语里直接包含了用户——“your”。这一节我会按“谁受益”来整理不同一致性保证。我把产品里的参与者分成三组:你(当前用户)、其他用户、系统。

表 9-1展示了你可提供的不同即时一致性保证,从最常见到更强逐步增强。

保证 含义
Write your Writes (WyW) 所有后续写都基于你之前的写。
Read your Writes (RyW) 用户读数据时,能看到自己此前做过的全部写入。
Write after system Writes (WsW) 自动化与智能体触发的更新会立刻生效。
Write after others’ Writes (WoW) 使用产品的其他用户无法在不知晓你写入的前提下写入,反之亦然。
Read others’ Writes (RoW) 一旦其他人的写入成功被确认,你会立刻看到其结果。
Read after system Writes (RsW) 自动化与智能体做出的编辑一经系统确认就会立刻传播。
表 9-1 以用户为中心的一致性保证

注意,这些保证是产品功能的属性,而不是数据库本身的属性。数据库通常并不知道“谁在读”,这意味着语义选择要由应用开发者谨慎完成。

事实上,你可以在弱保证数据库之上拼出更强保证;反过来,即使你对数据库发的是强一致查询,也不自动等于产品就有强一致。你会在产品架构多个层次上做决策。

例如,假设我们在为团餐下单场景构建端点(endpoints):不同用户可能同时编辑购物车,随后由订单创建者提交订单。下面按层看看如何为一致性做设计:

  • Database:我们可以用事务型数据库让所有人的写保持一致,或者把问题放到更高层去解。
  • System design:让每位点餐者在数据库里向列表追加,而不是在同一张表上做重叠写入。这会避免一整类 RoW 与 WoW 一致性问题。
  • Endpoint inputs:我们不希望客户端重复发送相同请求导致购物车重复加单。可以让客户端提供幂等键(idempotency keys),在放行相同请求前检查该键是否已提交。
  • Endpoint outputs:把更新后的数据返回给客户端。这样客户端无需再次查询并命中可能过期的数据库副本,也能看到购物车新状态。
  • Clients:用 cookie 或本地存储保留数据,提高应用响应性,并定期刷新。
  • Product decisions:当账户所有者进入结账流程时锁定购物车,避免“继续加单”和“提交支付”之间的竞态。若有人在最后时刻想加东西并抱怨被锁,所有者再手动解锁。

缩小“数据库能提供什么”与“产品真正需要什么”之间差距的桥接框架会很有帮助。比如,下面几类技术可用于“用户想要原子事务语义,但底层数据库不支持”的情况:

  • 事件溯源(Event sourcing):这类框架本质上把发生过的一切记录成“事件”,并在故障时重放和续接。例如可基于检查点重建新数据库。效果上,一整条操作链会被组织成序列,用户感知到的是该序列整体的最终一致行为。
  • Saga 模式:这种模式可叠加在事件溯源之上,在无法使用事务型数据库的操作之上,为产品功能提供“要么全部成功、要么全部回滚”的保证。你把一系列步骤串起来,并给每一步配一个失败时触发的“撤销”,系统确保最终要么全发生,要么全不发生。
  • 数据完整性检查器:根据你关心的用户结果,检查多个数据库间的一致性,比如确保一笔金融交易后付款方与收款方记录匹配。

我们看到:把一致性当作用户中心问题来思考,并对产品设计做全局思考,两者结合就能创造更一致的体验。接下来谈这三类 NFR 之间的权衡。

延迟、可用性与数据一致性的权衡

Martin Fowler说过:“架构不是构建完美系统,而是做权衡。”所以我们回到 Stripe 案例,看看产品思维如何帮助我们穿越这些两难。

像 Stripe 这样的公司通常用分片数据库存储模型。每个分片会做多副本复制以防数据丢失。其中一个副本是接收写入的“leader”。从 leader 读可立即一致;但也可从其他副本读。这类读在数据传播完成前可能过期,通常称为从读(secondary reads)

简而言之,主读取(primary reads)可用性更低但一致性更强;从读可用性更高但一致性更弱。

在 Stripe 早期,读取通常来自 leader,这让系统更易推理并保证正确性。但随着公司规模增长,它必须更规范化,让全公司工程师依据所服务的产品场景,在主读取和从读之间做选择。

练习一下:我们为几个 Stripe 场景选择语义,再选择实现方式。

  • 商户入驻:申请人在表单中逐步填写资料以完成审核并成为 Merchant。流程应根据场景正确分支,比如个体经营者还是公司主体。最后还应允许其回看提交内容。这里需要 RyW 与 WyW 语义。最省事的实现方式是主读取(primary reads)。这个用例规模远不如支付高,因为一个入驻 Merchant 平均会关联数量级更多的支付请求,因此我们可以承受更昂贵的读写。
  • 支付余额:当 Customer 向 Merchant 支付时,我们到底要在余额上强制什么?首先,Customer 余额不能为负,所以在其短时间连续支付不同订单时,这个对象应提供 WyW 语义。为实现该一致性,我们对 Customer 余额做主读取。相对地,收款 Merchant 不一定要立刻拿到钱——Customer 通常不关心对方何时到账,我们也不需要强制其余额上限。因此 Merchant 不需要 RoW 语义——当其查余额时,可做从读,看到的视图可能略旧。只有当他正和 Customer 当场对账时才会尴尬,而这显然很少见。
  • 商户黑洞化(Blackholing Merchants):一旦发现某 Merchant 是诈骗者或恐怖分子,我们希望尽快关闭其收款能力。一个简单做法是在 Merchant 表写入一个“blackhole bit”,每次 Customer 尝试向其支付时检查该位。考虑到问题严重性,是否应给用户 RsW 语义,即每次支付都对 Merchant 表做主读取?这可能带来可用性和性能问题。但若使用从读,在高负载时安全更新可能传播很慢,虽然通常仍在几秒内。这要具体分析:如果把商户标为欺诈是人工审核流程的一部分,由 Stripe 分析师做判断并点击按钮,那么相对整个流程而言,多几秒传播通常无关紧要。但如果该动作是自动触发,用于对抗快速联动威胁,这点传播延迟就可能很关键。这个场景需要深入设计,也许应使用专门针对该类威胁优化的方案——比如采用不同数据架构,或提升自动触发安全修复的传播优先级。

不一致带来的后果可能严重伤害用户体验,但一致性本身也可能对可用性造成灾难性冲击。正如前面所说,Stripe 随时间从“处处强一致”演进到混合策略。

这次转变的关键节点之一,源于几年前的一次故障。

前文没提到的是,Stripe 还为 Shopify 这样的在线商店平台、DoorDash 这样的餐饮与配送平台提供服务。它们把 Stripe 支付与其他服务打包给自己的客户。为此,它们会批量(成千上万甚至上百万)入驻 Merchants,并为其接入 Stripe 支付。

故障源于一个细节:每当 Shopify 或 DoorDash 旗下 Merchant 从 Customer 收到一笔付款,就必须向平台支付一小笔费用。

我们在每次支付请求里都会把这笔费用同步写到平台的主库(leader)数据库。不巧,我们是同步执行的。这给调用方提供了 RyW 一致性——他们可以立即收到这笔费用的明细。平时没问题,直到某个承载我们最大平台之一的数据库分片宕机近一小时,结果挂在其下的每个 Merchant 的支付请求全部失败。

事故复盘后,我的紧急任务是把平台费写入改成异步,消除这个瓶颈,避免类似的大面积故障再次发生。随后团队对一致性策略做了更广泛评估,必然引出了对许多产品场景的分析。

对数据一致性的谨慎再审,是 Stripe 在近年稳定做到 99.999% 可靠性的关键部分。

可扩展性

任何互联网产品里,规模一上来就会有问题。你的系统可能吞吐受限;一旦用户超过吞吐上限,后果可能很严重,成功的拒绝服务攻击就是例子。这源于缺少隔离:产品中的部分用户会负面影响其他用户体验。

限制吞吐的问题大致有两类:瓶颈与弹性。瓶颈是卡脖子点,阻止系统支持更多同时在线用户;而弹性问题指的是:系统“本可以”支撑更高吞吐,却对用户需求变化反应不够快——比如扩机器太慢。

这两类问题都可以通过测试提前定位,这也是本节重点。我们可以模拟用户争抢受争用资源、流量突变,以及故意超过吞吐上限,评估系统会退化到什么程度。

规模测试是个深而广的话题,本章不可能讲全。我会聚焦产品思维如何帮助你设计有效模拟,并让你更有信心去修复发现的问题。

规模模拟

整本书里,我们一直在模拟单个用户与产品/系统的交互。有时在脑中做,通过用户场景(第 1 章);有时在代码里做,通过场景测试(第 4 章);我们也让用户讲述自己的故事(第 5 章),并在客户发现访谈中模拟这些故事(第 6 章)。

但可扩展性动态太大,光靠脑补不够。我们需要更复杂的脚本来测试负载、流量尖峰和容量。

比起“test(测试)”和“injection(注入)”,我更偏好“simulation(模拟)”,因为你的主要目标之一,是构建尽可能贴近真实世界的测试环境与输入。对经验不足的工程师来说,“压测”听起来像“写个脚本对同一服务打几百万次请求就完事了”。

所以我使用以下术语,统称 规模模拟

  • 负载模拟(Load simulation)模拟一定量、真实生产风格的流量,判断系统能否扩到某个规模点。
  • 容量模拟(Capacity simulation)类似测试,但会逐步增加这类流量,尝试找出系统极限,并观察触顶后的退化是否平滑。
  • 尖峰模拟(Spike simulation)快速拉升流量,模拟真实请求突增,验证系统是否具备弹性。
说明

做规模模拟时,尽量追求高生产保真度。

先描述一个高保真规模模拟的理想图景,以 Stripe 这类互联网基础设施公司为例。设想你想知道:在当前系统下,负载翻倍是否可撑住;以及还能再扩多少容量。

  • 在代码中加入指标、日志、采样和分布式追踪,以便排查发现的任何问题。
  • 记录每个发起请求及其时间信息,方便重放模拟并复现问题。
  • 复制一套完整生产集群,称为“影子集群(shadow cluster)”。它应尽可能接近生产,唯一差别是它不与外部世界交互。
  • 每次请求进入生产集群时,复制成两条近似的合成请求(仅 ID 不同),一起发往影子集群。
  • 在容量模拟中可提高复制倍数。
  • 长时间运行,以捕捉“状态累积”导致的慢性问题,如内存泄漏、数据库索引增长等,它们会造成显著性能退化。
  • 能继续拉高流量,以精确定位问题出现点。
  • 监控场景指标,理解系统高负载时对典型用户的影响。

如果你要模拟的是新增一个大客户进入流量结构,也可以沿用同样架构,只是不做“流量加倍”,而是根据你对客户计划行为的判断,谨慎注入合成流量。这样能在既有总体使用背景下测试这位客户的流量。

这样的测试架构会带来这些收益:

  • 能发现真实场景中会发生的问题,避免路灯效应。
  • 工程师会更有信心地给问题排优先级并推进修复。
  • 工程师可根据问题出现的规模点及流量增长预测,判断修复紧迫性。
  • 团队可在同一套环境上验证性能优化效果。

这个愿景很全面,构建与运行成本也会很高。很多团队只能“因陋就简”。

下面我把它拆成一些可选的小建议。

规模模拟建议

在规模模拟中追求完整生产保真度是高目标。现实里,多数团队会在时间与预算允许范围内尽量接近它。

一种简单压测,比如高压请求某个已知可扩展性瓶颈端点(endpoint),因实现简单往往有不错投入产出比,尤其在你还没有真实用户流量可模拟之前。但它不能被误认为“全面测试”;若流量不真实,容易出现假阳性或假阴性。即便你走快速粗糙路线,也尽量喂给它接近生产的场景。

高保真模拟与简单定向压测脚本的组合也很有效:前者帮助发现瓶颈,后者让瓶颈更容易复现并验证。

首先,你通常会有一个发请求的测试 harness,以及一个接收请求的环境。这里要小心:

  • 确保 harness 自身没有限制,导致无法模拟生产。例如用简单循环发请求可能有问题,因为每个请求会受上一个请求时序影响而排队。现实里,用户不会这样排队。
  • 让模拟环境尽可能接近生产。甚至可以保留正常后台作业一起运行。

还要考虑如何激励队友真正去修复 harness 发现的问题:

  • 关注可复现性。例如让 harness 确定性执行并使用随机种子,降低结果波动。
  • 多花点时间让模拟“可信”。假设两种测试法:一种因为流量更真实,假阳性率 25%;另一种流量与现实关联很弱,假阳性率 90%。即便前者更难用,人们也会更愿意排查它报的问题。
  • 场景指标展示退化在现实中的影响。

如何模拟真实使用?影子跟随生产流量,或录制后重放,都是好实践。但如果你担心的流量与今天的实际流量不同,就需要预测客户真正会怎么用。

  • 如果你服务的是少量大客户,与他们紧密协作一起做负载测试。
  • 生成完整场景,而非单个调用。例如在 Stripe,客户可能先加载信用卡表单,随后很快提交支付。写一个把这两步串起来、并随机化间隔时间的场景。

最后,要注意时间维度:

  • 现实不会停,所以要做长跑测试。你会发现状态累积问题,比如触发垃圾回收的内存泄漏等。
  • 预测日历效应。例如:你预期流量是尖峰还是平滑?一天中的哪个时段会达峰,峰值是多少?一年中的哪些日子?在 Stripe,美国“黑五”和“网一”期间流量暴增,每年都要提前准备。
  • 把性能画像记录到数据库里,这样产品演进时能持续发现回归。

规模模拟确实极度受益于全栈思维。

桥接技术

市面上有很多负载模拟、流量捕获和故障注入工具,怎么选取决于很多因素。选择工具和方案时,把“能否建模生产”作为目标,同时确保你有足够埋点,在发现瓶颈后能快速调试。

向用户沟通非功能性需求

在线平台的客户不只想要可靠、可扩展的系统——他们还想要可兑现的保证。组织通常会和所依赖的公司协商,把某些质量门槛写进合同。作为工程师,你的工作常常是:搞清楚你能承诺哪些保证,向客户传递准确的信息,并做工程改进以达成可靠性目标。

围绕 NFR 与客户沟通有两个首要目标:对齐激励、建立信任。先说前者。

这些保证通常写在名为服务级别协议(SLA)的合同里,最常见约束包括:

  • 可用性服务可用时间占比(百分比)是多少。
  • 值班响应时间:工程师对事故必须多快响应?
  • 延迟:关键端点的响应性如何?

我先讲最常见的 SLA:可用性。一个典型合同可能约定,在某时间窗口内,你的 API 调用至少 99.9% 必须成功。若公司达不到标准,就要对客户做补偿,比如以代金券形式退款。这类合同很重要,因为它会激励公司高度重视可靠性。

SLA 不是目标,而是下限。现实中客户往往希望更高可用性。平台公司通常会设定一个更高的服务级别目标(SLO)作为内部追求。这个目标通常不对外公开,因为可能让客户困惑或误解。

因此,公司还需要其他方式建立信任,证明自己不会只做 SLA 的最低要求。

真正能建立信任的是诚实与透明:

  • 在类似 https://status.stripe.com 的站点展示可用性,长期、真实地报告运行状态。
  • 事故发生后向客户发布根因分析(RCA),更好的是公开到互联网。
  • 事故处置期间持续同步进展,说明你做了哪些动作、速度如何。
  • 列出你已完成或正在推进的整改,避免问题再次发生。

不要屈服于隐藏信息或找借口的诱惑,比如把责任推给你自己的基础设施供应商。这不会建立信任。

例如,如果你的云计算提供商是 AWS,你把某区域故障归咎于它,客户听到的是“你未来并不打算改进”。如果你改为说明将增加另一个区域或另一家云以应对灾难,客户会理解你在学习、在变好,也更愿意成为长期客户。

但在对客户透明之前,你必须先在内部透明。因为如果个人员工或其经理不敢讲出发生了什么——比如他们害怕被解雇或影响晋升——事实就会被掩盖,自然不可能传递给客户。这也是 Stripe 鼓励“激进透明”文化的一部分原因。

另一个文化实践,在 Facebook 与 Stripe 都极大加速了基础设施改进,那就是“无责复盘(blameless postmortem)”。在这种理念下,团队复盘生产事故时,主要聚焦导致事故发生的系统性失败,而不是责怪个人。当个人确有失误时,预期是学习改进而非惩罚。我很喜欢参加这种复盘——建设性、创造性,而且协作氛围好。

例如,如果某工程师在发布数据库时错误配置了一个参数,导致故障:

  • 这个人未来如何更好地验证发布?他可以在私下里和经理或技术负责人讨论一些改进方法。
  • 为什么没有健全性检查来防止这种问题?这正是团队复盘应该讨论的主题。

既然每一块基础设施本质上都是产品,那么无责复盘就是“责产品、不责用户”的实践。用这句话结束本章很合适。

本章小结

这一章把系统设计与用户中心思维综合到了产品架构领域。掌握产品架构能力后,你会做出更有策略、更有把握的选择,而用户也会因此受益。

我总结几个关键要点:

  • 不要只在路灯下找答案。要依赖你对用户及其行为的理解。
  • 拥有跨越系统-产品鸿沟的工具,你会轻松很多。选择工作流引擎、分布式追踪框架、负载模拟器等工具,既能从整体上建模产品行为,也能保留系统细节。
  • 不要只看底层指标。要追踪并设定目标到那些直接关系用户的行为,如并发用户数、场景完成率、客户端可用性。
  • 看待数据一致性,不要只盯数据库,要从用户视角理解“读到自己的写”“在他人写之后再写”等一致性概念。思考每个人的读写需要与谁同步,并创造性地在技术栈不同层次解决问题。
  • 可靠性与可扩展性的思考中,权衡无处不在。理解用户真正想要什么,才能选对平衡点。
  • 做规模模拟时,要尽量复现用户在真实世界里高强度使用你产品的方式。
  • 如果你在做平台,务必通过诚实与透明建立客户信任,同时建立机制让双方激励一致。

下面是一些练习,之后我会结束本书。

练习

你正在给在线表格产品增加协同编辑功能,类似 Google Sheets。你希望支持最多 50 人同时编辑,以覆盖团队头脑风暴、黑客松任务认领等场景(人们会把自己的名字填进单元格认领任务)。要做到这一点,你需要同时思考可扩展性、数据一致性、竞态条件和可用性。

我建议你每看完一道题的答案,再进入下一题。

  1. 如果用户想稳妥、不丢数据,他们会希望你为每个单元格提供哪一类一致性保证?
  2. 针对上一题场景,如果你希望在数据库层提供这种一致性,可以怎么做?在产品层又该如何处理?
  3. Google Sheets 实际上并没有强制强一致性保证,可能是需求不足,也可能是他们无法在高代价下做出合适设计。它采用了简单的“最后写入者获胜”策略。不提供 WoW 语义的前提下,有哪些替代办法能尽量接近一致体验?至少给出两个想法。(我的答案里一个是 Google 已在做的,一个不是。)
  4. 你会收集哪些延迟指标或追踪数据,既能看清产品整体表现,也能在延迟回归时快速缩小问题范围?至少给出一个和上面编辑场景相关的指标,以及一个无关指标。
  5. 你会如何做“多人同时编辑文档”的负载测试,才能达到较高现实保真度?

答案

  1. 谨慎用户会希望有 Write after others’ Writes(WoW)一致性——他们不想在不知情的情况下把别人刚写入单元格的内容覆盖掉。
  2. Google 可以采用标准的条件写入方案。每个客户端为每个单元格维护一个序列号,表示自己本地副本的新鲜度。每次用户提交写入时,受影响单元格的全局序列号递增。所有写入都要求“序列号匹配”这个条件成立。因此如果你提交写入时发现全局序列号已经“领先”于客户端序列号,就不允许这次写入。 这种情况下该给用户看什么?可以弹窗展示最新值和用户自己的编辑,并给出冲突解决选项。 如果一次编辑影响多个单元格(如复制/粘贴)怎么办?我猜用户会希望原子性:要么全部应用,要么全部拒绝,然后展示冲突解决界面。否则跨单元格数据不一致会很快变得混乱且难管理。
  3. Google 会给每个用户分配一个颜色,协作者在某用户点击某个单元格时,会看到该颜色的边框。这个视觉信号非常强,提示“这里可能发生冲突”,让用户自行规避,而且实现成本对 Google 来说可能相对可控。这也提醒我们:谈数据一致性时,不全是数据库问题。解决方案越贴近用户,往往越有性价比,也越有针对性。 Google 也可以做我在上一题提到的序列号和历史追踪,但不是在检测到序列冲突后直接抛硬错误,而是在冲突单元格旁显示警告图标,让用户点击处理。为了支持这一点,Google 需要保留编辑历史,给用户提供足够上下文来有效解决冲突。
  4. 我会先测每次服务器调用耗时,这样能先把问题收敛到客户端或服务端,再进一步测服务端关键数据库调用。 我还会分别以及联合追踪“表格加载”和“表格渲染”,因为这是最常见场景之一,也直接决定用户第一印象。还会测从用户在单元格输入数据开始,到数据写入云端并传播给其他用户的总时长。 颜色边框这种一致性方案如果显示不够快就会失效,所以我还会测“用户点击单元格后,其他用户看到颜色边框”的延迟。
  5. 第一阶段,我会先定义一个接近上限的高规模真实场景,然后找一群用户在同一房间里高强度使用它。或者从真实使用里筛选一些高规模会话,覆盖我要验证的特征。不论哪种,我都会记录全部行为(当然先做数据匿名化),以便在“无头浏览器”中重放这些动作,做用户动作级的负载模拟。如果我只想聚焦服务端压测,也可以去掉浏览器,仅记录并重放服务端请求。 仅重放真实数据有个隐患:真实行为受很多因素影响。比如我优化了客户端渲染延迟后,用户操作速度本身可能变快,反过来给数据库更大压力。所以重大改动后我可能要重新模拟。 基于真实数据定位出问题后,我可以再聚焦系统局部,构建更定向的负载测试去打这些热点。这样优化反馈回路更短,最后再用原始模拟流量验证整体优化效果。

收尾

像海獭一样,产品思维工程师拥有一种稀有的能力组合。把场景、人物画像、意符、可供性等思维和你的技术能力结合起来,会形成一种“结构化共情”。这与工程学科天然契合,因为它能帮助你处理权衡。

要获得这些能力,最重要的事就是持续练习把注意力放在用户身上:

  • 持续追问“为什么?”人们为什么这样行为?他们会如何从你构建的东西中受益?
  • 验证你的软件。内部试用、测试、模拟,并让你和团队直接接触客户及其反馈。
  • 迭代式构建,部分目的就是更频繁地接触用户。
  • 练习模拟用户旅程。就像棋手能预看越来越多步,你也可以提升自己讲出更长、更好的用户故事的能力。
  • 让你个人与团队目标和用户目标对齐,这会帮助你保持关注。

感谢阅读!希望这些内容对你有帮助。也欢迎反馈——如果你发现错误、认为我漏掉了重要权衡,或某些地方让你困惑,我仍然可以继续修改。

你可以在 LinkedIn 找到我,或在 Substack 关注 https://drewhoskins.substack.com,我在那里写软件工程与产品管理相关内容。

索引

符号

A

B

C

D

E

F

G

H

I

J

K

L

M

N

O

P

Q

R

S

T

U

V

W

关于作者

作为工程师,Drew Hoskins 曾为 Microsoft、Meta 和 Stripe 设计并构建过多种创新产品与平台。纵观其职业生涯,他始终致力于赋能开发者;这份热情最初源于 API 设计,随后逐步拓展到更广泛的产品与工程领域。其代表性工作包括:创立并设计 Meta 的核心 ORM EntSchema,打造 Stripe 异步服务体系中的主力 Workflow Engine,以及参与重构 Oculus VR 的服务栈。他还参与了 Stripe Connect、Stripe 的 API、Facebook 的 Graph API、Windows 7 和 Visual C++ 等项目。

最近,他加入 Temporal Technologies,担任产品经理,负责改进开发者体验。

他现居旧金山湾区,热衷于竞技桥牌,也喜欢学习技术及其历史。