Notebook 与数据科学智能体

9 分钟读完

U23
实战手册 · 编码与计算机操作智能体

Notebook 与数据科学智能体。

在 notebook 里,磁盘上的那个文件并不是产出答案的那个程序——内核才是,而没有任何东西把它记下来。这一个事实就打破了其他每一类编码智能体赖以运行的契约:智能体报告某个单元格干净地执行完了,复核者看到了输出,而两者都无从得知,它之所以能跑通,是因为某个变量定义在一小时前被改掉、如今已不复存在的单元格里。造这个智能体时,要让"它跑通了"由你的外壳执行的一次"重启并全部运行"来裁定,本页其余的一切都由这一道闸门推导而出。

STEP 1

成绩单不是程序。

其他每一类编码智能体作业的文件系统上,产物与运行之物是同一个对象:你复核的那份 diff 就是会执行的代码。notebook 一次性从三个方向打破了这种同一性,而且它们会叠加。

  • 隐藏状态。内核命名空间里存着一些当前没有任何单元格会创建的对象。删掉定义 df 的那个单元格,下游每个单元格照样能跑,直到内核重启为止——而在复核者的机器上,重启恰恰是第一件发生的事。
  • 乱序执行。单元格带着 execution_count,而 [1, 2, 7, 3] 这样的序列是合法的、常见的,且在渲染出的 diff 里完全看不见。存下来的输出记录的是一个执行顺序,而文档本身已不再描述那个顺序。
  • 输出是被提交进去的数据。.ipynb 把结果和源码存在一起,所以一个过期的数字可以在源码被改之后无限期存活,而且看上去和刚跑出来的一模一样。

由此产生的失败很具体,而且不是崩溃。分析在产出它的那台机器上是对的,在别处都是错的,于是它不在被写下来的那一刻失败,而在有人据此行动的那一刻失败。把智能体一直在编辑的 notebook 当作草稿纸,它的一切主张在从冷内核重新执行之前都未经核实——这与可复现性与非确定性面对一次无法重跑的失败运行时所取的姿态相同。

STEP 2

让"重启并全部运行"成为"完成"的唯一定义。

决定自己已经完成的,绝不能是智能体本身。给外壳设一道闸门:一个全新的内核自上而下执行每一个单元格,要么整段跑完,要么任务未完成。它造起来很便宜,是整个系统里价值最高的单个组件,而且它把一句无法证伪的主张变成了一个通过或失败。

  • 在干净的进程里跑,不要用智能体的内核。如果核验和探索共享同一个命名空间,它什么也没核验。用一次独立的执行——无头执行 notebook 在每个主流运行时里都是已解决的问题。
  • 比对输出,而不只是退出码。一个跑得干干净净、但现在产出的数字与文稿里那个不一样的 notebook,才是你真正要抓的失败。把重新执行的输出与提交进去的那份逐一比对,把每一处数值分歧都摆出来。
  • 先钉住输入,再去怪代码。一次打到实时表上的重跑会出于正当理由产生分歧。把查询结果做快照,或按一个固定的 as-of 时间戳去跑,好让"分歧"意味着一个 bug 而不是"今天是周二"。
  • 把这道闸门当作工具交给智能体。让它自己在任务途中随时调用"重启并全部运行",想调多少次都行。一个能检查自己作业的智能体会收敛;一个到提交时才知道结果的智能体只会乱扑腾。这是生成-核验落差最有利的那种形态——这里的核验是真的便宜。
STEP 3

喂给智能体的是命名空间,不是输出。

直觉是把单元格的输出塞进成绩单里,因为那正是人看的东西。可它在两个维度上都是错的:一份渲染出来的 dataframe 体量巨大,却几乎没告诉模型任何可据以行动的信息;而它写下一行真正需要的事实——列名、dtype、空值率、基数、某个键的实际取值范围——既紧凑,又几乎从不出现。

  • 返回一张 schema 卡片,而不是一张表。智能体碰到的每一个 dataframe 都给出:形状、带 dtype 的列名、每列的空值数、低基数列的去重计数,以及数值列与日期列的最小/最大值。这只要几百个令牌,却能挡掉打印五行表头所招来的大部分错误。
  • 在工具边界上按构造截断输出。在执行工具里设一个硬性的令牌上限来截断,而不是叮嘱模型别打印东西。在一张宽表上来一次 df.head(50),就会毒化后续每一步,理由见智能体成本控制
  • 把活的命名空间做成一个工具暴露出去。一个智能体可按需调用的 describe(name),胜过预先把状态一股脑倒进上下文——这是上下文工程里典型的"拉取优于推送"。
  • 把执行顺序摆到台面上。把当前的 execution_count 序列,以及"已定义但当前没有单元格产出"的那批名字,放进智能体的视野。模型没法对它看不见的陈旧状态做推理。
STEP 4

危险的边界是数仓,不是文件系统。

编码智能体的沙箱指引是围绕文件系统与网络写的,原封不动就能搬过来——见沙箱与执行。但数据科学智能体真正的触及范围,是连接串里的那份凭据,而容器边界对它毫无作用。智能体同时身处你的沙箱之内、和你的数仓之内。

  • 发一个只读角色,范围限定在任务点名的那些表上。不要用分析师的角色。这一类里最常见的事故不是一条恶意查询,而是一个智能体往共享 schema 里写了一张表,因为那是缓存中间结果最顺手的办法。
  • 给它一个 scratch schema,并把它设成默认的写入目标。智能体一定会物化中间结果;你能选的只是它落在不落在一个你能整片删掉的地方。
  • 限制查询本身,而不只是会话。默认就要有语句超时、扫描字节上限和行数上限。在列式数仓上做一次无界扫描是一张账单而不是一个报错,而它恰恰是模型在不知道表有多大时会犯的那种错。
  • 把"先采样"做成工作流,而不是一条规矩。让智能体对着采样抽取出的数据开发,最后再对全表跑一次。这既省钱又缩短迭代时延,还把那次昂贵的查询变成一个刻意为之的步骤,而不是被重复四十遍的意外。

"只读"是一句比听上去更小的承诺。一个只读智能体照样能把数据带出去——它会打印结果,而那些结果会落进成绩单,往往还会落进一份共享的 notebook。要按表限定范围,而不只按动词;并且按"这份凭据能触及什么"而不是"沙箱能写什么"去读爆炸半径

STEP 5

评的是那个数字,不是那段代码。

编码智能体的评估问的是测试过没过,而它之所以管用,是因为测试就是规格说明——见评估编码智能体。这里没有测试,交付物是一句关于世界的主张,而"干净的代码算出了错的量"才是主导性的失败。没人的外壳能抓到它,因为它跑得完美无缺。

  • 攒一组已知答案的问题。三十件你早已信得过其数字的真实分析,从零再问一遍。这比一个代码基准费事,也是唯一能量出你真正部署了什么的东西。
  • 把数字与方法分开评分。由错方法得出的对答案是一次恰好落对了的抛硬币,而它在下一个问题上不会再落对。这就是轨迹与结果之分,而在分析场景里,轨迹承载了大部分信号。
  • 让弃答的得分高于自信的错答。"这两张表之间的连接键有歧义,你指的是哪一个?"是比一个数字更好的结果,而你的评分标准必须明说这一点,否则模型永远会掏出那个数字。见不确定性与校准
  • 把语义错误单列成一套分类法来追踪。粒度错了、内连接悄悄丢了行、过滤条件把空值扔掉了、日期边界差了一个时区、分母的定义上个季度改过。这些会反复出现、可被评分,且对任何"只问代码跑没跑通"的检查完全不可见——失败分类法与分诊就是那套机械装置。
STEP 6

把分析搬出 notebook。

notebook 是智能体思考的地方。它并不适合让结果长住其中,而这个过渡恰恰是各团队会跳过的一步——于是一次性的探索就这样变成了三个看板都依赖的数字,却仍由一份"可复现性保证靠习惯"的文档托着。

  • 把抽取做成一项明确任务,而不是锦上添花。当一项分析注定会被重复时,让智能体产出一个参数化的模块外加一个薄薄的调用 notebook。模块能按寻常方式复核和测试;notebook 退回到呈现的角色。
  • 要求一行数据血缘,由生成而来而非由记忆而来。每一个对外发布的数字都点名它的查询、它的 as-of 时间戳,以及产出它的那个提交。智能体能把这件事做得完美,而人从来做不到。
  • 在提交边界上剥掉输出。被提交进去的输出正是过期数字得以存活的方式;第 2 步的那道重新执行闸门才是重新生成它们的东西。给人留一份渲染好的副本,并把它挡在任何被程序读取的路径之外。
  • 按计划重新核验,而不是等人来要。凡是被提拔为周期性的东西,都给它定时跑一次"重启并全部运行",好让上游的 schema 漂移以一个变红的任务的形式浮现,而不是以会议上一个错误的数字的形式浮现——这与第三方工具漂移是同一个论证,只是低了一层。

先把"重启并全部运行"这道闸门造出来,在智能体之前、在工具之前、在提示词之前——把它当作可调用的工具交给智能体,并拒收任何没有在钉住输入的冷内核上通过它的结果。然后做两件各花一个下午的事:在执行工具的边界上用 schema 卡片替掉打印出来的 dataframe;给智能体发一个限定到具名表、带扫描字节上限的只读角色。本页其余一切都是精修;把"可据以行动的 notebook 智能体"和"在别人机器上自信地出错的那个"分开的,就是这三件事。相关阅读:数据与分析智能体看领域框架,代码即动作看为什么单元格就是那次工具调用。