Skip to content

附录 D|Web 只消费会话事件,不依赖循环内部实现 ​

先别把"web 能力家族"当成 Web 界面 ​

第 01 章说"界面是控制脊的一个消费者,从会话事件里渲染"。这里把这句话落到具体机制上。但先拆一个几乎人人会踩的命名陷阱。

这个项目里名字带"web"的能力家族,不是 Web 界面本身。 它是一条独立的能力——网页搜索和抓取(一套跟具体服务商无关的搜索、抓取操作,加上消费它们的模型工具)。真正的浏览器图形界面住在另外三处:一个基于现代前端工具链的浏览器入口,一份浏览器半边的运行时代码(几十个界面组件加连接/运行时逻辑),以及宿主半边(负责代理请求、提供网页服务、托管静态资源)。

这个陷阱不是偶然的。第 06 章说"能力是定义 / 实现者 / 消费者";那个网页能力家族是"让 Agent 能上网"的一条能力,跟"让人能看见 Agent 在干什么"毫无关系。同一个字面意思,指了两种完全不同的东西——一个让 Agent 上网,一个让人看 Agent。


1. Web 界面拿数据只有一条路:会话事件广播 ​

先给判断:浏览器画出来的每一个对话气泡、每一张工具卡片,都是从同一条会话事件广播流投影出来的。没有第二条"界面专用数据通道"。

数据流是单向的:追加一条会话事件是唯一的写入口,提交后会广播出去。宿主端监听这条广播,逐事件计算出一份"给界面看的视图",把结果推进一份消息帧。浏览器端收到帧、把事件折叠组装成对话节点。浏览器里,原始事件和计算出来的视图是平行存放的两个数组——故意不合并,好让"原始事件"数组始终保持成一份未加工的日志切片,呼应第 05 章"模型看见的,等于已经被记下的"这条纪律。

关键证据是一条负面的:具体的循环实现代码,在浏览器端和宿主端代码里完全没有出现过。 浏览器端只依赖会话能力的类型定义(用于表面层投影)、工具能力的呈现类型,以及 Agent 接口的纯类型声明。第 03 章"界面依赖接口,不依赖具体循环实现"这条纪律,在依赖关系图里是真的——不是一个口头约定,是一个可以被静态检查证实或证伪的事实。

如果没有这条单一通道会怎样?Web 界面会维护一份自己的对话记录,注入、压缩、分叉之后和模型对不上。第 05 章的"两份真相"问题,在界面这一侧就变成"两份对话记录"。现在界面是日志的投影,日志对,界面就对。


2. 工具卡片是参数的纯函数,渲染不碰循环 ​

先给判断:工具长什么样,是工具作者在定义里事先写死的意图,不是界面按名字猜出来的。

渲染工具调用和渲染工具结果,是工具定义上的两个方法,返回带渲染标签(通用、终端、差异对比,加上位置信息)的数据,是参数的纯函数。第 07 章已经说过这条纪律;这里补上消费端:视图计算只对工具调用和工具结果这两类会话事件计算视图,渲染调用挂在工具调用记录之后、执行之前,渲染结果挂在耐久的工具结果记录之后。

这一层有两个精确的边界。第一,视图永不进日志:视图数据是宿主端每一帧动态算出来的,明确不会被持久化——会话日志只承载事件本身;同一个事件后一次投递,可以带不同的视图,甚至完全不带视图。第二,有一个字段是"纯推导失败"时的唯一例外:当纯函数算不出无损的结构化数据时(比如读文件的具体行号、网页引用、差异对比的上下文),工具会把这部分内容塞进工具结果事件里一个专门的元数据字段,随事件一起持久化——它是不透明的、没有固定结构的,但仍然要通过基本的格式校验,重放时能还原出同一张卡片。

所以渲染意图诞生在工具作者所在的那个服务端包里,不在浏览器里。换一套界面,不逼你改某个具体工具的执行逻辑;换一套执行器,不逼你改终端卡片的字段。这和第 06 章"模型契约、执行世界、呈现意图三者变化速率不同"是同一把刀。

如果没有纯函数这层会怎样?界面里会长出"如果工具名字是这个,就画成终端样式"这样散落的判断分支,每个新工具都要在浏览器里再登记一次"怎么画"。现在呈现函数和参数格式一起出生,界面只按渲染标签去选择对应的渲染器。


3. 人类记录走追加起源,压缩替换只渲染成一个标记 ​

先给判断:这是第 05 章"人和模型读的不是同一条投影"在界面上的落地,也是全书最值得记住的一个反直觉设计。

模型可见的表面层故意遮蔽被替换掉的那段范围,所以它是错误的人类记录来源——一次已经落地的替换,会抹掉用户已经看过的那些话。追加起源的事件才是人类记录该用的耐久素材,替换出来的副本只属于模型。

所以界面上的对话节点全部按"是否属于追加起源事件"来过滤,压缩产生的那条替换摘要,在人类界面里只会渲染成一个"压缩检查点"标记,而不是把旧对话悄悄删掉。模型读到压缩后的表面层,人读到完整历史,两件事同时成立——因为原始日志根本没被改动过。

如果没有追加起源这层会怎样?用户一压缩,界面上昨天的对话就消失了一半,看起来像"聊天记录被吞了"。而模型确实不该再为那些旧的 token 继续付费。两个消费者、两条投影、同一份日志——这是"一份真相"能同时服务两种不同读者的唯一办法。


4. Web 和无服务器模式是同一份底座上的两片叶子 ​

先给判断:浏览器界面不是"另一套系统",是基础分发层上再叠一层 Web 应用分发。无服务器模式叠的是另一层。

第 09 章已经讲过这套层叠机制。这里补一条增量事实:基础分发层里已经包含了默认循环实现(它是基础层配置里的一行),但浏览器端镶入的只是 Agent 的身份信息(会话编号、状态),从不直接接触 Agent 对象本身。真正跑循环的进程是宿主端,浏览器只是把它的日志投影画出来。

这解释了第 01 章那句"浏览器端文件再多,也只是控制脊的一个消费者"。文件多,是因为界面的组件槽位多,不是因为它在架构里占了一条新的脊柱。


5. 结语:带走一句话 ​

Web 界面是会话事件广播的投影消费者,不是循环的第二种实现——数据只有一条通道(日志广播 → 视图计算 → 消息帧 → 折叠组装),工具卡片是参数的纯函数,人类记录走追加起源;具体循环实现在浏览器端代码里完全不出现,是这条纪律最硬的证据。