WEBVTT

00:00:00.000 --> 00:00:02.040
为什么要做 Durable？

00:00:02.040 --> 00:00:08.580
Pi 这个编程 agent，过去的典型用法是：你一个人，

00:00:08.580 --> 00:00:12.640
坐在电脑前，或者通过 SSH 连到一台远程机器。

00:00:12.640 --> 00:00:18.600
启动 Pi 会话，做点事情，然后结束会话，通常之后就不再关心这个会话了。

00:00:18.600 --> 00:00:22.620
所以它更像是一对一的关系，会话基本上是临时的，

00:00:22.620 --> 00:00:28.680
而且所有东西都能轻松放进内存，因为 Pi

00:00:28.680 --> 00:00:35.580
一直以来都会把整个会话加载到内存里，再做各种操作。

00:00:36.020 --> 00:00:42.540
这并不是完全必要的，只是历史原因：它本来就是一对一的临时会话，

00:00:42.540 --> 00:00:44.240
通常也不会特别长，对吧？

00:00:44.240 --> 00:00:49.480
但这适合所有 agent 应用吗？

00:00:49.480 --> 00:00:51.400
我想答案是否定的。

00:00:52.620 --> 00:00:58.900
如果你想要一个陪你运行很多年、能同时回答多个

00:00:58.900 --> 00:01:03.480
用户，或者在线程里回复的 Slack 机器人，

00:01:03.480 --> 00:01:04.920
Pi 就不是最佳选择了。

00:01:04.920 --> 00:01:09.140
当然也能做，比如我们有一个 Discord 机器人 Piedolf，

00:01:09.140 --> 00:01:13.140
不过它已经休眠了几个月，因为没那么有用。

00:01:14.240 --> 00:01:18.740
但这种场景确实不是 Pi 编程 agent 最初的设计目标。

00:01:19.780 --> 00:01:24.580
所以今年夏天早些时候，我们决定需要一种不同的底层基础，

00:01:24.580 --> 00:01:27.360
一种不同的框架、库，随便你怎么称呼，

00:01:28.200 --> 00:01:33.820
让你也能构建这类 agent 或 agent 应用。

00:01:33.820 --> 00:01:39.480
长期运行的 agent、多人协作、多个用户或者其他 agent

00:01:39.480 --> 00:01:40.840
相互交互。

00:01:42.740 --> 00:01:46.460
而且不应该把所有东西都完整地放在内存里。

00:01:47.140 --> 00:01:48.880
它还应该能在任何地方运行。

00:01:49.200 --> 00:01:53.500
Pi 显然主要依赖于类似完整计算机的系统。

00:01:53.500 --> 00:01:57.320
我们也想让它运行在 Vercel、Cloudflare、

00:01:57.320 --> 00:02:00.520
E2B，或者你能想到的其他平台上，

00:02:01.060 --> 00:02:07.700
这些平台的计算环境可能并不是操作系统里的一个进程。

00:02:07.840 --> 00:02:14.100
比如 Cloudflare Durable Objects，它的执行模型就完全不同于

00:02:14.100 --> 00:02:15.940
你在 Mac 或 Windows 机器上运行的 Node 进程。

00:02:16.860 --> 00:02:19.080
Durable 就是这样诞生的。

00:02:19.080 --> 00:02:25.380
我觉得也有必要指出，

00:02:25.380 --> 00:02:28.900
Pi 现有的会话模型有哪些典型限制。

00:02:29.460 --> 00:02:34.640
为什么它很适合 Pi 编程 agent，却也会显露出局限。

00:02:34.880 --> 00:02:39.520
其实我觉得，很大程度上其他很多 agent 也有这些限制。

00:02:41.280 --> 00:02:44.920
首先，它写入的是 JSONL 文件。

00:02:45.460 --> 00:02:46.480
它们当然很有用。

00:02:46.980 --> 00:02:48.440
本身并没有什么根本性问题。

00:02:48.440 --> 00:02:53.260
但它们采用追加写入，对吧？

00:02:53.340 --> 00:02:57.260
所以要重写这些文件并不容易。

00:02:58.280 --> 00:03:04.620
随着你往里面放越来越大的工作负载，它们可能会积累大量

00:03:04.620 --> 00:03:06.480
中间结果，

00:03:08.660 --> 00:03:12.760
而这些东西可能很浪费。

00:03:12.760 --> 00:03:16.840
第二，如果你要加入子 agent，

00:03:16.840 --> 00:03:21.440
实际上得把它们拆到相互依赖的不同会话里。

00:03:22.280 --> 00:03:25.460
Pi 的树状结构确实

00:03:25.460 --> 00:03:29.720
非常好用，尤其是在单一写入者、单一用户的情况下。

00:03:29.900 --> 00:03:32.700
你可以跳到树的不同位置，回退会话，再做些操作。

00:03:32.700 --> 00:03:36.940
但如果把子 agent 都放进一个会话，

00:03:36.940 --> 00:03:41.340
记录行就会交错，

00:03:41.340 --> 00:03:44.160
还需要一些现在并不存在的锁机制。

00:03:44.160 --> 00:03:50.240
所以现有的整套树状系统很酷，

00:03:50.240 --> 00:03:53.200
但也有一些限制。

00:03:53.200 --> 00:03:58.880
因此，Durable 的很多工作，

00:03:58.880 --> 00:04:04.300
某种程度上是从研究如何让这些东西配合数据库开始的。

00:04:04.300 --> 00:04:08.660
这里其实涉及好几件事。

00:04:08.800 --> 00:04:11.740
持久化执行意味着，你应该能够停止它，再重新启动。

00:04:12.000 --> 00:04:14.300
也就是可以挂起，然后接着工作。

00:04:15.240 --> 00:04:20.580
如果其中有子 agent，或者同时进行的多个任务，

00:04:20.600 --> 00:04:25.100
这种能力就应该覆盖当时所有活跃的分支。

00:04:25.100 --> 00:04:31.440
第二点是，虽然

00:04:31.440 --> 00:04:34.200
现在的 Pi 已经有相当程度的持久性，

00:04:35.140 --> 00:04:41.640
但你仍然很容易用它做出一些

00:04:41.640 --> 00:04:42.860
无法接着运行的东西。

00:04:43.060 --> 00:04:49.100
甚至现有的核心 agent 循环，也不能自己恢复执行。

00:04:49.100 --> 00:04:55.840
如果你关掉再启动，必须输入“继续”之类的话，它才会继续工作。

00:04:56.660 --> 00:05:03.140
而很多你可能想加进 Pi 的功能，都需要在过程中

00:05:03.140 --> 00:05:04.320
持久化额外的状态。

00:05:04.580 --> 00:05:10.240
典型例子就是，打断一次工具执行，

00:05:10.240 --> 00:05:14.860
向用户请求批准。

00:05:14.860 --> 00:05:21.160
当然，现在这不算特别有用的例子，因为我觉得工具

00:05:21.160 --> 00:05:22.200
审批基本已经退出舞台了。

00:05:22.580 --> 00:05:25.400
但你确实无法在这个时间点挂起 agent，说：

00:05:25.460 --> 00:05:30.140
我们先确认能不能继续，然后再从这里恢复。

00:05:30.280 --> 00:05:35.800
所以，如果我们之前没有专门设计过某个挂起点，

00:05:35.800 --> 00:05:37.800
系统就很难在那里挂起并恢复。

00:05:38.480 --> 00:05:44.720
对，我们应该解释一下 durable 到底是什么意思，因为一直在说这个词。

00:05:44.860 --> 00:05:49.540
但很多人其实不太熟悉这个概念。

00:05:49.840 --> 00:05:53.680
在旧 Pi——不该说旧 Pi，是 Pi 编程 agent——里面，

00:05:53.680 --> 00:05:58.480
你启动一个会话，让 agent 做事，结果机器挂了。

00:05:58.900 --> 00:06:03.440
你重新启动 Pi，打开同一个会话，就像 Armin 说的，还得输入“继续”。

00:06:04.000 --> 00:06:11.260
如果某个扩展在后台做事，它就必须自己实现重新启动后台工作的逻辑。

00:06:11.820 --> 00:06:14.780
Pi 编程 agent 完全不会帮扩展处理这些。

00:06:14.860 --> 00:06:17.520
每个人都得重新发明一遍。

00:06:17.920 --> 00:06:23.840
如果进程崩溃时有个工具正在执行，

00:06:23.840 --> 00:06:26.540
这个工具就被中止了，不会自动重新运行。

00:06:26.540 --> 00:06:32.260
agent 得自己判断要不要重跑、怎么重跑，以及这次工具执行

00:06:32.260 --> 00:06:35.560
是不是破坏性的操作，等等。

00:06:35.560 --> 00:06:41.060
而且 Pi 没有真正的并发，

00:06:41.060 --> 00:06:45.680
也就是说，始终只有一个 agent 循环。

00:06:46.640 --> 00:06:51.460
如果进程挂了，我们不会恢复到那个循环内部。

00:06:51.460 --> 00:06:54.700
agent 循环基本上就是：获取用户输入，

00:06:54.700 --> 00:06:59.260
发给大模型，拿到回复，检查是否有工具调用，

00:06:59.260 --> 00:07:02.740
执行工具，再继续循环，直到模型说“我完成了”。

00:07:02.740 --> 00:07:06.740
在 Pi 编程 agent 里，这些执行中的信息就丢了。

00:07:06.880 --> 00:07:11.020
我们基本上不会记录循环执行到了哪里，对吧？

00:07:11.020 --> 00:07:17.680
Pi Durable 引入了一些机制，让这些事情基本上自动完成。

00:07:18.100 --> 00:07:21.380
它会促使你用持久化执行的方式思考。

00:07:21.380 --> 00:07:27.180
首先，我刚才描述的循环，会被拆成我们称为 task 的任务。

00:07:27.460 --> 00:07:31.200
一个任务基本上就是声明：我打算做一件事。

00:07:31.520 --> 00:07:35.040
先把这个意图存起来，以后还能记住。

00:07:35.340 --> 00:07:41.040
执行这件事，再把执行结果存起来。

00:07:41.040 --> 00:07:46.140
这就像一个三明治，我能从其中任意位置恢复。

00:07:46.260 --> 00:07:50.320
可以在意图记录之后、执行我们所说的 effect 之前恢复，

00:07:50.320 --> 00:07:57.260
也可以在 effect 执行后、最终结果记录前，或者结果记录后恢复。

00:07:57.820 --> 00:08:02.700
这样很好，因为你只需要写：我准备做这件事，

00:08:02.820 --> 00:08:03.720
我正在做这件事，

00:08:03.860 --> 00:08:04.560
我做完了。

00:08:05.280 --> 00:08:10.200
恢复的时候，我们就知道你停在这个“三明治”的哪一层，并从那里继续。

00:08:10.500 --> 00:08:12.100
这适用于所有操作。

00:08:12.260 --> 00:08:13.560
也包括请求，

00:08:13.660 --> 00:08:15.560
也就是发给大模型服务商的请求。

00:08:15.560 --> 00:08:21.220
工具也一样，现在工具可以把自己标记为可安全重放。

00:08:21.720 --> 00:08:24.340
例如，读取工具可以安全重放。

00:08:24.580 --> 00:08:27.600
你再调用一次，拿到结果就行。

00:08:28.360 --> 00:08:34.700
子 agent 工具也可以安全重放，因为子 agent 和主 agent

00:08:34.700 --> 00:08:36.060
运行的是同一套任务机制。

00:08:36.060 --> 00:08:40.400
所以，执行图的持久化能力同样适用于它。

00:08:40.620 --> 00:08:44.300
子 agent 可以重新启动，再告诉它——

00:08:44.300 --> 00:08:48.200
甚至不用告诉，它会自动从上次停下的地方继续。

00:08:49.660 --> 00:08:53.360
这大概是 Pi Durable 最大的卖点。

00:08:53.900 --> 00:08:59.320
我们借鉴了一些结构化并发的思想。

00:08:59.320 --> 00:09:04.940
不过它并不能完全套用于 agent 的所有场景，这个发现对我来说很有意思。

00:09:05.100 --> 00:09:10.740
结构化并发基本上就是，一个执行单元拥有另一个执行单元，后者去做一些事情。

00:09:11.240 --> 00:09:16.300
后者的生命周期不能超过前者。

00:09:16.880 --> 00:09:19.640
Pi Durable 中大约 90% 的任务都适用。

00:09:19.640 --> 00:09:25.680
但有少数情况，子执行单元需要

00:09:25.680 --> 00:09:27.360
比父执行单元活得更久。

00:09:27.360 --> 00:09:32.400
所以它不是百分之百的结构化并发，但已经非常接近。

00:09:32.500 --> 00:09:34.580
而且在你不需要这种约束时，也提供了跳出机制。

00:09:36.060 --> 00:09:37.200
对，这就是持久化执行。

00:09:37.420 --> 00:09:38.400
希望我讲清楚了。

00:09:38.420 --> 00:09:40.400
还有一件事值得提一下。

00:09:40.660 --> 00:09:45.240
如果你去看 earendil-works 的 GitHub 仓库，会发现一个叫 Absurd 的项目，

00:09:46.260 --> 00:09:49.580
它是构建在 Postgres 之上的持久化工作流引擎。

00:09:49.580 --> 00:09:55.500
我们最初做它，是为了支持一个由 Pi 驱动的 agent，

00:09:55.500 --> 00:09:56.960
叫 Levos，

00:09:57.880 --> 00:10:04.800
让它能在云端部署过程中挂起和恢复。

00:10:05.260 --> 00:10:09.360
最初的考虑是，沙箱挺贵的。

00:10:09.740 --> 00:10:14.920
所以我们想把 agent 设计成：任何愿意接手的 worker 都能分配到它，

00:10:14.920 --> 00:10:16.440
并在那里继续工作。

00:10:17.380 --> 00:10:22.400
不出所料，我们很早就遇到了一个问题，

00:10:22.440 --> 00:10:27.120
我觉得很多人都遇到过：状态不只有 agent 自身的状态。

00:10:28.240 --> 00:10:33.480
所以，光在各处传递会话记录，显然不够。

00:10:33.960 --> 00:10:35.900
agent 很喜欢往文件系统写东西，等等。

00:10:35.900 --> 00:10:43.240
你很快就会发现，持久化工作流引擎很好，

00:10:43.600 --> 00:10:49.960
但如果 agent harness 没有真正支持它，光有引擎是不够的。

00:10:50.080 --> 00:10:52.000
也就是说，agent harness 本身也得支持持久化执行。

00:10:53.360 --> 00:10:56.520
但这不意味着所有东西都会自动持久化，对吧？

00:10:56.580 --> 00:11:00.380
如果文件系统里存着非常重要的数据，

00:11:00.900 --> 00:11:02.720
你也得想办法给它做快照，

00:11:02.720 --> 00:11:05.060
或者把 agent 设计成即使这些数据消失也能运作。

00:11:06.900 --> 00:11:10.140
所以，有人可能看着这套东西说：

00:11:10.220 --> 00:11:12.640
我们有了持久化工作流引擎，所有问题都解决了。

00:11:12.720 --> 00:11:15.400
它确实可能有用，你也可能仍然想用它。

00:11:16.260 --> 00:11:21.060
但仅仅拥有它，并不会让你的 agent 自动具备持久化执行能力。

00:11:22.880 --> 00:11:29.040
实际上，agent 开箱即用时仍然可能缺乏这种能力，

00:11:29.040 --> 00:11:35.300
比如你让它把文件写到再也无法恢复的文件系统里。

00:11:35.900 --> 00:11:40.100
harness 内部的东西可以持久化，

00:11:40.480 --> 00:11:43.360
但 harness 还会操作外部环境。

00:11:43.980 --> 00:11:46.500
在我之前说的 effect“三明治”里，

00:11:46.760 --> 00:11:48.840
意图是 harness 内部可以持久化的部分；

00:11:48.960 --> 00:11:53.720
effect 则是写文件、执行 Bash、调用服务、部署东西，等等。

00:11:54.120 --> 00:11:56.820
那属于外部系统，你无法控制它。

00:11:56.820 --> 00:11:58.160
通常是这样。

00:11:58.780 --> 00:12:02.780
所以，要让这一部分也支持持久化执行，就需要某种——

00:12:02.820 --> 00:12:04.700
那个该死的词我念不出来——

00:12:04.940 --> 00:12:11.780
比如，我说用某个 ID 执行部署，

00:12:11.780 --> 00:12:15.800
再次调用时，如果这个 ID 对应的部署已经在运行，

00:12:16.020 --> 00:12:17.140
就不应该再触发一遍。

00:12:17.300 --> 00:12:18.280
我想说的就是这个。

00:12:18.280 --> 00:12:20.340
如果你的外部系统支持这种方式，那很好。

00:12:20.460 --> 00:12:24.380
文件系统并不这样工作，除非它支持快照之类的机制。

00:12:25.120 --> 00:12:31.580
不过，关于持久化执行，你可以有 Temporal、Absurd

00:12:31.580 --> 00:12:34.400
或者其他什么持久化工作流引擎，

00:12:35.280 --> 00:12:37.280
但随后就会遇到原子性的问题。

00:12:38.260 --> 00:12:42.960
为了实现持久化执行，同一步中所有需要持久化的内容，

00:12:42.960 --> 00:12:45.280
必须原子地保存下来。

00:12:45.280 --> 00:12:49.800
如果这个机制在 agent 外部，

00:12:50.000 --> 00:12:54.640
那么 agent 内部所有需要原子持久化才能保证恢复的东西，

00:12:54.640 --> 00:12:57.220
就得想办法硬塞进那套机制。

00:12:57.400 --> 00:12:59.200
这其实不太行得通。

00:12:59.280 --> 00:13:03.920
所以我们决定，让 Pi Durable 本身就是一个持久化工作流引擎。

00:13:04.360 --> 00:13:09.220
这解决了很多问题，因为你也可以为自己的应用构建任务，

00:13:09.220 --> 00:13:14.540
让它们运行在这个持久化调度器里面。

00:13:14.540 --> 00:13:17.880
而且它还支持持久化的另一部分：

00:13:18.620 --> 00:13:22.640
状态持久化，不仅包括 harness，也包括你的应用。

00:13:23.160 --> 00:13:29.460
当然，你不会想把所有应用状态都塞进 harness 的存储。

00:13:30.240 --> 00:13:36.660
但你可以把大块数据存到自己的存储里，

00:13:37.020 --> 00:13:40.920
再在 harness 存储里保存一个指向那块数据的引用。

00:13:40.920 --> 00:13:47.560
这样就很容易同时获得持久化执行和状态持久化。

00:13:47.560 --> 00:13:53.720
还有一点值得记住，

00:13:53.720 --> 00:13:57.320
Absurd 和 Temporal，

00:13:57.940 --> 00:14:02.380
以及其他无数持久化工作流执行引擎，

00:14:02.980 --> 00:14:09.060
大多建立在一个如今正在迅速失效的前提上。

00:14:09.180 --> 00:14:14.660
这个前提是，你可以在持久化执行上偷不少懒，

00:14:14.660 --> 00:14:21.760
只要假设这些代码是由专业程序员维护的。

00:14:22.580 --> 00:14:28.400
我这么说，是因为如今几乎所有持久化工作流引擎，

00:14:28.400 --> 00:14:35.240
都会要求工作流编排部分的代码完全确定，

00:14:35.300 --> 00:14:39.660
这样即使挂起一周，一周后再回来，

00:14:39.660 --> 00:14:43.460
代码还是一样，就可以依次走过所有检查点，

00:14:43.640 --> 00:14:46.280
取回当时写进持久化存储的数据，

00:14:46.860 --> 00:14:48.120
然后从那里继续。

00:14:49.620 --> 00:14:52.720
或者，代码可以不是完全确定的，

00:14:53.220 --> 00:14:58.000
但根据持久化信息选择的所有分支，仍然必须兼容，

00:14:58.000 --> 00:15:00.140
或者明确写好升级逻辑。

00:15:00.140 --> 00:15:06.640
大模型带来的问题是，我们

00:15:06.640 --> 00:15:08.180
如今生活在一个

00:15:08.180 --> 00:15:09.880
agent 会自己写代码的世界里。

00:15:10.000 --> 00:15:11.800
不是说 agent 不能成为专业程序员，

00:15:12.440 --> 00:15:18.080
而是它们很难始终

00:15:18.080 --> 00:15:22.660
完整理解所有可能再次遇到的任务，

00:15:22.660 --> 00:15:24.600
以及这些任务处于什么持久化状态。

00:15:25.240 --> 00:15:27.900
我觉得这个问题是无法彻底解决的。

00:15:27.900 --> 00:15:31.580
但如今的取舍，不再是靠抽象把细节藏起来，让程序员用得舒服，

00:15:31.580 --> 00:15:35.040
而是把这些细节变得更加显式。

00:15:35.900 --> 00:15:38.020
因为只有显式呈现出来，agent 才看得见。

00:15:38.080 --> 00:15:40.040
它才能知道：哦，我把这些持久化信息写出去了，

00:15:40.040 --> 00:15:41.120
现在需要重新加载。

00:15:41.540 --> 00:15:45.380
所以，这个取舍几乎完全反转了，

00:15:45.380 --> 00:15:46.440
和我们做 Absurd 时正好相反。

00:15:46.960 --> 00:15:49.340
当时我们尽量让人类程序员用起来方便。

00:15:49.840 --> 00:15:52.240
而现在，我们要把这些信息直接摆出来，

00:15:53.400 --> 00:15:54.380
让大模型看见。

00:15:55.100 --> 00:15:56.500
代码会稍微多一点，

00:15:56.500 --> 00:16:00.760
但大模型会显式地经过每一个检查点。

00:16:00.920 --> 00:16:05.920
比如，在 Absurd 中挂起状态时，

00:16:06.300 --> 00:16:08.020
每走到一个步骤，

00:16:08.440 --> 00:16:09.520
我们都会递增计数器。

00:16:09.840 --> 00:16:12.280
所以，如果你在循环里执行四次，

00:16:12.400 --> 00:16:15.680
就会隐含地记录第一次、第二次、

00:16:15.760 --> 00:16:16.560
第三次，以此类推。

00:16:17.560 --> 00:16:20.160
而在 Pi Durable 里，只是一个 memo 函数，

00:16:20.820 --> 00:16:24.240
你给它一个名字，就用这个名字。

00:16:24.240 --> 00:16:26.160
想做版本管理，就自己明确标版本。

00:16:27.540 --> 00:16:30.820
所以 Durable 里隐藏的魔法，

00:16:30.820 --> 00:16:32.700
比 Temporal 或 Absurd 少得多。

00:16:33.160 --> 00:16:33.620
对，确实如此。

00:16:33.800 --> 00:16:39.000
工作流调度器大概只有 1,200 行代码，

00:16:39.000 --> 00:16:40.680
你真的可以读懂它。

00:16:41.240 --> 00:16:44.680
也很容易理解执行图的行为。

00:16:44.820 --> 00:16:47.320
这正是它和 Temporal、Absurd 的区别。

00:16:47.900 --> 00:16:49.420
在 Temporal 或 Absurd 里，

00:16:49.420 --> 00:16:52.840
你仿佛只是在写一个普通的 JavaScript 程序，

00:16:53.160 --> 00:16:56.380
它就会神奇地从上次停下的地方继续，对吧？

00:16:56.520 --> 00:16:58.800
我觉得 Temporal 比 Absurd 更是如此。

00:17:00.020 --> 00:17:03.060
就像你说的，对人来说，这个模型很好，

00:17:03.060 --> 00:17:04.580
因为你大致能理解它。

00:17:04.580 --> 00:17:07.160
但当你真正深入使用时，

00:17:07.260 --> 00:17:08.120
你会发现，

00:17:08.760 --> 00:17:12.260
为了让它运转，底下其实得做一大堆事情。

00:17:12.420 --> 00:17:14.740
连 setTimeout 这么简单的东西，

00:17:14.840 --> 00:17:18.320
它们都得介入处理，

00:17:18.320 --> 00:17:19.900
确保它能配合这套机制工作。

00:17:20.460 --> 00:17:22.400
而这类工作流引擎

00:17:22.460 --> 00:17:23.720
最大的问题，

00:17:23.720 --> 00:17:25.940
其实在于你有一张执行图。

00:17:26.120 --> 00:17:27.660
比如函数 A 调用函数 B，

00:17:27.760 --> 00:17:28.620
函数 B 又调用函数 C。

00:17:29.200 --> 00:17:30.980
然后在其中某个地方，

00:17:31.320 --> 00:17:34.240
我们停止了进程，

00:17:34.240 --> 00:17:35.340
现在又重新启动它。

00:17:35.360 --> 00:17:38.200
于是必须重建调用栈，对吧？

00:17:38.340 --> 00:17:40.660
函数 A、函数 B、函数 C。

00:17:40.920 --> 00:17:41.820
怎么做呢？

00:17:42.220 --> 00:17:43.720
你得把这整套东西重放一遍。

00:17:44.460 --> 00:17:45.700
重放的过程是这样的：

00:17:45.740 --> 00:17:47.220
再次进入函数 A，

00:17:47.220 --> 00:17:49.280
函数 A 调用一个操作，说，

00:17:49.420 --> 00:17:50.240
做这件事。

00:17:50.720 --> 00:17:52.040
但它不会真的再做一次，

00:17:52.120 --> 00:17:53.360
而是返回上一次的结果，

00:17:53.940 --> 00:17:54.560
也就是那个操作

00:17:54.840 --> 00:17:56.080
上次运行时的结果。

00:17:56.340 --> 00:17:57.120
然后拿着这个结果，

00:17:57.120 --> 00:17:58.680
调用函数 B。

00:17:58.900 --> 00:17:59.940
函数 B 也做同样的事。

00:18:00.000 --> 00:18:00.600
再拿着它的结果，

00:18:00.600 --> 00:18:01.400
调用函数 C。

00:18:01.700 --> 00:18:03.420
仔细想想，

00:18:03.580 --> 00:18:04.440
这个模型其实挺糟糕的。

00:18:04.480 --> 00:18:06.480
因为它要求记录所有操作的日志，

00:18:06.480 --> 00:18:09.040
还要重新执行整段代码。

00:18:09.380 --> 00:18:10.740
而且，就像 Armin 说的，只要

00:18:10.860 --> 00:18:12.820
其中任何一个函数发生变化，

00:18:13.240 --> 00:18:14.080
你就麻烦大了。

00:18:14.320 --> 00:18:16.440
对 Temporal 最常见的批评之一，

00:18:16.440 --> 00:18:18.080
就是版本管理非常糟糕。

00:18:18.640 --> 00:18:21.260
那么 Pi Durable 到底怎么做？

00:18:21.780 --> 00:18:23.020
Pi Durable 提供的，

00:18:23.020 --> 00:18:25.880
并不是传统的 JavaScript 函数调用栈。

00:18:25.880 --> 00:18:29.860
它提供的是一棵任务树。

00:18:30.380 --> 00:18:31.900
每个任务都是独立完整的。

00:18:32.380 --> 00:18:34.840
它可以调用其他任务，并等待它们。

00:18:35.960 --> 00:18:38.380
相比普通的 JavaScript 函数调用图，

00:18:38.380 --> 00:18:42.720
这种结构很容易重建。

00:18:43.140 --> 00:18:44.420
这就是最大的区别。

00:18:44.800 --> 00:18:46.460
工作单元非常小，

00:18:46.740 --> 00:18:47.320
也非常简单。

00:18:47.460 --> 00:18:52.700
就是意图、effect、存储这样的模型。

00:18:52.700 --> 00:18:55.740
如果你想并行做更多事情，

00:18:55.920 --> 00:18:56.300
当然可以。

00:18:56.380 --> 00:18:56.860
非常容易。

00:18:56.960 --> 00:18:57.920
只需要创建一个新任务，

00:18:57.920 --> 00:18:59.440
其他任务就可以等待它。

00:18:59.560 --> 00:19:00.040
就这么简单。

00:19:00.200 --> 00:19:02.780
要回退，

00:19:03.120 --> 00:19:06.220
或者说重建这张虚拟调用图，

00:19:06.360 --> 00:19:07.180
也是非常容易的。

00:19:08.080 --> 00:19:09.340
既然已经聊到了 effect，

00:19:09.800 --> 00:19:10.140
哦，对。

00:19:10.140 --> 00:19:14.200
我们或许该谈谈另一个绕不开的问题，

00:19:14.320 --> 00:19:15.800
也就是 Effect.js。

00:19:16.800 --> 00:19:18.260
不过在聊 Effect.js 之前，

00:19:18.560 --> 00:19:22.820
我还想顺带提一句，

00:19:22.820 --> 00:19:26.460
另一个始终隐含在讨论里的问题，

00:19:26.540 --> 00:19:27.680
就是为什么首先要用 JavaScript。

00:19:27.680 --> 00:19:31.340
我想，这里用的还是 TypeScript。

00:19:32.240 --> 00:19:34.340
之所以选 TypeScript，是因为我们很喜欢

00:19:34.340 --> 00:19:36.360
让 agent 扩展自身能力这个想法。

00:19:36.500 --> 00:19:37.220
而就目前来说，

00:19:37.280 --> 00:19:38.680
JavaScript 看起来是一个不错的环境。

00:19:40.180 --> 00:19:41.240
但我也想说，

00:19:41.360 --> 00:19:43.380
从很多方面来看，

00:19:43.440 --> 00:19:44.540
我们现在想做的，

00:19:44.540 --> 00:19:45.780
其实是构建

00:19:45.780 --> 00:19:47.820
这样一种

00:19:47.820 --> 00:19:50.740
创建 agent 的方式，

00:19:50.740 --> 00:19:52.940
让它能够持久化保存自身。

00:19:53.560 --> 00:19:54.560
持久化存储意味着，

00:19:54.560 --> 00:19:57.740
你得遵循某个稳定的系统约定，

00:19:57.740 --> 00:20:00.200
而且应该能把它描述清楚，

00:20:00.200 --> 00:20:01.340
让别人可以用另一种语言

00:20:01.340 --> 00:20:02.380
重新实现，对吧？

00:20:02.440 --> 00:20:02.820
所以，

00:20:03.560 --> 00:20:04.480
要记住，

00:20:04.560 --> 00:20:06.260
虽然现在它确实是 JavaScript，

00:20:06.260 --> 00:20:07.620
也可能在

00:20:07.620 --> 00:20:08.760
很长很长的时间里一直是 JavaScript，

00:20:09.300 --> 00:20:12.140
但目标是让系统本身做到：

00:20:13.040 --> 00:20:13.200
就是说，

00:20:13.700 --> 00:20:15.600
你应该能够用它写一个 agent，

00:20:15.600 --> 00:20:18.120
维护一个已经运行两年的 Slack 频道，

00:20:18.120 --> 00:20:20.020
然后最终想用 TypeScript 重写它时，

00:20:20.140 --> 00:20:21.600
仍然可以接着使用原有的数据。

00:20:22.100 --> 00:20:22.180
对吧？

00:20:22.220 --> 00:20:23.500
所以这里的想法是，

00:20:23.500 --> 00:20:24.780
底层系统

00:20:24.780 --> 00:20:29.700
应该是一个值得信赖的基础。

00:20:30.740 --> 00:20:32.920
那我们为什么没有用 Effect.js？

00:20:34.360 --> 00:20:36.360
因为它没有解决我们的问题。

00:20:36.820 --> 00:20:37.740
开箱即用的

00:20:37.820 --> 00:20:40.400
Effect.js 很擅长处理内存中的、

00:20:40.500 --> 00:20:40.980
进程内的

00:20:41.180 --> 00:20:42.040
结构化并发，

00:20:42.160 --> 00:20:42.580
比如这一类事情。

00:20:42.720 --> 00:20:44.780
此外还有一大堆功能，

00:20:44.780 --> 00:20:46.080
比如结构化错误、

00:20:46.420 --> 00:20:47.300
依赖注入、

00:20:48.120 --> 00:20:49.340
取消和日志，

00:20:49.340 --> 00:20:50.640
或者遥测追踪，

00:20:50.640 --> 00:20:51.200
以及类似的东西。

00:20:51.280 --> 00:20:52.120
它非常棒。

00:20:52.120 --> 00:20:53.380
真的非常棒。

00:20:53.800 --> 00:20:54.580
不过我得说，

00:20:54.580 --> 00:20:56.900
最近有位维护者说，

00:20:56.900 --> 00:20:58.800
他发现

00:20:58.800 --> 00:21:01.540
自己已经没法亲手写 Effect.js 代码了。

00:21:03.460 --> 00:21:04.480
但这不是问题，

00:21:04.480 --> 00:21:05.700
因为 agent 能理解它。

00:21:08.260 --> 00:21:09.040
所以，再说一次，

00:21:09.240 --> 00:21:10.280
我们喜欢 Effect.js，

00:21:10.280 --> 00:21:11.620
而且觉得它对

00:21:11.620 --> 00:21:12.680
很多很多问题都是正确的解决方案。

00:21:12.820 --> 00:21:13.360
但对我们来说，

00:21:13.440 --> 00:21:14.400
它没有解决问题，

00:21:14.400 --> 00:21:16.340
因为开箱即用的 Effect.js

00:21:16.820 --> 00:21:18.460
对持久化执行的支持，

00:21:18.460 --> 00:21:19.820
恰好是零。

00:21:20.960 --> 00:21:22.500
也就是函数 A、

00:21:22.560 --> 00:21:22.960
函数 B、

00:21:23.080 --> 00:21:24.040
函数 C 那一套，

00:21:24.760 --> 00:21:27.540
在内存里工作得很好，

00:21:28.220 --> 00:21:30.440
但 Effect.js 没有提供基础机制，

00:21:30.440 --> 00:21:32.300
让你在恢复进程时

00:21:32.300 --> 00:21:34.660
重建这张调用图。

00:21:35.580 --> 00:21:37.400
所以接下来的问题是：

00:21:38.040 --> 00:21:38.300
好吧，

00:21:38.340 --> 00:21:39.780
如果它帮不了我们实现持久化执行，

00:21:39.780 --> 00:21:42.140
那我们还能从中得到什么？

00:21:42.140 --> 00:21:43.260
而且，要给它加上

00:21:43.260 --> 00:21:44.720
持久化执行能力有多难？

00:21:45.080 --> 00:21:45.880
结果发现，

00:21:45.880 --> 00:21:47.260
给它加入持久化执行能力，

00:21:47.260 --> 00:21:49.420
最后基本上

00:21:49.420 --> 00:21:50.680
就会变成我们现在做的东西：

00:21:51.460 --> 00:21:53.780
一个基于任务的工作流引擎。

00:21:54.260 --> 00:21:55.720
有个 Effect.js workflow

00:21:55.940 --> 00:21:56.660
软件包，

00:21:56.840 --> 00:21:57.220
或者叫 Effect

00:21:57.220 --> 00:21:58.880
/workflow，

00:21:59.480 --> 00:22:00.360
它基本上就是这样。

00:22:00.440 --> 00:22:01.520
它们把 task 叫作 activity，

00:22:01.520 --> 00:22:03.180
本质上

00:22:03.180 --> 00:22:04.800
是同一回事。

00:22:05.220 --> 00:22:07.660
只是你还得同时引入整套 Effect.js，

00:22:07.660 --> 00:22:09.280
我们不想这么做。

00:22:09.460 --> 00:22:10.080
这并不是说它是不可靠的选择，

00:22:10.080 --> 00:22:12.040
毕竟很多人都在用 Effect.js 构建项目。

00:22:12.340 --> 00:22:14.560
但我们希望自己的实现是独立完整的。

00:22:14.560 --> 00:22:15.900
而且从可移植性来说，

00:22:16.760 --> 00:22:18.960
要把 Pi Durable

00:22:18.960 --> 00:22:20.760
移植到别的环境，其实很容易。

00:22:20.940 --> 00:22:21.680
别问我是怎么知道的。

00:22:24.420 --> 00:22:25.780
相比之下，

00:22:25.780 --> 00:22:27.120
要移植 Effect.js

00:22:27.120 --> 00:22:31.020
以及它基于生成器的语义，就没那么简单了。

00:22:31.780 --> 00:22:32.700
所以最终，

00:22:32.860 --> 00:22:34.540
这就是我们没有选择 Effect.js 的原因。

00:22:35.240 --> 00:22:35.960
而且我也觉得，

00:22:35.960 --> 00:22:37.280
它很有用，

00:22:37.720 --> 00:22:38.500
其实我确实觉得

00:22:38.500 --> 00:22:39.400
Effect 相当有用。

00:22:39.720 --> 00:22:41.800
只是出于一些原因，我不认为

00:22:41.800 --> 00:22:42.520
它特别适合 Durable。

00:22:42.660 --> 00:22:43.580
但我确实

00:22:43.580 --> 00:22:44.900
尝试过，

00:22:44.900 --> 00:22:47.060
一方面是为了使用我们构建的东西，另一方面，

00:22:49.100 --> 00:22:50.780
我做的很多业余项目，

00:22:50.780 --> 00:22:53.100
比如为了弄清楚怎么构建软件工厂，

00:22:53.100 --> 00:22:54.060
以及类似的项目，

00:22:54.060 --> 00:22:56.440
实际上都是基于 Effect 的。

00:22:57.440 --> 00:22:58.080
之所以

00:22:58.080 --> 00:22:58.780
基于 Effect，

00:22:58.780 --> 00:22:59.400
是因为我觉得

00:22:59.400 --> 00:23:00.920
Effect 确实

00:23:00.920 --> 00:23:04.400
能解决目前的一些约束问题。比如说，

00:23:04.540 --> 00:23:04.940
举个例子，

00:23:05.520 --> 00:23:06.560
它强烈鼓励

00:23:06.560 --> 00:23:07.640
你不要直接抛出异常。

00:23:08.000 --> 00:23:09.340
也强烈鼓励

00:23:09.340 --> 00:23:10.380
你通过组合来组织服务。

00:23:11.100 --> 00:23:14.760
所以它几乎像一根很有用的拐杖，

00:23:14.760 --> 00:23:16.820
能帮你给

00:23:16.820 --> 00:23:18.720
TypeScript 项目

00:23:18.720 --> 00:23:19.920
增加一些结构。

00:23:19.920 --> 00:23:20.880
否则，

00:23:20.880 --> 00:23:21.800
当你把项目交给 agent 时，

00:23:21.800 --> 00:23:23.400
它可能不会有多少结构。

00:23:23.400 --> 00:23:25.540
但与此同时，

00:23:26.800 --> 00:23:29.280
如果我重新做同一个项目，

00:23:29.280 --> 00:23:30.840
不用 Effect，

00:23:30.980 --> 00:23:31.580
我现在已经这样试过

00:23:31.580 --> 00:23:32.220
好几次了，

00:23:32.300 --> 00:23:33.060
一遍用 Effect 来做，

00:23:33.180 --> 00:23:33.920
一遍不用 Effect 来做，

00:23:34.500 --> 00:23:35.880
其实还不能完全确定，

00:23:35.880 --> 00:23:36.640
Effect 方案

00:23:36.640 --> 00:23:38.220
也一定

00:23:38.220 --> 00:23:39.120
更适合 agent。

00:23:39.120 --> 00:23:41.060
因为至少，

00:23:41.120 --> 00:23:42.040
有点讽刺的是，最后你通常

00:23:42.040 --> 00:23:43.560
反而会写出更多代码。

00:23:44.400 --> 00:23:45.220
而代码更多，

00:23:45.220 --> 00:23:47.020
对这种场景来说，

00:23:47.020 --> 00:23:48.840
仍然是个挑战。

00:23:49.540 --> 00:23:50.960
但我觉得 Effect 团队

00:23:50.960 --> 00:23:51.820
总体上走在正确的方向，

00:23:51.820 --> 00:23:54.340
用这样的方式来表达软件。

00:23:55.140 --> 00:23:55.920
我已经

00:23:55.920 --> 00:23:57.740
向他们当中

00:23:57.740 --> 00:23:59.500
至少两个人提议过，

00:23:59.740 --> 00:24:01.480
为什么不试着把它

00:24:01.480 --> 00:24:04.400
直接做进 TypeScript 呢？

00:24:04.680 --> 00:24:06.320
做一个能够

00:24:06.320 --> 00:24:08.280
表达 effect 的 TypeScript 版本。

00:24:08.920 --> 00:24:09.720
因为这样，

00:24:09.720 --> 00:24:11.180
目前的很多变通手段，

00:24:11.180 --> 00:24:12.200
比如现在用的

00:24:12.200 --> 00:24:14.800
Effect.gen，

00:24:14.800 --> 00:24:18.120
以及服务声明，就有可能成为

00:24:18.120 --> 00:24:20.360
语言本身的核心基础能力。

00:24:20.360 --> 00:24:22.280
我很希望有人

00:24:22.280 --> 00:24:23.240
在这方面做些实验，

00:24:23.240 --> 00:24:24.580
看看会走到哪里。

00:24:25.960 --> 00:24:26.580
不过我觉得，

00:24:26.580 --> 00:24:29.100
可移植性确实是一个

00:24:29.100 --> 00:24:30.940
很重要的因素。

00:24:31.120 --> 00:24:32.080
Effect

00:24:32.080 --> 00:24:35.480
会让你大量依赖

00:24:35.480 --> 00:24:37.620
Effect 生态。

00:24:38.020 --> 00:24:38.720
基本上，它会不断蔓延，

00:24:38.860 --> 00:24:39.200
就是这样。

00:24:39.380 --> 00:24:40.380
确实会。

00:24:40.780 --> 00:24:43.400
甚至使用数据库，通常也要经过其他 Effect 库。

00:24:43.660 --> 00:24:43.920
所以，

00:24:43.920 --> 00:24:47.300
你得一并接受这一切。

00:24:47.300 --> 00:24:49.340
如果是在

00:24:49.340 --> 00:24:50.200
构建自己的应用，

00:24:50.200 --> 00:24:51.100
你可能愿意做这个选择。

00:24:51.520 --> 00:24:53.240
但如果你想做的是

00:24:53.240 --> 00:24:54.400
供其他人使用的

00:24:54.400 --> 00:24:55.680
底层框架，

00:24:55.680 --> 00:24:57.780
那就难以说服别人了。

00:24:58.000 --> 00:24:59.400
因为这基本上意味着，

00:24:59.400 --> 00:25:01.980
你把这个选择强加给用户。

00:25:02.940 --> 00:25:03.960
这挺难的。

00:25:04.920 --> 00:25:05.400
再说一次，

00:25:05.640 --> 00:25:08.080
我们绝不是在贬低 Effect。

00:25:08.440 --> 00:25:09.340
我们真的

00:25:09.440 --> 00:25:10.060
非常喜欢它。

00:25:10.180 --> 00:25:11.480
如果你在构建应用，

00:25:11.640 --> 00:25:12.740
你应该考虑使用它，

00:25:13.140 --> 00:25:14.620
尤其是用 TypeScript 构建应用的时候。

00:25:15.940 --> 00:25:16.100
对。

00:25:17.860 --> 00:25:18.260
对。

00:25:18.680 --> 00:25:19.920
这就是我们目前做出来的东西。

00:25:20.020 --> 00:25:21.240
我觉得有一件事，

00:25:21.240 --> 00:25:23.980
我们希望大家去做，

00:25:23.980 --> 00:25:25.980
就是试用一下，尤其是

00:25:25.980 --> 00:25:27.840
看看这个数据模型是否合理。

00:25:29.640 --> 00:25:31.120
我觉得那个 vacation

00:25:31.120 --> 00:25:32.420
示例，

00:25:32.420 --> 00:25:33.660
就在那个代码仓库里，

00:25:33.660 --> 00:25:34.660
是一个不错的起点。

00:25:35.680 --> 00:25:37.880
因为我们想先确认，

00:25:37.880 --> 00:25:41.100
它确实解决了你的问题，

00:25:41.100 --> 00:25:44.540
再彻底投入，并把所有东西

00:25:44.540 --> 00:25:45.760
都改造到它上面。

00:25:46.780 --> 00:25:50.300
你说的“改造所有东西”，是指改造 Pi Coding Agent 吗？

00:25:50.720 --> 00:25:51.060
比如，

00:25:51.180 --> 00:25:52.320
把 Pi Coding Agent 建立在它之上，

00:25:52.320 --> 00:25:54.620
或者再做一些

00:25:54.620 --> 00:25:58.100
大家可能需要的功能，

00:25:58.100 --> 00:26:00.080
因为这些能力

00:26:00.080 --> 00:26:00.900
核心目前还不支持。

00:26:01.740 --> 00:26:02.440
我知道，

00:26:02.440 --> 00:26:03.860
有些已经开始

00:26:03.860 --> 00:26:04.860
试用的人说过，

00:26:04.960 --> 00:26:08.660
也许需要在状态系统之上再加一层抽象。

00:26:09.840 --> 00:26:11.620
无论我们想在它之上

00:26:11.620 --> 00:26:12.380
构建什么，

00:26:12.380 --> 00:26:14.480
哪怕只是为了帮助大家更容易地使用它，

00:26:16.120 --> 00:26:16.600
理想情况下，

00:26:16.700 --> 00:26:18.100
都应该先确认核心的

00:26:18.100 --> 00:26:21.280
数据存储抽象是行得通的。

00:26:22.280 --> 00:26:22.760
所以，

00:26:22.980 --> 00:26:24.500
我很有信心，

00:26:24.500 --> 00:26:26.540
我们现在这种底层 API，

00:26:26.540 --> 00:26:30.560
经过一些增减调整之后，会是很好的基础能力。

00:26:31.100 --> 00:26:33.020
但我也同意，比如说，

00:26:33.560 --> 00:26:35.020
在上面加一点易用的封装，

00:26:35.240 --> 00:26:36.980
尤其是在状态管理方面，

00:26:36.980 --> 00:26:38.280
大概是个好主意。

00:26:38.400 --> 00:26:40.000
不只是对人，对 agent 也一样。

00:26:40.660 --> 00:26:41.720
只是我还不清楚，

00:26:41.720 --> 00:26:45.400
一个好的抽象应该长什么样。

00:26:45.940 --> 00:26:46.520
另外，

00:26:46.740 --> 00:26:48.960
我们也不只是依靠外部开发者使用

00:26:48.960 --> 00:26:50.820
Pi Durable，来判断什么可行、什么不可行。

00:26:51.080 --> 00:26:52.400
我们其实已经决定，

00:26:52.400 --> 00:26:55.400
今后的 Earendil 产品都建立在它之上。

00:26:56.220 --> 00:26:57.220
而在未来，

00:26:57.400 --> 00:26:57.980
也就是

00:26:57.980 --> 00:26:58.640
接下来的几周，

00:26:58.860 --> 00:27:00.360
会有几位团队成员

00:27:00.360 --> 00:27:01.400
真正深入地使用它，

00:27:01.400 --> 00:27:04.080
构建一些我们实际会在

00:27:04.080 --> 00:27:05.280
生产环境里使用的工具。

00:27:05.280 --> 00:27:12.180
这些工具会专注于具体任务。我们会记录整个过程和经验，也会提供

00:27:12.180 --> 00:27:13.540
过程中写出的代码，

00:27:13.540 --> 00:27:17.420
让其他人能够从中获得启发。

00:27:18.660 --> 00:27:23.260
另外，以后这些 Meditations 对谈里，如果出现一些新面孔，也不要意外。

00:27:23.400 --> 00:27:23.700
就这些。

00:27:23.820 --> 00:27:24.240
拜拜！

00:27:24.840 --> 00:27:25.220
拜拜。
