给编程智能体一个 Python 模块,它是在坚实的基础上运行的:磁盘上的文件就是程序。阅读它,编辑它,运行它,观察结果 —— 闭环达成。给同一个智能体一个 Jupyter notebook,所有这些假设都会悄然瓦解。智能体充满信心地编辑第 12 个单元格,却不知道你一小时前曾用不同的数据重新运行过第 3 个单元格,也不知道一个在早已删除的单元格中定义的变量在内核中仍然处于活跃状态,更不知道它刚刚在第 7 个单元格下读取的输出是三次内核重启前由一段已不存在的代码生成的。
Notebook 是一个披着文件外衣的 REPL。磁盘上的 .ipynb 看起来像源代码,但真正决定行为的东西 —— 内核累积的内存 —— 是不可见的、未序列化的,并且是由产生它的那一连串人工点击所塑造的。智能体是基于“代码决定行为”这一契约训练出来的。Notebook 废除了这一契约,而大多数智能体框架甚至对此一无所知。
文件并非程序
在正常的代码库中,从源码到行为的映射是一个函数:相同的文件,相同的输入,相同的行为。智能体所擅长的一切 —— 通过阅读代码预测其功能、通过编辑代码改变其功能、通过运行测试验证更改 —— 都依赖于这种映射。
Notebook 在三个方面打破了这种映射:
- 乱序执行。 单元格按照人类点击的顺序运行。执行计数器(
[3]、[17]、[5])记录了顺序,但没有任何机制强制要求自上而下的顺序,且计数器在重启时会重置。一项对 GitHub 上超过 860,000 次 Notebook 执行的大规模研究发现,只有约 24% 的 Notebook 能从头到尾无错运行,仅有 4% 能重现其存储的输出。大约 36% 的 Notebook 显示出乱序执行的直接证据。
- 已删除的代码,残留的状态。 删除一个定义了
df_clean 的单元格,该变量仍会保留在内核中。下游的单元格会继续工作 —— 直到下次重启,它们才会崩溃。屏幕上的代码现在是一个谎言:它引用了一个在文件中任何地方都不存在的定义。
- 陈旧的输出。 单元格输出是单元格上次运行时的快照,而不是当前代码的反映。编辑一个单元格而不重新运行它,其下方保存的输出描述的仍是程序的旧版本。
对于人类来说,这些是靠习惯和记忆来应对的常见烦恼:“我知道我重新运行了加载单元格,我知道那个绘图已经过时了。”人类在大脑中构建了一个文件并未记录的内核状态模型。智能体无法访问那个心理模型。它只有文件 —— 而文件是现场最不可信的产物。
为什么智能体在这里的表现比人类更糟
人们很容易认为 Notebook 对每个人来说都一样糟糕。其实不然。隐藏状态对智能体的惩罚是不成比例的,这源于智能体的工作方式。
首先,智能体将输出视为唯一真相。一个正在调试数据管道的智能体读取单元格存储的输出 —— DataFrame 预览、错误回溯、打印的 shape —— 并据此进行推理。如果该输出是陈旧的,智能体现在就是在根据一个伪造的观测进行推理。人类会根据上下文(“那是午饭前跑的”)排除陈旧输出;智能体没有时间线可以参考。一个误导性的观测通常比没有观测更糟糕,因为它会主动引导智能体的假设走向错误的方向。
其次,智能体通过重新运行来验证,而重新运行在 Notebook 中恰恰是不安全的。标准的智能体循环 —— 编辑、执行、检查 —— 假设执行是廉价且幂等的。在 Notebook 中,重新运行一个单元格可能会重复应用一个变换(两次执行 df = df.dropna() 没问题;但两次执行 df["price"] = df["price"] * 1.1 就是一个隐蔽的 bug)、重新触发 API 调用,或者破坏后续单元格依赖的状态。智能体的核心验证动作是对一个它无法检查的环境进行具有副作用的变更。
第三,JSON 格式不利于工具处理。.ipynb 文件是一个包含嵌入源码字符串、Base64 图像和元数据的 JSON 文档。将文件作为文本编辑的智能体要么会搞坏 JSON,要么会生成人类无法审阅的 diff。各种框架已经开发了针对 Notebook 的特定工具和 MCP 服务端,暴露了单元格层级的读取/编辑/执行操作,这些确实有帮助 —— 但它们解决的是格式问题,而不是状态问题。即便拥有完美的单元格编辑工具,智能体仍然无法获知内核中的内容。
结果就是,Notebook 本应服务的核心工作流反而成了陷阱。数据科学是人们最希望获得智能体帮助的领域 —— 探索性分析、特征工程、绘图迭代 —— 而这恰恰是其媒介最能削弱智能体能力的领域。
“重启并运行所有”是智能体唯一能验证的契约
有一种操作可以消除所有隐藏状态:重启内核并从头运行每个单元格。在一次干净的从头到尾运行之后,文件和程序在那一刻是等同的。每个输出都是新鲜的,每个变量都能溯源到可见代码,每个执行计数器都是顺序的。
这使得“重启并运行所有成功”成为智能体实际上唯一能检查的 Notebook 属性。其他一切 —— “Notebook 当前可以运行”、“输出是正确的”、“我的编辑是安全的” —— 都是关于智能体无法观测的内核状态的断言。
这为智能体驱动的 Notebook 工作提出了一种纪律,类似于我们对待数据库而不是源文件的方式:
- 将内核视为不可信的缓存。 在开始一个会进行实质性编辑的智能体会话之前,重启并运行所有。成本是一次完整的执行;收益是智能体的观测变得值得信赖。
- 通过完整运行而非单个单元格运行来验证。 在智能体编辑之后,验收测试应该是从头到尾的干净执行,最好是无头模式。这正是 papermill 和 nbval 等工具被创建的原因:线性执行 Notebook,在报错或输出不匹配时报错。fast.ai 的 nbdev 工作流在几年前就将其定为规范 —— 每个 Notebook 在 CI 中都必须干净地运行,否则构建失败。
- 使耗时的单元格对重启友好,而不是逃避重启。 通常的反对意见是第 2 个单元格需要运行 20 分钟,所以没人愿意重启。解决办法是在耗时边界处缓存到磁盘 —— 持久化获取的数据集、对训练过程进行记忆化(memoize)—— 这样完整运行就足够廉价,可以成为常规操作。Notebook 积累隐藏状态的程度与重启成本成正比。
如果一个 Notebook 无法在“重启并运行所有”中存活,它就还不算是一个程序;它只是某次特定会话的记录。智能体应当拒绝将记录视为代码 —— 而框架开发者应当将这种拒绝编码为默认门控,而不是指望每次智能体运行时都能记得这一点。
将 Cell 视为纯函数:Agent 安全风格
更深层次的修复在于风格。Agent 处理得好的 notebook 看起来不像草稿本,更像手工编写的数据流图。
这种风格有几条规则,人类也能从中受益,但对于 Agent 来说则是必须遵守的:
- 每个 cell 读取具名输入并产生具名输出。 没有任何 cell 会修改在别处定义的变量。使用
df_features = add_features(df_clean),而不是将 df["feature"] = ... 散落在五个 cell 中。状态修改(Mutation)是导致重新执行对顺序敏感的原因;禁止它,cell 就能以任何遵循数据依赖关系的顺序重新运行。
- 没有任何 cell 依赖于被运行两次,或者恰好一次。 幂等性将 Agent 的“编辑-运行-检查”循环从一场赌博变成了验证步骤。
- 定义留在模块中,编排留在 cell 中。 一旦函数稳定下来,就将其移动到 notebook 导入的
.py 文件中。Agent 就可以编辑带有真实 diff、真实 lint 和真实测试的源代码,而 notebook 则缩减为一个薄薄的驱动层——即隐藏状态造成的危害最小的一层。
这正是响应式 notebook(reactive notebooks)在机械层面强制执行的设计。marimo 解析每个 cell 的输入和输出,构建依赖关系的有向无环图(DAG),禁止在两个 cell 中重新定义同一个变量,并在发生更改时重新运行(或标记为过期)每个下游 cell。删除一个 cell,它的变量也随之消失。Notebook 以纯 Python 形式存储,因此 Agent 可以像编辑普通源码一样编辑它们。
响应式 notebook 将自己定位为 AI 原生选项绝非偶然:确定性的执行语义正是 Agent 预测编辑效果所需的。Jupyter 在 2011 年选择了最大的交互自由度,当时唯一的系统用户是拥有心理模型的人类。当系统的一半编辑来自于一个对会话毫无记忆的模型时,这种权衡就完全不同了。
你不需要为了获得大部分收益而进行迁移。以 仿佛 强制执行了依赖图的方式编写 Jupyter cell——纯 cell、具名交接、无跨 cell 状态修改——能让你获得一种 Agent 和未来的你都能正确执行的近似体验。
Notebook 工具欠 Agent 一个交代
人类容忍了十五年的隐藏状态,是因为人类自带会话记忆。Agent 不会,这使得现在正是构建 notebook 始终需要的“可供性”(affordances)的时机:
- 状态清单(A state manifest)。 一个机器可读的答案,用于回答“当前内核中有什么”:每个存活的变量、它的类型,以及——关键点——是哪个版本的 cell 产生了它,或者其定义代码是否已被编辑或删除。目前 Agent 通过在内核中执行自省代码来推断这一点;它应该成为任何 harness 在信任输出前都可以查询的一等公民 API。
- 执行顺序 Lint。
.ipynb 文件已经存储了执行计数器。一个标记非单调计数器、从未执行的 cell 以及比代码版本更旧的输出的 linter,将为 Agent(以及 CI 和审阅者)提供廉价的过期信号。乱序执行在今天是可以检测到的;我们只是没有将其展示在工具可以处理的地方。
- 可复现性作为可检查属性。 “重启并运行所有通过”应该是 notebook 携带的一种勋章——在 CI 中断言,在元数据中标记,一旦任何 cell 在没有重新完整运行的情况下被编辑,该标记即失效。打开 notebook 的 Agent 随后可以进行分支处理:经过验证可复现的 notebook 接受正常的代码编辑处理;未经验证的则接受隔离处理——在相信任何内容之前,从头开始运行所有内容。
- 无需响应式的过期传播。 即使 Jupyter 永远不自动重新运行 cell,它也可以标记它们:此 cell 的上游已更改,其输出不可信。响应式系统证明了依赖关系分析是可行的;保守的版本只是在 cell 上显示一个警告标志。
这些都不需要放弃 Jupyter 的执行模型。它们只需要承认,一类新的用户——通过阅读元数据而非记忆会话来理解环境的用户——现在正在编辑世界上的一半 notebook。
执行记录还是程序
令人不适的总结:带有隐藏状态的 notebook 不是源代码,而 Agent 迫使我们停止这种伪装。人类用记忆和迷信弥补了这个差距。Agent 缺乏这两者,它们的失败声势浩大,以至于这个差距最终必须被弥合——通过纪律(将“重启并运行所有”作为准入关口)、风格(纯 cell、模块化定义)或工具(响应式执行、状态清单、过期 lint)。
如果你今天要将 Agent 接入数据科学工作流,务实的做法是为每个 notebook 决定它属于哪一边。探索性的草稿本没问题——别让 Agent 碰它们,或者只让 Agent 在全新的内核中运行它们。任何承重(核心逻辑)的部分都要像程序一样对待:自上而下可复现、纯 cell、逻辑提取到模块、在 CI 中验证。在 Agent 时代蓬勃发展的 notebook 将是那些不再是执行记录(transcripts)而变成程序(programs)的 notebook——而最终胜出的工具将是那些让这种转变成为默认项而非靠纪律约束的工具。
会员专享
余下内容仅对会员开放。
会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
- —完整文章,包含未公开存档的部分
- —可落地的工作框架,附带权衡与决策依据
- —新文章抢先看,先于公开发布
随时取消 · 一次订阅,畅读全部