第二幕 一个外部世界,只有一条时间线
第二幕 一个外部世界,只有一条时间线
平行世界悖论
mistake-agent 的会话树是我亲手设计的机制:消息是一棵树,编辑旧消息长出分支,模型上下文只走活跃路径,< / > 翻看平行版本。那个系列的某一幕还专门讲过它的演进史。
把它搬进 iot-agent 的那天,它就死了。
问题出在"平行版本"这四个字。树里的每条分支都是一个完整世界:A 分支里风扇被关掉,B 分支里风扇还开着。模型顺着 B 分支读历史,读到"风扇开着",抬头一看真实世界,风扇是关的。模型会困惑,更糟的是它会照着假历史继续决策。对话可以分叉,外部世界不分叉。一个外部世界,只对应一条时间线。
同一天的两次翻转
事情是这么发生的。上午,把 mistake-agent 形态的会话管理落地:单棵会话树、树内分叉、决策器判定 start_new、< / > 导航、编辑派生分支,全套。真实模型跑通了,摘要节点、分叉、十九条消息的树,全对。
下午,越看越不对。悖论在脑子里成型之后,剩下的只是动手砍。当天砍掉:
- 会话切换全家:决策器、空闲超时、session::switch、Goal、树分叉;
- 版本导航:
</>、编辑、重新生成; - 用户可见的会话概念:会话列表、新建会话按钮。
会话只剩一条 append-only 线性链,模型上下文只增。收缩机制只留一种:上下文压缩。
编辑即伪造
砍掉"编辑"和"重新生成"的理由值得单独说。在 mistake-agent 里编辑无害:世界是对话,改一句话,重新生成一个回答,天塌不下来。
iot-agent 不一样。时间线上记录的是真实执行过的事件:工具调用已经对真实设备生效,风扇真的关了。编辑这条记录,等于在审计日志上撒谎。所以纠错的唯一方式是发新消息:"刚才那一下关错了,重新开回来",构成新的真实事件。设备命令是不可逆的副作用,能做的只有补偿,没有撤销。
压缩 = 遮蔽
时间线只增,模型上下文装不下,怎么收?
回合末,上下文估算量过阈值时,把前缀交给主模型压成一个摘要节点,尾部保留最近一段,摘要成为模型上下文的新起点。手动也可以:context::compact 工具,模型和用户都能发起,链太短时 no-op。
有一条铁律跟着压缩:历史不可因压缩而消失。被遮蔽的前缀完整保留在 JSONL 事件日志里,前端时间线照常渲染全部历史气泡,压缩点只是一个可展开的分割标记。压缩收缩的是模型上下文,不是时间线。
一个上游 bug
这套机制一落地就撞上一个上游 bug,现象很诡异:说"风扇关掉",执行的却是上一轮的"灯开到最亮"。
查下来是活跃路径推进的问题:用户消息追加进会话之后,活跃路径没有推进到这条新消息,模型上下文里没有自己刚刚听到的话。它按上一条指令行事,看起来像失忆,其实是没收到。修复提交上游后,每回合模型都能看到自己的用户消息了。
这个 bug 有种象征意味:在这条只增的时间线上,每一条消息都必须是真实发生过的事,包括模型自己听到的那一句。
收个尾
mistake-agent 的第二幕讲的是"控制消息和对话内容必须分开"。这一幕是它的续集:当对话的外面还有一个真实世界,连"对话本身"都要重新定义。
mistake-agent 的会话树没有错,它只是为一个可以随便改写的世界设计的。iot-agent 的世界不可以。机制没有对错,只有水土。
下一幕讲模型怎么在这条时间线上主动开口。
