Skip to content

RFC: 跨 Session 通信链路工程鲁棒性评审与改进方案 #35

@Difficult-Burger

Description

@Difficult-Burger

Stello 工程鲁棒性评审

2026-03-31 · 基于 v0.2.x 架构的系统性评审

聚焦跨 Session 通信链路的工程缺陷与改进方案。


背景

Stello 的核心价值链路:

L3(原始对话)──ConsolidateFn──► L2(技能描述)──IntegrateFn──► synthesis + insights

这条链路在 happy path 上设计优雅 — 技能隐喻、信息隔离、低 token 成本。但在 unhappy path 上缺乏防御,存在六个结构性工程问题。


问题一:L2 单向有损压缩,无反馈通道

现状

L3(数百条消息)经 ConsolidateFn 压缩为 L2(100-150 字),再经 IntegrateFn 二次压缩为 synthesis + insights。两次有损压缩串联,信息损耗是乘法关系。

具体风险

  • 无 query-back 机制:MainSession 无法向子 Session 追问 "关于 X 具体是什么情况"。只能等下一次 consolidation。
  • 质量不可观测:没有任何机制检测 L2 是否遗漏了关键信息。ConsolidateFn 的质量完全依赖 prompt engineering。
  • 不可逆:L2 生成后,原始 L3 对跨 Session 通信链路永久失效。ConsolidateFn 遗漏的关键决策,在 integration 视角里不存在。

改进方向

引入 Event Sourcing,用事件流替代快照式 L2。详见「统一解决方案」一节。


问题二:insights 替换策略导致记忆断崖

现状

设计决策(CLAUDE.md #3):

insights 替换策略(不追加)— 每次 integration 给出最新完整判断

子 Session 的上下文组装:system prompt + insights + L3 + 新消息。其中 insights 槽位每次 integration 被整体覆盖

具体风险

第一次 integration 推送的 insight:
  "英国方向发现学生看重就业,建议补充 OPT/H1B 数据"
  "学生预算上限 50 万 RMB,注意筛选学费"

第二次 integration 推送的 insight:
  "英国方向学生改为考虑 Oxford,竞争更激烈"
  (没有再提预算信息)

→ "预算 50 万"从子 Session 上下文中消失

注意:L3(对话历史)不受影响,断崖发生在 insights 这个跨 Session 通信信道上。insights 是一个无记忆的寄存器,每次写入清空前值。这会导致跨 Session 认知随时间静默退化。

改进方向

insights 从替换式改为 append-only + cursor-based consumption。详见「统一解决方案」一节。


问题三:时间盲区 — consolidation 间隔内不可见

现状

L2 仅在 consolidation 时生成(如 everyNTurns: 3)。CLAUDE.md 明确承认:

正在进行中的 Session 没有 L2,对 Main Session 暂时不可见 — 这是有意为之的取舍

具体风险

Turn 1  Turn 2  Turn 3  ← consolidate  Turn 4  Turn 5  Turn 6  ← consolidate
├────────────────┤                      ├────────────────┤
 盲区:MainSession 对                    盲区:用户在 Turn 4
 该 Session 进展一无所知                  说了"不考虑美国了"
                                         但要等 Turn 6 才能传播

关键决策在盲区内做出时,其他 Session 可能基于过时信息持续工作。

改进方向

事件在发生时即进入 log,不等 consolidation 周期。详见「统一解决方案」一节。


问题四:fire-and-forget 静默失败

现状

设计决策(CLAUDE.md #5, #6):

consolidate/integrate 均 fire-and-forget — 不阻塞对话
错误处理:emit error,不中断对话周期

具体风险

  • consolidation 失败 → L2 不更新 → integration 基于过时 L2 决策 → 无人感知
  • integration 失败 → insights 不推送 → 子 Session 用旧 insight → 无人感知
  • 多次失败静默累积,系统表面正常,跨 Session 认知严重滞后
  • 缺少最终一致性保证:没有重试、没有版本号、没有 staleness 检测

改进方向

append-only event log 提供持久化 + 可重放保证。消费失败不丢事件,下次从 cursor 继续。详见「统一解决方案」一节。


问题五:MainSession 单点中枢,无法 scale

现状

所有跨 Session 通信必须经过 MainSession:

Session A ──L2──► MainSession ──insight──► Session B
                 (单点瓶颈)

具体风险

  • O(N) integration 成本getAllSessionL2s() 每次全量收集所有 Session 的 L2,无增量机制
  • 无 freshness 标记:integration 读到的 L2 可能是 3 轮前的和刚生成的混在一起,IntegrateFn 不知道哪个是新的
  • synthesis 污染无自愈:synthesis 一旦基于错误 L2 生成,后续 integration 全部基于错误基线,无自我修复机制

改进方向

event log 上的 cursor 天然提供 freshness 感知和增量消费。详见「统一解决方案」一节。


问题六:配对函数无契约校验

现状

具体风险

两个函数由开发者分别编写。如果 ConsolidateFn 输出 markdown 而 IntegrateFn 期望 JSON,不会报错 — 只会 integration 质量静默退化。没有 schema 校验、没有 type check、没有运行时断言。

改进方向

引入 EventEnvelope 后,信封层有结构校验(sessionId、sequence、timestamp 必填)。content 字段内部仍然格式无感知,但至少信封层有保底断言。可选地,允许开发者注册 content schema 做运行时校验。


统一解决方案:EventEnvelope + Append-Only Log

核心思路

Event Sourcing 替代当前的快照式 L2 + 替换式 insights。框架只读信封 metadata,不解析 content — 保持格式无感知的设计哲学。

EventEnvelope 定义

interface EventEnvelope {
  /** 产出该事件的 Session */
  sessionId: string
  /** 单调递增序号,用于 cursor 追踪 */
  sequence: number
  /** 事件产生时间 */
  timestamp: string
  /** 框架不解析的内容 — ConsolidateFn/IntegrateFn 自行定义格式 */
  content: string
}

类比:Kafka message — broker 只管 topic/partition/offset,payload 是 opaque bytes。

数据流对比

当前模型(快照式):

Session A ──consolidate──► L2 快照
Session B ──consolidate──► L2 快照
                               │
                     integration 全量读所有 L2
                               │
                     ┌─────────┴─────────┐
                     ▼                   ▼
              synthesis(覆盖)    insights(覆盖)

改进模型(事件式):

Session A ──emit──► Event(seq=1, "学生确认预算50万")
Session A ──emit──► Event(seq=2, "倾向 CMU 而非 Stanford")
Session B ──emit──► Event(seq=1, "学生改考虑 Oxford")
                          │
                          ▼
                   Ordered Event Log(append-only, 持久化)
                          │
                          ▼
            MainSession 维护 cursor("已消费到全局 seq #5")
            各子 Session 维护 cursor("已消费到 insight seq #3")
                          │
                  ┌───────┴───────┐
                  ▼               ▼
           synthesis(增量更新) insights(增量追加)

问题 → 解决映射

问题 当前状态 Event Sourcing 如何解决
L2 有损压缩 不可逆快照 事件是细粒度增量,信息密度更高;历史事件可回溯
insights 记忆断崖 全量替换 append-only,cursor 只前进不回退,历史 insight 不丢失
时间盲区 等 consolidation 周期 事件在发生时即入 log,不等批处理
静默失败 fire-and-forget,无重试 事件持久化,消费失败可从 cursor 重放,保证最终一致
全量收集 O(N) getAllSessionL2s() 无增量 cursor-based 增量消费,只处理新事件
配对函数无校验 框架不感知 L2 格式 信封层有结构校验;content 仍格式无感知

与当前设计的兼容性

  • 格式无感知:保留。框架只读 EventEnvelope 的 metadata 字段,content 原样透传。
  • ConsolidateFn / IntegrateFn:仍为应用层配对函数。ConsolidateFn 改为产出 EventEnvelope[](而非单个 L2 字符串),IntegrateFn 改为消费 EventEnvelope[](而非 L2 字符串数组)。
  • fire-and-forget 语义:保留。consolidation/integration 仍不阻塞对话,但失败后事件不丢,下次可续消费。
  • MainSession 中心角色:保留。MainSession 仍是 integration 的唯一执行者,但从全量读改为增量消费。

需要新增的存储接口

interface EventLog {
  /** 追加事件 */
  append(event: EventEnvelope): Promise<void>
  /** 从指定 cursor 开始读取事件 */
  readFrom(cursor: number, limit?: number): Promise<EventEnvelope[]>
  /** 读取当前最大 sequence */
  getLatestSequence(): Promise<number>
}

interface CursorStore {
  /** 读取某个消费者的 cursor */
  getCursor(consumerId: string): Promise<number>
  /** 更新某个消费者的 cursor */
  setCursor(consumerId: string, sequence: number): Promise<void>
}

迁移策略

建议分阶段推进,不做一次性重写:

  1. Phase 1 — 加信封:现有 L2 包装进 EventEnvelope,consolidation 产出带 sequence 的事件。insights 仍替换式,但信封提供 freshness 标记。
  2. Phase 2 — 加 Event Log:引入 append-only log 和 cursor。integration 改为增量消费。
  3. Phase 3 — insights 改追加:insights 从替换式改为 append-only + cursor-based。子 Session 上下文组装改为读取 cursor 之后的增量 insights。

不在本文档范围内的问题

以下问题已识别但未深入讨论,留作后续评审:

  • L3 本身的上下文窗口管理(trimRecords 策略)
  • Session 数量增长时 EventLog 的存储和 GC 策略
  • 多个 integration 并发执行时的 cursor 竞争
  • event schema versioning(content 格式演进时的向后兼容)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions