YOWCon-2024-笔记-全-

YOWCon 2024 笔记(全)

001:字节大小架构会话入门 🧩

在本节课中,我们将学习“字节大小架构会话”这一协作工作坊格式。我们将探讨为什么在团队中共享知识如此困难,了解当前常见的知识共享方法及其局限性,并详细学习如何通过一系列简短、聚焦的会话,来构建团队对系统的共同心智模型,从而更有效地设计、理解和演进软件系统。

为什么共享知识如此困难?🤔

上一节我们介绍了课程概述,本节中我们来看看阻碍团队有效共享知识的核心挑战。

缺乏共享的心智模型是许多软件项目问题的根源。如果编写软件系统的程序员不清楚系统应该做什么或为什么这样做,后果可能很严重。我们行业有时过于关注“如何”做事,而忽略了“是什么”和“为什么”。

有趣的是,事件风暴的创建者阿尔贝托·布兰多利尼也提出了类似的观点,他指出“我们最大的问题是我们不分享知识”。这印证了知识共享是普遍性难题。

知识共享的困难主要源于两个方面:时间因素和角色差异。

时间带来的挑战

以下是随时间变化而产生的知识共享难题:

  • 人员变动:团队中有新成员加入,需要传授知识;或者掌握关键知识的成员离职,造成知识断层。
  • 跨团队协调:当需要两个团队协调时,反馈循环已经很大。增加更多团队,协调和知识共享的难度会呈指数级增长。
  • 计划冲突与变更:业务计划经常变化,而关于变更的知识像波浪一样在团队间传播,无法同时到达每个人,导致信息不同步。

角色差异带来的挑战

另一方面,知识共享的困难也与角色有关。不同角色(如程序员、工程经理、设计师、QA)关注的重点不同,这塑造了他们的思维方式。因此,在共享知识时,我们可能无意中遗漏了对其他角色至关重要的信息。即使团队没有正式的头衔,这些角色功能依然存在。

我们如何共享知识?📊

上一节我们探讨了知识共享的难点,本节中我们来看看目前常见的知识共享方法及其效果。

我们可以通过一个从“强反馈”到“弱反馈”的频谱来理解不同的知识共享方式。书面形式(如文档)迫使你思考所写的内容,但通常难以获得即时、高质量的反馈。同步交流(如面对面谈话或聊天)能获得更即时的反馈信号。

文档存在一个典型问题:它很难保持良好的反馈循环和及时更新。

“字节大小架构会话”位于这个频谱中“工作坊”的一侧。它是一个工作坊,意味着所有参与者都需要投入工作,而不仅仅是旁观。

什么是字节大小架构会话?🎯

上一节我们比较了不同的知识共享方法,本节我们来深入了解“字节大小架构会话”的具体定义和理念。

“字节大小架构会话”是一个工作坊格式。

  • “字节大小” 意味着它应该是短小精悍的。
  • “架构” 是希望你思考你的系统,而不是令人畏惧的宏大架构。
  • “会话” 的复数形式意味着你应该举行一系列小会话,而非一次大型活动。

唐纳德·诺曼在其关于系统的著作中指出:“系统是同时发生的”。不幸的是,它们并非按顺序发生,而我们习惯的交流方式(如语言)却是顺序性的。因此,我们需要工具来帮助理解同时发生的系统。

我们最终的目标是:理解系统当前的状态,并掌握足够的知识以保持其生命力。共享知识对于构建系统至关重要。

会话流程详解 🔄

上一节我们定义了字节大小架构会话,本节我们一步步拆解它的具体运行流程。

在运行会话之前需要准备:邀请合适的人员,并确保他们为参与做好了准备(例如,如果他们需要使用C4模型,则需要事先了解C4)。

会话本身(45-60分钟)遵循以下严格计时的流程:

  1. 设定目标:用几分钟时间共同决定本次会话要做什么(例如:“绘制系统上下文图”)。
  2. 独立协作:这是会话的核心。所有参与者(建议8-12人)在同一个物理或虚拟空间里,各自独立地完成会话目标(例如,各自绘制系统图)。计时器结束后,每人依次简要解释自己的成果。
    • 这个过程迫使你专注并梳理自己的理解。
    • 你会意识到自己遗忘或不确定的地方。
    • 倾听他人时,你能了解不同的思维角度和被你忽略的信息。
  3. 达成共识:这是耗时最长的部分(约30分钟)。基于从“独立协作”中获得的新视角,共同协作,完成同一个会话目标,尝试将所有人的理解整合在一起。
  4. 总结回顾:在会议最后用几分钟进行简短回顾,总结收获和感受。必须严格遵守计时器,如果未完成,可以留到后续会话中继续。

这种形式的感觉类似于合作式桌面游戏,大家为了解决问题而共同努力,而不是相互竞争。

为什么要采用字节大小架构会话?✨

上一节我们走完了会话流程,本节我们总结一下这种方法带来的核心好处。

采用字节大小架构会话能带来多重好处:

  • 构建共识与共同理解:它帮助团队在对系统的理解上达成一致,这为内省和有效协作奠定了基础。共识意味着找到共同点,不一定总是意见完全统一。
  • 赋能包容性协作:“独立协作”环节让每个人,包括神经多样性者,都有时间独立思考,这被许多人反馈为最高效的协作体验之一。
  • 催生涌现行为与设计资产:通过多次会话,团队会创建出一系列公认正确的模型或图表。当需要设计新功能时,可以直接在这些现有资产上构建,新的设计又会反过来丰富资产库,形成良性循环。
  • 在安全环境中培养架构实践:与其只在高压环境下进行架构讨论(选项A),不如经常性地进行这种轻量级会话(选项B),从而以小步快跑的方式学习如何建模、如何做出好的设计决策。

行业案例一:团队 onboarding 🚀

理论需要实践验证。本节我们将通过一个真实案例,看字节大小架构会话如何帮助新成员快速融入。

这是一个英国广播机构的案例。场景是:团队中有一半是新成员,领导希望帮助他们更快上手。

会前准备:邀请了在站会中活跃发言的成员(而非仅旁听者)。团队负责人负责提前教会他们C4模型。

会话过程

  1. 目标:在线协作绘制系统的C4上下文图。
  2. 独立协作:设置3分钟计时,每人独立绘制。下图展示了参与者各自绘制的、差异显著的图表(模糊处理),这直观揭示了每个人对系统的不同理解。
  3. 共识:团队共同绘制了一部分系统图,并在计时结束时意犹未尽。
  4. 总结:团队反馈他们“学到了很多”,具体指通过这个过程,他们发现了许多具体而重要的问题,从而能够开启正确的对话。

行业案例二:多团队复杂协作 🏗️

单个团队的案例展示了基础价值,本节我们看一个更复杂的场景:如何协调三个团队对齐一个复杂问题及其解决方案。

这是一个为英国电视频道改进视频管道的项目。计划是通过几轮会话:首先弄清现状,然后设计理想方案,最后决定实际实施方案。

第一轮会话:理解现状

  • 目标:绘制三个团队当前交互的系统上下文图。
  • 成果:图表显示系统交互相当复杂。一个关键发现是:每个团队成员能清晰描述自己团队与相邻团队的交互,但对更远端的交互则很模糊。这引发了非常具体的问题(例如:“团队1的这个黄色组件是如何与团队3交互的?”),而非泛泛而谈。

第二轮会话:探索理想方案

  • 目标:为改进管道寻找理想解决方案。
  • 过程:在“共识”环节,一些成员提出了看似“疯狂”的想法。这些想法反而为讨论创造了空间,让团队意识到他们可以就一个虽然不是最理想但切实可行的方案达成一致。
  • 后续:会话后,团队基于共识绘制了更详细的图表(如序列图),并明确了具体的技术细节(如契约格式)。

这个完整案例的细节被收录在尼克·滕的著作《协作软件设计》第17章中。

成功实施的关键与总结 🗝️

上一节我们看到了字节大小架构会话的强大应用,本节我们来探讨成功运行它的前提条件,并总结全课。

如果你打算尝试字节大小架构会话,有两件事至关重要:

  1. 心理安全:如果团队成员感到不安全,害怕暴露无知或受到评判,会话效果将大打折扣。需要首先营造安全的交流氛围。
  2. 成长心态与好奇心:如果参与者抱着“我没什么可学”的心态而来,那么他很可能一无所获。需要保持好奇和学习的心态。

总结与核心收获

  • 知识共享是系统构建的基石。
  • 你可以利用多样化的工具(如字节大小架构会话)来实现有效且高效的知识共享。
  • 一个系统不是其各部分的总和,而是各部分之间相互作用的总和。而我们人与人之间的沟通,正是这些相互作用中至关重要的一环。

最后,尝试运行一次字节大小架构会话吧!更多详细指南可访问 bytesizearchitecturesessions.com


本节课中我们一起学习了“字节大小架构会话”这一协作工作坊。我们从知识共享的挑战出发,了解了该会话的流程、价值,并通过两个案例看到了它在促进团队共识、解决复杂问题上的实际效果。记住,构建对系统的共同理解,是打造优秀软件的第一步。

002:简约之美——打造自己的技术

在本节课中,我们将探讨在软件开发中,尤其是在构建自己的技术栈时,保持简约的重要性。我们将分析选择现有解决方案与自主构建的利弊,并学习如何利用开源组件作为“构建块”来打造更贴合自身需求、更高效的产品。

开场与个人介绍

大家好,欢迎来到本次分享。

我想先问大家一个问题:当你们在开发技术或编程时,是否曾想过“这进展得很顺利,但我真希望它能更复杂一点”?希望没有。我们或许都认同,简约即便不一定优于复杂,但至少更可取。我们喜欢保持事物的简单。

这正是我们今天要讨论的主题:简约之美——打造自己的技术。

今天我想谈谈,为何很多时候我们被迫选择现有解决方案,而非自主构建。以及,如果你选择自主构建技术,简约如何成为这一核心流程的关键。

简单介绍一下我自己。大家好,我叫 Janan,感谢大家来听我的演讲,也感谢 YOWcon 的邀请。这是我第一年参加,非常高兴能在这里。

有些人可能通过我的 YouTube 频道 “Chno” 认识我。我在那里制作教育类编程视频,分享我正在进行的项目,还有一个代码审查系列,我会查看并点评他人提交的代码。我最出名的可能是一个教授 C++ 的系列视频。

在成为“网络人士”之前,我有一份正式工作,曾在 EA 担任软件工程师,从事游戏引擎开发。游戏引擎是构建游戏的平台,既包括开发者使用的工具,也包括游戏在用户设备上运行的“运行时”部分。

我曾参与开发 EA 的主要移动游戏引擎 Assiris,后来团队转向 Frostbite,成为 Frostbite 墨尔本分部。2019年我离开了 EA,部分原因是转向 Frostbite 后,事情开始变得复杂。在大型组织和产品中工作,流程驱动性强,代码库的变更非常缓慢。

我20岁大学毕业后就加入了EA,很想花时间消化这段“深水区”经历中学到的一切,并尝试构建自己的游戏引擎,以便透彻理解所有环节。当时我的 YouTube 频道发展得不错,所以我决定离开,全职制作视频并构建名为 Hazel 的引擎。

Hazel 游戏引擎项目

接下来我想谈谈 Hazel,因为本次演讲的核心正是我在开发这个引擎过程中学到的一切。

Hazel 是一个 3D 游戏引擎,上图是它的界面。大约五年前我开始开发它,最初是独自进行。但由于我制作了相关的 YouTube 视频,形成了一个社区,开始有志愿者对特定系统感兴趣并参与开发。团队最多时包括我在内大约有8人,我还雇佣了一些成员。这就是我们打造的成果。

我们仍在持续开发。构建游戏引擎很像艺术创作,它永远不会真正“完成”,总有新功能可以添加,新技术不断涌现,所以这是一个永无止境的过程。

当我提到我在构建自己的游戏引擎时,最常被问到的问题是:为什么?游戏引擎不是已经存在了吗?这难道不是很困难吗?

我可以花40分钟来回答这个问题,但这里尽量简短说明。首先,我从小就非常喜欢游戏引擎。玩游戏时,我对背后的技术比游戏本身更感兴趣。我认为这是一个很酷的技术挑战,因为游戏引擎是众多必须协同工作的不同系统的集合。无论你对 AI、渲染还是音频感兴趣,它都涵盖其中,并且必须在这样一个庞大的系统中协同工作。

加上严格的性能约束以及需要在多种不同硬件上运行的要求,使得解决这个问题非常酷。其次,我也想把它作为我教育视频的素材。在教授 C++ 时,有一个真实世界的例子来说明为何要这样做,比仅仅展示几行代码更有说服力。最后,我最终希望用它来制作游戏。虽然这不是主要目标,但游戏对我而言如同艺术品,而技术是艺术的一部分。因此,用我自己的技术制作游戏,就像是我的艺术创作方法。

我们确实制作过一些游戏。例如两年前,我们为一次 Game Jam 用三天时间制作了一款名为《Dichotomy》的小游戏。这是一款解谜游戏,你需要在蓝色沙盒中规划一系列动作,然后这些动作会在“真实世界”中重演。

我特别提到这个游戏,是因为它是一个重要的里程碑。在此之前,我们开发引擎时并没有明确的完成预期或必须达到能制作游戏的状态。没有投资者给我们设定截止日期,这只是我们出于兴趣在做的事情。它很好地服务于我的 YouTube 内容,并帮助我教授 C++ 和游戏引擎知识。

但有一天早上醒来,我突然意识到,只要完成某些特定工作,我就可以用它来制作并发布一款游戏了。所有必需的部件都已就位。这对我来说是个惊喜。我将其比作跑马拉松(虽然我没跑过)。马拉松看起来令人生畏,距离很长。但只要你一直向前,不总盯着终点线,朝着正确方向前进,你就在接近目标。终有一天,你会抬头看到终点线。

现在,我已经走过了那段路,打造出了能够实现其目的的技术,我可以反思:这一切真的像我想象的那么难吗?

我希望大家也能思考一下,当你完成某个项目后,回过头看,它真的有那么难吗?

如何实现目标:简约与取舍

那么,我们来谈谈我们是如何达到能够制作想要的东西的状态的。首先,技术必须简约,这不是一个选择,而是必须。因为你必须在相当严重的限制下工作,并且必须做出牺牲,意味着你无法打造一个无所不能的东西,必须坚持特定的路径。

这有点像既要聪明地工作,也要努力地工作。你不能只靠蛮力投入更多资源来解决问题,因为你没有那么多资源可以投入。

委派和外包也是其中重要的一部分,我们稍后会重点讨论。此外,你必须不断问自己:这真的像看起来那么难吗? 我能否用一种简单的方式来完成?也许这样就足够了。我认为人类天生就有把事情复杂化的倾向。

我想深入探讨“简约”这个概念,因为很多人可能认为简约是缺点,或者功能不全。但让我们看一个例子,看看简约如何成为优点。

简约的优势:以 Unity 为例

以 Unity 游戏引擎为例。这里有一个非常简单的场景,只有一个带有物理属性的立方体。请大家注意,当我点击播放按钮后,场景需要多长时间才能开始运行。

这是一个超级简单的场景,点击播放。我再操作一次,以证明这不是首次运行的延迟。这是上周下载的最新版 Unity。

现在,我们添加一些 C# 脚本行为,比如在发生碰撞时添加一个冲力。我需要返回编辑模式,Unity 需要编译和处理脚本,这需要一些时间,然后我才能点击播放。我发现自己陷入了等待,因为 Unity 的额外开销和复杂性。

对于我正在解决的这个小问题,我因为 Unity 而受到了影响。那么,对于这个小问题,Unity 是一个好的解决方案吗?

让我们看看一个更简单的例子。这是 Hazel,完全相同的场景。当我点击播放时,请大家计时,看看需要多长时间场景才开始运行。

相当快。如果我们做同样的事情,用 C# 添加一些行为(虽然 API 略有不同,但同样是添加冲力),首先通过 Visual Studio 编译,根据日志显示耗时 0.7 秒。当我切换回 Hazel 时,它已经重新加载了 C# DLL,我可以直接运行所有内容。

可以看到,因为 Hazel 更加简约,我们能够实现这种近乎无摩擦的工作流程。当然,我们并非试图与 Unity 竞争。Unity 功能多得多,能解决的问题也远超 Hazel。

但这里想让大家思考的是:如果技术简约,这可能实际上是一种优势。

那么,对于这个特定问题,Hazel 是比 Unity 更好的解决方案吗?我可能会说是。然而,你必须考虑成本。如果你花了五年时间和一千万美元来构建这个东西,那么也许你愿意等待 Unity 几秒钟,并支付几千美元的许可费。

现有解决方案 vs. 自主构建

今天接下来要讨论的概述,基本上就是选择现有解决方案与使用这些“构建块”自主构建的概念。

首先,我想谈谈我们为何使用现有解决方案,以及它们是什么。现有解决方案就像一个现成的产品,它能解决你的问题。游戏引擎就是“制作游戏”这个问题的现有解决方案。但这适用于任何行业和领域。例如,如果你是 Web 开发者,客户需要一个动态网站,支持登录、发帖、上传图片,WordPress 可能就是他们问题的现有解决方案。

在接下来的讨论中,我将继续以游戏引擎为例,因为这是我的领域。但希望大家能将其转化到自己的行业和领域,道理应该是相通的。

我认为这很大程度上归结为商业决策。作为工程师,如果被赋予解决问题的任务,我们可能更倾向于自己构建解决方案,因为我们可以做得更好。但当然,我们生活在现实世界,必须受预算或时间框架的约束。因此,选择现有解决方案在这方面会更容易。

让我们以虚幻引擎(Unreal Engine)作为现有解决方案的例子,看看其好处。首先,成本是多少?虚幻引擎收取超过100万美元后总收入的5%。我们能与之竞争吗?虚幻引擎始于1998年,此后由数千人开发。它经过尝试和测试,许多游戏和作品都在各种设备上用它发布。此外,它还包括未来的维护,你支付的许可费包含了后续开发、更新和错误修复。

还有隐藏成本,比如现有技能。因为它已经存在了26年并且公开,在就业市场上有许多人知道如何使用它。这意味着如果你需要招聘,可以找到已经准备好使用它的人。相比之下,在 EA 的 Frostbite(内部引擎)工作时,除非新员工以前在 EA 工作过,否则他们不知道如何使用,我们需要花费大约六个月的时间培训他们。这是选择现有解决方案的另一个好处。你实际上是将这部分工作外包给了第三方。

那么,如果我们不想外包,自己做需要多少钱?答案是:无限多的钱。因为你需要一个时光机回到1998年开始构建。所以看起来选择现有方案是必然的。

但现有解决方案也有其问题。它们也是商业实体,目标与你不同。它们制造产品,希望成功,因此会尽可能吸引更多客户,撒下一张“大网”。它们的产品号称适合所有人,但这可能导致复杂且不必要的软件。例如,你可能在制作一款2D PC游戏,但虚幻引擎能制作3D Android手机上的第一人称射击游戏,这完全不同。所有这些为不同场景设计的管道都必须共存于你的项目中,这对你毫无益处,甚至可能导致产品运行更慢。

此外,这些现有解决方案通常是“黑盒”(尽管虚幻引擎是源码可用的,这很好)。但很多时候你得不到源代码,只有二进制文件。这意味着如果存在 Bug,或者你需要支持另一个平台、架构或 API,你只能依赖他们为你做。这就产生了强烈的依赖性。

以 Unity 为例。大约一年前,Unity 试图引入“运行时费用”(Runtime Fee),即按游戏安装次数向开发者收费,这引起了公愤。他们后来收回了这个决定。但你不必看一年前,上周 Reddit 上就有一个例子:一家独立游戏工作室在 Steam 上发布了游戏,但他们的 Unity 账户被暂停了,合规团队说两个月后才能查看。他们因此无法访问自己制作的产品,这很疯狂。有人评论说:“这就是依赖单一供应商的风险。”

我喜欢这样总结这种情况:“你租用的工具成了你房东的工具。” 这确实是所有依赖关系的问题。当然,我认为人们并非主动选择依赖第三方,而是由于商业决策或时间限制,最终变成了这样。

自主构建:从“构建块”开始

因此,我希望大家考虑这个概念:如果你可以从头开始构建自己的定制解决方案,会不会更好? 答案可能是否定的,这完全可以。但让我们探索一下这个概念。

首先,“从头开始”是什么意思?自己生火?自己造 CPU?人们通常认为“从头开始”是一项不可能的任务,并常常以此开玩笑:“你要从头开始?难道还要自己写操作系统吗?”

但在本次演讲的语境中,“从头开始”是什么意思?我将通过一系列笑话来解释。

有人知道这是什么吗?我喜欢你们中这么多人真的说出来了(墨尔本的观众很安静,我还以为你们不知道)。我称之为“现代软件开发”。

这是我们的代码。而这些是库或框架。我在维基百科上找到了这张图。基本上,这边的小文件柜就像一个库,是一堆代码、子程序的集合。这些磁带,据我理解,她正在通过中间的机器读取,然后抄写到自己的程序中。这是在50年代,她实际上是从库中复制代码。所以,没什么变化。

这里有 Web 开发者吗?你们可能对框架和库很熟悉。好了,Web 开发者的笑话到此结束。

有人听说过 GitHub 吗?事实证明,GitHub 上有大量代码。有多少?2024年,开发者对超过5亿个开源项目做出了超过50亿次贡献。这意味着什么?我们被代码淹没了。

那么,“从头开始”在这里是什么意思?我的意思是,不要选择我们需要的那个确切产品(解决方案),而是选择构成那个产品的一切下层组件。 我们可以自由选择所有这些。以虚幻引擎为例,在其代码仓库的第三方文件夹中,你可以看到许多目录,其中大部分是第三方库。对于一个非游戏引擎的例子,比如 iOS 上的 Google Drive,在“帮助-关于”里,你可以看到所有使用的开源许可。当然,任何开发者都知道,我们依赖如此多的开源软件和库。

但我也想指出,如今这些不仅仅是支持库(如压缩库、数据结构或任务系统),还包括完整的系统

如果我们分解虚幻引擎,它由许多不同的系统组成。我随机挑选了这四个。这些系统本身又由其他系统组成,例如渲染可能有渲染硬件接口层。它们还会有许多支持库。所以,为什么我们不直接选择这些“系统”作为构建块呢? 它们是构成我们应用程序的基础。

我想聚焦于这一层。对我们来说,其中大部分将是库和开源代码,因为现在有海量的开源代码和知识。即使是在20年前,问“如何渲染3D图形”,你今天得到的答案也会全面得多。不仅有互联网上的资源,相关行业的研究也发展得非常深入。

再次强调,GitHub 上不仅有支持库,还有完整的系统和中间件。其中很多在过去是付费、许可或黑盒的。以我所在的行业为例,NVIDIA GameWorks。他们有一系列 SDK,可以提升游戏的图形效果。过去他们只提供 DLL,你只需将其插入管线,图形就会变得更好。但现在,NVIDIA GameWorks 在 GitHub 上是开源的,采用 MIT 许可证,所有 C++ 代码和着色器都在那里。时代真的变了。

这允许你将构建自己技术所需的许多系统和组件进行委派和外包,我认为这在某种程度上是新的趋势,并且越来越流行。当然,因为它是开源的,它实际上能增加你团队的资源,就像你自己完成了一样,因为你得到了相同的结果。

它还允许你交叉维护你的技术。这是好是坏,你可以自行判断。我的意思是,如果某个东西在 GitHub 上是公开的,人们在使用它,他们可能会贡献代码、提交拉取请求、进行测试。他们可能在不同的产品中使用它,做的测试比你内部团队能做的多得多。

未来的维护也是如此。即使原作者消失了,如果有足够多的人在使用那个库,他们就会有维护它的兴趣,并且通常具备专业知识。当然,GitHub 上代码质量参差不齐,但像 NVIDIA 这样的公司发布开源软件,EA、微软等许多公司也在 GitHub 上有库。这不仅仅是学生上传代码,实际上有很多高质量的东西。与自己开发相比,这可能是更好的选择。

因此,我将其视为与选择现有解决方案类似,但粒度更细。你可以在技术决策上做出更精细的选择,我们稍后会看一个真实世界的例子。

所以,请思考:与其选择确切的解决方案,不如考虑选择构成该解决方案的“构建块”。当然,如果它们是开源的,就不会是黑盒,这意味着它们可修改、可扩展

但你也必须记住,尽管如此,你仍然需要构建实际的产品。你不仅仅是把这些库粘在一起。你构建的实际产品位于这些库之上,而这些库只是将那些底层系统外包出去。

真实案例:Hazel 的构建块演变

让我们看一个真实例子,看看我所说的使用这些“构建块”是什么意思。

Hazel 也由许多不同的系统组成。这里有8个作为例子。如果我们看看2021年这些构建块是什么样子:我们有一些系统,因为我们有资源,并且团队成员有兴趣自己编写,所以它们是定制的(图中蓝色部分)。然后你可以看到绿色的部分,我们将其外包给了第三方库。当时网络系统还不存在。

随着时间的推移,随着需求的变化,情况也发生了变化。如果我们快进到今天,它看起来像这样。我用红色高亮了变化的部分。例如,对于骨骼动画,我们决定需要比之前更强大、更定制化的东西,所以我们编写了自定义方案。脚本方面,我们从 Mono 转向了基于 .NET Core 的自定义 C# 脚本引擎。网络方面,我们采用了 Valve 的 GameNetworkingSockets 库。物理方面,我们从 NVIDIA PhysX 转向了 Jolt,因为它更符合我们的需求。

这样,我们就能够审视并决定:解决方案中我不喜欢的部分,我可以直接更换那些构建块。一切都是模块化的,我可以进去改变它。而如果我们选择了一个像 Unity 那样的现有解决方案,如果它不适合,我们就必须完全转向,这可能更具灾难性,且更不可定制。

这就是我所说的“我们可以做出更精细的技术决策”的含义。与其选择确切的解决方案,我们可以选择构成它的构建块。这也允许我们将资源集中在需要的地方,以产生更具体的解决方案。

再深入一点。假设我们正在构建一个应用,我们不太关心其网络、UI 或音频部分。那没问题,我们可以将这些外包给第三方库。但也许我们应用的独特之处在于它在渲染方面做了些独特的事情。那么,我们可以再次不担心其他部分,而是将所有资源集中在那部分渲染上,构建一个完全符合我们需求的、很可能比现有解决方案更优化的定制方案。

当然,拥有所有这些构建块会带来更模块化的软件架构方法,我认为这非常好。构建块可以被替换(正如我们展示的),但产品本身更难替换。我认为在2024年,用这些外包给第三方库的构建块来制作东西,比你想象的要容易得多。

决策过程:是否外包一个构建块?

让我们看一个正在决定是否要外包某个构建块的例子。这正是 Hazel 目前正在经历的事情。

Hazel 过去使用 OpenGL 和 Vulkan 作为渲染 API。后来我们决定放弃 OpenGL,因为我们想拥抱下一代图形技术,而 OpenGL 不太适合。所以我们只用了 Vulkan。但现在,DirectX 11 特别是 12 变得很有吸引力。也许我们想在 Xbox 上发布,或者只是想要一个更原生支持 Windows 的 API。

那么,如果我们自己添加这个支持,工程成本是多少?维护成本呢?因为这不仅仅是“我要构建这个东西,然后就完成了”。如果你熟悉渲染就知道,总有新东西在开发,新版本在增加,API 在不断增长。你必须维护它。以前在某个 API 版本中可行的做法,可能在下一个版本中变成验证错误。你需要在产品的整个生命周期内支付维护成本。

那么,替代方案是什么?对于这个具体问题,NVIDIA 有一个方案:NVIDIA 的渲染硬件接口层,它抽象了所有这些 API,支持 Vulkan、DirectX 11 和 12,还有很多额外功能。我想指出,这是 NVIDIA 制作的库,不是 GitHub 上的某个随机开发者。NVIDIA 是制造这些 Vulkan 和 DirectX API 所编程的硬件的公司。显然,他们知道自己在做什么。维护方面,他们也会了解正在为其驱动程序添加和开发的新功能。所以看起来非常完美。

但真的完美吗?不。因为它不支持 Metal(苹果的渲染 API)。也许我们想为 PlayStation 制作游戏,那就需要索尼的渲染 API。所以它并不支持一切。但是,它是开源的,采用 MIT 许可证。所以至少,它是一个极好的起点。你可以基于已有的代码和抽象层,添加对其他后端(如 Metal)的支持,这比完全从零开始设计要容易得多。

构建实际产品

好了,我们讨论了构建块。现在谈谈如何在这些构建块之上构建实际产品。这是否意味着构建产品很容易,因为大部分工作已经完成了?不,因为我们仍然需要构建产品本身。构建块只是提供了周围的一切,而不是产品本身。

现在谈谈这个“90% vs 100%”的概念。对于你的特定产品,你可能会发现 90% 的工作确实是这些第三方库,你只需要完成最后 10% 来实现你想要的功能。这看起来不多。但如果你将这 10% 与选择现有解决方案时那“不属于你的 10%”相比,这最后的 10% 可能非常关键。这里存在一种平衡:这 10% 要足够小,以便在现实的时间框架内实现;又要足够大,以便你能精确地定制它以满足需求,并希望保持其优化和精简,这很重要。

当然,良好的技术决策对此也至关重要。你需要评估所有你打算外包的东西,查看这些库,运行一些测试,也许咨询他人,看看它们是否适合你。即使不适合,也不一定是灾难性的,正如我们所见,我们可以转向、根据实际经验更换它们。这也是这种方法的一个好处,你可以根据需要调整和更改它们。

关于构建实际产品,我喜欢将其归结为数据转换。我们有一些格式的数据输入,然后我们的代码(这个小小的转换块)对它做一些处理,然后输出不同格式的数据。我认为记住这一点很重要,尤其是当我们试图把事情复杂化时。本质上,我们只是需要一些代码来进行一点转换。

我认为这也有助于我们清晰地定义管道和工作流。正如我提到的,我们不能制作一个超级通用、适合一切的东西。相反,我们应该专注于:“在我的解决方案中,这就是我们做这件事的方式。” 如果有人想用不同的方式做,很遗憾,这就是规则。这允许你专注于解决核心问题,因为所有相邻的部分都可以委派出去。如果你在音频方面不需要专门的解决方案,可以直接使用现有系统。

另外,始终将 MVP(最小可行产品) 放在心中,因为这确实有助于保持事物的简约。为了说明这一点,我上周编了一个词叫 VSDD

垂直切片驱动开发。告诉你的朋友们,我想让它流行起来。垂直切片是指,当你有某个里程碑或截止日期时,强调展示项目所有组成部分的进展。你不是只在一个部分工作,而是确保项目的所有领域都在运作。

这让你能够审视从开始到结束的完整管道,看看它做了什么,然后根据需要回去改进。你负担不起钻得太深。我也喜欢将其比作“广度优先编程”而不是“深度优先”。不要在某些事情上钻得太深,让我们先保持简单,让一切运转起来。当然,之后我们可以在必要时增加复杂性。

因此,当你试图自己制作东西时,简约对于生存显然至关重要,保持这种状态非常重要。

关于优化

那么优化呢?很多人可能认为优化会增加很多复杂性。原本简单的东西可能需要变得更复杂,因为它们必须非常快。

但我发现,事实证明计算机很快。我认为我们习惯了使用那些开销巨大、功能繁多的现有解决方案。当我们自己编写简单、只做我们需要的事情的代码时,它通常就真的很快了。当然,这里有个巨大的星号:并非所有情况都如此,有很多需要考虑的例外。但结果证明,为了达到“足够快”而需要做的优化工作,远比我最初想象的要少。

总结与建议

那么,作为结论,你应该做什么?你应该构建自己的技术吗?我不知道,因为我不了解你的具体情况。但我希望你们记住,这是一个非常重要的收获:让简约成为你设计的核心。认真思考制作简单东西的重要性。

一定要考虑定制解决方案。但关键是,例如,我在 EA 从事游戏引擎工作,因此对我来说,制作自己的游戏引擎是有道理的,这是我熟悉的领域。如果你想在一个从未涉足的领域构建定制解决方案,那条路可能看起来非常不同。

再次强调,VSDD,试着让它流行起来。考虑垂直切片驱动开发。确保你始终关注全局,而不是只专注于一件事。我认为许多在大公司工作的人可能习惯了只负责产品的某个方面,甚至可能不理解一切是如何协同工作的。我鼓励你们中的一些人辞职,尝试自己构建整个产品。别告诉你的老板。

在进行 VSDD 时,缓慢而谨慎地增加复杂性。要意识到:“我正在给这部分增加复杂性。这真的必要吗?也许我可以用其他方式来做。”

最后,我想留给大家一句话:

“只有困难的事情才值得做。如果你做的事情不困难,重新思考你在做什么。”

谢谢大家。

👏 问答环节。

003:理解分布式架构的模式方法

在本节课中,我们将学习如何通过“模式”的方法来理解和描述分布式系统。我们将探讨为什么模式是理解像Kafka、Kubernetes这类复杂系统的有效工具,并通过具体例子展示模式如何帮助我们洞察其核心架构。

为什么选择模式方法?

上一节我们提到了分布式系统概念的广泛性。本节中,我们来看看为什么“模式”是描述和理解这些系统的有效途径。

分布式系统领域充斥着各种工具、框架和服务。例如:

  • 数据库:MongoDB、Cassandra、AWS Aurora等。
  • 消息代理:Apache Kafka等。
  • 基础设施编排器:Kubernetes等。
  • 内存数据网格。
  • 分布式文件系统。
  • 数百种云服务。

作为软件专业人员,要理解所有这些系统以进行架构设计或技术选型,是一个巨大挑战。我们需要理解它们为何如此设计、解决了什么问题、以及是否存在通用的解决方案和权衡取舍。模式方法正是为了应对这一挑战而生,它帮助我们在基本原理和基础构建块的层面理解系统。

实现平台共情

为了有效地使用和构建运行在现代数据中心(云环境)上的系统,我们需要理解这些环境的现实约束。这被称为“平台共情”。实现平台共情不能仅靠阅读书籍或听演讲,因为理论与实践之间存在鸿沟。

以下是两条重要的指导原则:

  1. 代码即数学:Martin Fowler曾言,代码是我们专业的数学,它能消除所有歧义。
  2. 警惕抽象:在《组织模式》一书中提到,“抽象不过是一种有纪律的忽视形式”。我们创造抽象来简化工作,但必须意识到它们隐藏了某些细节,而许多系统故障恰恰发生在这些被隐藏的细节中。

通过编写代码,我们可以穿透抽象,真正理解底层发生了什么。模式方法结合了描述性的解释和具体的代码示例,是实现平台共情的绝佳工具。

模式方法的优势

模式作为一种方法,具有以下优势:

  • 通用性与具体性的平衡:模式描述足够宽泛,可应用于多个系统(如Kafka、Kubernetes),同时又足够具体,可以用实际代码(而非伪代码)来演示。
  • 命名与结构:模式拥有公认的名称和与之关联的代码结构。这非常强大,因为当你了解一系列模式及其如何相互关联时,你就可以将整个架构视为一系列模式的组合,从而更容易理解。
  • 降低认知负荷:一旦理解了核心模式,就能更容易地理解具体系统的文档和实现。

模式实例解析:一致核心、租约与状态监视

让我们通过一个具体例子来看模式如何帮助我们理解系统。阅读Kubernetes和Kafka文档时,你会看到类似描述:

  • Kubernetes有一个控制平面,使用etcd作为后端存储。
  • Apache Kafka需要ZooKeeper(或新版本中的控制器集群)。

这些描述意味着什么?在高层次上,这些系统都呈现出一种相似的结构:一个由成千上万节点组成的大数据集群,和一个仅由3-5个节点组成的小集群(用于管理元数据)。这个小集群就是“一致核心”模式。

以下是这个模式组合的关键部分:

一致核心

  • 问题:如何在保证强一致性的前提下管理集群元数据,使得主集群能够独立扩展到数千个节点?
  • 解决方案:使用一个独立的小型集群来专门管理元数据。它实现了共识算法(如Raft、Zab),但因其性质,规模通常仅限于3-5个节点。这样,主数据集群就可以无状态地横向扩展。

租约
一致核心通常不会单独使用。一个常见需求是选举主节点或控制器。为了避免单点故障和无限期占用,系统使用“租约”模式。

  • 机制:节点向一致核心注册一个带有时限的租约(例如,注册为“server1”)。它必须定期续租。如果节点故障无法续租,一致核心会使租约过期并删除它,从而允许其他节点接管。

状态监视
大数据集群中的许多节点需要实时感知元数据或配置的变更。“状态监视”模式满足了这一需求。

  • 机制:节点可以向一致核心注册对特定数据(例如,所有以“/servers”开头的租约)的兴趣。当这些数据发生变化时,一致核心会主动通知所有感兴趣的节点。

Kubernetes的控制平面或Kafka使用ZooKeeper,本质上就是一致核心租约状态监视这三个模式协同工作的体现。理解这三者的 interplay,就能理解这些系统的协调机制。

代码实践:从模式到实现

理解了这些模式后,我们可以通过编写简化版系统来加深理解。例如,可以构建一个迷你版的Kafka或Kubernetes。

查看Apache Kafka源码,你会发现与我们描述的“租约”模式对应的代码:Broker启动时会向ZooKeeper(一致核心)注册一个“临时节点”(Ephemeral Node),这就是一种租约实现。

通过自己编写代码实现这些模式(例如,简易Kafka简易Kubernetes 项目),你可以:

  1. 创建一个可实验的“游乐场”,模拟故障,观察组件间交互。
  2. 穿透生产级代码的复杂性,直接理解核心概念。
  3. 更有效地使用生成式AI工具(如GitHub Copilot),因为你能够基于模式层面的理解给出更精准的指令并验证输出。

总结与价值

本节课中,我们一起学习了通过“模式”方法来理解分布式架构。

  • 模式是连接抽象理论与具体实现的桥梁,它通过命名、问题描述、解决方案和示例代码,帮助我们结构化地理解复杂系统。
  • 平台共情是高效架构和运维云时代系统的关键,而模式是达成平台共情的有力工具。
  • 通过分析一致核心租约状态监视等基础模式,我们能够洞悉Kubernetes、Kafka等系统协调层的本质。
  • 动手编写基于模式的简化系统,能显著加深理解,并帮助区分系统的本质复杂性(如分布式、容错)和偶然复杂性(由特定实现方式引入),从而使我们的设计决策更加清晰。

掌握分布式架构的模式,不仅能让你更容易地理解现有系统,也能让你在设计和构建新系统时做出更明智的选择。

005:构建一个模块化、AI增强的Spring Boot应用

在本教程中,我们将学习如何使用 Spring Boot 构建一个现代化的应用。我们将涵盖模块化设计、事件驱动架构、AI集成以及性能优化等核心概念,最终创建一个用于宠物领养的服务。

概述

我们将创建一个名为“Service”的宠物领养应用。这个应用将演示如何组织代码以实现模块化,如何通过事件解耦服务,如何集成AI来回答用户问题,以及如何利用Java 21和GraalVM来提升应用性能。

项目初始化

首先,我们需要创建一个新的Spring Boot项目。我们使用 start.spring.io 并选择以下配置:

  • 项目名称: Service
  • Java版本: 21
  • 依赖项: Spring Web, Spring Modulith, Spring Boot DevTools, PostgreSQL, Spring AI (OpenAI), Spring Data JDBC

生成项目后,在IDE中打开。我们需要配置数据库连接。在 application.properties 文件中添加:

spring.datasource.url=jdbc:postgresql://localhost/mydatabase
spring.datasource.username=myuser
spring.datasource.password=secret

确保本地PostgreSQL已运行并包含一个名为 dog 的表,其中包含 id, name, description, owner 等字段。

构建领养模块

上一节我们初始化了项目,本节中我们来看看如何构建第一个业务模块——领养模块。模块化的目标是减少变更的影响范围,使代码易于重构。

我们创建一个名为 adoptions 的包(模块),并在其中创建以下类:

1. 实体类 (Dog)
使用Java Record定义数据模型。

record Dog(Long id, String name, String description, String owner) {}

2. 数据访问层 (DogRepository)
使用Spring Data JDBC简化数据库操作。

interface DogRepository extends CrudRepository<Dog, Long> {}

3. 业务逻辑层 (DogAdoptionService)
处理领养业务,并发布领养事件。

@Service
@Transactional
class DogAdoptionService {
    private final DogRepository dogRepository;
    private final ApplicationEventPublisher publisher;

    DogAdoptionService(DogRepository dogRepository, ApplicationEventPublisher publisher) {
        this.dogRepository = dogRepository;
        this.publisher = publisher;
    }

    Dog adopt(Long dogId, String ownerName) {
        return dogRepository.findById(dogId)
                .map(dog -> {
                    Dog adoptedDog = new Dog(dog.id(), dog.name(), dog.description(), ownerName);
                    Dog savedDog = dogRepository.save(adoptedDog);
                    // 发布领养事件
                    publisher.publishEvent(new DogAdoptionEvent(dogId));
                    System.out.println("Adopted dog: " + savedDog);
                    return savedDog;
                }).orElseThrow();
    }
}

4. 控制器层 (DogAdoptionController)
提供HTTP API接口。

@RestController
class DogAdoptionController {
    private final DogAdoptionService adoptionService;

    DogAdoptionController(DogAdoptionService adoptionService) {
        this.adoptionService = adoptionService;
    }

    @PostMapping("/dogs/{dogId}/adoptions")
    void adopt(@PathVariable Long dogId, @RequestBody Map<String, String> payload) {
        adoptionService.adopt(dogId, payload.get("name"));
    }
}

5. 事件类 (DogAdoptionEvent)
这是一个公共类型,用于模块间通信。

public record DogAdoptionEvent(Long dogId) {}

启动应用后,可以通过CURL命令测试领养功能:

curl -X POST -H "Content-Type: application/json" -d '{"name":"Josh"}' http://localhost:8080/dogs/45/adoptions

集成兽医服务模块

上一节我们完成了领养功能,但领养宠物通常还需要安排体检。本节中我们来看看如何通过事件驱动的方式集成一个独立的兽医服务模块,而不是直接注入依赖。

我们创建另一个名为 vet 的包(模块),并在其中创建兽医服务。

1. 兽医服务 (DogDoctor)
这个服务监听领养事件,并异步处理体检安排。

@Service
class DogDoctor {
    @Async
    @TransactionalEventListener
    void checkup(DogAdoptionEvent event) {
        System.out.println("Scheduling checkup for dog ID: " + event.dogId());
        // 模拟耗时操作
        try { Thread.sleep(5000); } catch (InterruptedException e) {}
        System.out.println("Checkup scheduled for dog ID: " + event.dogId());
    }
}

这里使用了 @Async 让方法异步执行,并使用 @TransactionalEventListener 确保事件在事务提交后触发。

2. 启用异步和事务事件监听
在主应用类或配置类中添加注解:

@EnableAsync
@EnableTransactionManagement
public class ServiceApplication { ... }

通过事件驱动,adoptions 模块和 vet 模块实现了松耦合。adoptions 模块只负责发布事件,不关心谁来处理;vet 模块只监听事件,不关心是谁发布的。

使用Spring Modulith增强模块化

上一节我们通过事件实现了模块解耦,本节中我们来看看如何使用Spring Modulith来更正式地管理模块,并确保事件的可靠传递。

Spring Modulith 提供了 @ApplicationModuleListener 注解和“发件箱模式”,可以保证跨模块事件至少被传递一次,即使应用重启。

1. 修改事件监听方式
vet 模块中的监听器改为使用Spring Modulith的注解。

@Service
class DogDoctor {
    @ApplicationModuleListener
    void checkup(DogAdoptionEvent event) {
        System.out.println("Scheduling checkup for dog ID: " + event.dogId());
        try { Thread.sleep(5000); } catch (InterruptedException e) {}
        System.out.println("Checkup scheduled for dog ID: " + event.dogId());
    }
}

2. 观察发件箱模式
启动应用并执行领养操作后,查看数据库,会发现一个名为 event_publication 的表。它记录了所有待处理和已处理的事件,确保在应用崩溃重启后,未完成的事件能被重新处理。

这种方式为将来将单体应用拆分为分布式微服务铺平了道路,只需配置外部消息中间件(如Kafka、RabbitMQ)即可。

集成AI智能助手

上一节我们完善了应用的后端逻辑,本节中我们来看看如何为应用添加一个AI前端——一个能回答用户关于待领养宠物问题的智能助手。

我们将在 adoptions 模块中创建一个AI助手服务。

1. 创建AI助手 (Assistant)
使用Spring AI的 ChatClient 与大型语言模型交互。

@Service
class Assistant {
    private final ChatClient chatClient;
    // 系统提示词,定义AI的角色和上下文
    private final String systemPrompt = """
            You are a representative for the fictitious dog adoption agency “Pooch Palace”.
            Answer questions about dogs available for adoption based on the provided context.
            If you don't know the answer, say so.
            Context: {context}
            """;

    Assistant(ChatClient chatClient) {
        this.chatClient = chatClient;
    }
    // ... 后续将填充具体方法
}

需要在环境变量中设置 spring.ai.openai.api-key

2. 连接AI与业务数据(RAG)
为了让AI能回答关于我们数据库中宠物的问题,我们需要采用“检索增强生成”模式。首先,将数据库中的狗信息转换为文档并存入向量数据库。

@Component
class DataInitializer {
    private final VectorStore vectorStore;
    private final DogRepository dogRepository;

    DataInitializer(VectorStore vectorStore, DogRepository dogRepository) {
        this.vectorStore = vectorStore;
        this.dogRepository = dogRepository;
    }

    @PostConstruct
    void init() {
        List<Document> documents = dogRepository.findAll().stream()
                .map(dog -> new Document(
                        String.format("ID: %d, Name: %s, Description: %s", dog.id(), dog.name(), dog.description()),
                        Map.of("id", dog.id(), "name", dog.name())
                )).toList();
        vectorStore.add(documents);
    }
}

3. 实现问答功能
修改 Assistant 服务,在提问时先从向量库检索相关文档,再将文档作为上下文提供给AI。

@Service
class Assistant {
    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    private final String systemPrompt = ...; // 同上

    String answerQuestion(String question) {
        // 1. 检索相关文档
        List<Document> similarDocs = vectorStore.similaritySearch(question);
        String context = similarDocs.stream()
                                    .map(Document::getContent)
                                    .collect(Collectors.joining("\n"));
        // 2. 构建包含上下文的完整提示
        String userPrompt = systemPrompt.replace("{context}", context);
        // 3. 调用AI
        ChatResponse response = chatClient.call(
                new Prompt(userPrompt + "\nUser Question: " + question)
        );
        return response.getResult().getOutput().getContent();
    }
}

现在,当用户询问“是否有神经质的狗待领养?”时,AI会检索到Prancer的信息,并给出准确的回答。

优化性能:虚拟线程与原生镜像

上一节我们为应用添加了智能功能,本节中我们来看看如何让应用运行得更快、更高效。我们将利用Java 21的虚拟线程和GraalVM原生镜像技术。

1. 启用虚拟线程
虚拟线程可以大幅提高高并发I/O密集型应用的吞吐量。在 application.properties 中启用:

spring.threads.virtual.enabled=true

无需修改业务代码,现有的 @Async 和Servlet容器将自动使用虚拟线程。

2. 演示性能对比
创建一个简单的测试端点,模拟调用外部慢服务。

@RestController
class DelayController {
    private final RestClient restClient;

    DelayController(RestClient restClient) { this.restClient = restClient; }

    @GetMapping("/delay")
    String delay() {
        return restClient.get()
                         .uri("https://httpbin.org/delay/5")
                         .retrieve()
                         .body(String.class);
    }
}

使用压测工具(如 hey)分别测试启用和禁用虚拟线程时的性能,可以观察到启用虚拟线程后,吞吐量显著提升,延迟降低。

3. 使用GraalVM生成原生镜像
原生镜像能提供极快的启动速度和更低的内存占用。确保已安装GraalVM并配置了 native 构建工具。
在项目中,可能需要为反射等特性添加提示(例如使用 @RegisterReflectionForBinding 注解)。然后运行:

./mvnw -Pnative native:compile

编译完成后,会生成一个原生可执行文件。运行它,启动时间通常在几十毫秒,内存占用也远低于传统JVM模式。

总结

本节课中我们一起学习了如何构建一个现代化的Spring Boot应用。我们从模块化设计开始,通过清晰的包结构减少耦合。然后利用事件驱动架构,让领养模块和兽医服务模块解耦,并通过Spring Modulith保证了事件的可靠传递。接着,我们集成了Spring AI,通过RAG模式让智能助手能够基于我们自己的数据回答用户问题。最后,我们探讨了性能优化,使用Java 21的虚拟线程应对高并发场景,并通过GraalVM原生镜像技术获得了极致的启动速度和运行时效率。这套组合拳帮助我们构建了一个结构清晰、智能高效、易于扩展和维护的生产级应用。

006:遗留系统现代化的新纪元

在本节课中,我们将探讨如何利用生成式人工智能来理解和现代化遗留代码系统。我们将分析遗留系统现代化面临的挑战,并介绍一种结合了抽象语法树、知识图谱和检索增强生成技术的新方法。

概述:遗留系统现代化的挑战

上一节我们介绍了本次演讲的背景。本节中,我们来看看为什么遗留系统现代化如此困难。

现代化遗留代码实际上非常困难,这并非一个已被行业完全解决的问题。根据研究,超过70%的现代化项目最终失败。这背后有多种原因。

以下是导致遗留系统现代化困难的一些关键因素:

  • 系统规模庞大且复杂:超过70%的世界系统仍在大型机上运行,代码量巨大。
  • 不切实际的业务期望:业务方通常期望在短时间内完成现代化,例如解决许可问题。
  • 组织架构的影响:康威定律持续发挥作用,孤岛化的专业团队导致沟通问题和缓慢的发布周期。
  • 技术变化的快速步伐:围绕系统的生态系统(如向云迁移、AI兴起)在不断变化,而遗留代码库难以适应。
  • 过时的开发方法论:漫长的分析、开发、部署和测试周期,大量的交接工作,以及外包和短期项目,导致组织内缺乏理解代码的人,形成单点故障。

生成式人工智能能做什么?

既然遗留系统现代化如此困难,生成式人工智能与此有何关系?

自2022年以来,生成式人工智能领域经历了多轮实验。人们的焦点一直集中在如何让开发者更快地编写代码上。然而,对于资深工程师,这有时反而会降低效率,因为他们需要先审查生成的代码。对于初级工程师,则可能产生不理解所生成代码的问题。

但生成式人工智能真正擅长的是将大型语言模型视为非结构化数据,进行总结并允许你进行查询。考虑到遗留代码库,代码本身是结构化数据,但其周围发生着各种非结构化的事情。如果能将所有这些信息整合起来,你就能真正理解代码库在做什么。

在Thoughtworks,我们专注于重新架构和重建,而不是完全重写。我们思考如何理解现有系统的能力,哪些部分易于现代化,哪些部分困难,从哪里开始,以及如何利用绞杀者模式逐步剥离功能。然而,即使采用这种渐进式方法,仍然成本高昂且充满风险。

理解代码:核心问题

现代化过程非常复杂,涉及技术、流程、团队结构和沟通等多个方面。本节课我们将聚焦于理解代码

当你首次接触一个完全不了解的遗留系统时,你想理解哪些事情?

首先,你想知道它是做什么的。它试图实现什么目标?有哪些功能和能力?这通常是一个缓慢而艰巨的过程,需要大量与人员访谈。

其次,一旦理解了功能,你想知道它是如何工作的。这些功能是如何实现的?这时静态和动态分析工具会变得有用。

第三,你需要关心关系与依赖。代码是高内聚的,还是分散在许多不同系统中且高度耦合?理解这些对于评估风险和问题至关重要。

最后,你想进行假设分析。如果我做X或Y,真正的影响是什么?更改会产生什么后果?这通常会导致大量的恐惧和决策瘫痪。

为什么这些问题难以回答?

即使有静态分析工具,回答上述问题也很困难,原因如下:

以下是具体原因:

  • 缺乏文档:即使有文档,也往往很快过时。
  • 缺乏安全网和测试:为高度耦合的遗留系统添加测试框架可能非常困难且测试脆弱。
  • 领域专家稀缺:对于大型机系统,领域专家通常很少,且预约咨询耗时。
  • 人类记忆的局限性:即使是领域专家,也可能遗忘细节。
  • 代码本身的特性:例如,过程式代码对于面向对象背景的开发者可能难以理解。
  • 开发者的个人习惯:在缺乏编码标准的时代,不同开发者的代码风格差异巨大。
  • “大泥球”架构:基于项目制开发且缺乏团队持续改进所有权,导致代码高度耦合。
  • 陈旧的工具链:代码库越老,可用于分析它的现代工具就越少,效果也越差。

解决方案:将代码视为数据

考虑到上述所有问题,我们思考生成式人工智能是否能帮助解决。我们能否理解现有系统及其设计?能否在不依赖专家的情况下收集知识?能否以符合现代实践的方式转换代码?

我们决定从大型机代码开始尝试,因为如果能解决这个问题,其他代码库的问题也能迎刃而解。

我们采取的方法是逆向工程,试图提取出一些低层需求,以便理解系统的现有分析和需求,然后据此确定新系统的构建目标和方法。

我们构建了一个名为 Concise 的内部工具。其核心思想是将代码视为数据

我们的方法包含以下几个关键步骤:

  1. 解析代码并构建抽象语法树森林:我们将代码解析后存入图数据库,这非常适合此类数据,允许我们从高层概览深入到低层细节。
  2. 利用图数据库:我们使用 Neo4j 图数据库来存储和遍历代码的结构和行为视图,它非常易于与大语言模型、搜索和检索增强生成技术集成。
  3. 结合检索增强生成与图查询:我们使用 RAG 和图遍历技术,将相关信息整合起来,生成更简洁、有用的答案。

技术演进:从简单提示到知识图谱增强

该工具经历了多次迭代。

最初,我们尝试了最直接的方法:将整个代码文件丢进提示窗口进行询问。这对于大型文件会产生上下文窗口问题,模型可能会“忘记”部分内容。

接着,我们尝试利用检索增强生成。我们将代码块向量化,结合用户提示,从图谱中仅提取相关的节点,这有助于减少信息噪音和上下文窗口问题。

然后,我们通过遍历图谱来收集跨代码的更多信息。Neo4j 存储了这个丰富的知识图谱,为LLM提供了更多上下文,并利用了图结构。你可以获取关于代码的各种解释,包括描述、需求或能力。

我们为此工具构建了前端界面,允许用户直接询问并查看系统功能,从而填补知识空白。

演示:工具如何工作

想象一下,你的客户需要理解系统身份验证是如何工作的。通常,你需要与领域专家坐在一起询问。

但使用我们的遗留系统助手工具,你可以直接提问。该工具使用知识图谱作为后端存储的扩展RAG流程,它会根据具体问题的语义,在代码库的知识图谱中找到相关节点,然后引入与这些节点相连的其他节点。

我们通过解析代码并利用生成的抽象语法树来互连所有相关元素,从而构建这个知识图谱。这些元素不仅基于代码,还包括我们引入的描述和其他信息,共同构成了代码结构和行为模型的更全面视图。

这一切结合起来,使你摆脱了短暂的上下文窗口限制,帮助你处理现有代码的语义,并引入可能不在同一文件中的依赖关系。

映射业务能力

仅理解代码是不够的。我们还需要映射业务能力。在与客户合作时,我们会进行事件风暴和领域驱动设计等练习,以真正理解系统的能力。

拥有这些能力地图至关重要,否则你无法确定目标架构。你需要梳理支持它的所有流程和人员,创建庞大的流程图和能力地图。

在生成式人工智能出现之前,所有这些工作基本上都是手工完成的,由开发人员、分析师和领域专家携手合作,进行大量手动努力。对于小型代码库尚可,但对于数万行代码,理解系统需要耗费大量时间。

生成式人工智能改变了这一局面,它赋予了你总结大量文本的能力。在我们的集成开发环境中,团队可以选择访问能力地图,并进行逆向和正向工程。

通过获取这些能力地图并理解代码,你能够就保留什么、舍弃什么以及系统中哪些是死代码做出明智决策。这非常重要,因为系统中通常存在大量未被使用的代码。此外,它还允许你理解技术能力(如身份验证)与业务能力的区别。

现在,通过将代码库和业务能力地图整合到一个应用中,你可以将代码映射到能力上。这意味着你现在可以开始理解应用的接缝。

实际应用与成果

当我们采用这种方法——即结合大语言模型、RAG、抽象语法树和知识图谱来理解代码库时,我们看到了以下成果:

我们研究了两个不同的代码库。第一个是Java代码库的实验,开发者被要求查找并修复错误。使用此工具的开发者组比未使用组速度快了95%,因为大部分工作正是定位错误。

第二个案例涉及一个大型COBOL系统(约10,000行)。传统的逆向工程方法完全依赖领域专家。使用我们的方法后,原本需要领域专家和业务分析师花费六周理解系统的任务,被缩短到了两周

该工具在以下方面提供了帮助:

  • 减少对领域专家的依赖:非常关键。
  • 提高逆向工程的速度和质量:关于“幻觉”问题,我们遇到的并不多。因为领域专家也是人,他们也可能“记忆模糊”或完全忘记事情。最终,你仍然需要检查、编写测试并与领域专家沟通,但现在你可以带着更多的上下文和信息去交流。
  • 理解和隔离缺陷:如前所述,能够理解错误并隔离问题。
  • 映射和理解系统能力:这是逆向工程的关键部分。

总结与展望

本节课中,我们一起学习了生成式人工智能如何为遗留系统现代化带来变革。

生成式人工智能将赋予大多数人理解遗留代码库的能力,而这在过去只掌握在少数人手中。这对于我们行业是一个巨大的障碍和挑战。

我们的行业在过去10年(更不用说20、30、40年)已经发生了翻天覆地的变化。我们有了不同的实践、不同的软件架构方式和不同的方法论,而旧的系统根本无法支持或适应这些变化。正如之前有人提到的,处理遗留代码“不性感”,没人愿意做。但有了这样的工具,情况将发生改变。

我们预计未来几年在这个领域会出现更多进展。人们已经对此感到兴奋,即使不打算立即现代化系统,他们也希望使用这类工具来理解和管理系统中的风险。

我们期望有一天这类功能能集成到IDE中。我们可以想象在IDE内部就能询问和理解代码的运作方式。

我们没有看到太多偏见或幻觉问题。我认为这将改变我们对现代化的思考方式。我期待看到这个领域出现什么样的工具。

对于那些直面“全球70%系统运行在大型机上”和“70%现代化项目失败”问题的人来说,这比让开发人员速度提高10%(目前真实研究显示的效果)要有趣和激动得多。

007:一次以代码为中心的Gleam语言之旅 🚀

在本教程中,我们将跟随YOWcon 2024软件开发大会的一次演讲,探索Gleam编程语言。我们将从最基础的“Hello World”开始,逐步构建一个功能完整的Web服务器,涵盖项目创建、依赖管理、Web框架使用、数据库交互以及Gleam的核心特性。我们的目标是让初学者能够理解Gleam的设计哲学和开发体验。

1. 初识Gleam:友好的语言 🌟

Gleam被描述为一种友好类型安全可扩展的语言。这意味着它易于学习,拥有强大的静态类型系统,并且能够构建适应分布式和并发世界的系统。

2. 从“Hello World”开始 👋

旅程从最经典的示例开始。以下是一个简单的Gleam程序:

import gleam/io

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/yowcon-2024/img/40e7262097c11a640bf8c2d5d7ba8411_4.png)

pub fn greet(name: String) {
  io.println("Hello, " <> name)
}

pub fn main() {
  let names = ["Lucy", "Yao"]
  list.map(names, greet)
}

这段代码定义了一个greet函数来打印问候语,并在main函数中为列表中的每个名字调用它。如果你接触过JavaScript等主流语言,会发现其语法非常相似。Gleam的设计目标之一就是让语法不成为学习新概念的障碍。

3. 我们的目标:构建一个Web服务器 🏗️

仅仅打印字符串并不够。我们的目标是构建一个真正的服务器。它需要能够:

  • 处理客户端请求并返回HTML响应。
  • 可能需要进行模板渲染。
  • 从数据库读取数据。
  • 具备日志记录功能。
  • 高可用并能承受高负载。

但在深入编写代码之前,我们需要了解如何开始一个Gleam项目。

4. 项目创建与工具链 🛠️

Gleam提供了出色的开箱即用开发体验。安装Gleam二进制文件后,你可以通过以下命令轻松创建新项目:

gleam new my_project

项目结构非常直观,包含src(源代码)和test(测试)目录。要运行代码,只需执行:

gleam run

要运行测试,命令是:

gleam test

Gleam编译器速度极快,支持增量编译。这意味着在开发过程中,每次保存代码后重新运行测试,几乎能立即获得反馈,实现了真正的快速迭代开发。

5. 添加依赖:构建Web服务器 📦

要构建服务器,我们需要依赖项,如HTTP服务器和Web框架。Gleam内置了包管理器,添加依赖非常简单:

gleam add wisp mist

这里,wisp是Gleam的Web框架,mist是HTTP服务器。添加后即可在项目中使用。

Wisp框架体现了Gleam的哲学:简单、明确、无隐藏魔法。Gleam是一门显式的语言,没有隐式控制流、类型转换、空指针或未检查异常。代码即所见,这使代码更易于理解和维护。

6. 使用Wisp框架处理请求 🔄

在Wisp中,核心概念是处理器。处理器是一个函数,它接收客户端请求并返回要发送回客户端的响应。

以下是一个简单的处理器示例:

import wisp

pub fn handler(req: wisp.Request) -> wisp.Response {
  let html = "<h1>Yao</h1>"
  html
  |> wisp.string_to_html_tree
  |> wisp.html_response(200)
}

这段代码构建了一个HTML字符串,将其转换为Wisp用于高效构建响应的数据结构,最后返回一个状态码为200的HTML响应。

Gleam的管道操作符|>让代码更清晰。它将左侧表达式的结果作为第一个参数传递给右侧的函数,避免了为中间变量命名的麻烦。

7. 添加中间件:日志与错误恢复 🛡️

一个严肃的服务器需要日志记录和错误恢复。在Wisp中,这通过中间件函数实现。

以下是添加日志记录和崩溃恢复的示例:

import wisp.{type Request, type Response}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/yowcon-2024/img/40e7262097c11a640bf8c2d5d7ba8411_20.png)

pub fn handler(req: Request) -> Response {
  // ... 处理逻辑 ...
}

// 使用中间件包装处理器
pub fn main() {
  wisp.serve(
    wisp.log_request(_, handler)
    |> wisp.rescue_crashes
  )
}

log_request中间件记录请求,rescue_crashes中间件自动捕获处理器中的崩溃并返回500错误,防止客户端一直等待。

然而,随着中间件增多,基于回调的API可能导致代码嵌套过深,形成“回调地狱”或“末日金字塔”。Gleam提供了use关键字来解决这个问题。

8. 使用 use 关键字改善代码结构 🧹

use关键字是一种语法糖,它能将后续代码块转换为匿名函数,并传递给use右侧的函数。这能有效扁平化嵌套的回调。

例如,上面的代码可以用use重写:

pub fn main() {
  use req <- wisp.log_request
  use _ <- wisp.rescue_crashes
  handler(req)
}

这样,无论添加多少中间件,代码的缩进都不会无限增长,保持了可读性。Gleam的语言服务器可以轻松地在回调风格和use风格之间转换代码,帮助开发者理解。

9. 使用Lustre进行声明式HTML渲染 🎨

硬编码HTML字符串功能有限。我们可以使用lustre库,它以函数式、声明式的API来定义HTML文档。

在Lustre中,创建HTML元素就像调用函数一样简单:

import lustre/element.{html}
import lustre/element/html.{h1}

pub fn my_page() {
  h1([], [html.text("Yao")])
}

h1函数接受两个列表:属性列表和子元素列表。这种方式完全是函数调用,没有嵌入新的DSL或宏。这意味着你可以用组织普通Gleam代码的方式来组织HTML生成代码,重构起来非常方便。

例如,我们可以将显示用户账户列表的代码拆分成可复用的函数:

fn render_account(account: Account) {
  // 渲染单个账户
}

fn render_account_list(accounts: List(Account)) {
  list.map(accounts, render_account)
  |> html.ol([])
}

Lustre还支持构建交互式、客户端丰富的单页应用,无需每次交互都访问服务器。

10. 与数据库交互:拥抱SQL 🗄️

服务器通常需要与数据库对话。Gleam社区推崇的方式是拥抱SQL,而不是试图用ORM完全隐藏它。SQL本身就是强大的查询语言。

我们可以使用sq库。它的方法是使用普通的SQL文件。

首先,创建一个SQL文件,例如 src/database/accounts.sql

-- src/database/accounts.sql
SELECT id, display_name, joined
FROM accounts
WHERE joined BETWEEN ? AND ?

然后,运行sq命令,它会根据SQL文件自动生成类型安全的Gleam代码。

在Gleam代码中,可以这样使用生成的函数:

import my_app/database

pub fn handler(req: Request) -> Response {
  let today = date.today()
  let one_month_ago = date.sub_days(today, 30)

  case database.list_accounts(db_conn, one_month_ago, today) {
    Ok(accounts) -> render_account_list(accounts)
    Error(_) -> wisp.internal_server_error()
  }
}

sq生成的函数是类型安全的,编译器会确保你传入正确数量和类型的查询参数。这种方式避免了ORM可能导致的N+1查询或过度获取数据的问题,同时保留了SQL的全部能力。

11. Gleam的强大之处:多运行时支持 ⚡

Gleam能编译到两个强大的运行时:

  1. JavaScript:可以在任何能运行JavaScript的地方(浏览器、Node.js等)运行Gleam代码。
  2. Erlang:编译到Erlang,运行在BEAM虚拟机(Erlang VM)上。

BEAM虚拟机诞生于电信行业,专为构建高容错高并发分布式系统而设计。这些特性正是现代云服务所必需的。

因此,你可以用Gleam编写后端(编译到Erlang,运行于BEAM),同时用Gleam编写前端(编译到JavaScript,运行于浏览器),实现真正的全栈开发,并共享业务逻辑代码。

12. 总结与资源 📚

本节课中我们一起学习了Gleam语言的核心旅程。

Gleam是一门旨在提高生产力的语言。它通过简单的语法、强大的类型系统、出色的编译器错误信息以及一流的工具链(内置编译器、构建工具、包管理器、代码格式化器和语言服务器)来实现这一目标。

它基于两个基本构建块:数据函数。这种简单性使得代码易于推理、测试和维护。正如一位社区成员所说:“阅读我的Gleam代码让我感到平静。”

如果你想继续探索Gleam:

希望这次旅程能激发你对Gleam的兴趣!

008:糖动力加密 🐜🔐

概述

在本节课中,我们将学习如何利用一个看似奇特的混沌系统——蚂蚁农场——作为加密随机性的来源。我们将探讨伪随机数生成器的基础知识、现有的混沌熵源,并详细介绍一个将蚂蚁行为转化为可用加密熵的研究项目。


伪随机数生成器基础

首先,我们需要了解伪随机数生成器。它们是许多加密操作的基础。

伪随机数生成器用于生成加密密钥、签署安全证书、生成TLS握手密钥、文件邮件加密以及数字签名。它们在后台被广泛使用,我们通常对此习以为常。

计算机本质上是确定性的。它们完全按照指令执行,无法自行产生真正的随机性。因此,我们需要从外部为计算机寻找随机性来源。

我们将这种随机数据称为“熵”。伪随机数生成器需要一个“种子”来启动。它们基于这个种子生成可预测的随机数序列。虽然这听起来不是真正的随机,但它非常有用。例如,在测试中需要生成相同的随机序列时,或者因为伪随机数生成器比其他真随机数生成器更高效、运行更快时。


系统中的熵源

那么,我们如何找到这种随机性呢?在Unix系统中,常见的熵源设备是 /dev/random/dev/urandom。还有一个系统调用叫 getrandom

/dev/random/dev/urandom 的主要区别在于,前者是阻塞的,而后者是非阻塞的。如果你请求一定量的熵而当前不足,/dev/random 会阻塞调用,直到收集到足够的熵。

这些随机数据通常由系统事件填充。例如,键盘敲击的时间间隔是难以预测的。硬盘访问读取事件等系统事件也具有一定随机性。任何难以预测的、发生在需要随机性的机器上的系统事件,都可以作为方便的熵源。


OpenSSL 与熵

OpenSSL是一个开源密码学库,它广泛使用自己的伪随机数生成器进行加密操作。它被用于许多软件包,从Web服务器到命令行工具(如curl)。OpenSSL也作为一个命令行工具存在,例如在GitHub上生成密钥对时可能会用到。

通常,在Unix系统上,OpenSSL会使用 /dev/random/dev/urandom 来获取熵,为其伪随机数生成器提供种子。


其他混沌熵源

除了系统事件,世界上还存在其他受混沌系统启发的熵源。

Random.org 是一个提供随机数的网站。其随机数生成器使用大气噪声作为熵源。它通过分布在全球的无线电设备,收集未使用频率上的背景噪声来生成随机性。

一项2003年的研究探索了使用放射性衰变作为高效、简单的随机性来源。研究尝试从家用电离式烟雾探测器中的镅元素衰变中提取可用的熵。

更著名的例子是“熔岩灯熵”。这项技术于1998年获得专利。其核心思想是:熔岩灯中流体的动态行为是混沌且难以预测的。通过用摄像头对准一排熔岩灯,定期捕获其状态,并将图像转换为二进制字符串,再经过加密哈希函数处理,最终得到可用于播种伪随机数生成器的熵。

Cloudflare公司在其总部大厅就使用了这种方法。相关专利和博客文章提供了更详细的技术细节。


研究构想:蚂蚁农场熵

受到“熔岩灯熵”的启发,我们提出了一个想法:能否用蚂蚁群落作为混沌熵源?

蚂蚁的运动看起来是混乱且难以预测的。它们似乎一直在活动。研究表明,大多数蚂蚁物种会进行极短时间(最多一分钟)的小睡,然后恢复活动,而不是像人类一样长时间集体睡眠。这意味着它们可以近乎全天候地提供混沌行为。

这引出了两个核心研究问题:

  1. 蚂蚁群落作为一个混沌系统,是否是足够的熵源?
  2. 是否可能替换常用伪随机数生成器的默认熵源?

项目实践:硬件与设置

为了验证想法,我们开始了实践项目。

首先,我们获得了蚂蚁。蚁后可以存活长达15年,工蚁从卵到成虫需要8-10周。我们从一个装有蚁后、一些工蚁、水和糖水的试管开始。

蚂蚁被安置在一个“初创蚁巢”中。这是一个小空间,适合初期群落成长。未来它们会迁移到一个带有激光切割隧道、空间更大的“成长蚁巢”中。

为了24/7捕获蚂蚁状态,我们需要一个摄像头。我们选择了一款支持红外夜视的USB摄像头,以便在黑暗环境下(不打扰蚂蚁)也能清晰拍摄。摄像头通过3D打印的支架固定在蚁巢上方。


项目实践:软件实现

我们的应用目标是家中的Linux服务器。它运行着许多使用TLS/SSL的服务(如MediaWiki, Home Assistant),并且所有代码提交都使用数字签名。这些操作都依赖于伪随机数生成器。

我们决定从替换OpenSSL的熵源入手。审计发现系统上有大量软件依赖OpenSSL库。

接下来,我们实现类似于“熔岩灯专利”的流程,但针对蚂蚁农场:

  1. 记录混沌系统状态:定期(例如每5秒)拍摄蚂蚁农场的照片。
  2. 数字化状态:提取照片的RGB像素值,并将其转换为一个很长的二进制数字序列。
  3. 应用哈希函数:使用SHA-256哈希函数处理上一步得到的数字,生成一个固定长度(256位)的哈希值(摘要)。公式可以表示为:熵数据 = SHA256(图像像素数据)
  4. 添加到熵池:将这个哈希值添加到累积的熵池中。
  5. 提供种子:当OpenSSL等程序需要熵来播种其伪随机数生成器时,从该池中提供样本。

我们编写了一个Go语言实现的熵收集守护进程来管理这个过程。它使用OpenCV库捕获摄像头图像,计算SHA-256哈希,并将其与现有熵池进行异或混合。守护进程还会检查哈希的唯一性,并将重复项和对应图像记录到数据库以供分析,这是评估熵质量的重要部分。

OpenSSL可以通过重新编译来配置使用自定义熵源。我们使用 --with-rand-seed=egd 选项,并指定我们守护进程的Unix域套接字路径。


初步成果与未来步骤

目前,项目已取得一些初步成功:

  • 我们能够使用蚂蚁农场生成的熵,通过OpenSSL命令行工具重新生成SSL证书并为其签名。
  • 我们也用这个熵池重新生成了用于代码提交签名的GPG密钥对。现在,每次提交到该项目仓库的代码,都是由蚂蚁间接“签名”的。

当然,项目还在进行中。蚂蚁群落需要时间成长到更具规模的阶段,以提供更丰富的混沌行为。

下一步计划包括:

  1. 将蚂蚁迁移到更大的永久性蚁巢中。
  2. 重新设计并3D打印适配新蚁巢的摄像头支架。
  3. 长期收集熵数据,并使用更严格的测试套件(如Diehard测试)分析熵的质量。
  4. 最终,将服务器上依赖OpenSSL库的服务重新配置,使其使用我们的蚂蚁熵守护进程作为熵源。

总结与思考

本节课我们一起探索了伪随机数生成器和熵的概念,了解了从系统事件到熔岩灯等多种熵源。我们重点介绍了一个将蚂蚁农场作为混沌熵源的研究项目,涵盖了从生物习性研究、硬件搭建到软件实现的完整过程。

这个项目可能看似不切实际,但它充满了价值:它有趣,带来了意想不到的学习机会(如蚂蚁习性、OpenSSL内部机制、熵收集守护进程的历史),并且鼓励我们在常规工作之外进行探索和创造。并非每个项目都需要立即证明其商业实用性,享受学习过程、满足好奇心本身就是一个美好的目标。


(注:本教程根据演讲内容整理,所有技术实现均为研究性质,请勿用于生产环境。)

010:使用 Spring AI 构建聊天机器人

在本节课中,我们将学习如何利用 Spring AI 框架,将生成式人工智能(GenAI)能力集成到企业级 Java/Kotlin 应用程序中。我们将通过构建一个聊天机器人,探讨核心概念、最佳实践以及面临的挑战。

概述:企业开发者在 GenAI 浪潮中的角色

生成式人工智能是真实存在且不会消失的技术。然而,将其有效应用于商业应用程序中却极具挑战性。许多相关博客内容与实际应用场景相去甚远。本教程旨在展示如何利用我们已有的技能,特别是 Spring 框架的知识,来构建更贴近现实的 GenAI 应用。

我们将展示构建或赋能 GenAI 应用的核心概念,证明 JVM 平台在构建此类应用时可以与 Python 竞争,并介绍一些最佳实践和挑战。

Spring 的持久生命力与核心概念

Spring 框架诞生于本世纪初,至今依然蓬勃发展。其成功主要基于三个核心理念,它们构成了所谓的“Spring 三角”:

  1. 依赖注入:Spring 组件相互了解以及与基础设施交互的方式。
  2. 可移植的服务抽象:Spring 应用程序与底层资源和能力(如不同环境的事务管理)协作的方式。
  3. 面向切面编程:结合上述两者的技术,例如通过 @Transactional 注解透明地管理事务。

这些核心概念历经二十年考验,被证明极其强大,并且非常适合应对构建 GenAI 应用程序的挑战。

语言选择:Python 与 JVM

在 GenAI 和机器学习领域,Python 无疑占据主导地位。每个开发者都应该学习并能够阅读 Python,特别是现代 Python(如 3.12),其类型提示系统已相当完善。

然而,这并不意味着企业应用开发应该从 Java/.NET 等现有技术栈转向 Python。对于需要集成 GenAI 的企业级应用,坚持使用现有技术栈并利用其上不断增长的 GenAI 能力(如 Spring AI)通常是更好的选择。本教程将通过 Spring AI 的现场编码,展示 JVM 平台在 GenAI 领域的竞争力。

项目初始化与核心模型配置

我们将从创建一个新的 Spring Boot 应用开始。可以使用 start.spring.io 初始化项目,添加以下依赖:

  • spring-ai-openai-spring-boot-starter
  • spring-ai-ollama-spring-boot-starter
  • spring-boot-starter-data-neo4j
  • spring-boot-starter-actuator

以下是项目配置的核心部分,展示了如何设置不同的 AI 模型。

配置多个聊天模型

Spring AI 访问 GenAI 模型有两个核心概念:聊天模型(如 OpenAI 的 GPT-4)和嵌入模型(用于向量相似性搜索)。我们可以轻松配置多个模型。

@Configuration
public class AiConfig {
    @Bean
    @Primary
    public ChatModel openAiChatModel() {
        // 配置默认的 OpenAI 聊天模型
        return new OpenAiChatModel(...);
    }

    @Bean
    public ChatModel ollamaChatModel() {
        // 配置本地的 Ollama 聊天模型
        return new OllamaChatModel(...);
    }
}

通过 @Primary 注解,我们可以指定默认注入的模型。这种设计体现了 Spring 的 可移植服务抽象依赖注入 理念,使得在不同模型间切换变得容易。

最佳实践提示:尽可能使用能满足任务需求的最小模型。这既环保(减少能耗和水耗),也提高了应用的可解释性。将应用拆分为多个更小、更专注的交互,比完全依赖一个巨型“黑箱”模型更可控。

构建基础聊天服务

配置好模型后,我们可以开始构建聊天服务。Spring AI 在 ChatModel 之上提供了 ChatClient 的概念,用于管理具体的应用上下文,如系统提示词。

@Service
class ChatService(
    private val chatModel: ChatModel, // 注入默认的(OpenAI)模型
    private val ollamaChatModel: ChatModel // 注入 Ollama 模型
) {
    private val chatClient = ChatClient.builder(chatModel)
            .defaultSystemPrompt(ResourceSystemPromptTemplate("classpath:/prompts/system-prompt.md"))
            .build()

    fun chat(userMessage: String): String {
        return chatClient.prompt(userMessage).call().content
    }
}

系统提示词(system-prompt.md)设定了聊天机器人的角色和行为基线,例如:“你是一个乐于助人的助手,在古典音乐商城工作,帮助用户发现古典音乐并了解作曲家。”

运行此服务后,当我们询问“你的业务有实体店吗?”,模型可能会基于其训练数据生成一个看似合理但未经证实的回答,甚至“捏造”事实。这引出了 GenAI 应用的一个核心挑战:幻觉

实现检索增强生成以避免幻觉

为了避免模型产生幻觉,我们需要使用 检索增强生成(RAG)技术。其核心思想是:在回答用户问题前,先从我们自己的知识库中检索相关材料,并将这些材料作为上下文提供给模型。

RAG 的工作原理

  1. 拥有一个存储相关材料(如文档片段)的数据库,通常是向量数据库
  2. 将材料内容通过嵌入模型转换为向量并存储。
  3. 当用户提问时,将问题也转换为向量,并在向量数据库中执行相似性搜索,找到最相关的内容。
  4. 将检索到的内容与原始问题一起提交给 LLM,从而生成基于事实的答案。

在 Spring AI 中,可以通过 QuestionAnswerAdvisor 轻松实现 RAG。

@Service
class RagChatService(
    private val chatModel: ChatModel,
    private val vectorStore: VectorStore // 注入向量存储(如 Neo4j)
) {
    private val chatClient = ChatClient.builder(chatModel)
            .defaultAdvisors(
                MessageChatMemoryAdvisor(), // 管理对话记忆
                QuestionAnswerAdvisor(vectorStore) // RAG 顾问
            )
            .build()

    fun chat(userMessage: String): String {
        return chatClient.prompt(userMessage).call().content
    }
}

QuestionAnswerAdvisor 是一个 AOP 顾问,它会在幕后拦截用户消息,先查询向量库,然后将检索结果附加到提示词中,再发送给 LLM。重启应用后,再次询问“有实体店吗?”,回答将基于我们索引的文档数据,变得准确可靠。

利用 AOP 处理横切关注点:主题守卫

面向切面编程(AOP)非常适合处理像事务管理这样的横切关注点。在 GenAI 应用中,一个明显的横切关注点是内容安全与主题控制,例如防止提示词注入或过滤不相关/有害的话题。

我们可以创建一个自定义的 Advisor,例如 TopicGuardAdvisor,在对话开始前对用户消息的主题进行分类,并决定是否继续。

class TopicGuardAdvisor(private val localChatModel: ChatModel) : Advisor {
    // 简单的数据类定义主题
    data class TopicClassification(val topic: String) // 例如:sport, religion, politics, other

    private val classifierClient = ChatClient.builder(localChatModel)
            .defaultSystemPrompt(ResourceSystemPromptTemplate("classpath:/prompts/topic-guard.md"))
            .build()

    override fun advise(request: ChatRequest): ChatRequest {
        val userMessage = request.messages.last().content
        // 使用本地小模型对消息主题进行分类
        val classification = classifierClient.prompt(userMessage)
                .call(TopicClassification::class.java) // 请求结构化输出

        if (isBannedTopic(classification.topic)) {
            // 如果是禁止的话题,则短路流程,直接返回预设回复
            throw ShortCircuitException("抱歉,我只能帮助你了解古典音乐。")
        }
        return request
    }

    private fun isBannedTopic(topic: String) = topic in listOf("politics", "religion")
}

关键点说明

  • 使用专用模型:我们使用一个更小、更快的本地模型(如 Ollama 上的 Gemma 2B)专门进行分类任务,这比用大模型更经济高效。
  • 结构化输出:通过 call(TopicClassification::class.java),我们要求模型返回一个结构化的 JSON 对象。Spring AI 会自动生成 JSON Schema 并指导模型输出,我们无需手动解析。
  • 提示词模板:提示词 topic-guard.md 中使用了 {userContent} 这样的占位符,Spring AI 的模板语法与 Python f-string 兼容,便于动态内容插入。

当用户询问“你喜欢特朗普吗?”时,主题守卫会将其分类为“politics”,并直接返回“抱歉,我只能帮助你了解古典音乐”,而不会调用主聊天流程。

另一个关键技术:函数调用

除了 RAG,函数调用是另一个让 LLM 与真实世界交互、获取动态信息的强大技术。现代 LLM 经过训练,可以理解何时应该调用开发者提供的函数。

在 Spring AI 中,可以方便地将 Spring 管理的 Bean 作为函数暴露给 LLM。

@Service
class FunctionCallingChatService(
    private val chatModel: ChatModel,
    private val functionCallbacks: List<FunctionCallback> // 注入所有函数回调
) {
    private val chatClient = ChatClient.builder(chatModel)
            .defaultFunctions(functionCallbacks) // 注册函数
            .build()

    fun chat(userMessage: String): String {
        return chatClient.prompt(userMessage).call().content
    }
}

// 定义一个函数回调 Bean
@Bean
fun composerInfoFunction(neo4jTemplate: Neo4jTemplate): FunctionCallback {
    return FunctionCallback.builder("getComposerInfo")
            .withDescription("根据作曲家姓名获取其生平信息和代表作")
            .withExecutor { request ->
                val name = request["name"] as String
                // 执行实际的 Neo4j 查询
                val result = neo4jTemplate.query("MATCH (c:Composer {name: \$name}) RETURN c.bio, c.works", mapOf("name" to name))
                // 将结果转换为字符串返回给 LLM
                result.toString()
            }
            .build()
}

当用户问“告诉我贝多芬的信息”时,LLM 会识别出需要调用 getComposerInfo 函数,并传入参数 name=“贝多芬”。函数执行后,将真实的数据库查询结果返回给 LLM,由 LLM 整合成自然语言回复给用户。这使得聊天机器人能够执行具体的业务操作。

最佳实践与挑战总结

最佳实践

  1. 拥抱 Kotlin:现代 Spring 与 Kotlin 结合非常简洁高效。
  2. 外部化提示词:不要将提示词硬编码在代码字符串中,应放在外部资源文件(如 .md.txt)中,便于管理和优化。
  3. 使用多模型策略:根据任务场景(聊天、分类、摘要、翻译)选择最合适的模型,大模型并非万能。
  4. 利用现有生态:Spring AI 让你能充分利用 Spring 容器已有的能力(如数据库访问、事务管理)。

主要挑战

  1. 幻觉问题:需要通过 RAG、函数调用等技术将模型输出“锚定”在可信数据和逻辑上。
  2. 可解释性:LLM 作为黑箱,其决策过程难以解释。通过分解任务、使用小模型、加入人工审核环节可以提高可控性。
  3. 成本与环境影响:大型模型的推理耗费大量计算资源。优化模型使用策略至关重要。
  4. 操作复杂性:管理模型版本、提示词版本、向量库数据同步等是新的运维负担。

结语

本节课我们一起学习了如何使用 Spring AI 构建一个具备 RAG、主题控制和函数调用能力的聊天机器人。我们看到了 Spring 的核心概念——依赖注入、可移植抽象和 AOP——如何为构建坚实、可维护的 GenAI 应用提供强大支撑。

虽然面临幻觉、可解释性等挑战,但通过合理的架构设计和技术选型,企业开发者完全能够利用熟悉的 JVM 和 Spring 技术栈,将 GenAI 安全、有效地集成到生产应用中。

代码仓库:本教程的完整代码可在 GitHub 上获取,仓库地址为 https://github.com/johnsonr/spring-ai-demo(注:此为示例,实际地址请参考视频中提供的信息)。

011:遗留系统现代化模式

在本节课程中,我们将学习“可塑化开发”的核心理念与实践模式。我们将探讨如何通过开放领域模型,让软件系统变得可解释,从而能够回答关于其自身的问题。课程将通过具体示例,展示如何将这一理念应用于实际开发环境。

目标:构建可解释的软件系统

我们的目标是让软件系统变得可解释。那么,什么是可解释的系统呢?其对立面是不透明的系统。我们认为,当今大多数软件系统都是不透明的,很难回答关于它们的问题。一个可解释的系统则相反,它会暴露其内部运作,让你能够与之对话,从而获得问题的答案。

传统方法的局限性

以一个用Java实现的学生游戏为例。你可以通过其用户界面与之交互,例如双击骰子来投掷。但除此之外,你无法做更多事情,也无法通过交互获得对应用程序的深入理解。其界面非常有限。

我们还有其他一些方法,但都存在局限:

  • 阅读源代码:对于小型游戏或许可行,但对于任何真实的软件,这无法扩展。
  • 阅读文档:文档可能未涵盖你感兴趣的部分,且常常与实际系统不同步。
  • 使用通用分析工具:这类工具很多,但通常无法回答你的具体问题。
  • 查阅在线资源:它们可能不了解你的具体上下文,只能回答关于框架的通用问题。
  • 使用生成式AI:它可能未在你的软件上进行训练,并且你无法确定其答案的可靠性。

这些方法都存在局限性。

可塑化开发的核心思想

我们的观点是,软件本身是希望与你对话的,我们应该让它做到这一点。这就是今天要传达的核心信息:你应该尝试让软件与你对话。我们将展示一些实现这一目标的方法。这并非易事,但你可以从一些简单的事情开始,再逐步尝试更高级的方法。

可塑化开发的理念是:如果你想理解一个软件系统,就打开它,添加大量微小的自定义分析工具,这些工具能回答你关于软件内部运作的问题,从而使系统变得可解释。

本质上,这是在你的开发工具和软件应用之间建立一种对话。我们希望拥有开放的开发工具,而不是封闭的,让它们能够了解我们的软件应用。因此,它们向我们展示的不是通用界面,而是大量微小的自定义工具。

实践演示:从游戏到遗留系统

我们在自己的平台(Glamorous Toolkit)上实现了这一点。让我们通过几个例子来演示。

示例一:Ludo游戏

之前看到的Ludo游戏,现在不是通过用户界面查看,而是在一个对象检查器中查看游戏实例。我们打开了对象检查器,并针对Ludo游戏提出了各种问题,例如:游戏的玩家有哪些?游戏的方格及其状态是什么?游戏执行到当前状态有哪些移动步骤?

在左侧,我们有一个Ludo游戏实例;在右侧,我们有一个移动步骤的实例。这些都是我们提取出来并使其显式化的不同领域对象。通过“塑造”应用程序和对象检查器,工具变得非常有用,能告诉我们关于软件的信息。

示例二:COBOL遗留系统

Amazon Web Services 提供了一个用于演示遗留主机现代化的COBOL应用程序。下载后,你会得到一堆文件:图像文件、随机图表、大量源代码和COBOL文件。仅仅阅读源代码并不是理解其运行的有效方式。

我们可以将这些信息包装起来,使其成为可塑化的对象。例如,将所有菜单文件包装成领域对象。然后,我们可以为这些对象添加自定义工具。例如,为菜单集合添加一个工具,或者为单个菜单生成其可达性图,从而可视化整个应用程序的结构。

这些微小的分析工具通常只需几行代码即可实现。

现场演示:可塑化开发工作流

现在,我将用几分钟时间,现场演示在我们的平台上可塑化开发是如何工作的。我希望大家思考:如何在自己的平台中实现类似功能?哪些东西对自己有用?

我们使用Github托管Glamorous Toolkit的所有源代码。我们可以从Github获取组织及其仓库的信息,返回的是JSON数据。直接阅读JSON就像阅读COBOL程序一样乏味,但我们可以做得更好。

以下是工作流程:

  1. 从探索开始:通常从一个笔记本页面开始,描述我想要做什么,并编写代码片段进行探索。
  2. 包装原始数据:从Github获取的JSON数据本身不是领域对象。我们创建一个Organization领域类来包装这些原始数据。
  3. 添加自定义视图:为Organization对象编写一个视图方法,将其原始数据以更友好的方式展示出来。这通常只需几行代码。
  4. 深化领域模型:我们发现Organization有仓库列表,但返回的是字典数组。我们创建Repository领域类来包装每个字典。
  5. 迭代与完善:为Repository添加有用的方法(如name)和视图。然后,为Organization添加一个显示所有仓库的视图。
  6. 形成可探索模型:现在,当我们检查Organization对象时,可以立即看到所有仓库,并可以深入查看单个仓库,继续提问和探索。

这个过程几乎总是相同的。一旦我们这样做并迭代一段时间,就会得到一个可探索的领域模型。我们提出的问题、找到的答案以及创建的视图,都成为了应用程序的一部分。应用程序被“塑造”了,你的开发环境将展示这些内容,而不仅仅是原始源代码,从而使系统变得可解释。

核心模式总结

我们已经在演示中看到了许多模式。现在,让我们快速回顾其中几个关键模式。

模式一:自定义视图

如何轻松地保存在探索活动领域模型时发现的有趣信息?关键是将这些有趣的数据转化为自定义视图。

通常,开发者找到问题答案后,整个过程就丢失了。在这里,我们将整个过程转化为一个工件——自定义视图。它保留下来,你明天可以再次看到,你的同事也可以看到。它成为了应用程序的一部分。

自定义工具只需几行代码即可实现。如果实现成本低廉(例如只需几分钟),那么这将非常有效。如果像构建插件那样昂贵,就没人会去做。你的投入时间不应超过你为回答问题原本就需要的时间。

视图可以是简单的列表、树、文本编辑器,也可以是转发到现有视图、图形布局(如COBOL菜单图),甚至是复用已有的GUI。

模式二:示例对象

如何创建处于特定状态的对象以启动可塑化开发任务?答案是:将示例包装为实例方法,可选地评估一些测试,并返回示例实例。换句话说,将单元测试转化为工厂。

一个单元测试完成后通常只是显示“通过”。我们则说,除了“通过”,还要返回对象,以便你能用它做点什么。

这看起来是一个微小的改变,但影响巨大。现在你拥有了测试,如果你想理解这个测试,可以查看对象。你可以深入其中,拥有游戏实例,并开始与之交互和探索。这样的示例可以被重用,作为另一个测试的起点,并且可以组合。它们成为一种活的文档形式。

模式三:可塑化工具

对于工具构建者,如何在实践中实现这一点?这需要在一定程度上开放你的开发工具。

问题在于:如何扩展开发环境,添加能够解决你应用领域问题的自定义工具?你必须开放开发工具,通过将自定义行为与这些工件关联,使它们能够根据所处理工件的动态上下文进行“塑造”。

例如,对象检查器默认显示原始视图。为了让回答“第6步移动发生了什么”这个问题更容易,我设计了一个微小的自定义视图,让我能以简单的方式查看所有移动并深入其中。这是对对象检查器的一个小扩展。

你可以为其他工具做同样的事,比如代码浏览器、代码编辑器,甚至是调试器。我们最近开始开放调试器,创建了可塑化调试器,它提供比堆栈视图更友好、更有趣的视图(例如显示字符串差异)。

其工作原理完全相同:异常是一个对象,该对象定义了一些视图。其中一个视图作用于对象检查器,另一个作用于可塑化调试器。

模式库与总结

我们看到的这些模式都被记录在案。如果你下载Glamorous Toolkit,会找到动态笔记本页面,其中详细记录了所有模式。也有一篇PDF论文描述了所有模式的细节。

让我们回顾一下我们见过的模式:

  • 可解释系统:我们想要达到的目标。
  • 可塑化工具:已开放支持微小自定义扩展的工具。
  • 可塑化对象:我们为其添加视图的工作对象。
  • 示例对象:从测试返回的具体示例对象。
  • 上下文化工作区:对象检查器底部的小型REPL,用于编写可重构的实验性代码。
  • 自定义视图:我们看到的,大多数都非常简单。
  • 自定义操作:类似于视图,但表现为可执行的操作(如小按钮)。
  • 自定义搜索:为拥有大量对象的领域定义专门的搜索。
  • 可塑化数据包装器:将原始数据(如来自Github的JSON)包装为对象,以便为其添加视图。

为了让这一切工作,你至少需要开放一些工具使其变得可塑化,但这通常不需要巨大的实现努力。示例模式则需要修改你的测试框架,使测试能够返回对象。

课程总结

总而言之,可塑化开发是一种我们相信具有普遍适用性的方法论,旨在通过使领域对象可见并用自定义分析工具扩展它们,从而使软件系统变得可解释。

Glamorous Toolkit 是具体如何实现的一个例子。你可以自由下载它,但我更鼓励你思考如何将这些想法应用到你自己的开发环境、语言和工具中。

本节课中,我们一起学习了可塑化开发的核心理念,并通过多个实例看到了如何将不透明的软件系统转变为可解释、可对话的系统。关键步骤包括包装领域对象、创建自定义视图、利用示例对象作为起点,以及开放开发工具以支持微小的自定义扩展。希望这些模式能启发你在自己的项目中实践可塑化开发。

012:构建能够自我构建的API层

在本教程中,我们将探讨现代软件开发中一个核心但充满挑战的领域:系统集成。我们将分析传统集成方式的痛点,并介绍一种基于语义的自适应架构方法,旨在消除手动编写的“胶水代码”,让系统能够自动发现、连接并适应变化。

1️⃣:传统集成方式的挑战

集成始于一个简单的场景:一方是拥有数据或服务的生产者,另一方是希望使用这些数据或服务的消费者。消费者需要构建某种集成来连接两者。

然而,在企业环境中,这个过程远非如此简单。在开始编码之前,消费者首先需要经历一个漫长的发现之旅,以追踪他们所需的数据源位于何处、由哪个团队维护。这本身就是一个巨大的挑战。

当消费者终于找到生产者后,他们拿到API规范(如OpenAPI Spec),真正的工作才刚刚开始。他们需要:

  • 根据规范生成客户端代码。
  • 处理字段映射(将生产者的字段名映射到消费者内部模型)。
  • 考虑错误处理、重试机制。
  • 实现缓存以提升性能。
  • 处理多个服务间的编排、并发和级联故障。

更复杂的是,一个消费者通常需要连接多个生产者,这意味着上述工作需要重复多次。而整个组织中存在无数消费者,每个都在重复着类似的发现、构建和维护工作。这种集成代码通常是项目特定的,难以复用。

研究表明,在企业中构建并交付一个内部集成的平均时间长达3到9个月。这暴露了当前基于“胶水代码”的集成方式效率低下、成本高昂的问题。

2️⃣:问题的核心——紧耦合

上一节我们看到了传统集成方式的低效,其根本原因在于它导致了系统的紧耦合

耦合的定义是:一方的变更会导致另一方出现问题。在当前的集成模式下,生产者发布API变更(如V2版本)时,所有依赖它的消费者都可能“爆炸”,需要手动修复。

作为架构师,我们尝试过多种策略来解耦系统,但效果有限:

  • 微服务:旨在通过服务边界解耦,但API变更仍会破坏消费者。
  • 无服务器:减少了服务器管理,但连接点更多,变更影响依旧。
  • 消息队列:生产者和消费者通过消息中介解耦,但消息格式的变更仍会破坏消费者。
  • API网关/GraphQL:这是一个较好的策略,它在中间层封装了集成逻辑,保护了消费者。但当网关本身需要变更时,影响范围依然很大。

关键在于,我们并非要阻止变更,而是要适应变更。问题在于,我们手动编写的“胶水代码”将生产者和消费者紧密地耦合在了一起。

3️⃣:一种不同的思路——基于语义的集成

既然传统方式导致了紧耦合,本节我们来看看一种不同的思路。其核心思想是:让生产者和消费者发布声明,而非直接连接。

具体来说:

  1. 生产者发布其API契约(包含数据含义)。
  2. 消费者发布其数据需求(描述需要什么数据)。
  3. 将这些声明提交给一个中间层,由它来负责处理集成。

为了让这个中间层真正区别于传统方案,它必须遵循两个规则:

  1. 能够按需连接:当新的集成需求出现时,它必须能自行计算出如何编排已知的各个系统来满足需求,而无需工程师部署新代码。
  2. 能够适应变更:当底层系统重构、字段名更改或数据源切换时,集成应能自动适应,无需人工修复。

核心原则是:必须没有“胶水代码”。 没有任何需要我们去编写代码的地方。

这一切都始于API规范。我们拥有机器可读的API规范(如OpenAPI),但为什么第一件事总是交给人类工程师来处理呢?为什么机器不能自己构建“胶水”?

答案是:我们现有的API不够丰富。它们只描述了传输和结构(地址、协议、字段名),而没有描述语义(数据的含义)以及数据间的关系。

我们思考和讨论需求时使用语义(如“客户邮箱”、“账户余额”),但编写代码时却针对结构(如JSON路径)。业务语义很少变化,而系统结构却频繁变更。这造成了认知与实现之间的鸿沟。

4️⃣:Taxi——为API注入语义

为了解决语义缺失的问题,我们引入了 Taxi。Taxi是一种用于为API添加语义的语言。

它的核心作用是表达“这个字段和那个字段是同一个东西”。例如,Customer服务中的id字段和Cards服务中的customerNumber字段,虽然名称和所属系统不同,但都表示“客户ID”这个语义。

Taxi是语义元数据。它不替代现有的API规范,而是作为补充元数据嵌入其中。你可以在Protobuf、OpenAPI、SOAP或JSON Schema中加入Taxi注解。

Taxi本身是一个完整的语义类型系统,支持编译、构建工具和检查器,确保正确性。它也可以独立用于描述特定对象。

以下是三个不同团队使用不同技术栈时嵌入Taxi语义的示例:

  • 团队A(OpenAPI):在字段中添加x-taxi-type注解。
  • 团队B(Protobuf):在字段选项中添加Taxi注解。
  • 团队C(代码优先,如Kotlin):在代码中使用注解生成包含语义的API规范。

通过Taxi,我们将数据的含义(What)与位置和结构(Where & How)关联了起来。

5️⃣:TaxiQL与Orbital——查询与执行引擎

上一节我们介绍了如何用Taxi描述语义,本节我们来看看如何利用这些语义来查询数据并自动执行集成。这主要通过 TaxiQLOrbital 实现。

当我们为所有系统的API添加了Taxi语义并发布后,中间层(Orbital)就能构建一个庞大的数据关系图。基于此图,它可以回答语义化的问题。

例如,对于问题“给定邮箱地址,查询客户的账户余额”,Orbital可以自动推导出执行路径:1)调用REST API通过邮箱获取客户ID;2)查询数据库通过客户ID获取卡号;3)调用SOAP服务通过卡号获取账户余额。这一切都无需编写任何代码,满足了“按需连接”的要求。

当生产者发生变更(例如,数据库访问被新的gRPC API替代),只需更新并发布新的API规范(包含语义)。Orbital会更新关系图,并自动调整集成路径,无需消费者修改代码,满足了“适应变更”的要求。

那么,消费者如何提出问题呢?这就是 TaxiQL 的用武之地。它是一种语义查询语言,用于使用业务术语(语义)来请求数据。

TaxiQL的关键特性包括:

  • 声明式:不指定系统或字段名,只描述需要什么数据。
  • 语义化:使用业务语言,与生产者解耦。
  • 发布消费者契约:消费者定义自己需要的数据结构和字段名,而不是从一个集中式模式中挑选。这消除了一个关键的耦合点。
  • 内嵌表达式:支持在查询层进行数据转换(如字符串操作)。
  • 约束条件:可以添加过滤、排序等约束,帮助查询层选择最合适的源系统。

Orbital 则是一个查询执行引擎和可观测性平台。它的工作流程如下:

  1. 从Git仓库读取Taxi类型定义(语义词汇表)。
  2. 生产者发布包含Taxi语义的API规范。
  3. 消费者提交TaxiQL查询(定义了自己的数据契约)。
  4. Orbital利用所有信息,动态构建集成逻辑,执行查询,并返回结果。
  5. 同时,它会收集追踪、数据血缘和性能数据,便于调试。
  6. 当生产者变更并发布新规范时,Orbital自动适配查询,消费者不受影响。

6️⃣:实战演示——电影数据集成

现在,让我们通过一个实战演示来将以上概念串联起来。场景是一个电影制片公司,拥有电影数据库和多个微服务(提供评论、流媒体价格等信息)。

我们有一个Spring Boot微服务,它接收filmId并返回影评。为了给它添加语义,我们:

  1. 在Taxi项目中定义语义类型(如FilmId, FilmReviewScore)。
  2. 使用代码生成器(如Kotlin生成器)生成对应的类型别名代码。
  3. 在Spring Boot服务的API中,使用这些语义类型注解参数和返回对象(例如,将参数声明为FilmId类型,而不仅仅是Int)。
  4. 服务启动时,会自动将其包含语义的API模式发布到Orbital。

在Orbital的控制台中,我们可以看到已注册的数据源(电影数据库表、各微服务)。此时,我们可以编写TaxiQL查询:

  • 初始查询:find { Film[] },这只是简单地查询数据库。
  • 定义消费者契约:find { Film[]( id, title, releaseYear ) },指定了需要的字段和结构。
  • 扩展查询:添加reviewScoreprice字段。Orbital会自动识别到这些数据来自不同的微服务,并构建集成:先查数据库获取电影列表,然后并行或用影ID去调用影评服务和价格服务,最后将结果组合返回。整个过程无需编写任何集成代码

我们可以将此查询保存并发布为一个REST API端点,供其他服务直接调用。

接下来,模拟变更:公司决定改用外部服务(Squash Tomatoes)获取影评。新服务的API也接受filmId,但它们的ID体系不同(SquashTomatoesFilmId),且返回的字段名也可能不同。我们更新影评服务,使用新的语义类型(SquashTomatoesFilmId),并重新发布API模式到Orbital。

神奇的是,之前创建的消费者API端点依然有效。Orbital在后台更新了集成图:它发现无法直接将数据库的FilmId用于新服务,于是自动插入了一个“实体解析”步骤(可能通过某个映射服务),将FilmId转换为SquashTomatoesFilmId,再调用新服务。消费者完全感知不到底层数据源的切换

最后,演示流式集成:公司还有一个Kafka主题,发布Netflix上新电影的消息(Protobuf格式)。我们可以使用相同的TaxiQL消费者契约,但指定以流式方式查询。Orbital会自动创建从Kafka主题到满足契约的数据流的集成,处理其中可能更复杂的ID转换关系,而消费者只需关心他们想要的最终数据格式。

总结

在本教程中,我们一起学习了构建自适应API层的核心思想。我们首先剖析了传统“胶水代码”集成方式的弊端:耗时、低复用性且脆弱。其根源在于它造成了生产者和消费者之间的紧耦合。

接着,我们探索了一种基于语义的解决方案:

  • Taxi 作为语义元数据,回答了“数据是什么”的问题,将业务含义注入API。
  • TaxiQL 作为语义查询语言,让消费者能用业务语言声明所需数据,并自动生成集成逻辑。
  • Orbital 作为执行与观测平台,利用语义信息动态构建、执行并维护集成,并能自动适应底层系统的变更。

这种架构的核心优势在于消除了手动的胶水代码,将集成工作的重心从重复的、易错的编码,转变为对业务语义和数据契约的定义与管理,从而显著提升开发效率、系统灵活性和可维护性。

013:自主性如何拯救了Spotify最受欢迎的功能

在本教程中,我们将通过Spotify“每周发现”功能诞生的真实案例,深入探讨一种创新的产品开发模式——赋能型产品团队。我们将了解传统组织架构的局限性,以及Spotify如何通过赋予团队自主权,成功孵化出一个最初不被看好、最终却大获成功的产品功能。我们将学习核心概念,如“对齐的自主性”、“以结果为导向”和“数据驱动决策”,并理解如何构建一个鼓励创新、容忍失败的环境。

从音乐发现方式的演变说起

首先,让我们回顾一下在Spotify出现之前,人们是如何发现新音乐的。过去,人们可能需要花费数小时在唱片店里浏览CD,试图找到喜欢的音乐。对于像我母亲这样的普通用户来说,这个过程可能过于繁琐,她更倾向于选择更简单的聆听方式。

随后,Napster、Kazaa等文件共享服务出现了。它们提供了免费获取音乐的途径,但体验并不理想:搜索困难、下载缓慢,且文件质量参差不齐。

Spotify的诞生彻底改变了这一局面。它几乎囊括了所有音乐,并能即时播放,这对于早期的技术爱好者和音乐发烧友来说是一个巨大的飞跃。然而,当向像我母亲这样的普通用户展示时,问题出现了:如果用户不知道自己想听什么,仅仅提供一个搜索框是远远不够的。

Spotify面临的战略挑战

在Spotify内部,我们认识到产品在“主动使用”场景下表现出色,即用户明确知道自己想听什么并通过搜索来寻找。但对于更广泛的“被动收听”的主流用户,他们需要更多的引导,而我们的表现却不尽如人意。这是公司领导层为整个组织设定的战略意图:我们需要理解原因并尝试解决这个问题。

我们曾尝试过多种方法:

  • 电台功能:早期可以基于年代和流派生成电台,后来可以基于歌曲或歌单。但用户参与度的提升并不明显。
  • Pinterest/Netflix式推荐:展示“因为你听了A乐队,所以你可能喜欢B乐队”的关联推荐。公司里的音乐发烧友很喜欢,但对于只想轻松听歌的普通用户来说,仍然过于复杂和耗时。

当时,甚至有一种观点认为,主流用户并不热衷于发现新音乐,他们满足于听老歌。因此,为这些人构建推荐算法可能是徒劳的。这种情绪在Spotify内部也有一定市场。

“每周发现”创意的诞生

然而,有几位工程师并不相信这种观点。他们一直在思考这个问题,并坚信一定有更好的方法。

他们的灵感来源于一个年末功能——“年度回顾”(当时叫“Year in Review”)。这个功能会展示用户一年的收听数据,人们很喜欢并乐于分享。工程师们发现,即使过了几个月,仍有数百万用户在查看自己的“年度回顾”。他们意识到:如果将用于“发现”的算法应用到用户全年的收听历史上,生成一个名为“播放未来”的歌单,可能会很有趣。这个实验性的想法获得了很高的参与度,证明了其潜力。

于是,“每周发现”的核心创意形成了:利用7500万用户的收听数据,通过协同过滤算法,每周为每位用户生成一个包含30首全新歌曲的个性化歌单。

协同过滤 的基本原理是:假设你有一个包含20首歌曲的歌单,你认为自己是独一无二的。但实际上,系统可以找到大量拥有相似歌单的用户。如果这些用户中有80%都收藏了某三首你从未在Spotify上听过的歌曲,那么你也很可能喜欢它们。这就是“每周发现”的算法基础。

传统组织架构的瓶颈

在大多数传统公司中,上述情况可能根本不会发生。工程师通常不会挑战公司高层的普遍认知,也不会主动提出这样的产品创意。即使提出了,如果被设计师或产品经理以“想法糟糕”、“会破坏应用简洁性”或“可能引发用户反感”为由否决,这个想法也很难有后续。

传统官僚或计划驱动型组织通常这样做:

  • 聚焦大项目:通过商业案例、投资回报率分析来决定优先级。
  • 早期锁定决策:在详细分析阶段就确定了范围、资源和时间,实际上剥夺了团队的决策空间。
  • 部门墙:按职能划分的部门结构使得跨部门协作变得困难。
  • 反馈延迟:项目周期长,直到最终交付才能验证对错,且往往忙于交付而无暇验证。

Spotify的解决方案:赋能型产品团队

Spotify展示了一条不同的路径,我们称之为 “对齐的自主性”。其核心是最小化的、类似创业公司的 “自主小队”。后来,Marty Cagan在其著作《赋能》中将其称为 “赋能型产品团队”,两者本质相同。

关键在于:通过向团队指派需要解决的问题来赋能他们,然后给予团队解决问题的空间。

为什么需要这种模式?因为产品开发存在四大风险:

  1. 价值风险:用户不想要或不买账。
  2. 可行性风险:技术实现上太昂贵或太复杂。
  3. 可用性风险:用户找不到或不会用。
  4. 业务可行性风险:不符合商业模式或合规要求。

这四大风险持续互动,问题空间和解决方案空间也在不断变化、相互影响。正如“每周发现”的例子所示,工程师们每天与技术打交道,他们最清楚什么是当前可能实现的。许多创新正来源于此,因此他们需要参与决定尝试做什么。

从输出到成果的思维转变

我们关注的是成果和业务结果,而不仅仅是输出

我们生活的世界存在许多未被满足的需求、用户的痛点或公司的糟糕业绩,这给了我们大量改进的想法。这些想法最终会转化为产品、功能和需求文档。但我们必须警惕,不要爱上自己的解决方案甚至需求本身。

我们采取的行动(史诗、特性、用户故事)只是我们认为能达到目标的假设或赌注。如果数据没有改善,它们就必须改变。糟糕的是,我们非常不擅长判断哪个赌注是正确的。

数据显示:

  • 在面向用户的实验中,大约只有10%-20%能产生积极结果。
  • 在公司整体层面,大约三分之一有效,三分之一中性,三分之一甚至有负面影响。
  • 对于内部项目,超过60%的功能几乎从未被使用。

因此,我们必须 “用数据驱动决策,而不是HIPPO(最高薪者的意见)”

在Spotify,我们使用 “对齐的自主性”

  • 跨职能小队:小型、长期存在、自组织,专注于目标和业务成果。
  • 反馈循环:使用反馈循环来了解是否更接近目标。
  • 小队间直接协作:管理依赖关系,而非通过管理层级沟通。
  • 管理层角色转变:专注于愿景、目标、战略背景、服务型领导,为小队扫清障碍。
  • 信息透明双向流动:从一线学到的知识影响战略,战略又指导团队应聚焦解决哪些问题。

这意味着组织从 “守门情境” 转向 “促进情境”

  • 守门情境:产品经理等角色将市场、用户信息转化为待办事项交给团队,团队与用户脱节。
  • 促进情境:让所有情境信息更易于被团队获取和消化,邀请团队基于这些信息共同决定最佳解决方案。

“每周发现”小队的实践

“每周发现”小队(最初叫Lambda小队)获得了清晰的战略方向:提升被动收听主流用户的参与度。他们明确了衡量指标:触达(更多人用)、深度(用得更久)、留存(更常回来)。

他们可以自主决定尝试与使命相符的想法。两位工程师、产品经理和设计师一起,花了几周时间迭代出一个可用的原型。

第一步:内部“钓鱼”测试
他们先在小队内部使用(“钓鱼测试”),不断改进。

第二步:内部“狗粮”测试
然后,他们悄悄地将歌单放入同事的播放列表中,没有大肆宣传。同事们纷纷在内部通讯工具上询问这是什么,功能在内部迅速传播开来。随后,他们正式邀请同事试用,并通过歌单描述中的链接收集反馈,结果非常积极。

第三步:小范围用户发布
Spotify的文化和基础设施支持这种实验。CTO曾表示,团队可以无需许可向5%的用户发布任何功能。于是,他们向1.5%的用户(仍超过百万人)发布了“每周发现”。通过数据监控和调查,他们发现用户反馈极其正面,之前关于“用户反感强推内容”的担忧被证明是过虑的。社交媒体上也充满了惊喜和赞誉。

第四步:数据驱动的优化
获得大量用户数据后,他们进行了大量A/B测试:

  • 歌单封面:测试发现,使用用户本人的Facebook头像(叠加色彩和Logo)的个性化封面,比通用的“宇航员登月”图片,带来了10%的周均使用量提升。尽管工程师们最初认为构建这套系统成本高昂,但数据证明了其价值。
  • 歌单长度:测试发现,30首歌(约2小时)是最佳长度。定性研究揭示,100首歌令人压力山大,而30首歌则像朋友为你精心制作的“混音带”,感觉更亲切。
  • 更新频率:每周更新一次恰到好处,能保持用户的好奇心。
  • 歌曲熟悉度:一个有趣的“bug”让两首用户最近听过的歌出现在“每周发现”中。数据表明,这个“bug”版本表现更好。定性分析发现,这几首熟悉的歌增加了用户对算法推荐其他陌生歌曲的信任度。

最终,成功的公式是:每周一早上更新、时长2小时、完全个性化、以标准Spotify歌单形式呈现、使用用户Facebook头像作为封面的音乐推荐。

遭遇挑战与最终成功

然而,一个巨大的技术挑战出现了:为7500万用户每周同时更新一个共享歌单,现有的歌单系统无法支撑。团队曾多次被告知“做不到”,重建歌单系统的计划在 backlog 里躺了一两年。

但此时,团队已经用数据证明了这是一个爆款功能。他们拥有了一个 “高完整性商业案例”,优先级变得极高。于是,多个团队协作,花费数月时间解决了技术架构问题,并重新逐步发布。

在正式发布时,他们优化了营销信息,甚至发现英文版的“Discover Weekly”比本地化翻译版本表现更好,因为它更像一个独立的品牌功能。最终,“每周发现”在10周内实现了10亿次歌曲播放,成为Spotify有史以来最成功的发布之一。它甚至改变了人们对周一的感受,从“ dreaded ”变成了“期待”。

核心启示与总结

Spotify的CEO Daniel Ek后来在采访中坦言:“如果只由我决定,我100%会砍掉这个功能。 我从未看到它的美妙之处,甚至质疑过团队两三次……我记得在媒体上看到它时,心想‘这将会是一场灾难’。但显然,它最终成为了我们最受喜爱的产品功能之一。”

这充分展示了 赋能型产品团队 的力量。最高决策者(HIPPO)不喜欢这个想法,不愿提供额外资源,认为它会失败。然而,因为团队能够通过低成本实验一步步验证自己的想法,最终赢得了胜利。这是一种 基于能力和客户聚焦的层级,而非基于权力的层级。

成功的创新配方是:将人才和问题置于一个对失败友好的环境中,组建跨职能团队,只要能从中学到东西,就允许抛弃想法和失败。 通过提供足够的弹性空间、清晰的成功指标以及反馈基础设施(支持用户测试和反馈循环),新的好东西终将涌现。

创新无法被强制,只能被赋能、鼓励和支持。管理者无法让创新发生,但他们可以创造一个支持创新、防止创新被扼杀的环境

本节课中,我们一起学习了赋能型产品团队的核心模式,并通过“每周发现”的案例,看到了自主性、数据驱动和以成果为导向的开发方式如何催生出伟大的产品。希望这些经验能对你的工作有所启发。

014:GitHub Copilot 深度解析

在本教程中,我们将深入探讨 GitHub Copilot 这一 AI 开发工具。我们将了解它的起源、核心工作原理、当前功能,并展望其未来发展方向。本教程旨在为初学者提供一个清晰、全面的认识。

起源与发展历程

上一节我们介绍了本教程的概述,本节中我们来看看 GitHub Copilot 的起源。

GitHub Copilot 源于 GitHub 内部一个名为 GitHub Next 的团队。这个团队类似于公司的研发部门,负责进行各种实验,探索软件开发的新可能性。

最初,团队每隔半年左右就会讨论一次构建通用 AI 编程工具的想法,但每次都因为技术不成熟而放弃。直到 OpenAI 发布了 GPT-3 模型。GitHub Next 团队和微软的研究人员获得了 API 访问权限,并开始进行测试。

他们最初给模型一些自包含的编程问题。GPT-3 在没有任何特定指令的情况下,成功解决了大约一半的问题。通过调整提问方式和提供上下文,成功率很快提升到了 90%。正是这个转折点让他们意识到,这可以成为一个有用的工具。

早期的测试形式是提供小型、自包含的问题,但后来他们不再采用这种方式,因为模型在这方面变得过于强大,而且这种测试并不符合日常开发的真实场景。

最初的想法是将其构建成一个聊天机器人。但在 2020 年 6 月,团队中有人提出了将其作为智能代码补全工具的想法。当他们开始以这种方式实现时,发现这是一个更好的范式,并在一段时间内完全放弃了聊天功能,专注于提供我们如今称为“幽灵文本”的代码建议。

此后,GitHub Copilot 经历了一年的技术预览期,然后才正式发布。自发布以来,团队不断扩展其功能,例如重新引入 Copilot Chat、推出 Copilot Workspace、支持扩展以及最近发布的模型选择和 Copilot Edits 等功能。

核心理念:提升开发者幸福感与生产力

上一节我们回顾了 Copilot 的发展历程,本节中我们来探讨其背后的核心理念。

开发 Copilot 这类工具的核心目标是提升开发者的幸福感。这听起来可能像一句营销口号,但研究表明,开发者的幸福感与生产力以及公司的盈利状况密切相关。

传统的生产力衡量方式(如代码行数、提交次数)存在很大缺陷。一些最有价值的贡献可能并不产生新代码,甚至可能是删除代码。

一种更好的衡量框架是 SPACE 框架,它从多个维度评估软件开发团队的生产力。其中,“满意度”和“幸福感”与开发者快乐程度直接相关。

GitHub Next 团队在 2021 进行的“美好一天”项目研究揭示了几个关键发现:

  • 开发者需要深度专注时间。一天内会议越多,达成目标的可能性急剧下降。
  • 干扰越少,开发者越有可能报告自己度过了“美好的一天”。

总结来说,如果开发者感到快乐,能够进入心流状态而不被打断,那么他们的生产力和幸福感都会提升,这对组织也有实际好处。

像语法高亮、自动补全这样的工具,其目的就是节省时间,让开发者无需频繁中断工作去查阅资料。Copilot 等 AI 开发工具的目标也是如此:帮助开发者保持专注,提升效率。

GitHub Copilot 核心功能演示

上一节我们了解了工具背后的理念,本节中我们通过实际演示来看看 Copilot 的核心功能。

大多数用户最熟悉的是其自动代码补全功能。在编辑器中,它会根据上下文给出代码建议,用户可以通过按 Tab 键接受。

除了自动补全,Copilot Chat 功能也很有用。用户可以在聊天窗口中要求它解释代码、生成文档或编写测试。

例如,可以选中一段复杂的正则表达式,在聊天框中输入 /explain,它就会给出解释。用户甚至可以要求它“像对五岁孩子一样解释”。

对于导航和理解现有代码库,Copilot 也提供了帮助。在一个不熟悉的 React 项目中,用户可以打开聊天窗口并询问:“这个项目中的个人资料页面在哪里?” Copilot 会分析整个工作区,找到相关文件并给出简洁答案。用户还可以询问“这个文件有测试吗?”或“为这个文件中的所有函数生成测试”等问题。

Copilot Edits 是一个较新的功能,允许用户通过一个提示,让 AI 同时修改多个文件来解决一个问题。例如,用户可以提示:“创建一个可复用的面积图来显示积分活动。它应该看起来像 #onTimeLineChart(引用项目中的现有组件),并显示在个人资料页面的表格下方。确保添加测试,并从 pointsActivityService 拉取数据。” 用户还可以选择使用不同的底层大语言模型(如 GPT-4o、Claude、Gemini)来执行此任务。

Copilot Instructions 功能允许用户通过一个 markdown 文件,为 Copilot 设定每次响应的固定指令。例如,可以要求“创建新的可复用 React 组件时,添加一个 LaunchDarkly 功能标志”。这可以设置在项目级别或用户个人设置中。

未来方向:从工具到伙伴

上一节我们演示了 Copilot 的当前能力,本节中我们来展望其未来的发展方向。

业界存在 AI 工具是否会取代开发者的担忧。但 Copilot 的定位是帮助开发者提高生产力的工具,而非替代他们。GitHub 的目标是服务更多开发者,而非减少其数量。

未来的方向是让 AI 从单纯的代码生成工具,逐渐转变为开发者的“思维伙伴”。这意味着 AI 将更多地帮助开发者导航、理解代码库,并进行构思。

设想一下,一个像在代码库中工作了几十年的“AI 同事”,它拥有完美的记忆力和即时回忆能力,并且真心希望你成功。你可以对它说:“我需要在应用的支付屏幕上添加一种新的支付方式。” 这个 AI 工具可以拉取所有相关的支付代码文件,指出需要修改的部分,甚至建议更新网站 FAQ,或者根据新支付方式的名称去查找实现细节并组装代码。

这引向了“AI 原生开发”的概念。目前,AI 开发工具是显性的,用户需要主动打开聊天窗口或等待补全。未来,理想的状态可能是“两院制用户体验”:一侧是自然语言描述的需求,另一侧是跨多个文件的正式代码,修改任何一侧,另一侧都会相应变化。这本质上是在更高层次上对代码进行抽象。

GitHub 近期宣布的目标是达到 10 亿开发者。实现这一目标的方式之一是重新定义“开发者”的范畴。Github Spark 就是一个面向更广泛用户的低代码/无代码工具的实验。它假设应用 UI 和基础架构(如数据存储、主题)可以相对标准化,用户只需用自然语言描述他们想要的应用,并通过多次迭代来完善它。这使得更多非专业开发者也能创建有用的程序。

总结

在本教程中,我们一起学习了 GitHub Copilot 的完整图景。我们从其起源于 GitHub Next 团队的研发实验开始,了解了它如何从解决自包含编程问题发展到成为智能代码补全工具。我们探讨了其核心设计理念——通过减少干扰、提升效率来增加开发者幸福感和生产力。通过功能演示,我们看到了它在代码补全、解释、测试生成、代码库导航和多文件编辑等方面的实际应用。最后,我们展望了未来,Copilot 正朝着成为开发者的“思维伙伴”和实现“AI 原生开发”的方向演进,同时也在通过像 Github Spark 这样的工具降低开发门槛,让更多人能够进行软件创作。

015:获取认同与克服拉曼定律

在本教程中,我们将探讨如何在组织中推动新的工作方式,特别是如何克服由“拉曼定律”所描述的组织惯性。我们将学习如何通过自下而上和自上而下的策略来获取认同,并最终建立一个鼓励实验和学习的文化。

引言:文化、学习与阻力

上一节我们介绍了本次讨论的背景。本节中,我们来看看组织文化在采纳新实践时所扮演的角色。

在开始之前,我想先问几个问题。有多少人认为自己所在的组织拥有很棒的文化?请举手。
现在,在举手的人中,有多少人听说过“群体编程”或“合奏编程”?请继续举手。
最后,请只有在您实际实践过至少一周并有所体验后,才继续举手。在您的组织中,实践后遇到了多少阻力?阻力是高、中还是低?
在最后一个问题中,举手的人寥寥无几。这表明,问题在于即使拥有很棒的文化,也可能并非一种学习文化、实验文化,或对适应新工作方式开放的文化。良好的工作与生活平衡是好事,但这还不够。当谈论拉曼定律时,您试图克服的正是组织文化的问题。

解析拉曼定律

上一节我们提到了组织文化的挑战。本节中,我们来具体看看克雷格·拉曼提出的几条观察,他明确指出这些并非规定性法则,而是对现状的描述。

以下是拉曼定律的核心内容:

  1. 组织被隐性地优化,以避免改变现状。 中层管理职位、权力结构等都倾向于维持现状。这与康威定律相关,即组织内的沟通路径会直接反映在您生产的软件以及生产软件的方式中。我们的目标是建立一个没有这种维持现状的持续拉力的文化。中层管理尤其是个问题,因为转向新的工作方式(例如跨职能团队)会触及现有权力结构。

  2. 当尝试引入新事物时,组织通常会对现有事物贴上新标签,并声称正在做新事物。 例如,您的经理现在被称为“Scrum Master”,产品人员被称为“产品负责人”。但这并不奏效。维持现状的一部分是将自己锁定在当前位置,可以接受头衔变更,但不愿做不同的事,因为那很困难。学习是困难的。

  3. 每当开始讨论变革时,人们会开始嘲笑您,称您为“理论纯粹主义者”或“象牙塔中人”,并声称这在现实世界中行不通。 我将以群体编程为例。人们会说“在现实世界中无法进行群体编程”。然而,阿拉斯加航空、Spotify、Salesforce等公司都在实践。反驳“这在这里行不通”的论点,需要探究“为什么在这里行不通”以及“如何解决”。

  4. 一旦做出改变,被取代的人会开始将自己标榜为专家,并作为顾问被引入其他公司。 他们的建议会被认真对待,尽管他们可能并不真正了解情况。因此,在这个领域找到优秀的顾问可能非常困难。

  5. 在大型组织中,文化追随结构。 这意味着当团队开始以新方式做事时,组织的文化最终会被拉向支持团队工作方式的思维模式。在小型组织中,文化可以先行,例如初创公司可以主动创建良好文化。大多数阻力来自大型组织,而我们将主要针对这些情况。

克服阻力的策略:自下而上

上一节我们了解了拉曼定律的具体表现。本节中,我们来看看第一种应对策略:自下而上的方法。

自下而上的思考是指团队自发地尝试新实践。例如,一个团队可以建立一个所谓的“臭鼬工厂”式环境,将自己与组织其他部分隔离开来进行实验。关键是要创造一个“安全”的空间。

这里需要谈谈“心理安全”。心理安全并不意味着在一个所有人都同意您的房间里。心理安全是指在一个人们可以争论、而您不害怕发言的房间里。如果您曾坐在会议中决定不说出某些想法,那么您所处的环境就缺乏心理安全。这与冲突无关,心理安全的环境中可以有大量冲突,但冲突是安全的。

自下而上方法的一个有效杠杆是实验心态。与其说“我们都将进行群体编程”,不如提议:“让我们先运行一个为期几周的实验试试看。如果无效,我们就不做。如果有效,我们就继续。”这通常效果很好。但问题可能出现在实验成功后,管理者说“在适当的时候使用它”。这又是一种拉曼定律式的阻力。实际上,许多大公司在所有事情上都100%采用群体编程,因为它是一种有效的工作方式。

关于自下而上方法的最后一点是,在远程或混合办公模式下,您可以将自己树立为榜样。例如,一个团队在公共区域进行群体编程,邀请路过的同事加入体验,从而将实践传播到其他团队。这完全不需要高层管理的参与。

克服阻力的策略:自上而下

上一节我们探讨了自下而上的实验方法。本节中,我们来看看更具挑战性的自上而下策略,即如何说服管理层。

首先,永远不要提及“敏捷”这个词。它已不再具有积极的含义。不要将变革呈现为“我们将采用这个流程”,因为这听起来风险很高。您需要用管理者的语言——商业语言——进行沟通。

程序员作为一个群体,通常不了解企业如何运作,这是一个大问题。管理者主要思考两件事:金钱时间。他们雇佣您就是为了不必考虑冲刺、待办事项列表等细节。因此,您需要构建一个商业案例

商业案例始终关乎成本:做这件新事要花多少钱?维持现状要花多少钱?这两个数字孰高孰低?这才是能引起他们注意的东西。没有哪个高层管理者会关心冲刺或待办事项梳理。

另一个关键是理解管理者的恐惧。作为顾问,我首先会问高层管理者:“是什么让您夜不能寐?最让您担忧的问题是什么?”然后针对那个最令他们恐惧的问题开展工作。通常,解决方案可能很简单,例如减少在制品数量。

构建商业案例:从概念到数字

上一节我们提到要用商业语言沟通。本节中,我们学习如何将抽象概念转化为具体的财务数字,构建令人信服的商业案例。

让我们以群体编程为例,因为它在许多组织中颇具争议。管理者典型的反对意见是:“我不想让四个人坐着看一个人工作。”因此,第一步是邀请他们亲身体验,亲眼看看实际情况并非如此。

第二步是量化分析。我们需要比较“群体编程”和传统的“分散-集中”工作模式。

在“分散-集中”模式中:

  • 团队以每日站会开始。假设站会15分钟,但考虑到上下文切换、闲聊等,实际恢复工作可能需45分钟到1小时。这里有大量的浪费
  • 然后成员分散独立工作。
  • 最后再集中进行集成,此时常发现问题,导致返工(支付两次编写代码的成本)和机会成本(本可用于做有用工作的时间)。

杰里·温伯格观察到,每增加一项并行工作,生产率会损失约20%。这意味着同时处理三件事,效率可能降低60%,导致交付时间翻倍,收入延迟。

现在,让我们赋予这些概念具体的价值。假设一名程序员年薪10万,公司的总负担成本约为20万/年(负载系数约2.0)。按每年2000个工作日计算,每小时成本约为100美元。

  • 一次4小时、10人参加的会议,成本就是 4小时 * 10人 * 100美元/小时 = 4000美元。会议产出通常难以匹配此成本。
  • 在“分散-集中”模式中,站会后的上下文切换、集成返工都是可测量的浪费。

而在群体编程中:

  • 无需每日站会(因全天协作)。
  • 无需专门集成(因持续同步)。
  • 减少返工(问题在编写时即被集体发现)。
  • 减少查找资料时间(团队内知识共享)。
  • 无需代码审查/拉取请求(每行代码在编写时已被多人审视)。
  • 减少瓶颈、依赖和等待
  • 可能减少对专职Scrum Master、项目经理甚至产品负责人的需求
  • 降低对Jira等复杂工具的需求

您可以测量当前在这些活动上花费的时间,制作电子表格,然后向管理者展示:“如果我们采用合奏编程,每年可以节省X百万美元,并且能提前Y周将产品交到客户手中,从而更早产生收入。”这是一个强有力的商业论据。

总结:实验心态与文化变革

在本教程中,我们一起学习了如何克服拉曼定律所描述的组织惯性,以推动新的工作方式。

拉曼定律是真实存在的,人们总会抵制变革。关键是要培养一种实验心态。无论是自下而上还是自上而下,核心都是进行实验。

  • 自下而上:以“寻求原谅而非许可”的方式,在小范围内进行为期数周的实验。收集真实数据作为决策依据。
  • 自上而下:针对管理层最头疼的问题(如交付速度),用具体的商业案例(节省成本、加速收入)进行沟通,并提议将其作为实验来运行。

最终,我们需要推动的文化变革是:让人们愿意尝试新事物,并且拥有允许失败的权利。如果尝试后无效,没关系,我们可以再尝试别的。通过持续的、数据驱动的实验,我们可以逐步克服阻力,建立一个真正支持学习和高效工作的组织文化。

016:将AI模型引入画布

在本课程中,我们将探讨如何超越传统的聊天界面,将人工智能模型的能力引入到交互式画布中。我们将回顾人机交互的历史演变,分析当前AI交互的局限性,并展示一系列将AI与画布结合的创新实验。

1. 引言:什么是画布?

大家好,我是Lou,也可以叫我Luke。我在网上使用的名字是Toadponnd。今天演讲的主题是“超越聊天:将模型引入画布”。这里的“模型”指的是AI模型,但我真正想深入探讨的是“画布”本身。

那么,什么是画布呢?

为了回答这个问题,我们需要回顾43年前的计算机历史。

2. 历史的启示:从命令行到图形界面

43年前,最流行的计算机交互方式是这样的:你有一个聊天框,输入一条消息,计算机会回复你。你通过这种一来一回的对话,逐步构建起与计算机的交互。这就是当时使用计算机的方式。

让我概括一下:你发送一条聊天消息,得到一条回复。你再发送一条消息,再得到一条回复。尽管当时已有大量关于更好交互方式的研究,但这就是我们使用计算机的方式。

实际上,在更早的研究中,我们已经看到了未来的雏形。你们知道这是什么吗?对,这是鼠标。这个呢?这是光标,一个指针箭头。那这些呢?这是来自同时期另一篇研究论文的概念——重叠窗口。如今,我们在每台台式机或笔记本电脑上都对此习以为常,但当时必须有人去发明它。直到多年后,第一台Macintosh才首次在个人电脑上普及了这种界面,这彻底改变了计算机。

让我们概括一下:我们从基于文本的聊天框开始,最终发展到了画布。

3. 移动设备的演变:从文本到触摸屏

让我们谈谈手机。有人用过这种手机吗?它们基本上就像一个命令行界面。你不停地按“上、上、上”直到找到想要的命令,然后你输入文字。是的,我们称之为“发短信”是有原因的,因为你发送的就是文本。

那么,如果整个世界都遵循这种以文本为范式的交互方式,你会得到类似“文本增强”这样的东西。但正如史蒂夫·乔布斯著名地指出的那样:我们不需要文本,我们需要的是一个可以用手指(我们的光标)来指向和操作的画布。这永远地改变了手机,从文本交互转向了画布交互。

到目前为止,大家能跟上吗?能看出这个趋势将引向何方吗?

4. 当前AI的交互范式:困于聊天框

现在,让我们谈谈AI。在场有谁听说过AI?很好,看来我是全场最幸运的人。AI代表人工智能,这可能是本次会议上唯一关于AI的演讲。

目前使用AI的方式是:你有一个小聊天框,输入一条消息,然后开始与计算机对话。每个人都在这样做,这就是当前的范式。无论是ChatGPT、Claude还是Gemini,有时你可以稍微跳出聊天框,比如使用Claude的“工件”功能,你们可以共同在一个画布上工作,但使用它的主要途径仍然是聊天框。你必须通过聊天来交互。ChatGPT可以上传画布的图片,但那不是一回事,而且区域也很小,你仍然需要谈论它。

概括一下:我们向AI发送一条聊天消息,得到一条回复。我们再发送一条消息,这次又得到一条回复。然后我们再发送一条消息,有时还需要检查答案是否正确。总之,我们从AI的文本交互开始,但目前仍然处在这个文本范式中。

那么,我们能超越聊天吗?大家认为我们可以吗?

5. 实验探索:将AI引入画布

我非常幸运,在一家名为Tldraw的公司工作,这是一家画布库公司。我们构建SDK,供其他人用它来构建画布应用。我很幸运能和团队一起进行实验,尝试解决如何将AI引入画布的问题。

接下来,我将快速展示多年来我们进行的许多不同实验,让大家一窥我们的工作。需要声明的是,这里有些是我做的,有些是团队其他成员做的,但所有成果都是协作努力的结晶。

好的,现在开始。

这是Tldraw,它正在运行我的幻灯片软件。Tldraw是一个画布软件,我们做了很多工作,比如让文本渲染得非常顺滑,让箭头连接得非常漂亮。即使我旋转这个星星,箭头也会自动调整。我甚至可以弯曲它,当我移动它时,它会动态变化。看,如果我把这个文本移到前面,会有一个小小的白色边框,这意味着即使它覆盖在其他东西上,也是可见的。当我们使用虚线时,我们会确保虚线的点总是落在拐角处,这样看起来总是很美观。我们在努力让画布变得好用方面获得了许多乐趣。

在展示实验之前,大家需要知道的另一件事是:这一切都是基于Web的。所以你在画布上看到的所有东西都只是网页内容,这意味着它是HTML。看,我们甚至可以放入一个YouTube视频,并且仍然可以移动它。它可以与所有其他东西交互,我甚至可以给它画一个箭头,当我旋转这个视频时……(暂停视频)我可以向你们证明这一点。看,如果我打开检查器,这是一个H2标签,这是一个段落,这是一个链接。这全是网页内容。这一点很重要,是为后面的内容做铺垫。

因为在2023年3月14日,OpenAI发布了(或者说宣布了)其首个公开可用的多模态模型。这基本上意味着你不仅可以输入文本,还可以发送图像,虽然这个阶段你得到的回复仍然是文本。但他们展示了一个例子:你可以在餐巾纸上画一个粗糙的网站草图,然后它会返回一个可工作的网站。这引出了我们今天的第一个实验,叫做“Make Real”。它最初是由社区里有人使用Tldraw开始的,我们接手并继续开发。我想展示一下它的功能。

我在这里画了一个小应用的用户界面。我可以全选它们,然后点击右上角这个“Make Real”按钮。现在,我在向演示之神祈祷。好的,它正在工作。大家能看到它在做什么吗?它把我的设计变成了一个真实的网站。如果我悬停在这些元素上,它们会有交互行为。看,Grow(放大)效果有效,Bounce(弹跳)效果有效。Rotate(旋转)效果无效。所以我们要拒绝OpenAI的版本。让我们看看Anthropic的模型做得对不对。这个会放大,这个会弹跳,这个会旋转。有趣的是,它把放大效果做得特别大,这里面有些巧思。

好的,这是另一个例子。如果我点击“Make Real”,它会根据草图创建出东西。很好。我可以在这里输入内容,然后提交,它会发送到某处。但大家看到错误了吗?提交按钮目前在这里,但它不应该在这里。我把它画在了这边。所以我需要做的是把它移到这里。然后我再次全选并点击“Make Real”,祈祷它会成功。大家觉得这会成功吗?看,它做到了!两种情况都做到了。我可以继续下去,现在这就像一个反馈循环。我可以说,实际上我想要这个是绿色的,并且在这里放一个大的红色取消按钮。让我们看看会发生什么。

我认为这其中的重大突破在于:当你发送一些东西出去时,这并不是终点。你并没有被取代,你仍然是这个过程的一部分。它完成了我的更改。我不认为这是在取代我的工作,但它是一个有趣的实验。

我们意识到的另一件事是:你不必只给它一个屏幕。你可以给它多个不同的视图。这里我给了它移动端和桌面端应该是什么样子的草图,我想看看它是否能理解我的意思。看起来它正在处理移动端视图。如果我拉伸它,我们得到了一个响应式设计,这非常令人惊讶。我们想,好吧,我们还能给这个东西什么?

这里有一个状态图。我不知道大家昨天是否听了David的演讲,但我确保了我演讲中所有的箭头都加了标签。如果我给它这个状态图,它会尝试生成对应的用户界面。或者,如果我只给它用户界面,它会尝试推断行为,做相反的事情。我也可以说,就做一个秒表,我不在乎细节。最终你会得到不同保真度的结果,取决于你在乎的程度。让我们试试添加秒数和开始按钮。很好,它正在倒计时,就像我在状态图中指定的那样。这是它根据我给的UI生成的另一个版本,让我们看看会发生什么。这个在正数计时,因为我没有指定足够的细节。而这个只是一个通用的秒表,因为我说了我不在乎细节。

社区的人们真的在大力推动这个实验。我们对人们的创造力和他们探索这个东西能做什么、不能做什么感到非常惊讶。我接下来要尝试一堆例子,我们看看它的成功率如何。一个JavaScript终端,这回到了演讲的开头。还有一个视频游戏。现在让我们看看它做得怎么样。

这是我的颜色选择器。有趣,它把顺序搞反了。所以,如果我有更多时间,也许我会这样做,然后……让我们试试,我不知道那是否会成功。下一个,这个不可能成功吧?哇,它做到了。它成功在Tldraw里做出了Tldraw吗?让我们看看。这里有个bug,大家可能觉得这不好,但实际上这是好事,因为我还有工作。下一个,终端。很好,这个成功了。最后一个,视频游戏。游戏玩法还有待改进,但这仍然相当令人惊讶。

我们继续实验,看看还能做什么。最终我们得到了这个叫做“Draw Fast”的实验,我今天不能展示它,因为它会杀死场馆的Wi-Fi。基本上,这是图像生成,但不是通常那种。在这个实验里,你不是输入一些词然后发送出去让AI生成,我认为那样很令人沮丧。在这个实验里,你是在直接操作底层图像,来塑造你想要的东西并进行探索。我认为这是一种有趣的方式,可以学习和理解这些图像模型是如何工作的,因为看下面,它只是这样。但疯狂的是,如今它的速度足够快,让你感觉像是在自己移动图像。你点击、拖拽、拉伸、挤压,这太神奇了。

当然,因为Tldraw只是一个网页画布,你可以在上面放任何东西。你可以放一个视频流,我们在办公室里玩得很开心。

然而,有一个大问题,一个非常大的问题。

6. 核心挑战:让AI理解并操作画布

到目前为止,我们所做的是:获取画布上的内容,无论是什么,发送给模型,然后它返回给我们一些东西,一个工件、一个网站或一张图片。然后,游戏就结束了。如果你想做别的东西,你必须从头开始。我们真正想做的是让模型生成更多的Tldraw内容,生成更多的画布,因为这样你就形成了一个闭环。谁知道会发生什么呢?

这就是大问题:模型非常不擅长处理画布。这不是它们设计的目的。

不同的画布团队一直在尝试找出如何绕过这个弱点。这是Figma的FigJam,他们所做的是有一个巨大的模板列表,然后用信息填充这些模板。但不幸的是,这有时并不是你真正想要的。如果你输入“蜜蜂的生命周期”,你会得到这种流程图,有时你会得到一个甘特图,这很有趣。

但梦想是让模型能够像你一样操作画布。从一开始,我们就尝试了所有这些不同的方法。但方法太多了,我们不知道哪条路是对的。这里,模型像是在控制一个假的光标,我们只是给它光标,看看会发生什么。它做得相当不错,但只能走这么远。

这是另一个方法,我们让它慢慢地画出墨迹,这有点像恐怖电影里的场景。从字母E和M可以看出,有一些困难,但它做得相当好。我特别喜欢这个人头上出现的帽子。

另一种方法是创建一个假想的API,让它使用那个API,利用函数调用和结构化数据。如果想了解更多细节,可以在演讲结束后找我。这些方法都相当有限,但都显示出了潜力。

那么,最好的方法是什么?它们可能都有效,但有些可能比其他更难。

我们做的另一个完全不同的方法是:不是让它遵循指令,而是让它继续你正在做的事情。这是自动补全,但不是像你Gmail收件箱里的那种自动补全,这是在画布上的自动补全。当你使用更快的模型时,看到什么是可能的,这相当令人惊讶。而且我们会得到越来越快的模型。

7. 当前方案:Teach——教AI使用画布

所有这些工作最终引导我们开发了这个叫做“Teach”的东西。我想现在向大家展示它。这是我们目前让模型“玩”Tldraw的最佳尝试。

我这里有一个小提示词和一个工作区。我要求它画一个雪人。再次向演示之神祈祷,希望它能正常工作,并且保持在线。很好,这不是图像生成。这不是一张图片,而是我可以交互的形状,即使在它还在绘制的时候。之后我可以操作它。我可以说“加一条围巾”,它能看见那里有什么,它知道东西的位置。它会尽力画一条围巾。但因为我们在布里斯班,我会说这里的冬天有点不一样。它也可以修改已经存在的东西。大家喜欢它,别担心。

它画了一些非常滑稽的图画。如果你给它这些工具,它能画得多好,这让我感到惊讶。当然,有些东西它画得比其他的好。像城堡这样的结构化东西,它画得相当好。我真的很喜欢它画的猪。像人脸这样的东西,就稍微棘手一些。那是一只强壮的企鹅。

当然,我认为在我们场景中,真正的潜在用例是帮助我们完成非常枯燥、繁琐的任务,比如绘制图表。请看这个,它实际上给大部分箭头都加了标签。如果你没听过David的演讲,你真应该听听,这样你就能理解更多这些笑话了。但好处是,这还不是终点。这是我现在可以操作、改变和改进的东西。事实上,我要去掉这些。这并没有取代我,而是在帮助我。

我们经常做团队会议,做回顾板会议,所以看看它对回顾板的版本是什么样子也很有趣。还有,什么在困扰它?会议超时。这个呢?更好的故事估算。好吧,这像是回顾吗?总之,有趣的是,我之前展示的“Make Real”功能,我们意识到它可以做相反的事情。它可以拿一个网站,然后把它变成线框图。但它对字体大小非常保守。所以我可能会说,请增加字体大小。我发现使用大写字母和句号似乎能让它表现得更好,我想是因为它知道自己处于专业语境中。好的,它把字体都增大了一点。

我一直在用它来画表格,以及所有在这些工具中手动操作起来很烦人的事情。好的,开始了。它正在决定我在蛋糕义卖会上要卖什么。我要让它用示例数据填充。看看它认为我会卖什么。这是相当不错的一天。现在我想把它转换成条形图。请用句号。让我们看看会发生什么。好的,它移动了一些东西。我真正喜欢的是它倾向于使用颜色编码。一旦我们给了它这些工具,它真的会利用它们,这相当令人惊讶。

我们一直在尝试的另一件事(尚未公开,但我可以在这里展示)是:如果你放一些花括号,它就像是搜索网络的快捷方式。所以如果它花的时间有点长,那是因为它要去搜索布里斯班当前的天气。现在我们有了一个个性化的天气预报。

如果你想知道这个东西能做什么,你可以问我,但也可以问它。所以我为它放出了这个小模板,我发现这通常有助于给它一些结构来围绕。然后它正在用它的答案填充。我喜欢它在一个拉伸的星星上,因为我要求它使用整个宽度。缩放文本,缩放文本大小,大。它正在使用一种特定的XML格式,我稍后会解释一下。

我认为它做的是:它没能写出格式,因为通过写出格式,它创建了形状。让我们看看。我要求它在这里向我解释它的格式是什么。但它有点困惑。希望……我认为一个反复出现的主题是:当它做错事时,这并不是终点。我认为当我们使用AI工具时,重要的是……它又失败了,我们稍后再试。演讲结束后可以找我聊。

8. 关键技术:AI友好的格式

我们使用的这种格式,我称之为“AI驱动格式”。这是什么意思呢?当你要求这些模型做某事时,它们倾向于一遍又一遍地做某事。这只是它们所知道的一切的自然总和与平均。但这不一定是它们擅长的。我们尝试做的是:找到它们想做的事和它们擅长做的事之间的重叠部分。有时我们可以顺应它自然想做的事,有时我们需要稍微引导它一下。我们称这种格式为“Easy”,不是因为它对我们来说容易,而是因为它对模型来说容易。

我来帮它处理这些。好了。“Easy”。这是终极测试。它能画出蜜蜂的生命周期吗?让我们看看。好的,看起来不错。我们有箭头了。我要它说明每个阶段。检查一下:蜂王产下单个卵,喂食蜂王浆和蜂蜜,然后转化。好的,这里有漂亮的图画。那里有两个小翅膀。所以我们有卵、幼虫、蛹和蜜蜂。很好。

9. 进阶探索:多步骤AI工作流

到目前为止,一切都只是我们所谓的“一次性”或“零次”提示。我们向其中一个模型发送请求,得到回复,就这样。但在现实中,在大多数AI工具和应用中,情况并非如此。它更像这样:当人们使用ChatGPT时,它发送一个请求,然后由一个模型处理,再发送给另一个,再另一个。通常这对用户是完全不可见的,他们脱离了循环。他们看不到发生了什么,只看到一个旋转的加载图标。

所以,我非常高兴能非常简要地向大家展示我们最新的实验,它甚至还没有发布或宣布。这是一个节点连线图,一种节点连线编程语言,我这里有不同的组件。如果我点击播放,它就会发送请求。我可以有更多这样的组件,它们会组合在一起。如果我这样设置,它们会一起工作。我实际上可以用这个创建循环,希望它不会崩溃。但让我们看看,它会停止工作流。

有趣的是,当你添加其中一个块时,它叫做“指令块”。我在这里放一个数字。然后我说,这里,我说“增加一”。现在当我点击播放,它会使用AI模型作为计算机。这就是为什么……它做到了。我可以做的是,实际上把它绕回到这里,再次创建一个循环。现在我们有一个数字变得越来越大,但我不想杀死Wi-Fi,所以我停下来,改为添加一个按钮。现在每当我点击这个按钮,它就会增加一。大家跟上了吗?

因为在这里进行计算的是AI,我可以说像“数字词”这样的东西。我可以说“增加三”。希望这应该能成功。它做到了。但另一件事是,我可以更……我可以使用一些词汇。我可以说“增加一倍”。我不需要教它那是什么意思,它会尽力去做。我可以说一些更抽象的东西,像这样。我不知道它会做什么。我们可以做任何我们想要的操作,比如“从数字中移除零”。它会尝试弄清楚该做什么。它做到了。我说,转换成罗马数字。这没什么,让我们继续。

更有趣的是把东西组合在一起。看这里,我现在有两个数字输入。我只是要求它把所有东西加在一起。但关于AI有趣的是,它相当擅长合并东西。这可能不会成功。它可能不会成功,我可能需要给它一些帮助。它没有成功,因为它非常理智。它说“生命的意义”不是一个数字,你在说什么?我只是忽略它。

所以我要做的是添加另一个指令,我会说“从这个输入推断数字”。我把它连接到这里。我们在这里做的是创建一个多步骤的提示链。这就是现在所有AI应用在幕后工作的方式。但目前,用户没有办法与之交互。请成功。它成功了!

我认为这很有趣,因为到目前为止,只有程序员才能做这种多步骤提示。ChatGPT会为你做,它从“章鱼”推断出“8”。但更有趣的是,如果你拿像这样的东西,然后尝试推断“8”。如果这不成功,那不是AI的错,是我的绘画能力。它说是“8”。我认为更有趣的是,如果我们看到这个相机。这不可能成功。让我们试试。拜托,有很多人在看。是的,然后添加一些东西,很好。

所以,当把本不应该连接在一起的东西组合在一起时,看看会发生什么,这很有趣。总之,这是我的食谱生成器。你做的就是把不同的食材放进盒子里,然后它会尽力从中制作食谱。但当你尝试给它一些不太合理的东西时,比如巧克力和汉堡,这很有趣。我不完全确定会是什么。巧克力汉堡,来了。我这里有一些我可能想放进去的购物清单。我正试图变得更素食一点。我要扔进去一个苹果和巧克力。让我们看看。素食巧克力苹果甜点,这很不错。我认为,我们为什么不试试完全抽象的东西,比如星星,不要再有巧克力了,我想我已经受够了有毒废物。

顺便说一下,每当你创建一个这样的流程时,模型会分解它认为需要遵循的步骤。小心星星食谱。这个食谱是一个受输入图像启发的隐喻。小心处理所有食材。如果是不适合食用的东西,它会给你警告。

就像我说的,它会分解它认为需要遵循的步骤,尽力遵循你的指令。我要再展示两个。这个我称之为“我的大杂烩机器”,我的AI大杂烩机器。这对于发明创造非常有用。我已经发明了这个叫做“猫用太阳镜”的发明。我现在愿意接受新发明的建议,所以我想请大家想一个物体,任何物体,然后喊出来。现在。哇,这比我想象的要好。我听到了“过山车”和“骑士长枪”。我们要做一个“骑士长枪过山车”。这是我得到过的最好的建议之一。现在我要做的是把整个过程放慢,这样你们就能看到它在我制作的这个极其复杂的东西中移动。

它会为这个发明构思五个名字,然后替换掉。骑士长枪过山车,长枪……它现在也在写一个宣传语。它从那些工作坊名字中选出了最好的一个:“翻滚骑士”,有趣。翻滚骑士,长枪翻滚,过山车。我们这里有一个完整的宣传语,以防房间里有风险投资人。它现在要尝试创作几个宣传口号,并选出最好的一个。“翻滚进入历史”,“征服刺激”。这里有一个概念图。那实际上很糟糕,很好。它甚至为我制作了着陆页。它试图使用一些公开可用的知识共享图片,但URL错了。不过我们确实有这个小着陆页的模型,包括2023年版权信息。“获取门票”、“功能”、“推荐语”。“这是有史以来最棒的过山车体验。我的孩子们喜欢骑士主题和互动元素。”

我只是想让大家一瞥,你可以真的深入其中,你可以疯狂探索。我一直在尽可能多地推动它。这是另一个,我的最后一个,我称之为“战斗模拟器”。

我之前尝试的是想知道在一场战斗中谁会赢:一万亿只狮子还是太阳。我们这里有战斗本身的图片和一个播客。“伟大的太阳狮子战争”,这里有口语诗篇:“现在,讲述一场不为人知的战争。一万亿只狮子,勇敢而大胆,对抗太阳。”你们可以自己找时间听。但赢家是太阳(剧透)。所以,我想要一个建议,我想要一个可能打败太阳的东西的建议。我想让大家现在喊出一些东西。后面那位说什么?黑洞。好的,但我要让这更有趣一点。我想是黑洞。然后,代替太阳,我想我要用我自己。所以让我们看看,这需要放到那里,那个是我。让我们看看,你们认为谁会赢?哦,我应该站在这里。你们认为谁会赢?你们认为可能是我,还是我们银河系的中心?

我们来了,这是一张非常科学准确(或不准确)的图片。但让我们看看它是什么。你在开玩笑吗?等等。人类以惊人的事件转折成为胜利者,不是通过彻底的毁灭,而是通过力量的展示和对宇宙力量的新理解。黑洞,尽管拥有可怕的力量,却退缩了,变得虚弱并充满敬畏。黑洞和人类。一个具有难以想象引力的旋转漩涡,黑洞隐约出现。它的事件视界,一个不归点,脉动着恶意的能量。与之对抗的是一个孤独的人类,渺小而脆弱,却散发着蔑视。这个人类,奇怪地被赋予了力量,感到宇宙能量在她的血管中流动。

10. 总结与展望

让我们结束吧。当我们试图为AI逃离聊天框时,会是什么样子?我认为我们还不知道。我希望我已经让大家一瞥我们有多么不了解。当我们在画布上使用时,有如此多的可能性。我希望我已经表明,我今天展示的例子是如此不同,但又如此有趣和令人兴奋,也许它给了你们一个稍微新的视角。

我确实知道一件事:这总是会发生。我们从文本开始,然后转向画布。这已经一次又一次地发生了。我认为它会继续发生,不仅对AI,而且对各种领域和技术。

看,这就是我们作为人类的交流方式。我们一直在使用画布。我们一直都是这样做的,无论是画地图、草图,还是用箭头和评论进行注释。这是我们传递信息的方式。这是我最喜欢的标志,我知道这很能说明我的为人。这是在苏格兰的滑雪缆车上的指示,关于如何下缆车。想象一下,如果那是几段文字,你可能会直接又坐下去。当我们想向团队和他人传达信息时,我们使用画布。

所以我认为值得探索,从列奥纳多·达·芬奇,甚至追溯到洞穴壁画,画布是我们使用的东西。我认为如果我们停留在文本和聊天中,那将是令人沮丧的,而这就是我们目前的困境。对这个没什么可说的。哦,这是来自Andrea的演讲。她已经向大家解释了在团队中使用图表进行沟通的重要性和价值。这是在David的演讲中,一切都是图。一切都可以在画布上表达。只是构建它们真的很难。有时需要很多年才会有人出现并说,是的,这将是我们的产品,这是我们要做的,我们要让它变得负担得起。

如果你想帮助构建画布,那么请和我或我团队的人谈谈,我们热爱画布。如果你想今天就开始,那么我认为,去这个网站。我真的很想看到你们用画布构建什么。

谢谢大家。


本节课总结:

在本节课中,我们一起学习了人机交互从命令行到图形界面、再到触摸屏的历史演变,并分析了当前AI交互主要局限于聊天框的现状。我们探讨了将AI模型能力引入交互式画布所面临的核心挑战,并详细介绍了Tldraw团队进行的一系列创新实验,如“Make Real”、“Draw Fast”和“Teach”。这些实验展示了AI如何从被动生成内容,发展到能够理解、操作甚至与用户在画布上协同创作。最后,我们展望了多步骤AI工作流的潜力,并强调了画布作为人类自然沟通和思考工具的重要性。未来,超越聊天框,在画布上释放AI的创造力,将是人机交互发展的一个重要方向。

posted @ 2026-03-29 09:33  布客飞龙II  阅读(2)  评论(0)    收藏  举报