← 返回目录

第 3 卷

第 3 / 6 卷日期:

复制代码 1--- ② 塞一个无法无损序列化的值 ---

2被拒:session event "turn/start" carries non-JSON-serializable data 3

4--- ③ 试着改一下已经写下的历史 --- 5Object.isFrozen(events[0])= true

6Object.isFrozen(events[0].data) = true 7events[0].data.turn = 99→TypeError: Cannot assign to read only property 'tu

8events[0].data.turn 仍然是 1

三条硬约束,前两条恰好被这两段输出演出来了:

  1. 追加时校验,坏数据在入口就被拒。

事件数据必须是无损 JSON:BigInt、函数、symbol、undefined、 NaN/Infinity、-0、循环引用、稀疏数组、Map/Set/Date / 类实例……统统不行。为什

么偏偏卡在 append 这一行?三个理由,一个比一个硬:

append 承诺的是已经落盘。官方对后端的要求是 append resolves only after durability——它返回成功,就意味着东⻄已经写下去了。若放一个存不下的值进日志,

就会出现一条内存里算数、磁盘上没有的事件,这个承诺当场就破了。

内存里的日志,恒等于磁盘上能存下的东⻄。官方的说法是:错误事件绝不会进入日志,因此 session.events 始终与后端可持久化的内容一致。于是你眼前那串事件,

就是能原样存下来的那一串,不存在看着在、其实存不下这种事。

还原不靠猜。有人会说:Date 变成字符串再变回 Date,也不是做不到。确实做得到,但那要求读的人知道这个字段原本是 Date。事件类型由插件扩展、还会跨版本演进,

这份哪个字段原本是什么类型的表既没处放、又会漂移。dsh 的选择是不猜:宁可在入口拒掉,也不在重建时猜(这个取向本节还会再遇到一次)。

所以宁可在 append 这一行就炸掉,绝不让存不下的值溜进日志。

事件和它嵌套的数据在通过校验时就被深冻结了,输出里连查两层 isFrozen,连 data 里嵌的对象也没放过。所以那句赋值直接抛 TypeError:turn 还是 1。历史一旦写下就改不

动,这正是唯一真源该有的样子。

  1. 读不懂的事件,默认不许跳过。

这条不在 data 里,而是写在信封上,所以两段输出里也看不见它。

先说清一个动作:日志要落盘,落盘之后还得读回来,把磁盘上那串事件一条条装回内存、重新冻结、重新派生出消息历史,这个动作叫重建(官方叫 load,崩溃之后就是靠它接着

跑)。麻烦出在重建的时候:插件可以往日志里加自己的事件类型,于是读的那一方,常常是个更旧的版本,可能撞上一个它根本没见过的类型。怎么办?

dsh 的答案是默认拒绝。事件可以带一个 ignorable 标记;不带这个标记的事件,读的那一方遇到不认识的类型时必须拒绝重建整个会话,宁可一个字都读不出来,也不许悄悄把那一条

丢掉,因为一条你读不懂的事件,很可能改变后面所有事件的解释方式:少一条,后面的账全跟着错。

这个标记长什么样?它的类型写死成 ignorable?: true,要么不写,要么写 true,没有 false 这一档。这不是偷懒:类型上就不给你写 false 的机会,免得冒出写了 false

却自以为是默认值的糊涂账;想说这条可以丢,只能明明白白写一个 true。官方还补了一道限制:只有纯信息性的记录才配标它,丢了也不影响重建的那种。

于是默认值就是不写 = 必需。为什么定在这一头?因为写事件的人迟早会忘,而官方把这笔账算得很清楚:

忘了标记时后果性质
默认必需(dsh选的这个)过度拒绝:日志读不出来麻烦,但不危险

是不是很眼熟?守卫不许放行、审批无人应答降级为拒绝、沙箱不可用就抛异常,第 06、07 讲你已经见过这个选择的三种样子。它有个正式名字:失败关闭(fail closed),拿不准时,

宁可停下来,也不带着不确定性继续。这一节的第三条硬约束,读不懂就不重建,是它的第四种样子,形状一模一样:认不出这条事件,就停下来、拒绝重建,而不是带着一个被掏空的会

话继续。

另两条严格说不属于它,各有各的名字:

坏数据进不来是尽早失败(fail fast):在 append 这一行就炸,绝不让坏数据拖到落盘或重放时才暴露。它跟失败关闭不冲突,只是角度不同,失败关闭讲往哪边倒(安全侧),尽

早失败讲什么时候炸(越早越好)。

写下就锁死根本不是失败处理,而是让失败无从发生:深冻结之后,改这个动作在类型和运行时两处都不存在,也就无所谓失败不失败。

三条合起来是同一个取向:不猜、不拖、不给悄悄出错的机会。

第一个投影:消息历史

日志这头稳住了,从它算出来的东⻄才可信。上面记下了 8 条事件,可模型并不需要看到全部。为什么?

那 8 条里只有 3 条是对话内容——用户问了什么、模型答了什么、工具返回了什么。剩下 5 条都是记账:一轮从哪开始到哪结束(turn/start、turn/end)、一次模型调用何时起止

(step/start、step/end)、模型要求了什么调用(tool/call)。

它们不是没用,恰恰相反,少了这些边界信息,事后就没法把当时发生的事原样还原。只是还原给谁都行,唯独不必念给模型听。

全站分卷索引 · 6 卷
  1. 1第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷
  5. 5第 5 卷
  6. 6第 6 卷