深入解析“小师傅”:Flutter 智能助手模块架构与实践


一、 整体架构设计:分层与解耦

为了应对 SSE 消息类型的多样性(如普通文本、建议卡片、评分卡片、约工列表等)以及流式传输的不确定性,我们采用了分层解耦的架构设计。

整体架构自下而上分为四层:

1. 网络传输层 (Transport Layer)

  • 职责:负责与服务端的 SSE 长连接维护。

  • 核心组件

    LittleMasterHttpClient

  • 特点:摒弃了第三方库,自研实现了完整的 SSE 协议栈,支持自动重连、断点续传和流式解析。

2. 解析分发层 (Parser & Registry Layer)

  • 职责:将原始的 JSON 数据转换为类型安全的业务实体对象。

  • 核心组件

    SseJsonParserRegistry(统一注册中心)

  • 设计:采用注册表模式,维护了一个 <EventType, MessageType> -> Parser 的映射关系。它像一个交通指挥官,根据消息特征自动将数据分发给特定的解析器。

3. 业务服务层 (Service Layer)

  • 职责:管理会话状态、处理业务逻辑交互。

  • 核心组件

    SseService

  • 工作:负责会话初始化、停止生成、发送评价以及处理卡片内的按钮点击事件(Action Handling)。

4. UI 展示层 (Presentation Layer)

  • 职责:流式渲染消息流。

  • 核心组件

    SseCardWidget

  • 特点:统一的卡片容器,根据解析出的数据模型动态渲染 Markdown 文本或交互按钮。


二、 核心机制拆解

2.1 统一 JSON 解析注册系统 (The Brain)

SSE 的最大挑战在于消息类型繁多且格式多变。我们设计了 

SseJsonParserRegistry 来解决这个问题。

  • 特征检测 (Feature Detection): 不仅依赖 type 字段,还引入了“检测器函数”机制。例如,通过检测是否包含 suggestion 字段来判定为“猜你想问”卡片,通过检测是否有 reasonId 来区分“咨询场景”和“约工场景”的推理消息。

  • 动态分发

    Input JSON -> 自动类型推断 -> 查找 Parser 映射表 -> 执行解析 -> Output Entity

    这种设计使得每新增一种卡片类型,只需注册一个新的 Parser,无需修改核心分发逻辑,极大地提高了可扩展性。

2.2 健壮的网络重连机制

我们重写了底层网络客户端,实现了工业级的重连策略:

  • 指数退避 (Exponential Backoff):重试间隔按指数级增长(3s, 6s, 12s…),避免服务端抖动时发生惊群效应。
  • 随机抖动 (Jitter):在等待时间中加入随机因子,打散客户端的重连请求。
  • 断点续传 (Last-Event-ID):自动记录最后收到的 Event ID,重连时带上 Last-Event-ID 请求头,确保消息不丢失、不重复。

2.3 完备的兜底策略 (Fallback)

为了防止服务端下发异常数据导致 App 崩溃,我们在解析层做了非常“厚”的防御:

  • 统一 Try-Catch:所有解析器的执行都被包裹在 try-catch 中。

  • 降级渲染:一旦解析失败,自动降级调用 

    _createFallbackData,尝试提取原始文本进行展示,确保用户至少能看到文本内容,而不是报错红屏。

2.4 连接状态监控与断开机制 (Disconnection Detection)

既然 SSE 是基于 HTTP 长连接的,如何精准捕获连接断开呢?

  • 监听 Stream 结束:我们并不是监听 SSE 协议层面的指令(SSE 协议本身没有“断开”帧),而是**直接监听 Dart HttpClientResponse 数据流 (Stream) 的 onDone 和 

    onError 回调**。

    • onDone: 服务端正常关闭(发送 TCP FIN)、网络超时或 Keep-Alive 到期时触发。
    • onError: 网络异常中断(如断网)时触发。
  • 自动重连触发:一旦捕获到上述回调,且当前处于非正常关闭状态,客户端会立即启动指数退避流程尝试重连。


三、 亮点与难点

3.1 难点:流式数据的极致体验 (Stream Consistency & Optimization)

SSE 返回的是一个个操作指令(Delta),而非完整数据。为了实现“如丝般顺滑”的打字机效果,我们在端侧实现了复杂的积木式拼装逻辑。

  • 数据增量合并: 

    SseConversationManager 内部维护了有序的 LinkedHashMap。当收到新片段时,我们采用 Copy-On-Write 策略,基于旧对象创建新 Turn 对象并追加 fragments

    为什么不直接修改原对象?

    1. 驱动 UI 刷新:Flutter 的 Selector 依赖引用比对 (old != new) 来判定是否重绘。如果直接修改原对象,引用地址不变,UI 不会刷新。
    2. 线程安全:在渲染线程遍历数据时,网络线程如果同时修改集合,会导致并发读写异常。创建新对象保证了 UI 始终渲染的是一个不可变的快照。
  • 内存优化 (Memory Efficiency): 为了防止长会话导致 OOM(内存溢出),我们实施了组合策略:

    1. 窗口化管理:默认仅保留活跃会话数据,历史消息按需分页加载。

    2. 享元模式:利用 Dart 字符串池机制,高频重复的 key 或状态字段共享内存引用。

    3. 生命周期清理:页面销毁时调用 

      clearAll() 彻底释放引用链,配合 Flutter Engine 的高效 GC 机制回收内存。

  • UI 性能调优: 面对每秒数十次的 SSE 推送,直接刷新 UI 会导致严重卡顿。

    1. 版本号控制 (Versioning):引入 dataVersion 整数。Selector 仅监听版本号变化,过滤掉无效的重绘请求。
    2. 局部刷新:Flutter 的 Widget Diff 算法配合 const 构造函数,确保只有“正在生成中”的那一行气泡会被重绘。

    代码实证:在 LittleMasterChatConfig.dart 中,我们使用 Selector2 精准监听 version 变化,而非监听整个 Provider:

    Selector2<LittleMasterConversationProvider, LittleMasterChatConfig, _MessageAreaWithConfigState>(
    
      selector: (context, provider, config) => _MessageAreaWithConfigState(
    
        conversations: provider.conversations,
    
        version: provider.conversationsVersion, // 关键:仅当版本号变化时才重建
    
        isInHistoryMode: provider.isInHistoryMode,
    
      ),
    
      shouldRebuild: (prev, next) => prev.version != next.version || ...
    
      builder: ...
    
    )
    

3.2 亮点:混合卡片的能够力

业务中经常出现“一段文本 + 一组按钮 + 一个推荐列表”的组合消息。

  • 实现:设计了 

    SseCardWidget 作为原子组件,内部通过 Section 概念将复杂卡片拆分为 MarkdownSectionButtonSectionListSection 等。数据层解析为 Section 列表,UI 层按序渲染,完美支持了任意复杂的组合 UI。

3.3 亮点:Action 统一路由

卡片上的按钮点击逻辑及其复杂(有的发请求,有的跳页面,有的回填输入框)。

  • 实现:将按钮行为抽象为标准化的 Action 模型(包含 urlmethodbodytype)。

    SseService 提供统一的 

    handleCardAction 方法,根据配置自动执行相应操作,将 UI 与 业务逻辑完全解耦。

3.4 亮点:快捷指令与 Resume 模式 (Quick Commands & Context Resume)

为了提升输入效率,我们设计了快捷指令系统上下文恢复 (Resume) 机制

  • 快捷指令:不只是简单的文本上屏,支持携带结构化 Payload (commandType + payload)。支持“静默发送”,点击后直接触发业务逻辑(如“提交申诉”),不在对话流中打断用户视觉。
  • Resume 模式: 在复杂的多轮对话中(如“请选择项目” -> “选中项目A”),后端需要知道上下文。 我们设计了 resume 消息类型。当收到该类型指令时,Provider 自动标记 _needsResumeMode = true。下一条用户发送的消息会自动附带 type: resume 标志,告诉服务端“这条消息是上一轮对话的延续”,完美解决了多轮对话的上下文丢失问题。

“小师傅”模块的架构设计遵循了高内聚、低耦合的原则。通过自研网络层保证了连接的稳定性,通过注册表模式解决了消息解析的复杂性,通过分层设计实现了业务的可维护性。这套架构不仅支撑了当前的智能问答业务,也为未来支持更多模态的交互打下了坚实基础。