第 4 卷
nodes = [2,3,5],8 条里只有这三个 seq 浮到了模型看得见的那一层上。浮上去的这三条,官方叫节点(node),nodes 里存的就是它们的 seq:seq 2 是 user/message、seq
3 是 assistant/message、seq 5 是 tool/result,正是刚才表里标是的那三种。
这里有两个容易混的东⻄,先分清楚:surface 是名单——哪几条事件浮上来了、按什么顺序(nodes 里那几个 seq 就是它,靠每条事件的 surfaceOp 维护);消息历史是照这份名单
算出来的投影。名单归日志管,投影归算法管,不是一回事。
旁边的 replaceGeneration 先记着,讲替换时会用到。
框架把这件事钉成了类型:
复制代码 1type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result'
先分清两个词:
事件:日志里的一条记录。账上实实在在的一笔,有 seq、进日志即冻结、能落盘。
消息:发给模型的那一句。它不是日志里的记录:日志里只有事件,没有消息;消息是 deriveMessages() 从事件算出来的结果。
8 条事件,最后只算出 3 条消息。
只有这三种事件会变成消息,不过还有个附加条件:事件里得真的有内容。
(用的哪个模型);但内容为空,它就算不出消息。
还有一种类型容易被误会:assistant/chunk,它在表格里标的是否,可它明明是模型说的话。为什么不进?因为它是流式输出的原始分片,一个分片一条;模型要看的是组装好的那一
条 assistant/message(官方原话:derived history uses this)。分片留在日志里,为的是 token 级的回放保真。
光知道哪些类型能进还不够,每条事件追加时还得声明自己进来时的动作:
复制代码 1type SurfaceOp = 'append' | { op: 'replace'; start: number; end: number }
'append' 是加到尾巴,常规路径。replace 是把某一段替换掉,那是压缩(compaction)把一大段历史换成一段摘要时才用的——压缩本身不在本讲范围。配合它的
replaceGeneration 每发生一次替换就 +1,好让消费方分清只是尾巴长了和历史被重写了。
这里藏着整套设计最漂亮的一处:想改掉已经写下的历史(比如把前面几轮收成一段摘要),你不能回头去涂改那几条事件,只能在后面再追加一条替换指令。账本本身一字未动,动的是
新追加的那条指令;至于看起来被改了,是读的时候照着指令算出来的。
于是两个本来打架的诉求,被同一套机制同时满足了:底下那本账永远不变,上头看起来又能改,压缩、裁剪照做不误。
再看挑完之后的历史长什么样。第 ⑤ 段就是它:
复制代码 1--- ⑤ deriveMessages():从日志算出的消息历史 ---
输出里第三行是工具结果,角色却写着 user。这不矛盾,API 就是这么规定的:工具结果必须以 user 角色、装进一个 tool-result 块里回给模型。日志的事件类型和消息的角色是
两套分类,各管各的。
最后,谁来算。是 deriveMessages()。它做的事说白了就两步:一是过滤(只认浮到 surface 上的那几个节点);二是加映射(每个节点照规则换成一条消息,一对一地换,不是
多条并一条;内容为空的换不出来,就跳过。官方管这一步叫 fold)。合起来就是把整本账压成模型实际能看到的那份会话记录。这就是本讲最基础的一个投影。
注意,它是算出来的,不是存出来的:消息历史不在任何地方落库。deriveMessages() 给你的,就是日志里那几条事件的 data 本身,同一块内存,只是换了个角度看。所以准确的
说法是随时能从日志重建,而不是每次从头把整本账重算一遍。
至于开销,它带缓存,不必每次从头算:每个节点只在首次出现时算一次,之后再调用只算新增的那几个;只有 surface 被整体重写(replace)时才整份作废重算,靠的就是前面那个
replaceGeneration。缓存怎么存、什么时候扔、为什么它不作数,第五节一并讲。
于是改历史这条路被堵得死死的,而且只堵了一处:投影和日志是同一块内存,那块内存进日志时就冻上了。日志改不动,投影自然也改不动,本来就是一件事。
投影单元(Projection unit):三个纯函数加一点声明
消息历史只是一个投影——官方内置的那一个。这套机制对谁都开放:任何插件都能注册自己的投影,会话标题、待办清单、跑了多少次工具……都能这么算。这些投影互不干扰,也都不
改账本:deriveMessages() 和你注册的单元读的是同一本账,各算各的,谁也改不了账上任何一个字。
这套机制里,你想算的每样东⻄都配一个投影单元:一个普通小对象,装着三个函数,外加一